diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 8be2fc96..cc0ac0b3 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -143,6 +143,7 @@ namespace Default constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power }; constexpr float rtDecay { 0.0f }; + constexpr bool rtDead { false }; constexpr Range rtDecayRange { 0.0f, 200.0f }; // Performance parameters: Filters diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 25d38f40..0c1a228f 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -112,7 +112,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) loopMode = SfzLoopMode::loop_sustain; break; default: - DBG("Unkown loop mode:" << std::string(opcode.value)); + DBG("Unkown loop mode:" << opcode.value); } break; case hash("loop_end"): // also loopend @@ -162,7 +162,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) offMode = SfzOffMode::normal; break; default: - DBG("Unkown off mode:" << std::string(opcode.value)); + DBG("Unkown off mode:" << opcode.value); } break; case hash("polyphony"): @@ -182,7 +182,16 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) selfMask = SfzSelfMask::dontMask; break; default: - DBG("Unkown self mask value:" << std::string(opcode.value)); + DBG("Unkown self mask value:" << opcode.value); + } + break; + case hash("rt_dead"): + if (opcode.value == "on") { + rtDead = true; + } else if (opcode.value == "off") { + rtDead = false; + } else { + DBG("Unkown rt_dead value:" << opcode.value); } break; // Region logic: key mapping @@ -275,7 +284,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) velocityOverride = SfzVelocityOverride::previous; break; default: - DBG("Unknown velocity mode: " << std::string(opcode.value)); + DBG("Unknown velocity mode: " << opcode.value); } break; @@ -338,7 +347,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) trigger = SfzTrigger::release_key; break; default: - DBG("Unknown trigger mode: " << std::string(opcode.value)); + DBG("Unknown trigger mode: " << opcode.value); } break; case hash("start_locc&"): // also on_locc& @@ -469,7 +478,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) crossfadeKeyCurve = SfzCrossfadeCurve::gain; break; default: - DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + DBG("Unknown crossfade power curve: " << opcode.value); } break; case hash("xf_velcurve"): @@ -481,7 +490,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) crossfadeVelCurve = SfzCrossfadeCurve::gain; break; default: - DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + DBG("Unknown crossfade power curve: " << opcode.value); } break; case hash("xfin_locc&"): @@ -517,7 +526,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) crossfadeCCCurve = SfzCrossfadeCurve::gain; break; default: - DBG("Unknown crossfade power curve: " << std::string(opcode.value)); + DBG("Unknown crossfade power curve: " << opcode.value); } break; case hash("rt_decay"): @@ -637,7 +646,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) filters[filterIndex].type = *ftype; else { filters[filterIndex].type = FilterType::kFilterNone; - DBG("Unknown filter type: " << std::string(opcode.value)); + DBG("Unknown filter type: " << opcode.value); } } break; @@ -746,7 +755,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) equalizers[eqNumber - 1].type = *ftype; else { equalizers[eqNumber - 1].type = EqType::kEqNone; - DBG("Unknown EQ type: " << std::string(opcode.value)); + DBG("Unknown EQ type: " << opcode.value); } } break; @@ -1054,24 +1063,37 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu keySwitched = true; } - const bool keyOk = keyRange.containsWithEnd(noteNumber); - if (!isSwitchedOn()) return false; if (!triggerOnNote) return false; + // Prerequisites + + const bool keyOk = keyRange.containsWithEnd(noteNumber); const bool velOk = velocityRange.containsWithEnd(velocity); const bool randOk = randRange.contains(randValue); - bool releaseTrigger = (trigger == SfzTrigger::release_key); + + if (!(velOk && keyOk && randOk)) + return false; + + // Release logic + + if (trigger == SfzTrigger::release_key) + return true; + if (trigger == SfzTrigger::release) { if (midiState.getCCValue(sustainCC) < sustainThreshold) - releaseTrigger = true; - else - noteIsOff = true; + 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)); } - return keyOk && velOk && randOk && releaseTrigger; + + return false; } bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept @@ -1085,11 +1107,6 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept if (!isSwitchedOn()) return false; - if (sustainCC == ccNumber && ccValue < sustainThreshold && noteIsOff) { - noteIsOff = false; - return true; - } - if (!triggerOnCC) return false; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index f226a331..76113910 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -289,6 +289,7 @@ struct Region { absl::optional notePolyphony {}; // note_polyphony unsigned polyphony { config::maxVoices }; // polyphony SfzSelfMask selfMask { Default::selfMask }; + bool rtDead { Default::rtDead }; // Region logic: key mapping Range keyRange { Default::keyRange }; //lokey, hikey and key @@ -382,6 +383,9 @@ struct Region { // Parent RegionSet* parent { nullptr }; + + // Started notes + std::vector> delayedReleases; private: const MidiState& midiState; bool keySwitched { true }; @@ -390,7 +394,6 @@ private: bool pitchSwitched { true }; bool bpmSwitched { true }; bool aftertouchSwitched { true }; - bool noteIsOff { false }; std::bitset ccSwitched; absl::string_view defaultPath { "" }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 71f9f500..606d3497 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -168,6 +168,9 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) lastRegion->parent = currentSet; currentSet->addRegion(lastRegion.get()); + // Adapt the size of the delayed releases to avoid allocating later on + lastRegion->delayedReleases.reserve(lastRegion->keyRange.length()); + regions.push_back(std::move(lastRegion)); } @@ -849,6 +852,15 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept noteOffDispatch(delay, noteNumber, replacedVelocity); } +bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) { + return ( + !voice.isFree() + && voice.getTriggerType() == sfz::Voice::TriggerType::NoteOn + && region.keyRange.containsWithEnd(voice.getTriggerNumber()) + && region.velocityRange.containsWithEnd(voice.getTriggerValue()) + ); +} + void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -856,6 +868,20 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { + if (region->triggerOnNote && region->trigger == SfzTrigger::release && !region->rtDead) { + // check that a voice with compatible trigger is playing + // FIXME: we're going twice over the voices, when the synth + // handles the regions completely these dispatch functions + // should be overhauled, also to include voice stealing on + // all events + const auto compatibleVoice = [region](const VoicePtr& v) -> bool { + return matchReleaseRegionAndVoice(*region, *v); + }; + + if (absl::c_find_if(voices, compatibleVoice) == voices.end()) + continue; + } + auto voice = findFreeVoice(); if (voice == nullptr) continue; @@ -1015,18 +1041,44 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept SisterVoiceRingBuilder ring; for (auto& region : ccActivationLists[ccNumber]) { - if (region->registerCC(ccNumber, normValue)) { + if (ccNumber == region->sustainCC) { + if (!region->rtDead) { + // check that a voice with compatible trigger is playing + // FIXME: we're going twice over the voices, when the synth + // handles the regions completely these dispatch functions + // should be overhauled, also to include voice stealing on + // all events + const auto compatibleVoice = [region](const VoicePtr& v) -> bool { + return matchReleaseRegionAndVoice(*region, *v); + }; + + if (absl::c_find_if(voices, compatibleVoice) == voices.end()) { + region->delayedReleases.clear(); + continue; + } + } + + for (auto& note: region->delayedReleases) { + // FIXME: we really need to have some form of common method to find and start voices... + auto voice = findFreeVoice(); + if (voice == nullptr) + continue; + + voice->startVoice(region, delay, note.first, note.second, Voice::TriggerType::NoteOff); + + ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); + } + + region->delayedReleases.clear(); + } else if (region->registerCC(ccNumber, normValue)) { auto voice = findFreeVoice(); if (voice == nullptr) continue; - if (!region->triggerOnCC) { - // This is a sustain trigger - const auto replacedVelocity = resources.midiState.getNoteVelocity(region->pitchKeycenter); - voice->startVoice(region, delay, region->pitchKeycenter, replacedVelocity, Voice::TriggerType::NoteOff); - } else { - voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); - } + + voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); ring.addVoiceToRing(voice); RegionSet::registerVoiceInHierarchy(region, voice); diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 472ab8fc..01469495 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1635,6 +1635,18 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.selfMask == SfzSelfMask::dontMask); } + SECTION("Release dead") + { + REQUIRE(region.rtDead == false); + region.parseOpcode({ "rt_dead", "on" }); + REQUIRE(region.rtDead == true); + region.parseOpcode({ "rt_dead", "off" }); + REQUIRE(region.rtDead == false); + region.parseOpcode({ "rt_dead", "on" }); + region.parseOpcode({ "rt_dead", "garbage" }); + REQUIRE(region.rtDead == true); + } + SECTION("amplitude") { REQUIRE(region.amplitude == 1.0_a); @@ -1754,7 +1766,8 @@ TEST_CASE("[Region] Release and release key") { MidiState midiState; Region region { 0, midiState }; - region.parseOpcode({ "key", "63" }); + region.parseOpcode({ "lokey", "63" }); + region.parseOpcode({ "hikey", "65" }); region.parseOpcode({ "sample", "*sine" }); SECTION("Release key without sustain") { @@ -1770,9 +1783,8 @@ TEST_CASE("[Region] Release and release key") REQUIRE( !region.registerCC(64, 1.0f) ); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); - midiState.ccEvent(0, 64, 0.0f); - REQUIRE( !region.registerCC(64, 0.0f) ); } + SECTION("Release without sustain") { region.parseOpcode({ "trigger", "release" }); @@ -1780,20 +1792,53 @@ TEST_CASE("[Region] Release and release key") REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); } + SECTION("Release with sustain") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 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) ); + REQUIRE( region.delayedReleases.size() == 1 ); + std::vector> expected = { + { 63, 0.5f } + }; + REQUIRE( region.delayedReleases == expected ); } - SECTION("Release with sustain") + + SECTION("Release with sustain and 2 notes") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 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) ); - midiState.ccEvent(0, 64, 0.0f); - REQUIRE( region.registerCC(64, 0.0f) ); + midiState.noteOnEvent(0, 64, 0.6f); + 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 ); + std::vector> expected = { + { 63, 0.5f }, + { 64, 0.6f } + }; + REQUIRE( region.delayedReleases == expected ); + } + + SECTION("Release with sustain and 2 notes but 1 outside") + { + region.parseOpcode({ "trigger", "release" }); + midiState.ccEvent(0, 64, 1.0f); + midiState.noteOnEvent(0, 63, 0.5f); + REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); + midiState.noteOnEvent(0, 66, 0.6f); + 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 ); + std::vector> expected = { + { 63, 0.5f } + }; + REQUIRE( region.delayedReleases == expected ); } } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index ccd92b94..a71cb91d 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -8,6 +8,7 @@ #include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" #include "sfizz/NumericId.h" +#include #include "catch2/catch.hpp" using namespace Catch::literals; using namespace sfz::literals; @@ -200,6 +201,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t synth.setNumVoices(1); synth.loadSfzString(fs::current_path() / "tests/TestFiles/envelope_trigger_release.sfz", R"( lovel=0 hivel=127 + sample=*silence trigger=release sample=*noise loop_mode=one_shot ampeg_attack=0.02 ampeg_decay=0.02 ampeg_release=0.1 ampeg_sustain=0 )"); @@ -626,14 +628,48 @@ TEST_CASE("[Synth] Release") { sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*silence key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); synth.cc(0, 64, 127); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 0 ); - synth.cc(0, 64, 0); REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); +} + +TEST_CASE("[Synth] Release (pedal was already down)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*silence + key=62 sample=*sine trigger=release + )"); + synth.cc(0, 64, 127); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); +} + + + +TEST_CASE("[Synth] Release samples don't play unless there is another playing region that matches") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.cc(0, 64, 127); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 0); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); } TEST_CASE("[Synth] Release key (Different sustain CC)") @@ -654,14 +690,15 @@ TEST_CASE("[Synth] Release (Different sustain CC)") sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( sustain_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(true) == 0 ); - synth.cc(0, 54, 0); REQUIRE( synth.getNumActiveVoices(true) == 1 ); + synth.cc(0, 54, 0); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); } TEST_CASE("[Synth] Sustain threshold default") @@ -681,17 +718,224 @@ TEST_CASE("[Synth] Sustain threshold") sfz::Synth synth; synth.loadSfzString(fs::current_path(), R"( sustain_lo=63 + key=62 sample=*silence key=62 sample=*sine trigger=release )"); synth.noteOn(0, 62, 85); synth.cc(0, 64, 1); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 1 ); - synth.noteOn(0, 62, 85); - synth.noteOff(0, 62, 85); REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 4 ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 5 ); synth.cc(0, 64, 64); synth.noteOff(0, 62, 85); - REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( synth.getNumActiveVoices(true) == 5 ); +} + +template +void sortAll(C& container) +{ + std::sort(container.begin(), container.end()); +} + +template +void sortAll(C& container, Args&... others) +{ + std::sort(container.begin(), container.end()); + sortAll(others...); +} + +const std::vector getActiveVoices(const sfz::Synth& synth) +{ + std::vector activeVoices; + for (int i = 0; i < synth.getNumVoices(); ++i) { + const auto* voice = synth.getVoiceView(i); + if (!voice->isFree()) + activeVoices.push_back(voice); + } + return activeVoices; +} + +TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*sine trigger=release_key + )"); + synth.noteOn(0, 62, 85); + synth.noteOn(0, 63, 78); + synth.noteOn(0, 64, 34); + synth.cc(0, 64, 127); + synth.noteOff(0, 64, 0); + synth.noteOff(0, 63, 2); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm}; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); +} + +TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices after)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*silence + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + synth.noteOn(0, 63, 78); + synth.noteOn(0, 64, 34); + synth.cc(0, 64, 127); + synth.noteOff(0, 64, 0); + synth.noteOff(0, 63, 2); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared the delayed voices after)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*silence + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.cc(0, 64, 127); + synth.noteOn(1, 62, 85); + synth.noteOn(1, 63, 78); + synth.noteOn(1, 64, 34); + synth.noteOff(2, 64, 0); + synth.noteOff(2, 63, 2); + synth.noteOff(2, 62, 3); + REQUIRE( synth.getNumActiveVoices(true) == 3 ); + synth.cc(3, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + + std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=*silence + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + synth.noteOff(0, 62, 0); + synth.noteOn(0, 62, 78); + synth.noteOff(0, 62, 2); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 4 ); + + std::vector requiredVelocities { 78_norm, 85_norm, 78_norm, 85_norm }; + std::vector actualVelocities; + for (auto* v: getActiveVoices(synth)) { + actualVelocities.push_back(v->getTriggerValue()); + } + sortAll(requiredVelocities, actualVelocities); + REQUIRE( requiredVelocities == actualVelocities ); + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] No release sample after the main sample stopped sounding by default") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(4096); + sfz::AudioBuffer buffer { 2, 4096 }; + + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); +} + +TEST_CASE("[Synth] If rt_dead is active the release sample can sound after the attack sample died") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(4096); + sfz::AudioBuffer buffer { 2, 4096 }; + + synth.loadSfzString(fs::current_path(), R"( + lokey=62 hikey=64 sample=tests/TestFiles/closedhat.wav loop_mode=one_shot + lokey=62 hikey=64 sample=*sine trigger=release + loopmode=one_shot ampeg_attack=0.02 ampeg_release=0.1 + )"); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + REQUIRE( synth.getNumActiveVoices(true) == 1 ); + for (unsigned i = 0; i < 100; ++i) { + synth.renderBlock(buffer); + } + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.noteOff(0, 62, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 0 ); + + REQUIRE( synth.getRegionView(1)->delayedReleases.empty() ); }