diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index db286b01..60c2f0a2 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -50,7 +50,6 @@ namespace config { constexpr int allNotesOffCC { 123 }; constexpr int omniOffCC { 124 }; constexpr int omniOnCC { 125 }; - constexpr float halfCCThreshold { 0.5f }; constexpr int centPerSemitone { 100 }; constexpr float virtuallyZero { 0.001f }; constexpr float fastReleaseDuration { 0.01f }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 940b6872..8be2fc96 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -57,6 +57,7 @@ namespace Default // common defaults constexpr Range midi7Range { 0, 127 }; + constexpr Range float7Range { 0.0f, 127.0f }; constexpr Range normalizedRange { 0.0f, 1.0f }; constexpr Range symmetricNormalizedRange { -1.0, 1.0 }; @@ -216,6 +217,7 @@ namespace Default constexpr float start { 0.0 }; constexpr float sustain { 100.0 }; constexpr uint16_t sustainCC { 64 }; + constexpr float sustainThreshold { 0.0039f }; // sforzando default (0.5f/127.0f) 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 1f24931c..0a19625c 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -281,6 +281,11 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("sustain_cc"): setValueFromOpcode(opcode, sustainCC, Default::ccNumberRange); break; + case hash("sustain_lo"): + if (auto value = readOpcode(opcode.value, Default::float7Range)) { + sustainThreshold = normalizeCC(*value); + } + break; case hash("sustain_sw"): checkSustain = readBooleanFromOpcode(opcode).value_or(Default::checkSustain); break; @@ -1038,7 +1043,7 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu const bool randOk = randRange.contains(randValue); bool releaseTrigger = (trigger == SfzTrigger::release_key); if (trigger == SfzTrigger::release) { - if (midiState.getCCValue(sustainCC) < config::halfCCThreshold) + if (midiState.getCCValue(sustainCC) < sustainThreshold) releaseTrigger = true; else noteIsOff = true; @@ -1057,7 +1062,7 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept if (!isSwitchedOn()) return false; - if (sustainCC == ccNumber && ccValue < config::halfCCThreshold && noteIsOff) { + if (sustainCC == ccNumber && ccValue < sustainThreshold && noteIsOff) { noteIsOff = false; return true; } diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 322f96ec..772ee399 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -306,6 +306,7 @@ struct Region { bool checkSustain { Default::checkSustain }; // sustain_sw bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw uint16_t sustainCC { Default::sustainCC }; // sustain_cc + float sustainThreshold { Default::sustainThreshold }; // sustain_cc // Region logic: internal conditions Range aftertouchRange { Default::aftertouchRange }; // hichanaft and lochanaft @@ -372,7 +373,7 @@ struct Region { bool triggerOnCC { false }; // whether the region triggers on CC events or note events bool triggerOnNote { true }; - + // Parent RegionSet* parent { nullptr }; private: diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index adfaa718..6dff257b 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(region->sustainCC) < config::halfCCThreshold) + if (!region->checkSustain || resources.midiState.getCCValue(region->sustainCC) < region->sustainThreshold) 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 == region->sustainCC && ccValue < config::halfCCThreshold) + if (region->checkSustain && noteIsOff && ccNumber == region->sustainCC && ccValue < region->sustainThreshold) release(delay); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index bce1012a..ecfea499 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1216,6 +1216,19 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.sustainCC == 0); } + SECTION("sustain_lo") + { + REQUIRE(region.sustainThreshold == Approx(0.5_norm).margin(1e-3)); + region.parseOpcode({ "sustain_lo", "-1" }); + REQUIRE(region.sustainThreshold == 0_norm); + region.parseOpcode({ "sustain_lo", "1" }); + REQUIRE(region.sustainThreshold == 1_norm); + region.parseOpcode({ "sustain_lo", "63" }); + REQUIRE(region.sustainThreshold == 63_norm); + region.parseOpcode({ "sustain_lo", "128" }); + REQUIRE(region.sustainThreshold == 127_norm); + } + SECTION("Filter stacking and cutoffs") { REQUIRE(region.filters.empty()); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index c0d761c9..bd6b847b 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -663,3 +663,35 @@ TEST_CASE("[Synth] Release (Different sustain CC)") synth.cc(0, 54, 0); REQUIRE( synth.getNumActiveVoices() == 1 ); } + +TEST_CASE("[Synth] Sustain threshold default") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 1); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 0 ); +} + +TEST_CASE("[Synth] Sustain threshold") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sustain_lo=63 + key=62 sample=*sine trigger=release + )"); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 1); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.noteOn(0, 62, 85); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); + synth.noteOn(0, 62, 85); + synth.cc(0, 64, 64); + synth.noteOff(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); +}