diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 918264ca..c6e2e843 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -44,7 +44,6 @@ namespace config { constexpr uint8_t gainSmoothing { 5 }; 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 f5edae7f..1bcc7771 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 287fc7ad..12f2ae33 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -273,6 +273,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; @@ -984,7 +987,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; } @@ -999,13 +1008,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 de80f53e..3e5c4dad 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -301,6 +301,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 @@ -373,6 +374,7 @@ private: bool pitchSwitched { true }; bool bpmSwitched { true }; bool aftertouchSwitched { true }; + bool noteIsOff { false }; std::bitset ccSwitched; bool triggerOnCC { false }; absl::string_view defaultPath { "" }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 66a46d99..1b10fb3b 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -198,7 +198,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); } } @@ -212,7 +212,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 e4a9c09d..5d508ca7 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1171,6 +1171,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()); @@ -1685,3 +1694,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) ); + } +}