From ff0257dc076a516e96d96c54f8fe0c1ee97b54df Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Thu, 30 Dec 2021 20:23:47 +0100 Subject: [PATCH] Add loprog/hiprob in the API and engine --- src/sfizz.h | 17 +++++++++++++++ src/sfizz.hpp | 17 +++++++++++++++ src/sfizz/BeatClock.h | 6 ++++++ src/sfizz/Defaults.cpp | 2 ++ src/sfizz/Defaults.h | 2 ++ src/sfizz/Layer.cpp | 32 +++++++++++------------------ src/sfizz/Layer.h | 7 +++++++ src/sfizz/MidiState.cpp | 11 ++++++++++ src/sfizz/MidiState.h | 19 +++++++++++++++++ src/sfizz/Region.cpp | 6 ++++++ src/sfizz/Region.h | 1 + src/sfizz/Synth.cpp | 16 ++++++++++++--- src/sfizz/Synth.h | 8 ++++++++ src/sfizz/SynthMessaging.cpp | 8 ++++++++ src/sfizz/sfizz.cpp | 5 +++++ src/sfizz/sfizz_wrapper.cpp | 4 ++++ tests/RegionActivationT.cpp | 40 ++++++++++++++++++++++++++++++++++++ tests/RegionValuesT.cpp | 25 ++++++++++++++++++++++ 18 files changed, 203 insertions(+), 23 deletions(-) diff --git a/src/sfizz.h b/src/sfizz.h index 5c473cde..b3abfb43 100644 --- a/src/sfizz.h +++ b/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 diff --git a/src/sfizz.hpp b/src/sfizz.hpp index 589676a4..cfb42391 100644 --- a/src/sfizz.hpp +++ b/src/sfizz.hpp @@ -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 * diff --git a/src/sfizz/BeatClock.h b/src/sfizz/BeatClock.h index a3fbc38d..83673f88 100644 --- a/src/sfizz/BeatClock.h +++ b/src/sfizz/BeatClock.h @@ -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. diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index ef821e46..eb56d33d 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -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 }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 0ddcba3e..fc4ef546 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -171,6 +171,8 @@ namespace Default extern const OpcodeSpec xfinLo; extern const OpcodeSpec loBend; extern const OpcodeSpec hiBend; + extern const OpcodeSpec loProgram; + extern const OpcodeSpec hiProgram; extern const OpcodeSpec loNormalized; extern const OpcodeSpec hiNormalized; extern const OpcodeSpec loBipolar; diff --git a/src/sfizz/Layer.cpp b/src/sfizz/Layer.cpp index f59976be..9e06c081 100644 --- a/src/sfizz/Layer.cpp +++ b/src/sfizz/Layer.cpp @@ -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 diff --git a/src/sfizz/Layer.h b/src/sfizz/Layer.h index d049e2c0..e675ec44 100644 --- a/src/sfizz/Layer.h +++ b/src/sfizz/Layer.h @@ -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 ccSwitched_; diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 1e31d156..b1992f26 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -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; +} diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index 290c60d4..fb187005 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -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 polyAftertouchEvents; + /** + * @brief Current midi program + */ + int currentProgram { 0 }; + float sampleRate { config::defaultSampleRate }; int samplesPerBlock { config::defaultSamplesPerBlock }; float alternate { 0.0f }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 09f24573..216b1c9f 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -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; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 1ebb1818..43165d1e 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -278,6 +278,7 @@ struct Region { // Region logic: MIDI conditions UncheckedRange bendRange { Default::loBend, Default::hiBend }; // hibend and lobend + UncheckedRange programRange { Default::loProgram, Default::hiProgram }; // loprog and hiprog CCMap> ccConditions {{ Default::loCC, Default::hiCC }}; absl::optional lastKeyswitch {}; // sw_last absl::optional> lastKeyswitchRange {}; // sw_last diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 6960375a..0a72ba88 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -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); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index e4192164..251ad06b 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -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 * diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 95cbce8d..26ec6102 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -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]; diff --git a/src/sfizz/sfizz.cpp b/src/sfizz/sfizz.cpp index 089f3582..c8be05f8 100644 --- a/src/sfizz/sfizz.cpp +++ b/src/sfizz/sfizz.cpp @@ -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); diff --git a/src/sfizz/sfizz_wrapper.cpp b/src/sfizz/sfizz_wrapper.cpp index 99327590..fb82435b 100644 --- a/src/sfizz/sfizz_wrapper.cpp +++ b/src/sfizz/sfizz_wrapper.cpp @@ -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); diff --git a/tests/RegionActivationT.cpp b/tests/RegionActivationT.cpp index f36dba8a..67389f85 100644 --- a/tests/RegionActivationT.cpp +++ b/tests/RegionActivationT.cpp @@ -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 buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + + SECTION("Default value") { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sw_previous.sfz", R"( + 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"( + sample=*saw hiprog=2 + sample=*sine loprog=1 hiprog=126 + sample=*tri loprog=-1 hiprog=200 + )"); + synth.noteOn(0, 51, 64); + synth.renderBlock(buffer); + REQUIRE(playingSamples(synth) == std::vector { "*saw", "*tri" }); + synth.programChange(0, 5); + synth.noteOn(0, 53, 64); + synth.renderBlock(buffer); + REQUIRE(playingSamples(synth) == std::vector { "*saw", "*tri", "*sine", "*tri" }); + synth.programChange(0, 127); + synth.noteOn(0, 54, 64); + synth.renderBlock(buffer); + REQUIRE(playingSamples(synth) == std::vector { "*saw", "*tri", "*sine", "*tri", "*tri" }); + } +} diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 82d0639a..8cd89ea5 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -706,6 +706,31 @@ TEST_CASE("[Values] Bend range") REQUIRE(messageList == expected); } +TEST_CASE("[Values] Program range") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav loprog=1 hiprog=45 + sample=kick.wav loprog=-1 hiprog=555 + 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 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;