From 65353486a92b06640eccc2c522efc276792dd2c1 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 17:22:09 +0200 Subject: [PATCH 1/7] Use noteStates to check for the note status in the midi state --- src/sfizz/MidiState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 6bd07b62..73f7ea42 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -101,7 +101,7 @@ float sfz::MidiState::getNoteDuration(int noteNumber, int delay) const if (noteNumber < 0 || noteNumber >= 128) return 0.0f; - if (noteOnTimes[noteNumber] != 0 && noteOffTimes[noteNumber] != 0 && noteOnTimes[noteNumber] > noteOffTimes[noteNumber]) + if (!noteStates[noteNumber]) return 0.0f; const unsigned timeInSamples = internalClock + static_cast(delay) - noteOnTimes[noteNumber]; From 78e20c68bf68c445ecc36c261414c8a5ec9ad745 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 17:26:40 +0200 Subject: [PATCH 2/7] Override the velocity in the startVoice method --- src/sfizz/Synth.cpp | 8 +++----- src/sfizz/Voice.cpp | 3 +++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e1bc441f..15395563 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1144,9 +1144,6 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex } TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; - if (region.velocityOverride == VelocityOverride::previous) - triggerEvent.value = resources_.midiState.getLastVelocity(); - startVoice(layer, delay, triggerEvent, ring); } } @@ -1199,7 +1196,7 @@ void Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept { SisterVoiceRingBuilder ring; - const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value }; + TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, value }; for (Layer* layer : ccActivationLists_[ccNumber]) { const Region& region = layer->getRegion(); @@ -1217,8 +1214,9 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept } } - if (layer->registerCC(ccNumber, value)) + if (layer->registerCC(ccNumber, value)) { startVoice(layer, delay, triggerEvent, ring); + } } } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 65b13cb9..ff5610ea 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -401,6 +401,9 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc if (impl.triggerEvent_.type == TriggerEventType::CC) impl.triggerEvent_.number = region.pitchKeycenter; + if (region.velocityOverride == VelocityOverride::previous) + impl.triggerEvent_.value = resources.midiState.getLastVelocity(); + if (region.disabled()) { impl.switchState(State::cleanMeUp); return false; From ac27e060ba76cb9852074818cb91d8cb2bfc0cb6 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 17:54:08 +0200 Subject: [PATCH 3/7] Apply velocity-related note gain to a CC-triggered note if the velocity has been replaced with `sw_vel=previous` --- src/sfizz/Voice.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index ff5610ea..612ce8cf 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -472,8 +472,9 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc impl.baseVolumedB_ = region.getBaseVolumedB(resources.midiState, impl.triggerEvent_.number); impl.baseGain_ = region.getBaseGain(); - if (impl.triggerEvent_.type != TriggerEventType::CC) + if (impl.triggerEvent_.type != TriggerEventType::CC || region.velocityOverride == VelocityOverride::previous) impl.baseGain_ *= region.getNoteGain(impl.triggerEvent_.number, impl.triggerEvent_.value); + impl.gainSmoother_.reset(); impl.resetCrossfades(); From b3ff1469edd0fdcc2975d4e75d49c2a1022c387a Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 21:22:22 +0200 Subject: [PATCH 4/7] Correct how sampleEnd is computed sampleSize_ is (end - offset) and is used to wrap back the sample. --- src/sfizz/Voice.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 612ce8cf..af7e1a01 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -228,6 +228,7 @@ struct Voice::Impl int initialDelay_ { 0 }; int age_ { 0 }; uint32_t count_ { 1 }; + int sampleEnd_ { 0 }; int sampleSize_ { 0 }; struct { @@ -489,7 +490,8 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc impl.triggerDelay_ = delay; impl.initialDelay_ = delay + static_cast(region.getDelay(resources.midiState) * impl.sampleRate_); impl.baseFrequency_ = resources.tuning.getFrequencyOfKey(impl.triggerEvent_.number); - impl.sampleSize_ = int(region.getSampleEnd(resources.midiState, resources.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1); + impl.sampleEnd_ = int(region.getSampleEnd(resources.midiState, resources.filePool.getOversamplingFactor())); + impl.sampleSize_ = impl.sampleEnd_- impl.sourcePosition_ - 1; impl.bendStepFactor_ = centsFactor(region.bendStep); impl.bendSmoother_.setSmoothing(region.bendSmooth, impl.sampleRate_); impl.bendSmoother_.reset(centsFactor(region.getBendInCents(resources.midiState.getPitchBend()))); @@ -1064,11 +1066,7 @@ void Voice::Impl::fillWithData(AudioSpan buffer) noexcept numPartitions = 1; } - const auto sampleEnd = min( - int(sampleSize_), - int(currentPromise_->information.end), - int(source.getNumFrames())) - - 1; + const auto sampleEnd = min( int(sampleEnd_), int(currentPromise_->information.end), int(source.getNumFrames())) - 1; int blockRestarts { 0 }; int oldIndex {}; From 7f02c413c1e632e9b609ae3f445bf67b82422447 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 21:23:05 +0200 Subject: [PATCH 5/7] Correct the sostenuto/sustain logic Only release if the received CC is one of the above AND if the region is not a one-shot --- src/sfizz/Voice.cpp | 20 +++++++++++++------- tests/SynthT.cpp | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index af7e1a01..de92d4c6 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -614,32 +614,38 @@ void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept if (impl.region_ == nullptr) return; + const Region& region = *impl.region_; + if (impl.state_ != State::playing) return; - if (impl.region_->checkSustain && (ccNumber == impl.region_->sostenutoCC)) { - if (ccValue < impl.region_->sostenutoThreshold) { + if (ccNumber != region.sustainCC && ccNumber != region.sostenutoCC) + return; + + if (region.checkSustain && (ccNumber == region.sostenutoCC)) { + if (ccValue < 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) { + if (region.checkSostenuto && (ccNumber == region.sustainCC)) { + if (ccValue < region.sustainThreshold) { impl.sustainState_ = Impl::SustainState::Up; } else { impl.sustainState_ = Impl::SustainState::Sustaining; } } - const bool sustainPedalReleaseCondition = !impl.region_->checkSustain + const bool sustainPedalReleaseCondition = !region.checkSustain || (impl.sustainState_ != Impl::SustainState::Sustaining); - const bool sostenutoPedalReleaseCondition = !impl.region_->checkSostenuto + const bool sostenutoPedalReleaseCondition = !region.checkSostenuto || (impl.sostenutoState_ != Impl::SostenutoState::Sustaining); - if (impl.noteIsOff_ && sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) + if (impl.noteIsOff_ && region.loopMode != LoopMode::one_shot + && sostenutoPedalReleaseCondition && sustainPedalReleaseCondition) release(delay); } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 5ae2cb51..8db5b06c 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -926,6 +926,52 @@ TEST_CASE("[Synth] Release (sustain + sostenuto)") } } +TEST_CASE("[Synth] One shot regions with sustain + sostenuto") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/one_shot_sustain.sfz", R"( + key=60 sample=kick.wav loop_mode=one_shot + )"); + SECTION("Sustain") + { + synth.noteOn(0, 60, 85); + synth.cc(1, 64, 127); + synth.noteOff(2, 60, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(1, 64, 0); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + } + SECTION("Sostenuto") + { + synth.noteOn(0, 60, 85); + synth.cc(1, 66, 127); + synth.noteOff(2, 60, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(1, 66, 0); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + } + SECTION("Sostenuto up first") + { + synth.noteOn(0, 60, 85); + synth.cc(1, 66, 127); + synth.cc(1, 64, 127); + synth.noteOff(2, 60, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(3, 66, 0); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(4, 64, 0); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + } +} + TEST_CASE("[Synth] Sustain threshold default") { sfz::Synth synth; From f189292a6043c68df651e178b307ab38436ee45c Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 21:54:19 +0200 Subject: [PATCH 6/7] CC events can also off notes Same as note-on events --- src/sfizz/Synth.cpp | 6 ++++++ src/sfizz/Voice.cpp | 2 +- tests/SynthT.cpp | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 15395563..7b5a7369 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1215,6 +1215,12 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept } if (layer->registerCC(ccNumber, value)) { + for (auto& voice : voiceManager_) { + if (voice.checkOffGroup(®ion, delay, ccNumber)) { + const TriggerEvent& event = voice.getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } + } startVoice(layer, delay, triggerEvent, ring); } } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index de92d4c6..836f1075 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -1572,7 +1572,7 @@ bool Voice::checkOffGroup(const Region* other, int delay, int noteNumber) noexce return false; if (impl.triggerEvent_.type == TriggerEventType::NoteOn - && region->offBy == other->group + && region->offBy && *region->offBy == other->group && (region->group != other->group || noteNumber != impl.triggerEvent_.number)) { off(delay); return true; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 8db5b06c..538fd606 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1609,6 +1609,23 @@ TEST_CASE("[Synth] Off by with CC switches") REQUIRE( getPlayingVoices(synth).front()->getRegion()->sampleId->filename() == "*saw" ); } +TEST_CASE("[Synth] Off by a CC event") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + + synth.loadSfzString(fs::current_path(), R"( + group=1 off_by=2 sample=*saw + group=2 hikey=-1 on_locc67=127 on_hicc67=127 sample=*sine + )"); + synth.noteOn(0, 60, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.cc(10, 67, 127); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); +} + TEST_CASE("[Synth] Initial values of CC") { sfz::Synth synth; From 0c1687b5fad03c5c6c9353edec7cc3a570b08ba3 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 4 Apr 2021 22:24:43 +0200 Subject: [PATCH 7/7] Voices can also be offed by note-off events Also add a check to see if a voice is already released in checkOffGroup, preventing and endless recursion --- src/sfizz/Synth.cpp | 26 +++++++++++++------------- src/sfizz/SynthPrivate.h | 10 ++++++++++ src/sfizz/Voice.cpp | 3 +++ tests/SynthT.cpp | 22 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7b5a7369..1febb49c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1088,6 +1088,16 @@ void Synth::Impl::startVoice(Layer* layer, int delay, const TriggerEvent& trigge ring.addVoiceToRing(selectedVoice); } +void Synth::Impl::checkOffGroups(const Region* region, int delay, int number) +{ + for (auto& voice : voiceManager_) { + if (voice.checkOffGroup(region, delay, number)) { + const TriggerEvent& event = voice.getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } + } +} + void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution_(Random::randomGenerator); @@ -1106,6 +1116,7 @@ void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noe if (region.trigger == Trigger::release && !region.rtDead && !voiceManager_.playingAttackVoice(®ion)) continue; + checkOffGroups(®ion, delay, noteNumber); startVoice(layer, delay, triggerEvent, ring); } } @@ -1136,13 +1147,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex for (Layer* layer : noteActivationLists_[noteNumber]) { if (layer->registerNoteOn(noteNumber, velocity, randValue)) { const Region& region = layer->getRegion(); - for (auto& voice : voiceManager_) { - if (voice.checkOffGroup(®ion, delay, noteNumber)) { - const TriggerEvent& event = voice.getTriggerEvent(); - noteOffDispatch(delay, event.number, event.value); - } - } - + checkOffGroups(®ion, delay, noteNumber); TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; startVoice(layer, delay, triggerEvent, ring); } @@ -1215,12 +1220,7 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept } if (layer->registerCC(ccNumber, value)) { - for (auto& voice : voiceManager_) { - if (voice.checkOffGroup(®ion, delay, ccNumber)) { - const TriggerEvent& event = voice.getTriggerEvent(); - noteOffDispatch(delay, event.number, event.value); - } - } + checkOffGroups(®ion, delay, ccNumber); startVoice(layer, delay, triggerEvent, ring); } } diff --git a/src/sfizz/SynthPrivate.h b/src/sfizz/SynthPrivate.h index 3b8835a6..1fafde39 100644 --- a/src/sfizz/SynthPrivate.h +++ b/src/sfizz/SynthPrivate.h @@ -219,6 +219,16 @@ struct Synth::Impl final: public Parser::Listener { */ void setDefaultHdcc(int ccNumber, float value); + /** + * @brief Check if we have to kill any voice when starting a new one + * on the specified region with the specified note/cc number + * + * @param region + * @param delay + * @param number + */ + void checkOffGroups(const Region* region, int delay, int number); + int numGroups_ { 0 }; int numMasters_ { 0 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 836f1075..0a3f8c74 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -1571,6 +1571,9 @@ bool Voice::checkOffGroup(const Region* other, int delay, int noteNumber) noexce if (region == nullptr || other == nullptr) return false; + if (impl.released()) + return false; + if (impl.triggerEvent_.type == TriggerEventType::NoteOn && region->offBy && *region->offBy == other->group && (region->group != other->group || noteNumber != impl.triggerEvent_.number)) { diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 538fd606..cb1a2c9e 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -1626,6 +1626,28 @@ TEST_CASE("[Synth] Off by a CC event") REQUIRE( numPlayingVoices(synth) == 1 ); } +TEST_CASE("[Synth] Off by a note-off event") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, static_cast(synth.getSamplesPerBlock()) }; + + synth.loadSfzString(fs::current_path(), R"( + key=60 group=1 off_by=2 sample=*saw + key=62 sample=*silence + key=62 trigger=release group=2 sample=*silence + )"); + synth.noteOn(0, 60, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(0, 62, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.noteOff(10, 62, 85); + synth.renderBlock(buffer); + REQUIRE( numPlayingVoices(synth) == 1 ); + // TODO: check the samples; the last one should be *silence +} + TEST_CASE("[Synth] Initial values of CC") { sfz::Synth synth;