diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index ad5d0b87a..db286b018 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -45,7 +45,6 @@ namespace config { constexpr uint8_t gainSmoothing { 0 }; constexpr unsigned powerTableSizeExponent { 11 }; constexpr int maxFilePromises { maxVoices }; - constexpr int sustainCC { 64 }; constexpr int allSoundOffCC { 120 }; constexpr int resetCC { 121 }; constexpr int allNotesOffCC { 123 }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 0846c3707..940b6872e 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -215,6 +215,7 @@ namespace Default constexpr float vel2release { 0.0f }; constexpr float start { 0.0 }; constexpr float sustain { 100.0 }; + constexpr uint16_t sustainCC { 64 }; constexpr float vel2sustain { 0.0 }; constexpr int depth { 0 }; constexpr Range egTimeRange { 0.0, 100.0 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 205ed9f41..1f24931c6 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -278,6 +278,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) } break; + case hash("sustain_cc"): + setValueFromOpcode(opcode, sustainCC, Default::ccNumberRange); + break; case hash("sustain_sw"): checkSustain = readBooleanFromOpcode(opcode).value_or(Default::checkSustain); break; @@ -1033,7 +1036,13 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu const bool velOk = velocityRange.containsWithEnd(velocity); const bool randOk = randRange.contains(randValue); - const bool releaseTrigger = (trigger == SfzTrigger::release || trigger == SfzTrigger::release_key); + bool releaseTrigger = (trigger == SfzTrigger::release_key); + if (trigger == SfzTrigger::release) { + if (midiState.getCCValue(sustainCC) < config::halfCCThreshold) + releaseTrigger = true; + else + noteIsOff = true; + } return keyOk && velOk && randOk && releaseTrigger; } @@ -1048,13 +1057,18 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept if (!isSwitchedOn()) return false; + if (sustainCC == ccNumber && ccValue < config::halfCCThreshold && noteIsOff) { + noteIsOff = false; + return true; + } + if (!triggerOnCC) return false; if (ccTriggers.contains(ccNumber) && ccTriggers[ccNumber].containsWithEnd(ccValue)) return true; - else - return false; + + return false; } void sfz::Region::registerPitchWheel(float pitch) noexcept diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index ea8ebcdba..322f96ec5 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -305,6 +305,7 @@ struct Region { SfzVelocityOverride velocityOverride { Default::velocityOverride }; // sw_vel bool checkSustain { Default::checkSustain }; // sustain_sw bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw + uint16_t sustainCC { Default::sustainCC }; // sustain_cc // Region logic: internal conditions Range aftertouchRange { Default::aftertouchRange }; // hichanaft and lochanaft @@ -369,6 +370,9 @@ struct Region { // Modifiers ModifierArray> modifiers; + bool triggerOnCC { false }; // whether the region triggers on CC events or note events + bool triggerOnNote { true }; + // Parent RegionSet* parent { nullptr }; private: @@ -379,9 +383,8 @@ struct Region { bool pitchSwitched { true }; bool bpmSwitched { true }; bool aftertouchSwitched { true }; + bool noteIsOff { false }; std::bitset ccSwitched; - bool triggerOnCC { false }; - bool triggerOnNote { true }; absl::string_view defaultPath { "" }; int sequenceCounter { 0 }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 027936929..c2c7779c1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -518,7 +518,9 @@ void sfz::Synth::finalizeSfzLoad() } for (int cc = 0; cc < config::numCCs; cc++) { - if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc)) + if (region->ccTriggers.contains(cc) + || region->ccConditions.contains(cc) + || (cc == region->sustainCC && region->trigger == SfzTrigger::release)) ccActivationLists[cc].push_back(region); } @@ -991,7 +993,14 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept if (voice == nullptr) continue; - voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); + 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); + } + ring.addVoiceToRing(voice); RegionSet::registerVoiceInHierarchy(region, voice); polyphonyGroups[region->group].registerVoice(voice); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7709e360a..adfaa718e 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -206,7 +206,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (region->loopMode == SfzLoopMode::one_shot) return; - if (!region->checkSustain || resources.midiState.getCCValue(config::sustainCC) < config::halfCCThreshold) + if (!region->checkSustain || resources.midiState.getCCValue(region->sustainCC) < config::halfCCThreshold) release(delay); } } @@ -220,7 +220,7 @@ void sfz::Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept if (state != State::playing) return; - if (region->checkSustain && noteIsOff && ccNumber == config::sustainCC && ccValue < config::halfCCThreshold) + if (region->checkSustain && noteIsOff && ccNumber == region->sustainCC && ccValue < config::halfCCThreshold) release(delay); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 459daaab5..bce1012aa 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1207,6 +1207,15 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.checkSostenuto); } + SECTION("sustain_cc") + { + REQUIRE(region.sustainCC == 64); + region.parseOpcode({ "sustain_cc", "63" }); + REQUIRE(region.sustainCC == 63); + region.parseOpcode({ "sustain_cc", "-1" }); + REQUIRE(region.sustainCC == 0); + } + SECTION("Filter stacking and cutoffs") { REQUIRE(region.filters.empty()); @@ -1721,3 +1730,52 @@ TEST_CASE("[Region] Non-conforming floating point values in integer opcodes") region.parseOpcode({ "pitch_keytrack", "-2.1" }); REQUIRE(region.pitchKeytrack == -2); } + + +TEST_CASE("[Region] Release and release key") +{ + MidiState midiState; + Region region { 0, midiState }; + region.parseOpcode({ "key", "63" }); + region.parseOpcode({ "sample", "*sine" }); + SECTION("Release key without sustain") + { + region.parseOpcode({ "trigger", "release_key" }); + midiState.ccEvent(0, 64, 0.0f); + REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); + REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); + } + SECTION("Release key with sustain") + { + region.parseOpcode({ "trigger", "release_key" }); + midiState.ccEvent(0, 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) ); + midiState.ccEvent(0, 64, 0.0f); + REQUIRE( !region.registerCC(64, 0.0f) ); + } + SECTION("Release without sustain") + { + region.parseOpcode({ "trigger", "release" }); + midiState.ccEvent(0, 64, 0.0f); + 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); + 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); + 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) ); + } +} diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index ecc002483..c0d761c93 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -609,3 +609,57 @@ TEST_CASE("[Synth] Sisters and off-by") REQUIRE( synth.getNumActiveVoices() == 2 ); REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); } + +TEST_CASE("[Synth] Release key") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine trigger=release_key + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); +} + +TEST_CASE("[Synth] Release") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); + synth.cc(0, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 1 ); +} + +TEST_CASE("[Synth] Release key (Different sustain CC)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sustain_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 sustain CC)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sustain_cc=54 + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 54, 127); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); + synth.cc(0, 54, 0); + REQUIRE( synth.getNumActiveVoices() == 1 ); +}