From df07f3ac82f4f561645d88a45062cc85af58ee04 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Tue, 23 Mar 2021 12:52:09 +0100 Subject: [PATCH 1/6] Read and parse sostenuto-related stuff --- src/sfizz/Defaults.cpp | 2 ++ src/sfizz/Defaults.h | 2 ++ src/sfizz/Region.cpp | 6 +++++ src/sfizz/Region.h | 2 ++ src/sfizz/SynthMessaging.cpp | 10 ++++++++ tests/RegionValuesT.cpp | 46 ++++++++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+) diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index d3c6393d..9319083c 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -61,7 +61,9 @@ UInt16Spec ccNumber { 0, {0, config::numCCs}, 0 }; UInt16Spec smoothCC { 0, {0, 100}, kPermissiveUpperBound }; UInt8Spec curveCC { 0, {0, 255}, 0 }; UInt8Spec sustainCC { 64, {0, 127}, 0 }; +UInt8Spec sostenutoCC { 66, {0, 127}, 0 }; FloatSpec sustainThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds }; +FloatSpec sostenutoThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds }; BoolSpec checkSustain { true, {0, 1}, kEnforceBounds }; BoolSpec checkSostenuto { true, {0, 1}, kEnforceBounds }; FloatSpec loBPM { 0.0f, {0.0f, 500.0f}, kPermissiveBounds }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 28dba5fa..70735abf 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -161,9 +161,11 @@ namespace Default extern const OpcodeSpec curveCC; extern const OpcodeSpec smoothCC; extern const OpcodeSpec sustainCC; + extern const OpcodeSpec sostenutoCC; extern const OpcodeSpec checkSustain; extern const OpcodeSpec checkSostenuto; extern const OpcodeSpec sustainThreshold; + extern const OpcodeSpec sostenutoThreshold; extern const OpcodeSpec loBPM; extern const OpcodeSpec hiBPM; extern const OpcodeSpec sequence; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e066e430..b8431e07 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -308,9 +308,15 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("sustain_cc"): sustainCC = opcode.read(Default::sustainCC); break; + case hash("sostenuto_cc"): + sostenutoCC = opcode.read(Default::sostenutoCC); + break; case hash("sustain_lo"): sustainThreshold = opcode.read(Default::sustainThreshold); break; + case hash("sostenuto_lo"): + sostenutoThreshold = opcode.read(Default::sostenutoThreshold); + break; case hash("sustain_sw"): checkSustain = opcode.read(Default::checkSustain); break; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 5cf21180..f885971c 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -406,7 +406,9 @@ struct Region { bool checkSustain { Default::checkSustain }; // sustain_sw bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw uint16_t sustainCC { Default::sustainCC }; // sustain_cc + uint16_t sostenutoCC { Default::sostenutoCC }; // sustain_cc float sustainThreshold { Default::sustainThreshold }; // sustain_cc + float sostenutoThreshold { Default::sostenutoThreshold }; // sustain_cc // Region logic: internal conditions UncheckedRange aftertouchRange { Default::loChannelAftertouch, Default::hiChannelAftertouch }; // hichanaft and lochanaft diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index bfeda787..26009d6c 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -1042,11 +1042,21 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'i'>(delay, path, region.sustainCC); } break; + MATCH("/region&/sostenuto_cc", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'i'>(delay, path, region.sostenutoCC); + } break; + MATCH("/region&/sustain_lo", "") { GET_REGION_OR_BREAK(indices[0]) client.receive<'f'>(delay, path, region.sustainThreshold); } break; + MATCH("/region&/sostenuto_lo", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'f'>(delay, path, region.sostenutoThreshold); + } break; + MATCH("/region&/oscillator_phase", "") { GET_REGION_OR_BREAK(indices[0]) client.receive<'f'>(delay, path, region.oscillatorPhase); diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 6b5dfbf5..1feb9cf1 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -2425,6 +2425,52 @@ TEST_CASE("[Values] Sustain low") REQUIRE(messageList == expected); } +TEST_CASE("[Values] Sostenuto CC") +{ + 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 sostenuto_cc=10 + sample=kick.wav sostenuto_cc=20 sostenuto_cc=-1 + )"); + synth.dispatchMessage(client, 0, "/region0/sostenuto_cc", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/sostenuto_cc", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/sostenuto_cc", "", nullptr); + std::vector expected { + "/region0/sostenuto_cc,i : { 66 }", + "/region1/sostenuto_cc,i : { 10 }", + "/region2/sostenuto_cc,i : { 66 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Values] Sostenuto low") +{ + 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 sostenuto_lo=10 + sample=kick.wav sostenuto_lo=10 sostenuto_lo=-1 + )"); + synth.dispatchMessage(client, 0, "/region0/sostenuto_lo", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/sostenuto_lo", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/sostenuto_lo", "", nullptr); + std::vector expected { + "/region0/sostenuto_lo,f : { 0.00787402 }", + "/region1/sostenuto_lo,f : { 0.0787402 }", + "/region2/sostenuto_lo,f : { -0.00787402 }", + }; + REQUIRE(messageList == expected); +} + TEST_CASE("[Values] Oscillator phase") { Synth synth; From 0a065a392cee833547f40acf329632fd79415090 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Tue, 23 Mar 2021 15:05:32 +0100 Subject: [PATCH 2/6] WIP --- src/sfizz/Voice.cpp | 27 ++++++++++++++++ tests/SynthT.cpp | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 94aba5bb..ce3edd70 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -202,6 +202,8 @@ struct Voice::Impl State state_ { State::idle }; bool noteIsOff_ { false }; + enum class SostenutoState { Up, Sustaining, PreviouslyDown }; + SostenutoState sostenutoState_ { SostenutoState::Up }; TriggerEvent triggerEvent_; absl::optional triggerDelay_; @@ -459,6 +461,13 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe impl.resources_.modMatrix.initVoice(impl.id_, region->getId(), impl.initialDelay_); impl.saveModulationTargets(region); + if (region->checkSostenuto) { + const bool sostenutoPressed = + impl.resources_.midiState.getCCValue(region->sostenutoCC) >= region->sostenutoThreshold; + impl.sostenutoState_ = + sostenutoPressed ? Impl::SostenutoState::PreviouslyDown : Impl::SostenutoState::Up; + } + return true; } @@ -546,6 +555,10 @@ void Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept if (!impl.region_->checkSustain || impl.resources_.midiState.getCCValue(impl.region_->sustainCC) < impl.region_->sustainThreshold) release(delay); + + if (!impl.region_->checkSostenuto + || impl.sostenutoState_ != Impl::SostenutoState::Sustaining) + release(delay); } } @@ -564,6 +577,19 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept && ccNumber == impl.region_->sustainCC && ccValue < impl.region_->sustainThreshold) release(delay); + + if (impl.region_->checkSostenuto + && ccNumber == impl.region_->sostenutoCC) { + if (ccValue < impl.region_->sostenutoThreshold) { + impl.sostenutoState_ = Impl::SostenutoState::Up; + } else { + if (impl.sostenutoState_ == Impl::SostenutoState::Up) + impl.sostenutoState_ = Impl::SostenutoState::Sustaining; + } + + if (impl.sostenutoState_ != Impl::SostenutoState::Sustaining && impl.noteIsOff_) + release(delay); + } } void Voice::registerPitchWheel(int delay, float pitch) noexcept @@ -1512,6 +1538,7 @@ void Voice::reset() noexcept impl.count_ = 1; impl.floatPositionOffset_ = 0.0f; impl.noteIsOff_ = false; + impl.sostenutoState_ = Impl::SostenutoState::Up; impl.resetLoopInformation(); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 3bf2cc64..8175b740 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -818,6 +818,35 @@ TEST_CASE("[Synth] Release (Different sustain CC)") REQUIRE( synth.getNumActiveVoices() == 2 ); } +TEST_CASE("[Synth] Release key (Different sostenuto CC)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + sostenuto_cc=54 + key=62 sample=*sine trigger=release_key + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 54, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); +} + +TEST_CASE("[Synth] Release (Different sostenuto CC)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + sostenuto_cc=54 + key=62 sample=*silence + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 54, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(0, 54, 0); + REQUIRE( synth.getNumActiveVoices() == 2 ); +} + TEST_CASE("[Synth] Sustain threshold default") { sfz::Synth synth; @@ -852,6 +881,55 @@ TEST_CASE("[Synth] Sustain threshold") REQUIRE( synth.getNumActiveVoices() == 5 ); } +TEST_CASE("[Synth] Sostenuto") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sostenuto.sfz", R"( + sample=*sine + )"); + + SECTION("1 note") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 1); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(3, 66, 0); + REQUIRE( synth.getNumActiveVoices() == 0 ); + } + + SECTION("2 notes") + { + synth.noteOn(0, 62, 85); + synth.noteOn(0, 64, 85); + synth.cc(1, 66, 1); + synth.noteOff(2, 62, 85); + synth.noteOff(2, 64, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); + synth.cc(3, 66, 0); + REQUIRE( synth.getNumActiveVoices() == 0 ); + } + + SECTION("1 note but after the pedal is depressed") + { + synth.cc(0, 66, 1); + synth.noteOn(1, 62, 85); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); + } + + SECTION("2 notes, 1 after the pedal is depressed") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 1); + synth.noteOn(2, 64, 85); + synth.noteOff(3, 62, 85); + synth.noteOff(3, 64, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( getActiveVoices(synth)[0]->getTriggerEvent().number == 62 ); + } +} + TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") { sfz::Synth synth; From ed09e30fcf18e98173f95b6e3b8aa2c349713e6b Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Tue, 23 Mar 2021 22:33:41 +0100 Subject: [PATCH 3/6] Missing the delayed release logic for sostenuto --- src/sfizz/Synth.cpp | 1 - src/sfizz/Voice.cpp | 54 +++++++++++++++++++++------------ tests/SynthT.cpp | 73 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 98 insertions(+), 30 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 879816b1..4e2acebb 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1155,7 +1155,6 @@ void Synth::Impl::startDelayedReleaseVoices(Region* region, int delay, SisterVoi } for (auto& note: region->delayedReleases) { - // FIXME: we really need to have some form of common method to find and start voices... const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; startVoice(region, delay, noteOffEvent, ring); } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index ce3edd70..47cfec83 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -202,6 +202,8 @@ struct Voice::Impl State state_ { State::idle }; bool noteIsOff_ { false }; + enum class SustainState { Up, Sustaining }; + SustainState sustainState_ { SustainState::Up }; enum class SostenutoState { Up, Sustaining, PreviouslyDown }; SostenutoState sostenutoState_ { SostenutoState::Up }; @@ -461,6 +463,13 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe impl.resources_.modMatrix.initVoice(impl.id_, region->getId(), impl.initialDelay_); impl.saveModulationTargets(region); + if (region->checkSustain) { + const bool sustainPressed = + impl.resources_.midiState.getCCValue(region->sustainCC) >= region->sustainThreshold; + impl.sustainState_ = + sustainPressed ? Impl::SustainState::Sustaining : Impl::SustainState::Up; + } + if (region->checkSostenuto) { const bool sostenutoPressed = impl.resources_.midiState.getCCValue(region->sostenutoCC) >= region->sostenutoThreshold; @@ -552,12 +561,13 @@ void Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept if (impl.region_->loopMode == LoopMode::one_shot) return; - if (!impl.region_->checkSustain - || impl.resources_.midiState.getCCValue(impl.region_->sustainCC) < impl.region_->sustainThreshold) - release(delay); + const bool sustainPedalReleaseCondition = !impl.region_->checkSustain + || impl.sustainState_ != Impl::SustainState::Sustaining; - if (!impl.region_->checkSostenuto - || impl.sostenutoState_ != Impl::SostenutoState::Sustaining) + const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto + || impl.sostenutoState_ != Impl::SostenutoState::Sustaining; + + if (sustainPedalReleaseCondition && sostenutoPedalReleaseCondition) release(delay); } } @@ -572,24 +582,30 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept if (impl.state_ != State::playing) return; - if (impl.region_->checkSustain - && impl.noteIsOff_ - && ccNumber == impl.region_->sustainCC - && ccValue < impl.region_->sustainThreshold) - release(delay); - - if (impl.region_->checkSostenuto - && ccNumber == impl.region_->sostenutoCC) { + if (impl.region_->checkSustain && (ccNumber == impl.region_->sostenutoCC)) { if (ccValue < impl.region_->sostenutoThreshold) { impl.sostenutoState_ = Impl::SostenutoState::Up; - } else { - if (impl.sostenutoState_ == Impl::SostenutoState::Up) - impl.sostenutoState_ = Impl::SostenutoState::Sustaining; + } else if (impl.sostenutoState_ == Impl::SostenutoState::Up) { + impl.sostenutoState_ = Impl::SostenutoState::Sustaining; } - - if (impl.sostenutoState_ != Impl::SostenutoState::Sustaining && impl.noteIsOff_) - release(delay); } + + if (impl.region_->checkSostenuto && (ccNumber == impl.region_->sustainCC)) { + if (ccValue < impl.region_->sustainThreshold) { + impl.sustainState_ = Impl::SustainState::Up; + } else { + impl.sustainState_ = Impl::SustainState::Sustaining; + } + } + + const bool sustainPedalReleaseCondition = !impl.region_->checkSustain + || (impl.noteIsOff_ && (impl.sustainState_ != Impl::SustainState::Sustaining)); + + const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto + || (impl.noteIsOff_ && (impl.sostenutoState_ != Impl::SostenutoState::Sustaining)); + + if (sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) + release(delay); } void Voice::registerPitchWheel(int delay, float pitch) noexcept diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 8175b740..a1692aaa 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -835,15 +835,15 @@ TEST_CASE("[Synth] Release (Different sostenuto CC)") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( - sostenuto_cc=54 + sostenuto_cc=54 key=62 sample=*silence key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); - synth.cc(0, 54, 127); - synth.noteOff(0, 62, 85); + synth.cc(1, 54, 127); + synth.noteOff(2, 62, 85); REQUIRE( synth.getNumActiveVoices() == 1 ); - synth.cc(0, 54, 0); + synth.cc(3, 54, 0); REQUIRE( synth.getNumActiveVoices() == 2 ); } @@ -881,6 +881,59 @@ TEST_CASE("[Synth] Sustain threshold") REQUIRE( synth.getNumActiveVoices() == 5 ); } +TEST_CASE("[Synth] Sustain") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sostenuto.sfz", R"( + sample=*sine + )"); + + SECTION("1 note") + { + synth.noteOn(0, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(1, 64, 1); + synth.noteOff(2, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(3, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("2 notes") + { + synth.noteOn(0, 62, 85); + synth.noteOn(0, 64, 85); + synth.cc(1, 64, 1); + synth.noteOff(2, 62, 85); + synth.noteOff(2, 64, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.cc(3, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("1 note after the pedal is depressed") + { + synth.cc(0, 64, 1); + synth.noteOn(1, 62, 85); + synth.noteOff(2, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(3, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("2 notes, 1 after the pedal is depressed") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 64, 1); + synth.noteOn(2, 64, 85); + synth.noteOff(3, 62, 85); + synth.noteOff(3, 64, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.cc(4, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } +} + TEST_CASE("[Synth] Sostenuto") { sfz::Synth synth; @@ -893,9 +946,9 @@ TEST_CASE("[Synth] Sostenuto") synth.noteOn(0, 62, 85); synth.cc(1, 66, 1); synth.noteOff(2, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( numPlayingVoices(synth) == 1 ); synth.cc(3, 66, 0); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( numPlayingVoices(synth) == 0 ); } SECTION("2 notes") @@ -905,9 +958,9 @@ TEST_CASE("[Synth] Sostenuto") synth.cc(1, 66, 1); synth.noteOff(2, 62, 85); synth.noteOff(2, 64, 85); - REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( numPlayingVoices(synth) == 2 ); synth.cc(3, 66, 0); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( numPlayingVoices(synth) == 0 ); } SECTION("1 note but after the pedal is depressed") @@ -915,7 +968,7 @@ TEST_CASE("[Synth] Sostenuto") synth.cc(0, 66, 1); synth.noteOn(1, 62, 85); synth.noteOff(2, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 0 ); + REQUIRE( numPlayingVoices(synth) == 0 ); } SECTION("2 notes, 1 after the pedal is depressed") @@ -925,7 +978,7 @@ TEST_CASE("[Synth] Sostenuto") synth.noteOn(2, 64, 85); synth.noteOff(3, 62, 85); synth.noteOff(3, 64, 85); - REQUIRE( synth.getNumActiveVoices() == 1 ); + REQUIRE( numPlayingVoices(synth) == 1 ); REQUIRE( getActiveVoices(synth)[0]->getTriggerEvent().number == 62 ); } } From 8539508e0a0ffa970e337c81ad87387052e03eca Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Wed, 24 Mar 2021 01:35:49 +0100 Subject: [PATCH 4/6] WIP on the region state --- src/sfizz/Config.h | 5 +++ src/sfizz/Region.cpp | 46 +++++++++++++++++++++++-- src/sfizz/Region.h | 6 +++- src/sfizz/Synth.cpp | 35 +++++++++++++++---- src/sfizz/SynthPrivate.h | 13 ++++++-- tests/DirectRegionT.cpp | 13 ++++---- tests/SynthT.cpp | 72 +++++++++++++++++++++++++++++++--------- 7 files changed, 156 insertions(+), 34 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index a551b4f6..9a0bb82a 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -183,6 +183,11 @@ namespace config { * */ constexpr int playheadMovedFrames { 16 }; + /** + * @brief Max number of voices to start on release pedal up + * + */ + constexpr unsigned delayedReleaseVoices { 16 }; } // namespace config } // namespace sfz diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index b8431e07..eedc64d2 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -9,6 +9,7 @@ #include "Macros.h" #include "Debug.h" #include "Opcode.h" +#include "SwapAndPop.h" #include "StringViewHelpers.h" #include "ModifierHelpers.h" #include "modulations/ModId.h" @@ -1503,6 +1504,29 @@ bool sfz::Region::isSwitchedOn() const noexcept return keySwitched && previousKeySwitched && sequenceSwitched && pitchSwitched && bpmSwitched && aftertouchSwitched && ccSwitched.all(); } +void sfz::Region::delaySustainRelease(int noteNumber, float velocity) +{ + if (delayedSustainReleases.size() == delayedSustainReleases.capacity()) + return; + + delayedSustainReleases.emplace_back(noteNumber, velocity); +} + +void sfz::Region::delaySostenutoRelease(int noteNumber, float velocity) +{ + if (delayedSostenutoReleases.size() == delayedSostenutoReleases.capacity()) + return; + + delayedSostenutoReleases.emplace_back(noteNumber, velocity); +} + +void sfz::Region::removeFromSostenutoReleases(int noteNumber) +{ + swapAndPopFirst(delayedSostenutoReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }); +} + bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); @@ -1529,6 +1553,13 @@ bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue const bool attackTrigger = (trigger == Trigger::attack); const bool notFirstLegatoNote = (trigger == Trigger::legato && midiState.getActiveNotes() > 1); + if (trigger == Trigger::release && + keyOk && velOk + && checkSostenuto && midiState.getCCValue(sostenutoCC) < sostenutoThreshold) { + // This note on will possibly be "sostenutoed" + delaySostenutoRelease(noteNumber, velocity); + } + return keyOk && velOk && randOk && (attackTrigger || firstLegatoNote || notFirstLegatoNote); } @@ -1557,13 +1588,22 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu return true; if (trigger == Trigger::release) { - if (midiState.getCCValue(sustainCC) < sustainThreshold) + if (checkSostenuto && midiState.getCCValue(sostenutoCC) < sostenutoThreshold) + removeFromSostenutoReleases(noteNumber); + + const bool shouldSustain = checkSustain && midiState.getCCValue(sustainCC) >= sustainThreshold; + const bool shouldSostenuto = + checkSostenuto && midiState.getCCValue(sostenutoCC) >= sostenutoThreshold + && absl::c_find_if(delayedSostenutoReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }) != delayedSostenutoReleases.end(); + + if (!shouldSustain && !shouldSostenuto) return true; // If we reach this part, we're storing the notes to delay their release on CC up // This is handled by the Synth object - - delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber)); + delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); } return false; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index f885971c..74df3291 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -508,7 +508,11 @@ struct Region { RegionSet* parent { nullptr }; // Started notes - std::vector> delayedReleases; + std::vector> delayedSustainReleases; + std::vector> delayedSostenutoReleases; + void delaySustainRelease(int noteNumber, float velocity); + void delaySostenutoRelease(int noteNumber, float velocity); + void removeFromSostenutoReleases(int noteNumber); const MidiState& midiState; bool keySwitched { true }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 4e2acebb..a63b4550 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -205,7 +205,12 @@ void Synth::Impl::buildRegion(const std::vector& regionOpcodes) } // Adapt the size of the delayed releases to avoid allocating later on - lastRegion->delayedReleases.reserve(lastRegion->keyRange.length()); + if (lastRegion->trigger == Trigger::release) { + const auto keyLength = static_cast(lastRegion->keyRange.length()); + const auto size = max(config::delayedReleaseVoices, keyLength); + lastRegion->delayedSustainReleases.reserve(size); + lastRegion->delayedSostenutoReleases.reserve(size); + } regions_.push_back(std::move(lastRegion)); } @@ -1147,20 +1152,33 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex region->previousKeySwitched = (*region->previousKeyswitch == noteNumber); } -void Synth::Impl::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +void Synth::Impl::startDelayedSustainReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept { if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) { - region->delayedReleases.clear(); + region->delayedSustainReleases.clear(); return; } - for (auto& note: region->delayedReleases) { + for (auto& note: region->delayedSustainReleases) { const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; startVoice(region, delay, noteOffEvent, ring); } - region->delayedReleases.clear(); + region->delayedSustainReleases.clear(); } +void Synth::Impl::startDelayedSostenutoReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +{ + if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) { + region->delayedSostenutoReleases.clear(); + return; + } + + for (auto& note: region->delayedSostenutoReleases) { + const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; + startVoice(region, delay, noteOffEvent, ring); + } + region->delayedSostenutoReleases.clear(); +} void Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept { @@ -1173,8 +1191,11 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept SisterVoiceRingBuilder ring; const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value }; for (auto& region : ccActivationLists_[ccNumber]) { - if (ccNumber == region->sustainCC) - startDelayedReleaseVoices(region, delay, ring); + if (ccNumber == region->sustainCC && value < region->sustainThreshold) + startDelayedSustainReleases(region, delay, ring); + + if (ccNumber == region->sostenutoCC && value < region->sostenutoThreshold) + startDelayedSostenutoReleases(region, delay, ring); if (region->registerCC(ccNumber, value)) startVoice(region, delay, triggerEvent, ring); diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 502f548b..b40ebcc7 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -155,13 +155,22 @@ struct Synth::Impl final: public Parser::Listener { void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; /** - * @brief Start all delayed release voices of the region if necessary + * @brief Start all delayed sustain release voices of the region if necessary * * @param region * @param delay * @param ring */ - void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + void startDelayedSustainReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + + /** + * @brief Start all delayed sostenuto release voices of the region if necessary + * + * @param region + * @param delay + * @param ring + */ + void startDelayedSostenutoReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; /** * @brief Finalize SFZ loading, following a successful execution of the diff --git a/tests/DirectRegionT.cpp b/tests/DirectRegionT.cpp index 75c66453..5a40b804 100644 --- a/tests/DirectRegionT.cpp +++ b/tests/DirectRegionT.cpp @@ -37,6 +37,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") region.parseOpcode({ "lokey", "63" }); region.parseOpcode({ "hikey", "65" }); region.parseOpcode({ "sample", "*sine" }); + region.delayedSustainReleases.reserve(config::delayedReleaseVoices); SECTION("Release key without sustain") { region.parseOpcode({ "trigger", "release_key" }); @@ -68,11 +69,11 @@ TEST_CASE("[Direct Region Tests] Release and release key") midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) ); - REQUIRE( region.delayedReleases.size() == 1 ); + REQUIRE( region.delayedSustainReleases.size() == 1 ); std::vector> expected = { { 63, 0.5f } }; - REQUIRE( region.delayedReleases == expected ); + REQUIRE( region.delayedSustainReleases == expected ); } SECTION("Release with sustain and 2 notes") @@ -85,12 +86,12 @@ TEST_CASE("[Direct Region Tests] Release and release key") REQUIRE( !region.registerNoteOn(64, 0.6f, 0.0f) ); REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) ); REQUIRE( !region.registerNoteOff(64, 0.2f, 0.0f) ); - REQUIRE( region.delayedReleases.size() == 2 ); + REQUIRE( region.delayedSustainReleases.size() == 2 ); std::vector> expected = { { 63, 0.5f }, { 64, 0.6f } }; - REQUIRE( region.delayedReleases == expected ); + REQUIRE( region.delayedSustainReleases == expected ); } SECTION("Release with sustain and 2 notes but 1 outside") @@ -103,10 +104,10 @@ TEST_CASE("[Direct Region Tests] Release and release key") REQUIRE( !region.registerNoteOn(66, 0.6f, 0.0f) ); REQUIRE( !region.registerNoteOff(63, 0.0f, 0.0f) ); REQUIRE( !region.registerNoteOff(66, 0.2f, 0.0f) ); - REQUIRE( region.delayedReleases.size() == 1 ); + REQUIRE( region.delayedSustainReleases.size() == 1 ); std::vector> expected = { { 63, 0.5f } }; - REQUIRE( region.delayedReleases == expected ); + REQUIRE( region.delayedSustainReleases == expected ); } } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index a1692aaa..eea284f9 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -771,8 +771,6 @@ TEST_CASE("[Synth] Release (pedal was already down)") REQUIRE( synth.getNumActiveVoices() == 2 ); } - - TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches") { sfz::Synth synth; @@ -793,7 +791,7 @@ TEST_CASE("[Synth] Release key (Different sustain CC)") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( - sustain_cc=54 + sustain_cc=54 key=62 sample=*sine trigger=release_key )"); synth.noteOn(0, 62, 85); @@ -806,7 +804,7 @@ TEST_CASE("[Synth] Release (Different sustain CC)") { sfz::Synth synth; synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( - sustain_cc=54 + sustain_cc=54 key=62 sample=*silence key=62 sample=*sine trigger=release )"); @@ -818,6 +816,20 @@ TEST_CASE("[Synth] Release (Different sustain CC)") REQUIRE( synth.getNumActiveVoices() == 2 ); } +TEST_CASE("[Synth] Release (don't check sustain)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + sustain_cc=54 sustain_sw=off + key=62 sample=*silence + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 54, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); +} + TEST_CASE("[Synth] Release key (Different sostenuto CC)") { sfz::Synth synth; @@ -831,6 +843,20 @@ TEST_CASE("[Synth] Release key (Different sostenuto CC)") REQUIRE( synth.getNumActiveVoices() == 1 ); } +TEST_CASE("[Synth] Release (don't check sostenuto)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + sostenuto_cc=54 sostenuto_sw=off + key=62 sample=*silence + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 54, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); +} + TEST_CASE("[Synth] Release (Different sostenuto CC)") { sfz::Synth synth; @@ -838,13 +864,29 @@ TEST_CASE("[Synth] Release (Different sostenuto CC)") sostenuto_cc=54 key=62 sample=*silence key=62 sample=*sine trigger=release + key=64 sample=*silence + key=64 sample=*sine trigger=release )"); - synth.noteOn(0, 62, 85); - synth.cc(1, 54, 127); - synth.noteOff(2, 62, 85); - REQUIRE( synth.getNumActiveVoices() == 1 ); - synth.cc(3, 54, 0); - REQUIRE( synth.getNumActiveVoices() == 2 ); + SECTION("One note with sostenuto") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 54, 127); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(3, 54, 0); + REQUIRE( synth.getNumActiveVoices() == 2 ); + } + SECTION("Two notes, only one with sostenuto") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 54, 127); + synth.noteOn(2, 64, 85); + synth.noteOff(3, 62, 85); + synth.noteOff(3, 64, 85); + REQUIRE( synth.getNumActiveVoices() == 3 ); + synth.cc(4, 54, 0); + REQUIRE( synth.getNumActiveVoices() == 4 ); + } } TEST_CASE("[Synth] Sustain threshold default") @@ -1034,7 +1076,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); - REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); + REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); } TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)") @@ -1064,7 +1106,7 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); - REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); + REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); } TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") @@ -1091,7 +1133,7 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); - REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); + REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); } TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default") @@ -1126,7 +1168,7 @@ TEST_CASE("[Synth] No release sample after the main sample stopped sounding by d synth.cc(0, 64, 0); REQUIRE( synth.getNumActiveVoices() == 0 ); - REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); + REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); } TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died") @@ -1161,7 +1203,7 @@ TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the a synth.cc(0, 64, 0); REQUIRE( synth.getNumActiveVoices() == 0 ); - REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); + REQUIRE( synth.getRegionView(1)->delayedSustainReleases.empty() ); } TEST_CASE("[Synth] sw_default works at a global level") From 65ea4a27bfc5a4fb2166d9d67446df943f2fb408 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Wed, 24 Mar 2021 13:23:51 +0100 Subject: [PATCH 5/6] Working state --- src/sfizz/MidiState.cpp | 3 ++ src/sfizz/MidiState.h | 16 +++++++++ src/sfizz/Region.cpp | 78 ++++++++++++++++++++++++++++------------- src/sfizz/Region.h | 11 ++++-- src/sfizz/Synth.cpp | 18 +++++++--- src/sfizz/Voice.cpp | 6 ++-- tests/DirectRegionT.cpp | 6 ++++ tests/SynthT.cpp | 35 ++++++++++++++++++ 8 files changed, 139 insertions(+), 34 deletions(-) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 938fba1e..c8df355a 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -23,6 +23,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex noteOnTimes[noteNumber] = internalClock + static_cast(delay); lastNotePlayed = noteNumber; activeNotes++; + noteStates[noteNumber] = true; } } @@ -37,6 +38,7 @@ void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noe noteOffTimes[noteNumber] = internalClock + static_cast(delay); if (activeNotes > 0) activeNotes--; + noteStates[noteNumber] = false; } } @@ -181,6 +183,7 @@ void sfz::MidiState::reset() noexcept activeNotes = 0; internalClock = 0; lastNotePlayed = 0; + noteStates.reset(); absl::c_fill(noteOnTimes, 0); absl::c_fill(noteOffTimes, 0); } diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index f9766c81..bb42f09a 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -6,6 +6,7 @@ #pragma once #include +#include #include "CCMap.h" #include "Range.h" @@ -142,6 +143,15 @@ public: */ void flushEvents() noexcept; + /** + * @brief Check if a note is currently depressed + * + * @param noteNumber + * @return true + * @return false + */ + bool isNotePressed(int noteNumber) const noexcept { return noteStates[noteNumber]; } + /** * @brief Get the CC value for CC number * @@ -191,6 +201,12 @@ private: MidiNoteArray noteOffTimes { {} }; + /** + * @brief Store the note states + * + */ + std::bitset<128> noteStates; + /** * @brief Stores the velocity of the note ons for currently * depressed notes. diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index eedc64d2..733474f4 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1504,7 +1504,7 @@ bool sfz::Region::isSwitchedOn() const noexcept return keySwitched && previousKeySwitched && sequenceSwitched && pitchSwitched && bpmSwitched && aftertouchSwitched && ccSwitched.all(); } -void sfz::Region::delaySustainRelease(int noteNumber, float velocity) +void sfz::Region::delaySustainRelease(int noteNumber, float velocity) noexcept { if (delayedSustainReleases.size() == delayedSustainReleases.capacity()) return; @@ -1512,7 +1512,7 @@ void sfz::Region::delaySustainRelease(int noteNumber, float velocity) delayedSustainReleases.emplace_back(noteNumber, velocity); } -void sfz::Region::delaySostenutoRelease(int noteNumber, float velocity) +void sfz::Region::delaySostenutoRelease(int noteNumber, float velocity) noexcept { if (delayedSostenutoReleases.size() == delayedSostenutoReleases.capacity()) return; @@ -1520,13 +1520,37 @@ void sfz::Region::delaySostenutoRelease(int noteNumber, float velocity) delayedSostenutoReleases.emplace_back(noteNumber, velocity); } -void sfz::Region::removeFromSostenutoReleases(int noteNumber) +void sfz::Region::removeFromSostenutoReleases(int noteNumber) noexcept { swapAndPopFirst(delayedSostenutoReleases, [=](const std::pair& p) { return p.first == noteNumber; }); } +void sfz::Region::storeSostenutoNotes() noexcept +{ + ASSERT(delayedSostenutoReleases.empty()); + for (int note = keyRange.getStart(); note <= keyRange.getEnd(); ++note) { + if (midiState.isNotePressed(note)) + delaySostenutoRelease(note, midiState.getNoteVelocity(note)); + } +} + + +bool sfz::Region::isNoteSustained(int noteNumber) const noexcept +{ + return absl::c_find_if(delayedSustainReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }) != delayedSustainReleases.end(); +} + +bool sfz::Region::isNoteSostenutoed(int noteNumber) const noexcept +{ + return absl::c_find_if(delayedSostenutoReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }) != delayedSostenutoReleases.end(); +} + bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); @@ -1553,13 +1577,6 @@ bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue const bool attackTrigger = (trigger == Trigger::attack); const bool notFirstLegatoNote = (trigger == Trigger::legato && midiState.getActiveNotes() > 1); - if (trigger == Trigger::release && - keyOk && velOk - && checkSostenuto && midiState.getCCValue(sostenutoCC) < sostenutoThreshold) { - // This note on will possibly be "sostenutoed" - delaySostenutoRelease(noteNumber, velocity); - } - return keyOk && velOk && randOk && (attackTrigger || firstLegatoNote || notFirstLegatoNote); } @@ -1588,22 +1605,21 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu return true; if (trigger == Trigger::release) { - if (checkSostenuto && midiState.getCCValue(sostenutoCC) < sostenutoThreshold) + const bool sostenutoed = isNoteSostenutoed(noteNumber); + + if (sostenutoed && !sostenutoPressed) { removeFromSostenutoReleases(noteNumber); + if (sustainPressed) + delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); + } - const bool shouldSustain = checkSustain && midiState.getCCValue(sustainCC) >= sustainThreshold; - const bool shouldSostenuto = - checkSostenuto && midiState.getCCValue(sostenutoCC) >= sostenutoThreshold - && absl::c_find_if(delayedSostenutoReleases, [=](const std::pair& p) { - return p.first == noteNumber; - }) != delayedSostenutoReleases.end(); - - if (!shouldSustain && !shouldSostenuto) - return true; - - // If we reach this part, we're storing the notes to delay their release on CC up - // This is handled by the Synth object - delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); + if (sustainPressed) { + if (!sostenutoPressed || !sostenutoed) + delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); + } else { + if (!sostenutoPressed || !sostenutoed) + return true; + } } return false; @@ -1613,6 +1629,20 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept { ASSERT(ccValue >= 0.0f && ccValue <= 1.0f); + if (ccNumber == sustainCC) + sustainPressed = checkSustain && ccValue >= sustainThreshold; + + if (ccNumber == sostenutoCC) { + const bool newState = checkSostenuto && ccValue >= sostenutoThreshold; + if (!sostenutoPressed && newState) + storeSostenutoNotes(); + + if (!newState && sostenutoPressed) + delayedSostenutoReleases.clear(); + + sostenutoPressed = newState; + } + if (ccConditions.getWithDefault(ccNumber).containsWithEnd(ccValue)) ccSwitched.set(ccNumber, true); else diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 74df3291..aa089908 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -508,11 +508,16 @@ struct Region { RegionSet* parent { nullptr }; // Started notes + bool sustainPressed { false }; + bool sostenutoPressed { false }; std::vector> delayedSustainReleases; std::vector> delayedSostenutoReleases; - void delaySustainRelease(int noteNumber, float velocity); - void delaySostenutoRelease(int noteNumber, float velocity); - void removeFromSostenutoReleases(int noteNumber); + void delaySustainRelease(int noteNumber, float velocity) noexcept; + void delaySostenutoRelease(int noteNumber, float velocity) noexcept; + void storeSostenutoNotes() noexcept; + void removeFromSostenutoReleases(int noteNumber) noexcept; + bool isNoteSustained(int noteNumber) const noexcept; + bool isNoteSostenutoed(int noteNumber) const noexcept; const MidiState& midiState; bool keySwitched { true }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index a63b4550..b0f6fc71 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -659,7 +659,8 @@ void Synth::Impl::finalizeSfzLoad() for (int cc = 0; cc < config::numCCs; cc++) { if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc) - || (cc == region->sustainCC && region->trigger == Trigger::release)) + || (cc == region->sustainCC && region->trigger == Trigger::release) + || (cc == region->sostenutoCC && region->trigger == Trigger::release)) ccActivationLists_[cc].push_back(region); } @@ -1163,6 +1164,7 @@ void Synth::Impl::startDelayedSustainReleases(Region* region, int delay, SisterV const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; startVoice(region, delay, noteOffEvent, ring); } + region->delayedSustainReleases.clear(); } @@ -1191,11 +1193,19 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept SisterVoiceRingBuilder ring; const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value }; for (auto& region : ccActivationLists_[ccNumber]) { - if (ccNumber == region->sustainCC && value < region->sustainThreshold) + if (region->checkSustain && ccNumber == region->sustainCC && value < region->sustainThreshold) startDelayedSustainReleases(region, delay, ring); - if (ccNumber == region->sostenutoCC && value < region->sostenutoThreshold) - startDelayedSostenutoReleases(region, delay, ring); + if (region->checkSostenuto && ccNumber == region->sostenutoCC && value < region->sostenutoThreshold) { + if (region->sustainPressed) { + for (const auto& v: region->delayedSostenutoReleases) + region->delaySustainRelease(v.first, v.second); + + region->delayedSostenutoReleases.clear(); + } else { + startDelayedSostenutoReleases(region, delay, ring); + } + } if (region->registerCC(ccNumber, value)) startVoice(region, delay, triggerEvent, ring); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 47cfec83..92294130 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -599,12 +599,12 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept } const bool sustainPedalReleaseCondition = !impl.region_->checkSustain - || (impl.noteIsOff_ && (impl.sustainState_ != Impl::SustainState::Sustaining)); + || (impl.sustainState_ != Impl::SustainState::Sustaining); const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto - || (impl.noteIsOff_ && (impl.sostenutoState_ != Impl::SostenutoState::Sustaining)); + || (impl.sostenutoState_ != Impl::SostenutoState::Sustaining); - if (sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) + if (impl.noteIsOff_ && sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) release(delay); } diff --git a/tests/DirectRegionT.cpp b/tests/DirectRegionT.cpp index 5a40b804..8f5bf6a6 100644 --- a/tests/DirectRegionT.cpp +++ b/tests/DirectRegionT.cpp @@ -42,6 +42,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release_key" }); midiState.ccEvent(0, 64, 0.0f); + region.registerCC(64, 0.0f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); } @@ -49,6 +50,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release_key" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(64, 1.0f); REQUIRE( !region.registerCC(64, 1.0f) ); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); @@ -58,6 +60,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 0.0f); + region.registerCC(64, 0.0f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); } @@ -66,6 +69,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(64, 1.0f); midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( !region.registerNoteOff(63, 0.5f, 0.0f) ); @@ -80,6 +84,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(64, 1.0f); midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); midiState.noteOnEvent(0, 64, 0.6f); @@ -98,6 +103,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(64, 1.0f); midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); midiState.noteOnEvent(0, 66, 0.6f); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index eea284f9..cb97a0f7 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -889,6 +889,41 @@ TEST_CASE("[Synth] Release (Different sostenuto CC)") } } +TEST_CASE("[Synth] Release (sustain + sostenuto)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + key=62 sample=*silence + key=62 sample=*sine trigger=release + key=64 sample=*silence + key=64 sample=*sine trigger=release + )"); + SECTION("Sustain up first") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 127); + synth.cc(1, 64, 127); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(3, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(4, 66, 0); + REQUIRE( synth.getNumActiveVoices() == 2 ); + } + SECTION("Sostenuto up first") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 127); + synth.cc(1, 64, 127); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(3, 66, 0); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(4, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 2 ); + } +} + TEST_CASE("[Synth] Sustain threshold default") { sfz::Synth synth; From 2bbd2011d2126c06a19dbac4247a84e7e19aac9b Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Wed, 24 Mar 2021 13:30:58 +0100 Subject: [PATCH 6/6] Minor changes --- src/sfizz/Region.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 733474f4..e68fa48c 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -1613,11 +1613,10 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); } - if (sustainPressed) { - if (!sostenutoPressed || !sostenutoed) + if (!sostenutoPressed || !sostenutoed) { + if (sustainPressed) delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); - } else { - if (!sostenutoPressed || !sostenutoed) + else return true; } }