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]; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e1bc441f..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,17 +1147,8 @@ 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 }; - if (region.velocityOverride == VelocityOverride::previous) - triggerEvent.value = resources_.midiState.getLastVelocity(); - startVoice(layer, delay, triggerEvent, ring); } } @@ -1199,7 +1201,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 +1219,10 @@ void Synth::Impl::ccDispatch(int delay, int ccNumber, float value) noexcept } } - if (layer->registerCC(ccNumber, value)) + if (layer->registerCC(ccNumber, 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 65b13cb9..0a3f8c74 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 { @@ -401,6 +402,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; @@ -469,8 +473,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(); @@ -485,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()))); @@ -608,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); } @@ -1060,11 +1072,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 {}; @@ -1563,8 +1571,11 @@ 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 == 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 5ae2cb51..cb1a2c9e 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; @@ -1563,6 +1609,45 @@ 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] 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;