Add loprog/hiprob in the API and engine
This commit is contained in:
parent
3d50afc794
commit
ff0257dc07
18 changed files with 203 additions and 23 deletions
17
src/sfizz.h
17
src/sfizz.h
|
|
@ -445,6 +445,23 @@ SFIZZ_EXPORTED_API void sfizz_send_cc(sfizz_synth_t* synth, int delay, int cc_nu
|
|||
*/
|
||||
SFIZZ_EXPORTED_API void sfizz_send_hdcc(sfizz_synth_t* synth, int delay, int cc_number, float norm_value);
|
||||
|
||||
/**
|
||||
* @brief Send a program change event to the synth.
|
||||
* @since 1.1.2
|
||||
*
|
||||
* This command should be delay-ordered with all other midi-type events
|
||||
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
|
||||
* synth is undefined.
|
||||
*
|
||||
* @param synth The synth.
|
||||
* @param delay The delay of the event in the block, in samples.
|
||||
* @param program The program number, in domain 0 to 127.
|
||||
*
|
||||
* @par Thread-safety constraints
|
||||
* - @b RT: the function must be invoked from the Real-time thread
|
||||
*/
|
||||
SFIZZ_EXPORTED_API void sfizz_send_program_change(sfizz_synth_t* synth, int delay, int program);
|
||||
|
||||
/**
|
||||
* @brief Send a high precision CC automation to the synth.
|
||||
* @since 1.0.0
|
||||
|
|
|
|||
|
|
@ -495,6 +495,23 @@ public:
|
|||
*/
|
||||
void hdcc(int delay, int ccNumber, float normValue) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Send a program change event to the synth
|
||||
* @since 1.1.2
|
||||
*
|
||||
* This command should be delay-ordered with all other midi-type events
|
||||
* (notes, CCs, aftertouch and pitch-wheel), otherwise the behavior of the
|
||||
* synth is undefined.
|
||||
*
|
||||
* @param delay the delay at which the event occurs; this should be lower
|
||||
* than the size of the block in the next call to renderBlock().
|
||||
* @param program the cc number, in domain 0 to 127.
|
||||
*
|
||||
* @par Thread-safety constraints
|
||||
* - @b RT: the function must be invoked from the Real-time thread
|
||||
*/
|
||||
void programChange(int delay, int program) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Send a high precision CC automation to the synth
|
||||
*
|
||||
|
|
|
|||
|
|
@ -152,6 +152,12 @@ public:
|
|||
* @return float
|
||||
*/
|
||||
double getBeatsPerFrame() const { return beatsPerSecond_ * samplePeriod_; }
|
||||
/**
|
||||
* @brief Get beats per second
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
double getBeatsPerSecond() const { return beatsPerSecond_; }
|
||||
/**
|
||||
* @brief Create a normalized phase signal for LFO which completes a
|
||||
* period every N-th beat.
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ FloatSpec loPolyAftertouch { 0, {0, 127}, kNormalizeMidi|kPermissiveBounds };
|
|||
FloatSpec hiPolyAftertouch { 127, {0, 127}, kNormalizeMidi|kFillGap|kPermissiveBounds };
|
||||
FloatSpec loBend { -8191, {-8192.0f, 8191.0f}, kNormalizeBend|kPermissiveBounds };
|
||||
FloatSpec hiBend { 8191, {-8192.0f, 8191.0f}, kNormalizeBend|kFillGap|kPermissiveBounds };
|
||||
UInt8Spec loProgram { 0, {0, 127}, 0 };
|
||||
UInt8Spec hiProgram { 127, {0, 127}, 0 };
|
||||
FloatSpec loNormalized { 0.0f, {0.0f, 1.0f}, kPermissiveBounds };
|
||||
FloatSpec hiNormalized { 1.0f, {0.0f, 1.0f}, kPermissiveBounds };
|
||||
FloatSpec loBipolar { -1.0f, {-1.0f, 1.0f}, kPermissiveBounds };
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ namespace Default
|
|||
extern const OpcodeSpec<float> xfinLo;
|
||||
extern const OpcodeSpec<float> loBend;
|
||||
extern const OpcodeSpec<float> hiBend;
|
||||
extern const OpcodeSpec<uint8_t> loProgram;
|
||||
extern const OpcodeSpec<uint8_t> hiProgram;
|
||||
extern const OpcodeSpec<float> loNormalized;
|
||||
extern const OpcodeSpec<float> hiNormalized;
|
||||
extern const OpcodeSpec<float> loBipolar;
|
||||
|
|
|
|||
|
|
@ -38,12 +38,14 @@ void Layer::initializeActivations()
|
|||
pitchSwitched_ = true;
|
||||
bpmSwitched_ = true;
|
||||
aftertouchSwitched_ = true;
|
||||
programSwitched_ = true;
|
||||
ccSwitched_.set();
|
||||
}
|
||||
|
||||
bool Layer::isSwitchedOn() const noexcept
|
||||
{
|
||||
return keySwitched_ && previousKeySwitched_ && sequenceSwitched_ && pitchSwitched_ && bpmSwitched_ && aftertouchSwitched_ && ccSwitched_.all();
|
||||
return keySwitched_ && previousKeySwitched_ && sequenceSwitched_ && pitchSwitched_
|
||||
&& programSwitched_ && bpmSwitched_ && aftertouchSwitched_ && ccSwitched_.all();
|
||||
}
|
||||
|
||||
bool Layer::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept
|
||||
|
|
@ -152,10 +154,7 @@ void Layer::updateCCState(int ccNumber, float ccValue) noexcept
|
|||
if (!conditions)
|
||||
return;
|
||||
|
||||
if (conditions->containsWithEnd(ccValue))
|
||||
ccSwitched_.set(ccNumber, true);
|
||||
else
|
||||
ccSwitched_.set(ccNumber, false);
|
||||
ccSwitched_.set(ccNumber, conditions->containsWithEnd(ccValue));
|
||||
}
|
||||
|
||||
bool Layer::registerCC(int ccNumber, float ccValue, float randValue) noexcept
|
||||
|
|
@ -189,30 +188,23 @@ bool Layer::registerCC(int ccNumber, float ccValue, float randValue) noexcept
|
|||
|
||||
void Layer::registerPitchWheel(float pitch) noexcept
|
||||
{
|
||||
const Region& region = region_;
|
||||
if (region.bendRange.containsWithEnd(pitch))
|
||||
pitchSwitched_ = true;
|
||||
else
|
||||
pitchSwitched_ = false;
|
||||
pitchSwitched_ = region_.bendRange.containsWithEnd(pitch);
|
||||
}
|
||||
|
||||
void Layer::registerProgramChange(int program) noexcept
|
||||
{
|
||||
programSwitched_ = region_.programRange.containsWithEnd(program);
|
||||
}
|
||||
|
||||
void Layer::registerAftertouch(float aftertouch) noexcept
|
||||
{
|
||||
const Region& region = region_;
|
||||
if (region.aftertouchRange.containsWithEnd(aftertouch))
|
||||
aftertouchSwitched_ = true;
|
||||
else
|
||||
aftertouchSwitched_ = false;
|
||||
aftertouchSwitched_ = region_.aftertouchRange.containsWithEnd(aftertouch);
|
||||
}
|
||||
|
||||
void Layer::registerTempo(float secondsPerQuarter) noexcept
|
||||
{
|
||||
const Region& region = region_;
|
||||
const float bpm = 60.0f / secondsPerQuarter;
|
||||
if (region.bpmRange.containsWithEnd(bpm))
|
||||
bpmSwitched_ = true;
|
||||
else
|
||||
bpmSwitched_ = false;
|
||||
bpmSwitched_ = region_.bpmRange.containsWithEnd(bpm);
|
||||
}
|
||||
|
||||
void Layer::delaySustainRelease(int noteNumber, float velocity) noexcept
|
||||
|
|
|
|||
|
|
@ -118,6 +118,12 @@ public:
|
|||
* @param secondsPerQuarter
|
||||
*/
|
||||
void registerTempo(float secondsPerQuarter) noexcept;
|
||||
/**
|
||||
* @brief Register program change
|
||||
*
|
||||
* @param secondsPerQuarter
|
||||
*/
|
||||
void registerProgramChange(int program) noexcept;
|
||||
|
||||
// Started notes
|
||||
bool sustainPressed_ { false };
|
||||
|
|
@ -136,6 +142,7 @@ public:
|
|||
bool previousKeySwitched_ {};
|
||||
bool sequenceSwitched_ {};
|
||||
bool pitchSwitched_ {};
|
||||
bool programSwitched_ {};
|
||||
bool bpmSwitched_ {};
|
||||
bool aftertouchSwitched_ {};
|
||||
std::bitset<config::numCCs> ccSwitched_;
|
||||
|
|
|
|||
|
|
@ -292,3 +292,14 @@ const sfz::EventVector& sfz::MidiState::getPolyAftertouchEvents(int noteNumber)
|
|||
|
||||
return polyAftertouchEvents[noteNumber];
|
||||
}
|
||||
|
||||
int sfz::MidiState::getProgram() const noexcept
|
||||
{
|
||||
return currentProgram;
|
||||
}
|
||||
|
||||
void sfz::MidiState::programChangeEvent(int delay, int program) noexcept
|
||||
{
|
||||
ASSERT(program >= 0 && program <= 127);
|
||||
currentProgram = program;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,20 @@ public:
|
|||
*/
|
||||
float getPolyAftertouch(int noteNumber) const noexcept;
|
||||
|
||||
/**
|
||||
* @brief Get the current midi program
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
int getProgram() const noexcept;
|
||||
/**
|
||||
* @brief Register a program change event
|
||||
*
|
||||
* @param delay
|
||||
* @param program
|
||||
*/
|
||||
void programChangeEvent(int delay, int program) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Register a CC event
|
||||
*
|
||||
|
|
@ -275,6 +289,11 @@ private:
|
|||
*/
|
||||
std::array<EventVector, 128> polyAftertouchEvents;
|
||||
|
||||
/**
|
||||
* @brief Current midi program
|
||||
*/
|
||||
int currentProgram { 0 };
|
||||
|
||||
float sampleRate { config::defaultSampleRate };
|
||||
int samplesPerBlock { config::defaultSamplesPerBlock };
|
||||
float alternate { 0.0f };
|
||||
|
|
|
|||
|
|
@ -243,6 +243,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode, bool cleanOpcode)
|
|||
case hash("hibend"):
|
||||
bendRange.setEnd(opcode.read(Default::hiBend));
|
||||
break;
|
||||
case hash("loprog"):
|
||||
programRange.setStart(opcode.read(Default::loProgram));
|
||||
break;
|
||||
case hash("hiprog"):
|
||||
programRange.setEnd(opcode.read(Default::hiProgram));
|
||||
break;
|
||||
case hash("locc&"):
|
||||
if (opcode.parameters.back() >= config::numCCs)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ struct Region {
|
|||
|
||||
// Region logic: MIDI conditions
|
||||
UncheckedRange<float> bendRange { Default::loBend, Default::hiBend }; // hibend and lobend
|
||||
UncheckedRange<uint8_t> programRange { Default::loProgram, Default::hiProgram }; // loprog and hiprog
|
||||
CCMap<UncheckedRange<float>> ccConditions {{ Default::loCC, Default::hiCC }};
|
||||
absl::optional<uint8_t> lastKeyswitch {}; // sw_last
|
||||
absl::optional<UncheckedRange<uint8_t>> lastKeyswitchRange {}; // sw_last
|
||||
|
|
|
|||
|
|
@ -871,9 +871,10 @@ void Synth::Impl::finalizeSfzLoad()
|
|||
region.velCurve = Curve::buildFromVelcurvePoints(
|
||||
region.velocityPoints, Curve::Interpolator::Linear);
|
||||
|
||||
layer.registerPitchWheel(0);
|
||||
layer.registerAftertouch(0);
|
||||
layer.registerTempo(2.0f);
|
||||
layer.registerPitchWheel(midiState.getPitchBend());
|
||||
layer.registerAftertouch(midiState.getChannelAftertouch());
|
||||
layer.registerTempo(resources_.getBeatClock().getBeatsPerSecond());
|
||||
layer.registerProgramChange(midiState.getProgram());
|
||||
maxFilters = max(maxFilters, region.filters.size());
|
||||
maxEQs = max(maxEQs, region.equalizers.size());
|
||||
maxLFOs = max(maxLFOs, region.lfos.size());
|
||||
|
|
@ -1530,6 +1531,15 @@ void Synth::hdPitchWheel(int delay, float normalizedPitch) noexcept
|
|||
impl.performHdcc(delay, ExtendedCCs::pitchBend, normalizedPitch, false);
|
||||
}
|
||||
|
||||
void Synth::programChange(int delay, int program) noexcept
|
||||
{
|
||||
Impl& impl = *impl_;
|
||||
impl.resources_.getMidiState().programChangeEvent(delay, program);
|
||||
for (const Impl::LayerPtr& layer : impl.layers_)
|
||||
layer->registerProgramChange(program);
|
||||
}
|
||||
|
||||
|
||||
void Synth::channelAftertouch(int delay, int aftertouch) noexcept
|
||||
{
|
||||
const float normalizedAftertouch = normalize7Bits(aftertouch);
|
||||
|
|
|
|||
|
|
@ -408,6 +408,14 @@ public:
|
|||
* @param normValue the normalized cc value, in domain 0 to 1
|
||||
*/
|
||||
void hdcc(int delay, int ccNumber, float normValue) noexcept;
|
||||
/**
|
||||
* @brief Send a program change event to the synth
|
||||
*
|
||||
* @param delay the delay at which the event occurs; this should be lower than the size of
|
||||
* the block in the next call to renderBlock().
|
||||
* @param ccNumber the program number
|
||||
*/
|
||||
void programChange(int delay, int program) noexcept;
|
||||
/**
|
||||
* @brief Send a high precision CC automation to the synth
|
||||
*
|
||||
|
|
|
|||
|
|
@ -435,6 +435,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co
|
|||
client.receive(delay, path, "ff", args);
|
||||
} break;
|
||||
|
||||
MATCH("/region&/program_range", "") {
|
||||
GET_REGION_OR_BREAK(indices[0])
|
||||
sfizz_arg_t args[2];
|
||||
args[0].i = region.programRange.getStart();
|
||||
args[1].i = region.programRange.getEnd();
|
||||
client.receive(delay, path, "ii", args);
|
||||
} break;
|
||||
|
||||
MATCH("/region&/cc_range&", "") {
|
||||
GET_REGION_OR_BREAK(indices[0])
|
||||
sfizz_arg_t args[2];
|
||||
|
|
|
|||
|
|
@ -200,6 +200,11 @@ void sfz::Sfizz::automateHdcc(int delay, int ccNumber, float normValue) noexcept
|
|||
synth->synth.automateHdcc(delay, ccNumber, normValue);
|
||||
}
|
||||
|
||||
void sfz::Sfizz::programChange(int delay, int program) noexcept
|
||||
{
|
||||
synth->synth.programChange(delay, program);
|
||||
}
|
||||
|
||||
void sfz::Sfizz::pitchWheel(int delay, int pitch) noexcept
|
||||
{
|
||||
synth->synth.pitchWheel(delay, pitch);
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ void sfizz_send_hd_pitch_wheel(sfizz_synth_t* synth, int delay, float pitch)
|
|||
{
|
||||
synth->synth.hdPitchWheel(delay, pitch);
|
||||
}
|
||||
void sfizz_send_program_change(sfizz_synth_t* synth, int delay, int program)
|
||||
{
|
||||
synth->synth.programChange(delay, program);
|
||||
}
|
||||
void sfizz_send_aftertouch(sfizz_synth_t* synth, int delay, int aftertouch)
|
||||
{
|
||||
synth->synth.channelAftertouch(delay, aftertouch);
|
||||
|
|
|
|||
|
|
@ -706,3 +706,43 @@ TEST_CASE("[Keyswitches] sw_default with octave_offset")
|
|||
REQUIRE(messageList == expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[Region activation] Program change")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
sfz::AudioBuffer<float> buffer { 2, static_cast<unsigned>(synth.getSamplesPerBlock()) };
|
||||
|
||||
SECTION("Default value") {
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
|
||||
<region> sample=*saw
|
||||
)");
|
||||
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
|
||||
synth.noteOn(0, 51, 64);
|
||||
synth.renderBlock(buffer);
|
||||
REQUIRE(numPlayingVoices(synth) == 1);
|
||||
synth.programChange(0, 45);
|
||||
REQUIRE(synth.getLayerView(0)->isSwitchedOn());
|
||||
synth.noteOn(0, 53, 64);
|
||||
synth.renderBlock(buffer);
|
||||
REQUIRE(numPlayingVoices(synth) == 2);
|
||||
}
|
||||
|
||||
SECTION("Change range") {
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"(
|
||||
<region> sample=*saw hiprog=2
|
||||
<region> sample=*sine loprog=1 hiprog=126
|
||||
<region> sample=*tri loprog=-1 hiprog=200
|
||||
)");
|
||||
synth.noteOn(0, 51, 64);
|
||||
synth.renderBlock(buffer);
|
||||
REQUIRE(playingSamples(synth) == std::vector<std::string> { "*saw", "*tri" });
|
||||
synth.programChange(0, 5);
|
||||
synth.noteOn(0, 53, 64);
|
||||
synth.renderBlock(buffer);
|
||||
REQUIRE(playingSamples(synth) == std::vector<std::string> { "*saw", "*tri", "*sine", "*tri" });
|
||||
synth.programChange(0, 127);
|
||||
synth.noteOn(0, 54, 64);
|
||||
synth.renderBlock(buffer);
|
||||
REQUIRE(playingSamples(synth) == std::vector<std::string> { "*saw", "*tri", "*sine", "*tri", "*tri" });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -706,6 +706,31 @@ TEST_CASE("[Values] Bend range")
|
|||
REQUIRE(messageList == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[Values] Program range")
|
||||
{
|
||||
Synth synth;
|
||||
std::vector<std::string> messageList;
|
||||
Client client(&messageList);
|
||||
client.setReceiveCallback(&simpleMessageReceiver);
|
||||
synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"(
|
||||
<region> sample=kick.wav
|
||||
<region> sample=kick.wav loprog=1 hiprog=45
|
||||
<region> sample=kick.wav loprog=-1 hiprog=555
|
||||
<region> sample=kick.wav hiprog=-1
|
||||
)");
|
||||
synth.dispatchMessage(client, 0, "/region0/program_range", "", nullptr);
|
||||
synth.dispatchMessage(client, 0, "/region1/program_range", "", nullptr);
|
||||
synth.dispatchMessage(client, 0, "/region2/program_range", "", nullptr);
|
||||
synth.dispatchMessage(client, 0, "/region3/program_range", "", nullptr);
|
||||
std::vector<std::string> expected {
|
||||
"/region0/program_range,ii : { 0, 127 }",
|
||||
"/region1/program_range,ii : { 1, 45 }",
|
||||
"/region2/program_range,ii : { 0, 127 }",
|
||||
"/region3/program_range,ii : { 0, 127 }",
|
||||
};
|
||||
REQUIRE(messageList == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[Values] CC condition range")
|
||||
{
|
||||
Synth synth;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue