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/Defaults.cpp b/src/sfizz/Defaults.cpp index 4deee968..2d2886f0 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -61,7 +61,9 @@ UInt16Spec ccNumber { 0, {0, config::numCCs}, 0 }; UInt16Spec smoothCC { 0, {0, 100}, kPermissiveUpperBound }; UInt8Spec curveCC { 0, {0, 255}, 0 }; UInt8Spec sustainCC { 64, {0, 127}, 0 }; +UInt8Spec sostenutoCC { 66, {0, 127}, 0 }; FloatSpec sustainThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds }; +FloatSpec sostenutoThreshold { 1.0f, {0.0f, 127.0f}, kNormalizeMidi|kPermissiveBounds }; BoolSpec checkSustain { true, {0, 1}, kEnforceBounds }; BoolSpec checkSostenuto { true, {0, 1}, kEnforceBounds }; FloatSpec loBPM { 0.0f, {0.0f, 500.0f}, kPermissiveBounds }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 28dba5fa..70735abf 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -161,9 +161,11 @@ namespace Default extern const OpcodeSpec curveCC; extern const OpcodeSpec smoothCC; extern const OpcodeSpec sustainCC; + extern const OpcodeSpec sostenutoCC; extern const OpcodeSpec checkSustain; extern const OpcodeSpec checkSostenuto; extern const OpcodeSpec sustainThreshold; + extern const OpcodeSpec sostenutoThreshold; extern const OpcodeSpec loBPM; extern const OpcodeSpec hiBPM; extern const OpcodeSpec sequence; diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 938fba1e..c8df355a 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -23,6 +23,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex noteOnTimes[noteNumber] = internalClock + static_cast(delay); lastNotePlayed = noteNumber; activeNotes++; + noteStates[noteNumber] = true; } } @@ -37,6 +38,7 @@ void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noe noteOffTimes[noteNumber] = internalClock + static_cast(delay); if (activeNotes > 0) activeNotes--; + noteStates[noteNumber] = false; } } @@ -181,6 +183,7 @@ void sfz::MidiState::reset() noexcept activeNotes = 0; internalClock = 0; lastNotePlayed = 0; + noteStates.reset(); absl::c_fill(noteOnTimes, 0); absl::c_fill(noteOffTimes, 0); } diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index f9766c81..bb42f09a 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -6,6 +6,7 @@ #pragma once #include +#include #include "CCMap.h" #include "Range.h" @@ -142,6 +143,15 @@ public: */ void flushEvents() noexcept; + /** + * @brief Check if a note is currently depressed + * + * @param noteNumber + * @return true + * @return false + */ + bool isNotePressed(int noteNumber) const noexcept { return noteStates[noteNumber]; } + /** * @brief Get the CC value for CC number * @@ -191,6 +201,12 @@ private: MidiNoteArray noteOffTimes { {} }; + /** + * @brief Store the note states + * + */ + std::bitset<128> noteStates; + /** * @brief Stores the velocity of the note ons for currently * depressed notes. diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index e066e430..e68fa48c 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" @@ -308,9 +309,15 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("sustain_cc"): sustainCC = opcode.read(Default::sustainCC); break; + case hash("sostenuto_cc"): + sostenutoCC = opcode.read(Default::sostenutoCC); + break; case hash("sustain_lo"): sustainThreshold = opcode.read(Default::sustainThreshold); break; + case hash("sostenuto_lo"): + sostenutoThreshold = opcode.read(Default::sostenutoThreshold); + break; case hash("sustain_sw"): checkSustain = opcode.read(Default::checkSustain); break; @@ -1497,6 +1504,53 @@ bool sfz::Region::isSwitchedOn() const noexcept return keySwitched && previousKeySwitched && sequenceSwitched && pitchSwitched && bpmSwitched && aftertouchSwitched && ccSwitched.all(); } +void sfz::Region::delaySustainRelease(int noteNumber, float velocity) noexcept +{ + if (delayedSustainReleases.size() == delayedSustainReleases.capacity()) + return; + + delayedSustainReleases.emplace_back(noteNumber, velocity); +} + +void sfz::Region::delaySostenutoRelease(int noteNumber, float velocity) noexcept +{ + if (delayedSostenutoReleases.size() == delayedSostenutoReleases.capacity()) + return; + + delayedSostenutoReleases.emplace_back(noteNumber, velocity); +} + +void sfz::Region::removeFromSostenutoReleases(int noteNumber) noexcept +{ + swapAndPopFirst(delayedSostenutoReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }); +} + +void sfz::Region::storeSostenutoNotes() noexcept +{ + ASSERT(delayedSostenutoReleases.empty()); + for (int note = keyRange.getStart(); note <= keyRange.getEnd(); ++note) { + if (midiState.isNotePressed(note)) + delaySostenutoRelease(note, midiState.getNoteVelocity(note)); + } +} + + +bool sfz::Region::isNoteSustained(int noteNumber) const noexcept +{ + return absl::c_find_if(delayedSustainReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }) != delayedSustainReleases.end(); +} + +bool sfz::Region::isNoteSostenutoed(int noteNumber) const noexcept +{ + return absl::c_find_if(delayedSostenutoReleases, [=](const std::pair& p) { + return p.first == noteNumber; + }) != delayedSostenutoReleases.end(); +} + bool sfz::Region::registerNoteOn(int noteNumber, float velocity, float randValue) noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); @@ -1551,13 +1605,20 @@ bool sfz::Region::registerNoteOff(int noteNumber, float velocity, float randValu return true; if (trigger == Trigger::release) { - if (midiState.getCCValue(sustainCC) < sustainThreshold) - return true; + const bool sostenutoed = isNoteSostenutoed(noteNumber); - // If we reach this part, we're storing the notes to delay their release on CC up - // This is handled by the Synth object + if (sostenutoed && !sostenutoPressed) { + removeFromSostenutoReleases(noteNumber); + if (sustainPressed) + delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); + } - delayedReleases.emplace_back(noteNumber, midiState.getNoteVelocity(noteNumber)); + if (!sostenutoPressed || !sostenutoed) { + if (sustainPressed) + delaySustainRelease(noteNumber, midiState.getNoteVelocity(noteNumber)); + else + return true; + } } return false; @@ -1567,6 +1628,20 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept { ASSERT(ccValue >= 0.0f && ccValue <= 1.0f); + if (ccNumber == sustainCC) + sustainPressed = checkSustain && ccValue >= sustainThreshold; + + if (ccNumber == sostenutoCC) { + const bool newState = checkSostenuto && ccValue >= sostenutoThreshold; + if (!sostenutoPressed && newState) + storeSostenutoNotes(); + + if (!newState && sostenutoPressed) + delayedSostenutoReleases.clear(); + + sostenutoPressed = newState; + } + if (ccConditions.getWithDefault(ccNumber).containsWithEnd(ccValue)) ccSwitched.set(ccNumber, true); else diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 5cf21180..aa089908 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -406,7 +406,9 @@ struct Region { bool checkSustain { Default::checkSustain }; // sustain_sw bool checkSostenuto { Default::checkSostenuto }; // sostenuto_sw uint16_t sustainCC { Default::sustainCC }; // sustain_cc + uint16_t sostenutoCC { Default::sostenutoCC }; // sustain_cc float sustainThreshold { Default::sustainThreshold }; // sustain_cc + float sostenutoThreshold { Default::sostenutoThreshold }; // sustain_cc // Region logic: internal conditions UncheckedRange aftertouchRange { Default::loChannelAftertouch, Default::hiChannelAftertouch }; // hichanaft and lochanaft @@ -506,7 +508,16 @@ struct Region { RegionSet* parent { nullptr }; // Started notes - std::vector> delayedReleases; + bool sustainPressed { false }; + bool sostenutoPressed { false }; + std::vector> delayedSustainReleases; + std::vector> delayedSostenutoReleases; + void delaySustainRelease(int noteNumber, float velocity) noexcept; + void delaySostenutoRelease(int noteNumber, float velocity) noexcept; + void storeSostenutoNotes() noexcept; + void removeFromSostenutoReleases(int noteNumber) noexcept; + bool isNoteSustained(int noteNumber) const noexcept; + bool isNoteSostenutoed(int noteNumber) const noexcept; const MidiState& midiState; bool keySwitched { true }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 879816b1..b0f6fc71 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)); } @@ -654,7 +659,8 @@ void Synth::Impl::finalizeSfzLoad() for (int cc = 0; cc < config::numCCs; cc++) { if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc) - || (cc == region->sustainCC && region->trigger == Trigger::release)) + || (cc == region->sustainCC && region->trigger == Trigger::release) + || (cc == region->sostenutoCC && region->trigger == Trigger::release)) ccActivationLists_[cc].push_back(region); } @@ -1147,21 +1153,34 @@ 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) { - // FIXME: we really need to have some form of common method to find and start voices... + 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 { @@ -1174,8 +1193,19 @@ 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 (region->checkSustain && ccNumber == region->sustainCC && value < region->sustainThreshold) + startDelayedSustainReleases(region, delay, ring); + + if (region->checkSostenuto && ccNumber == region->sostenutoCC && value < region->sostenutoThreshold) { + if (region->sustainPressed) { + for (const auto& v: region->delayedSostenutoReleases) + region->delaySustainRelease(v.first, v.second); + + region->delayedSostenutoReleases.clear(); + } else { + startDelayedSostenutoReleases(region, delay, ring); + } + } if (region->registerCC(ccNumber, value)) startVoice(region, delay, triggerEvent, ring); diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index b4bd7e5e..3a713858 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -1042,11 +1042,21 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'i'>(delay, path, region.sustainCC); } break; + MATCH("/region&/sostenuto_cc", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'i'>(delay, path, region.sostenutoCC); + } break; + MATCH("/region&/sustain_lo", "") { GET_REGION_OR_BREAK(indices[0]) client.receive<'f'>(delay, path, region.sustainThreshold); } break; + MATCH("/region&/sostenuto_lo", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'f'>(delay, path, region.sostenutoThreshold); + } break; + MATCH("/region&/oscillator_phase", "") { GET_REGION_OR_BREAK(indices[0]) client.receive<'f'>(delay, path, region.oscillatorPhase); 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/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 94aba5bb..92294130 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -202,6 +202,10 @@ struct Voice::Impl State state_ { State::idle }; bool noteIsOff_ { false }; + enum class SustainState { Up, Sustaining }; + SustainState sustainState_ { SustainState::Up }; + enum class SostenutoState { Up, Sustaining, PreviouslyDown }; + SostenutoState sostenutoState_ { SostenutoState::Up }; TriggerEvent triggerEvent_; absl::optional triggerDelay_; @@ -459,6 +463,20 @@ bool Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe impl.resources_.modMatrix.initVoice(impl.id_, region->getId(), impl.initialDelay_); impl.saveModulationTargets(region); + if (region->checkSustain) { + const bool sustainPressed = + impl.resources_.midiState.getCCValue(region->sustainCC) >= region->sustainThreshold; + impl.sustainState_ = + sustainPressed ? Impl::SustainState::Sustaining : Impl::SustainState::Up; + } + + if (region->checkSostenuto) { + const bool sostenutoPressed = + impl.resources_.midiState.getCCValue(region->sostenutoCC) >= region->sostenutoThreshold; + impl.sostenutoState_ = + sostenutoPressed ? Impl::SostenutoState::PreviouslyDown : Impl::SostenutoState::Up; + } + return true; } @@ -543,8 +561,13 @@ void Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept if (impl.region_->loopMode == LoopMode::one_shot) return; - if (!impl.region_->checkSustain - || impl.resources_.midiState.getCCValue(impl.region_->sustainCC) < impl.region_->sustainThreshold) + const bool sustainPedalReleaseCondition = !impl.region_->checkSustain + || impl.sustainState_ != Impl::SustainState::Sustaining; + + const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto + || impl.sostenutoState_ != Impl::SostenutoState::Sustaining; + + if (sustainPedalReleaseCondition && sostenutoPedalReleaseCondition) release(delay); } } @@ -559,10 +582,29 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept if (impl.state_ != State::playing) return; - if (impl.region_->checkSustain - && impl.noteIsOff_ - && ccNumber == impl.region_->sustainCC - && ccValue < impl.region_->sustainThreshold) + if (impl.region_->checkSustain && (ccNumber == impl.region_->sostenutoCC)) { + if (ccValue < impl.region_->sostenutoThreshold) { + impl.sostenutoState_ = Impl::SostenutoState::Up; + } else if (impl.sostenutoState_ == Impl::SostenutoState::Up) { + impl.sostenutoState_ = Impl::SostenutoState::Sustaining; + } + } + + if (impl.region_->checkSostenuto && (ccNumber == impl.region_->sustainCC)) { + if (ccValue < impl.region_->sustainThreshold) { + impl.sustainState_ = Impl::SustainState::Up; + } else { + impl.sustainState_ = Impl::SustainState::Sustaining; + } + } + + const bool sustainPedalReleaseCondition = !impl.region_->checkSustain + || (impl.sustainState_ != Impl::SustainState::Sustaining); + + const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto + || (impl.sostenutoState_ != Impl::SostenutoState::Sustaining); + + if (impl.noteIsOff_ && sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) release(delay); } @@ -1512,6 +1554,7 @@ void Voice::reset() noexcept impl.count_ = 1; impl.floatPositionOffset_ = 0.0f; impl.noteIsOff_ = false; + impl.sostenutoState_ = Impl::SostenutoState::Up; impl.resetLoopInformation(); diff --git a/tests/DirectRegionT.cpp b/tests/DirectRegionT.cpp index 75c66453..8f5bf6a6 100644 --- a/tests/DirectRegionT.cpp +++ b/tests/DirectRegionT.cpp @@ -37,10 +37,12 @@ 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" }); midiState.ccEvent(0, 64, 0.0f); + region.registerCC(64, 0.0f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); } @@ -48,6 +50,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release_key" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(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) ); @@ -57,6 +60,7 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 0.0f); + region.registerCC(64, 0.0f); REQUIRE( !region.registerNoteOn(63, 0.5f, 0.0f) ); REQUIRE( region.registerNoteOff(63, 0.5f, 0.0f) ); } @@ -65,48 +69,51 @@ TEST_CASE("[Direct Region Tests] Release and release key") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(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 ); + 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") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(64, 1.0f); midiState.noteOnEvent(0, 63, 0.5f); REQUIRE( !region.registerNoteOn(63, 0.5f, 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 ); + 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") { region.parseOpcode({ "trigger", "release" }); midiState.ccEvent(0, 64, 1.0f); + region.registerCC(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 ); + REQUIRE( region.delayedSustainReleases.size() == 1 ); std::vector> expected = { { 63, 0.5f } }; - REQUIRE( region.delayedReleases == expected ); + REQUIRE( region.delayedSustainReleases == expected ); } } diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 6b5dfbf5..1feb9cf1 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -2425,6 +2425,52 @@ TEST_CASE("[Values] Sustain low") REQUIRE(messageList == expected); } +TEST_CASE("[Values] Sostenuto CC") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav sostenuto_cc=10 + sample=kick.wav sostenuto_cc=20 sostenuto_cc=-1 + )"); + synth.dispatchMessage(client, 0, "/region0/sostenuto_cc", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/sostenuto_cc", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/sostenuto_cc", "", nullptr); + std::vector expected { + "/region0/sostenuto_cc,i : { 66 }", + "/region1/sostenuto_cc,i : { 10 }", + "/region2/sostenuto_cc,i : { 66 }", + }; + REQUIRE(messageList == expected); +} + +TEST_CASE("[Values] Sostenuto low") +{ + Synth synth; + std::vector messageList; + Client client(&messageList); + client.setReceiveCallback(&simpleMessageReceiver); + + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav sostenuto_lo=10 + sample=kick.wav sostenuto_lo=10 sostenuto_lo=-1 + )"); + synth.dispatchMessage(client, 0, "/region0/sostenuto_lo", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/sostenuto_lo", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/sostenuto_lo", "", nullptr); + std::vector expected { + "/region0/sostenuto_lo,f : { 0.00787402 }", + "/region1/sostenuto_lo,f : { 0.0787402 }", + "/region2/sostenuto_lo,f : { -0.00787402 }", + }; + REQUIRE(messageList == expected); +} + TEST_CASE("[Values] Oscillator phase") { Synth synth; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 3bf2cc64..cb97a0f7 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,114 @@ 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; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + sostenuto_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 (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; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + sostenuto_cc=54 + key=62 sample=*silence + key=62 sample=*sine trigger=release + key=64 sample=*silence + key=64 sample=*sine trigger=release + )"); + 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] Release (sustain + sostenuto)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/release.sfz", R"( + key=62 sample=*silence + key=62 sample=*sine trigger=release + key=64 sample=*silence + key=64 sample=*sine trigger=release + )"); + SECTION("Sustain up first") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 127); + synth.cc(1, 64, 127); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(3, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(4, 66, 0); + REQUIRE( synth.getNumActiveVoices() == 2 ); + } + SECTION("Sostenuto up first") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 127); + synth.cc(1, 64, 127); + synth.noteOff(2, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(3, 66, 0); + REQUIRE( synth.getNumActiveVoices() == 1 ); + synth.cc(4, 64, 0); + REQUIRE( synth.getNumActiveVoices() == 2 ); + } +} + TEST_CASE("[Synth] Sustain threshold default") { sfz::Synth synth; @@ -852,6 +958,108 @@ TEST_CASE("[Synth] Sustain threshold") REQUIRE( synth.getNumActiveVoices() == 5 ); } +TEST_CASE("[Synth] Sustain") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sostenuto.sfz", R"( + sample=*sine + )"); + + SECTION("1 note") + { + synth.noteOn(0, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(1, 64, 1); + synth.noteOff(2, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(3, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("2 notes") + { + synth.noteOn(0, 62, 85); + synth.noteOn(0, 64, 85); + synth.cc(1, 64, 1); + synth.noteOff(2, 62, 85); + synth.noteOff(2, 64, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.cc(3, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("1 note after the pedal is depressed") + { + synth.cc(0, 64, 1); + synth.noteOn(1, 62, 85); + synth.noteOff(2, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(3, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("2 notes, 1 after the pedal is depressed") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 64, 1); + synth.noteOn(2, 64, 85); + synth.noteOff(3, 62, 85); + synth.noteOff(3, 64, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.cc(4, 64, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } +} + +TEST_CASE("[Synth] Sostenuto") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/sostenuto.sfz", R"( + sample=*sine + )"); + + SECTION("1 note") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 1); + synth.noteOff(2, 62, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(3, 66, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("2 notes") + { + synth.noteOn(0, 62, 85); + synth.noteOn(0, 64, 85); + synth.cc(1, 66, 1); + synth.noteOff(2, 62, 85); + synth.noteOff(2, 64, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.cc(3, 66, 0); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("1 note but after the pedal is depressed") + { + synth.cc(0, 66, 1); + synth.noteOn(1, 62, 85); + synth.noteOff(2, 62, 85); + REQUIRE( numPlayingVoices(synth) == 0 ); + } + + SECTION("2 notes, 1 after the pedal is depressed") + { + synth.noteOn(0, 62, 85); + synth.cc(1, 66, 1); + synth.noteOn(2, 64, 85); + synth.noteOff(3, 62, 85); + synth.noteOff(3, 64, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( getActiveVoices(synth)[0]->getTriggerEvent().number == 62 ); + } +} + TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") { sfz::Synth synth; @@ -903,7 +1111,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)") @@ -933,7 +1141,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)") @@ -960,7 +1168,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") @@ -995,7 +1203,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") @@ -1030,7 +1238,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")