diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 93dbc515..4e7cc743 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -128,14 +128,6 @@ struct SisterVoiceRing { */ class SisterVoiceRingBuilder { public: - ~SisterVoiceRingBuilder() noexcept { - if (lastStartedVoice != nullptr) { - ASSERT(firstStartedVoice); - lastStartedVoice->setNextSisterVoice(firstStartedVoice); - firstStartedVoice->setPreviousSisterVoice(lastStartedVoice); - } - } - /** * @brief Add a voice to the sister ring * @@ -145,6 +137,9 @@ public: if (firstStartedVoice == nullptr) firstStartedVoice = voice; + firstStartedVoice->setPreviousSisterVoice(voice); + voice->setNextSisterVoice(firstStartedVoice); + if (lastStartedVoice != nullptr) { voice->setPreviousSisterVoice(lastStartedVoice); lastStartedVoice->setNextSisterVoice(voice); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 4fc9b0f1..beae4e64 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -9,6 +9,7 @@ #include "Debug.h" #include "Macros.h" #include "MidiState.h" +#include "TriggerEvent.h" #include "ModifierHelpers.h" #include "ScopedFTZ.h" #include "StringViewHelpers.h" @@ -660,7 +661,23 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - return {}; + // Engine polyphony reached + Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); + if (stolenVoice == nullptr) + return {}; + + // Never kill age 0 voices + if (stolenVoice->getAge() == 0) + return {}; + + + auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); + SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) { + renderVoiceToOutputs(*v, *tempSpan); + v->reset(); + }); + + return stolenVoice; } int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept @@ -882,163 +899,203 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept noteOffDispatch(delay, noteNumber, replacedVelocity); } -bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) { - return ( - !voice.isFree() - && voice.getTriggerType() == sfz::Voice::TriggerType::NoteOn - && region.keyRange.containsWithEnd(voice.getTriggerNumber()) - && region.velocityRange.containsWithEnd(voice.getTriggerValue()) - ); +void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept +{ + checkNotePolyphony(region, delay, triggerEvent); + checkRegionPolyphony(region, delay); + checkGroupPolyphony(region, delay); + checkSetPolyphony(region, delay); + + Voice* selectedVoice = findFreeVoice(); + if (selectedVoice == nullptr) + return; + + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, triggerEvent); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); +} + +bool sfz::Synth::playingAttackVoice(const Region* releaseRegion) noexcept +{ + const auto compatibleVoice = [releaseRegion](const Voice* v) -> bool { + const sfz::TriggerEvent& event = v->getTriggerEvent(); + return ( + !v->isFree() + && event.type == sfz::TriggerEventType::NoteOn + && releaseRegion->keyRange.containsWithEnd(event.number) + && releaseRegion->velocityRange.containsWithEnd(event.value) + ); + }; + + if (absl::c_find_if(voiceViewArray, compatibleVoice) == voiceViewArray.end()) + return false; + else + return true; } void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::NoteOff, noteNumber, velocity }; for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { - if (region->triggerOnNote && region->trigger == SfzTrigger::release && !region->rtDead) { - // check that a voice with compatible trigger is playing - // FIXME: we're going twice over the voices, when the synth - // handles the regions completely these dispatch functions - // should be overhauled, also to include voice stealing on - // all events - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; - - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) - continue; - } - - auto voice = findFreeVoice(); - if (voice == nullptr) + if (region->trigger == SfzTrigger::release && !region->rtDead && !playingAttackVoice(region)) continue; - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + startVoice(region, delay, triggerEvent, ring); } } } +void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept +{ + tempPolyphonyArray.clear(); + + for (Voice* voice : voiceViewArray) { + if (voice->getRegion() == region && !voice->releasedOrFree()) { + tempPolyphonyArray.push_back(voice); + } + } + + if (tempPolyphonyArray.size() >= region->polyphony) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } +} + +void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept +{ + if (!region->notePolyphony) + return; + + unsigned notePolyphonyCounter { 0 }; + Voice* selfMaskCandidate { nullptr }; + + for (Voice* voice : voiceViewArray) { + const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); + const bool skipVoice = (triggerEvent.type == TriggerEventType::NoteOn && voice->releasedOrFree()) || voice->isFree(); + if (!skipVoice + && voice->getRegion()->group == region->group + && voiceTriggerEvent.number == triggerEvent.number + && voiceTriggerEvent.type == triggerEvent.type) { + notePolyphonyCounter += 1; + switch (region->selfMask) { + case SfzSelfMask::mask: + if (voiceTriggerEvent.value <= triggerEvent.value) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) { + selfMaskCandidate = voice; + } + } + break; + case SfzSelfMask::dontMask: + if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition()) + selfMaskCandidate = voice; + break; + } + } + } + + if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) { + SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); + } +} + +void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept +{ + const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices(); + tempPolyphonyArray.clear(); + for (Voice* voice : activeVoices) { + if (!voice->releasedOrFree()) { + tempPolyphonyArray.push_back(voice); + } + } + + if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } +} + +void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept +{ + auto parent = region->parent; + while (parent != nullptr) { + const auto& activeVoices = parent->getActiveVoices(); + tempPolyphonyArray.clear(); + for (Voice* voice : activeVoices) { + if (!voice->releasedOrFree()) { + tempPolyphonyArray.push_back(voice); + } + } + + if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) { + const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); + SisterVoiceRing::offAllSisters(voiceToSteal, delay); + } + + parent = parent->getParent(); + } +} + void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::NoteOn, noteNumber, velocity }; for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - unsigned notePolyphonyCounter { 0 }; - Voice* selfMaskCandidate { nullptr }; - Voice* selectedVoice { nullptr }; - regionPolyphonyArray.clear(); - for (auto& voice : voices) { - if (voice->isFree()) { - if (selectedVoice == nullptr) - selectedVoice = voice.get(); - continue; + if (voice->checkOffGroup(region, delay, noteNumber)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); } - - if (voice->getRegion() == region && !voice->releasedOrFree()) { - regionPolyphonyArray.push_back(voice.get()); - } - - if (region->notePolyphony) { - if (!voice->releasedOrFree() - && voice->getRegion()->group == region->group - && voice->getTriggerNumber() == noteNumber - && voice->getTriggerType() == Voice::TriggerType::NoteOn) { - notePolyphonyCounter += 1; - switch (region->selfMask) { - case SfzSelfMask::mask: - if (voice->getTriggerValue() <= velocity) { - if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) - selfMaskCandidate = voice.get(); - } - break; - case SfzSelfMask::dontMask: - if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition()) - selfMaskCandidate = voice.get(); - break; - } - } - } - - if (voice->checkOffGroup(delay, region->group)) - noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - // Polyphony reached on note_polyphony - // If there's a self-masking candidate, release it - if (region->notePolyphony - && notePolyphonyCounter >= *region->notePolyphony - && selfMaskCandidate != nullptr) { - SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); - } - - auto parent = region->parent; - - // Polyphony reached on region - if (regionPolyphonyArray.size() >= region->polyphony) { - const auto activeVoices = absl::MakeSpan(regionPolyphonyArray); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - - // Polyphony reached on polyphony group - if (polyphonyGroups[region->group].numPlayingVoices() - == polyphonyGroups[region->group].getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - - // Polyphony reached some parent group/master/etc - while (parent != nullptr) { - if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - parent = parent->getParent(); - } - - // Engine polyphony reached, we're stealing something - if (selectedVoice == nullptr) { - selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); - } - - // For some reason we did not find a voice to use. - // This is a degraded case but we'll just drop the note on. - if (selectedVoice == nullptr) - continue; - - // Kill voice if necessary, pre-rendering it into the output buffers - if (!selectedVoice->isFree()) { - auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); - SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); - } - - // Voice should be free now - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); + startVoice(region, delay, triggerEvent, ring); } } } +void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +{ + if (!region->rtDead && !playingAttackVoice(region)) { + region->delayedReleases.clear(); + return; + } + + for (auto& note: region->delayedReleases) { + // FIXME: we really need to have some form of common method to find and start voices... + const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; + startVoice(region, delay, noteOffEvent, ring); + } + region->delayedReleases.clear(); +} + + void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept { const auto normalizedCC = normalizeCC(ccValue); hdcc(delay, ccNumber, normalizedCC); } +void sfz::Synth::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->registerCC(ccNumber, value)) + startVoice(region, delay, triggerEvent, ring); + } +} + void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept { ASSERT(ccNumber < config::numCCs); @@ -1066,53 +1123,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept for (auto& voice : voices) voice->registerCC(delay, ccNumber, normValue); - SisterVoiceRingBuilder ring; - - for (auto& region : ccActivationLists[ccNumber]) { - if (ccNumber == region->sustainCC) { - if (!region->rtDead) { - // check that a voice with compatible trigger is playing - // FIXME: we're going twice over the voices, when the synth - // handles the regions completely these dispatch functions - // should be overhauled, also to include voice stealing on - // all events - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; - - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) - region->delayedReleases.clear(); - } - - for (auto& note: region->delayedReleases) { - // FIXME: we really need to have some form of common method to find and start voices... - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region, delay, note.first, note.second, Voice::TriggerType::NoteOff); - - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); - } - - region->delayedReleases.clear(); - } - - if (region->registerCC(ccNumber, normValue)) { - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - - voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); - - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); - } - } + ccDispatch(delay, ccNumber, normValue); } void sfz::Synth::pitchWheel(int delay, int pitch) noexcept @@ -1436,8 +1447,8 @@ void sfz::Synth::resetVoices(int numVoices) voiceViewArray.clear(); voiceViewArray.reserve(numVoices); - regionPolyphonyArray.clear(); - regionPolyphonyArray.reserve(numVoices); + tempPolyphonyArray.clear(); + tempPolyphonyArray.reserve(numVoices); for (int i = 0; i < numVoices; ++i) { auto voice = absl::make_unique(i, resources); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3fb85f7f..995bb5e6 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -725,9 +725,33 @@ private: fs::file_time_type checkModificationTime(); + /** + * @brief Check all regions and start voices for note on events + * + * @param delay + * @param noteNumber + * @param velocity + */ void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; + + /** + * @brief Check all regions and start voices for note off events + * + * @param delay + * @param noteNumber + * @param velocity + */ void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + /** + * @brief Check all regions and start voices for cc events + * + * @param delay + * @param ccNumber + * @param value + */ + void ccDispatch(int delay, int ccNumber, float value) noexcept; + template static void updateUsedCCsFromCCMap(std::bitset& usedCCs, const CCMap map) { @@ -765,18 +789,83 @@ private: using RegionSetPtr = std::unique_ptr; std::vector regions; std::vector voices; + // These are more general "groups" than sfz and encapsulates the full hierarchy RegionSet* currentSet; OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; std::vector sets; + // These are the `group=` groups where you can off voices std::vector polyphonyGroups; + // Views to speed up iteration over the regions and voices when events // occur in the audio callback - VoiceViewVector regionPolyphonyArray; + VoiceViewVector tempPolyphonyArray; + VoiceViewVector voiceViewArray; VoiceStealing stealer; - VoiceViewVector voiceViewArray; + /** + * @brief Check the region polyphony, releasing voices if necessary + * + * @param region + * @param delay + */ + void checkRegionPolyphony(const Region* region, int delay) noexcept; + + /** + * @brief Check the note polyphony, releasing voices if necessary + * + * @param region + * @param delay + * @param triggerEvent + */ + void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; + + /** + * @brief Check the group polyphony, releasing voices if necessary + * + * @param region + * @param delay + */ + void checkGroupPolyphony(const Region* region, int delay) noexcept; + + /** + * @brief Check the region set polyphony at all levels, releasing voices if necessary + * + * @param region + * @param delay + */ + void checkSetPolyphony(const Region* region, int delay) noexcept; + + /** + * @brief Start a voice for a specific region. + * This will do the needed polyphony checks and voice stealing. + * + * @param region + * @param delay + * @param triggerEvent + * @param ring + */ + void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; + + /** + * @brief Start all delayed release voices of the region if necessary + * + * @param region + * @param delay + * @param ring + */ + void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + + /** + * @brief Check if a playing voice matches the release region + * + * @param releaseRegion + * @return true + * @return false + */ + bool playingAttackVoice(const Region* releaseRegion) noexcept; + std::array noteActivationLists; std::array ccActivationLists; diff --git a/src/sfizz/TriggerEvent.h b/src/sfizz/TriggerEvent.h new file mode 100644 index 00000000..21108c02 --- /dev/null +++ b/src/sfizz/TriggerEvent.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + +namespace sfz +{ +enum class TriggerEventType { NoteOn, NoteOff, CC }; + +/** + * @brief Encapsulate a midi event with normalized values + * + */ +struct TriggerEvent +{ + TriggerEventType type; + int number; + float value; +}; + +} diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a6fdf375..4f120348 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -36,22 +36,18 @@ sfz::Voice::~Voice() { } -void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept +void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noexcept { - ASSERT(value >= 0.0f && value <= 1.0f); - - if (triggerType == TriggerType::CC) - number = region->pitchKeycenter; - - this->triggerType = triggerType; - triggerNumber = number; - triggerValue = value; + ASSERT(event.value >= 0.0f && event.value <= 1.0f); this->region = region; - if (region->disabled()) return; + triggerEvent = event; + if (triggerEvent.type == TriggerEventType::CC) + triggerEvent.number = region->pitchKeycenter; + switchState(State::playing); ASSERT(delay >= 0); @@ -106,18 +102,18 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, } // do Scala retuning and reconvert the frequency into a 12TET key number - const float numberRetuned = resources.tuning.getKeyFractional12TET(number); + const float numberRetuned = resources.tuning.getKeyFractional12TET(triggerEvent.number); - pitchRatio = region->getBasePitchVariation(numberRetuned, value); + pitchRatio = region->getBasePitchVariation(numberRetuned, triggerEvent.value); // apply stretch tuning if set if (resources.stretch) pitchRatio *= resources.stretch->getRatioForFractionalKey(numberRetuned); - baseVolumedB = region->getBaseVolumedB(number); + baseVolumedB = region->getBaseVolumedB(triggerEvent.number); baseGain = region->getBaseGain(); - if (triggerType != TriggerType::CC) - baseGain *= region->getNoteGain(number, value); + if (triggerEvent.type != TriggerEventType::CC) + baseGain *= region->getNoteGain(triggerEvent.number, triggerEvent.value); gainSmoother.reset(); resetCrossfades(); @@ -127,13 +123,13 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, const unsigned numChannels = region->isStereo() ? 2 : 1; for (auto& filter: region->filters) { - auto newFilter = resources.filterPool.getFilter(filter, numChannels, number, value); + auto newFilter = resources.filterPool.getFilter(filter, numChannels, triggerEvent.number, triggerEvent.value); if (newFilter) filters.push_back(newFilter); } for (auto& eq: region->equalizers) { - auto newEQ = resources.eqPool.getEQ(eq, numChannels, value); + auto newEQ = resources.eqPool.getEQ(eq, numChannels, triggerEvent.value); if (newEQ) equalizers.push_back(newEQ); } @@ -141,11 +137,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sourcePosition = region->getOffset(); triggerDelay = delay; initialDelay = delay + static_cast(region->getDelay() * sampleRate); - baseFrequency = resources.tuning.getFrequencyOfKey(number); + baseFrequency = resources.tuning.getFrequencyOfKey(triggerEvent.number); bendStepFactor = centsFactor(region->bendStep); bendSmoother.setSmoothing(region->bendSmooth, sampleRate); bendSmoother.reset(centsFactor(region->getBendInCents(resources.midiState.getPitchBend()))); - egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate); + egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, triggerEvent.value, sampleRate); resources.modMatrix.initVoice(id, region->getId(), delay); } @@ -197,7 +193,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (state != State::playing) return; - if (triggerNumber == noteNumber) { + if (triggerEvent.number == noteNumber && triggerEvent.type == TriggerEventType::NoteOn) { noteIsOff = true; if (region->loopMode == SfzLoopMode::one_shot) @@ -712,12 +708,14 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept #endif } -bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept +bool sfz::Voice::checkOffGroup(const Region* other, int delay, int noteNumber) noexcept { - if (region == nullptr) + if (region == nullptr || other == nullptr) return false; - if (triggerType == TriggerType::NoteOn && region->offBy == group) { + if (triggerEvent.type == TriggerEventType::NoteOn + && region->offBy == other->group + && noteNumber != triggerEvent.number) { off(delay); return true; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 0aec391e..a010aaa8 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "TriggerEvent.h" #include "Config.h" #include "ADSREnvelope.h" #include "HistoricalBuffer.h" @@ -43,12 +44,6 @@ public: ~Voice(); - enum class TriggerType { - NoteOn, - NoteOff, - CC - }; - /** * @brief Get the unique identifier of this voice in a synth */ @@ -113,11 +108,9 @@ public: * * @param region * @param delay - * @param number - * @param value - * @param triggerType + * @param evebt */ - void startVoice(Region* region, int delay, int number, float value, TriggerType triggerType) noexcept; + void startVoice(Region* region, int delay, const TriggerEvent& event) noexcept; /** * @brief Get the sample quality determined by the active region. @@ -170,11 +163,12 @@ public: * This will trigger the release if true. * * @param delay + * @param noteNumber * @param group * @return true * @return false */ - bool checkOffGroup(int delay, uint32_t group) noexcept; + bool checkOffGroup(const Region* other, int delay, int noteNumber) noexcept; /** * @brief Render a block of data for this voice into the span @@ -198,23 +192,11 @@ public: */ bool releasedOrFree() const noexcept; /** - * @brief Get the number that triggered the voice (note number or cc number) + * @brief Get the event that triggered the voice * * @return int */ - int getTriggerNumber() const noexcept { return triggerNumber; } - /** - * @brief Get the value that triggered the voice (note velocity or cc value) - * - * @return float - */ - float getTriggerValue() const noexcept { return triggerValue; } - /** - * @brief Get the type of trigger - * - * @return TriggerType - */ - TriggerType getTriggerType() const noexcept { return triggerType; } + const TriggerEvent& getTriggerEvent() const noexcept { return triggerEvent; } /** * @brief Reset the voice to its initial values @@ -432,9 +414,7 @@ private: State state { State::idle }; bool noteIsOff { false }; - TriggerType triggerType; - int triggerNumber; - float triggerValue; + TriggerEvent triggerEvent; absl::optional triggerDelay; float speedRatio { 1.0 }; @@ -496,13 +476,16 @@ inline bool sisterVoices(const Voice* lhs, const Voice* rhs) if (lhs->getAge() != rhs->getAge()) return false; - if (lhs->getTriggerNumber() != rhs->getTriggerNumber()) + const TriggerEvent& lhsTrigger = lhs->getTriggerEvent(); + const TriggerEvent& rhsTrigger = rhs->getTriggerEvent(); + + if (lhsTrigger.number != rhsTrigger.number) return false; - if (lhs->getTriggerValue() != rhs->getTriggerValue()) + if (lhsTrigger.value != rhsTrigger.value) return false; - if (lhs->getTriggerType() != rhs->getTriggerType()) + if (lhsTrigger.type != rhsTrigger.type) return false; return true; @@ -513,14 +496,17 @@ inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) if (lhs->getAge() != rhs->getAge()) return lhs->getAge() > rhs->getAge(); - if (lhs->getTriggerNumber() != rhs->getTriggerNumber()) - return lhs->getTriggerNumber() < rhs->getTriggerNumber(); + const TriggerEvent& lhsTrigger = lhs->getTriggerEvent(); + const TriggerEvent& rhsTrigger = rhs->getTriggerEvent(); - if (lhs->getTriggerValue() != rhs->getTriggerValue()) - return lhs->getTriggerValue() < rhs->getTriggerValue(); + if (lhsTrigger.number != rhsTrigger.number) + return lhsTrigger.number < rhsTrigger.number; - if (lhs->getTriggerType() != rhs->getTriggerType()) - return lhs->getTriggerType() > rhs->getTriggerType(); + if (lhsTrigger.value != rhsTrigger.value) + return lhsTrigger.value < rhsTrigger.value; + + if (lhsTrigger.type != rhsTrigger.type) + return lhsTrigger.type > rhsTrigger.type; return false; } diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 0fa4f1a7..7ea7c867 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -25,7 +25,7 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept // their sound, but it's reasonable for sounds with a quick attack and longer // release. const auto ageThreshold = - static_cast(voices.front()->getAge() * config::stealingAgeCoeff) + 1; + static_cast(voices.front()->getAge() * config::stealingAgeCoeff); Voice* returnedVoice = voices.front(); unsigned idx = 0; diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index b45b2e4c..b7de5c7a 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -227,11 +227,11 @@ TEST_CASE("[Polyphony] Self-masking") REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -248,11 +248,11 @@ TEST_CASE("[Polyphony] Not self-masking") REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -269,11 +269,11 @@ TEST_CASE("[Polyphony] Self-masking with the exact same velocity") REQUIRE( synth.getNumActiveVoices(true) == 3 ); // One of these is releasing synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // The first one is the masking candidate since they have the same velocity - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } @@ -286,9 +286,10 @@ TEST_CASE("[Polyphony] Self-masking only works from low to high") synth.noteOn(0, 64, 63 ); synth.noteOn(0, 64, 62 ); REQUIRE( synth.getNumActiveVoices(true) == 2 ); // Both notes are playing - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( numPlayingVoices(synth) == 2 ); // id + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } @@ -305,13 +306,13 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po REQUIRE( synth.getNumActiveVoices(true) == 4); synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 1 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(2)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); } @@ -333,9 +334,9 @@ TEST_CASE("[Polyphony] Note polyphony checks works across regions in the same po REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.renderBlock(buffer); REQUIRE( numPlayingVoices(synth) == 1 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } @@ -353,13 +354,13 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups") REQUIRE( synth.getNumActiveVoices(true) == 4); // Both notes are playing synth.renderBlock(buffer); REQUIRE(numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(0)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); REQUIRE( synth.getVoiceView(1)->releasedOrFree()); // got killed - REQUIRE( synth.getVoiceView(2)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); - REQUIRE( synth.getVoiceView(3)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); } @@ -381,8 +382,118 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi REQUIRE( synth.getNumActiveVoices(true) == 2 ); synth.renderBlock(buffer); REQUIRE(numPlayingVoices(synth) == 2 ); - REQUIRE( synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); - REQUIRE( synth.getVoiceView(1)->getTriggerValue() == 64_norm); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 64_norm); REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); } + +TEST_CASE("[Polyphony] Note polyphony operates on release voices") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + key=48 note_polyphony=1 sample=*saw trigger=release_key ampeg_attack=1 ampeg_decay=1 + )"); + synth.noteOn(0, 48, 63 ); + synth.noteOff(10, 48, 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); + synth.noteOn(20, 48, 65 ); + synth.noteOff(30, 48, 10 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + synth.renderBlock(buffer); + REQUIRE(numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 65_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works from low to high but takes into account the replaced velocity)") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + key=48 note_polyphony=1 sample=*saw trigger=release_key ampeg_attack=1 ampeg_decay=1 + )"); + synth.noteOn(0, 48, 63 ); + synth.noteOff(10, 48, 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 1); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(20, 48, 61 ); + synth.noteOff(30, 48, 10 ); + REQUIRE( synth.getNumActiveVoices(true) == 2 ); + REQUIRE( numPlayingVoices(synth) == 2 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 61_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + key=48 sample=*silence + key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1 + )"); + synth.cc(0, 64, 127); + synth.noteOn(0, 48, 61 ); + synth.noteOff(1, 48, 0 ); + synth.noteOn(2, 48, 62 ); + synth.noteOff(3, 48, 0 ); + synth.noteOn(4, 48, 63 ); + synth.noteOff(5, 48, 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 3); + REQUIRE( numPlayingVoices(synth) == 3 ); + synth.cc(20, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + REQUIRE( numPlayingVoices(synth) == 1 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 61_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 63_norm); + REQUIRE( synth.getVoiceView(2)->releasedOrFree()); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 61_norm); + REQUIRE( synth.getVoiceView(3)->releasedOrFree()); + REQUIRE( synth.getVoiceView(4)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(4)->releasedOrFree()); + REQUIRE( synth.getVoiceView(5)->getTriggerEvent().value == 63_norm); + REQUIRE(!synth.getVoiceView(5)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Note polyphony operates on release voices and sustain pedal (masking)") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path() / "tests/TestFiles/polyphony.sfz", R"( + key=48 sample=*silence + key=48 note_polyphony=1 sample=*saw trigger=release ampeg_attack=1 ampeg_decay=1 + )"); + synth.cc(0, 64, 127); + synth.noteOn(0, 48, 63 ); + synth.noteOff(1, 48, 0 ); + synth.noteOn(2, 48, 62 ); + synth.noteOff(3, 48, 0 ); + synth.noteOn(4, 48, 61 ); + synth.noteOff(5, 48, 0 ); + REQUIRE( synth.getNumActiveVoices(true) == 3); + REQUIRE( numPlayingVoices(synth) == 3 ); + synth.cc(20, 64, 0); + REQUIRE( synth.getNumActiveVoices(true) == 6 ); + REQUIRE( numPlayingVoices(synth) == 3 ); + REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); + REQUIRE( synth.getVoiceView(0)->releasedOrFree()); + REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); + REQUIRE( synth.getVoiceView(1)->releasedOrFree()); + REQUIRE( synth.getVoiceView(2)->getTriggerEvent().value == 61_norm); + REQUIRE( synth.getVoiceView(2)->releasedOrFree()); + REQUIRE( synth.getVoiceView(3)->getTriggerEvent().value == 63_norm); + REQUIRE(!synth.getVoiceView(3)->releasedOrFree()); + REQUIRE( synth.getVoiceView(4)->getTriggerEvent().value == 62_norm); + REQUIRE(!synth.getVoiceView(4)->releasedOrFree()); + REQUIRE( synth.getVoiceView(5)->getTriggerEvent().value == 61_norm); + REQUIRE(!synth.getVoiceView(5)->releasedOrFree()); +} diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 78f017b6..69f722ae 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -660,7 +660,7 @@ TEST_CASE("[Synth] Apply function on sisters") REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 3 ); float start = 1.0f; sfz::SisterVoiceRing::applyToRing(synth.getVoiceView(0), [&](const sfz::Voice* v) { - start += static_cast(v->getTriggerNumber()); + start += static_cast(v->getTriggerEvent().number); }); REQUIRE( start == 1.0f + 3.0f * 63.0f ); } @@ -828,7 +828,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release_key ignores the pedal)") std::vector requiredVelocities { 34_norm, 78_norm, 85_norm}; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -856,7 +856,7 @@ TEST_CASE("[Synth] Release (Multiple notes, release, cleared the delayed voices std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -886,7 +886,7 @@ TEST_CASE("[Synth] Release (Multiple notes after pedal is down, release, cleared std::vector requiredVelocities { 34_norm, 78_norm, 85_norm, 34_norm, 78_norm, 85_norm }; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -914,7 +914,7 @@ TEST_CASE("[Synth] Release (Multiple note ons during pedal down)") std::vector requiredVelocities { 78_norm, 85_norm, 78_norm, 85_norm }; std::vector actualVelocities; for (auto* v: getActiveVoices(synth)) { - actualVelocities.push_back(v->getTriggerValue()); + actualVelocities.push_back(v->getTriggerEvent().value); } sortAll(requiredVelocities, actualVelocities); REQUIRE( requiredVelocities == actualVelocities ); @@ -1256,8 +1256,24 @@ TEST_CASE("[Synth] Off by same group") REQUIRE( playingVoices.front()->getRegion()->keyRange.containsWithEnd(60) ); } +TEST_CASE("[Synth] Off by alone and repeated") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; -TEST_CASE("[Synth] Off by same note") + synth.loadSfzString(fs::current_path(), R"( + group=1 off_by=1 sample=*sine key=60 + )"); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 1 ); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.noteOn(0, 60, 85); + REQUIRE( numPlayingVoices(synth) == 3 ); +} + + +TEST_CASE("[Synth] Off by same note and group") { sfz::Synth synth; sfz::AudioBuffer buffer { 2, 256 }; @@ -1267,7 +1283,6 @@ TEST_CASE("[Synth] Off by same note") group=1 off_by=1 sample=*triangle key=60 )"); synth.noteOn(0, 60, 85); - REQUIRE( numPlayingVoices(synth) == 1 ); - auto playingVoices = getPlayingVoices(synth); - REQUIRE( playingVoices.front()->getRegion()->sampleId.filename() == "*triangle" ); + REQUIRE( numPlayingVoices(synth) == 2 ); + synth.noteOn(0, 60, 85); }