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")