From ed5fb61f3831dc3e39fb6646c1fdad1cca45c279 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 10:56:12 +0200 Subject: [PATCH 01/15] Move a variable declaration Refactor the polyphony checks into reusable blocks --- src/sfizz/Synth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 5a0b6103..4903a644 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -979,7 +979,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); } - auto parent = region->parent; // Polyphony reached on region if (regionPolyphonyArray.size() >= region->polyphony) { @@ -995,6 +994,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } // Polyphony reached some parent group/master/etc + auto parent = region->parent; while (parent != nullptr) { if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); From ca7d74e8411a0769bb683bda48eb664135d61edd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 19:12:03 +0200 Subject: [PATCH 02/15] Move the voice selection after the polyphony checks --- src/sfizz/Synth.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 4903a644..e6da53ad 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -932,16 +932,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc 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->getRegion() == region && !voice->releasedOrFree()) { regionPolyphonyArray.push_back(voice.get()); } @@ -1003,6 +996,8 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc parent = parent->getParent(); } + Voice* selectedVoice = findFreeVoice(); + // Engine polyphony reached, we're stealing something if (selectedVoice == nullptr) { selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); From 5139c648f0dab00c3b9c600bbe5091e460436efa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 19:31:47 +0200 Subject: [PATCH 03/15] Engine polyphony is checked in findFreeVoice() --- src/sfizz/Synth.cpp | 29 ++++++++++++----------------- src/sfizz/Synth.h | 5 ++++- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e6da53ad..09fa9736 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -659,7 +659,18 @@ 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 {}; + + 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 @@ -997,27 +1008,11 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } Voice* selectedVoice = findFreeVoice(); - - // 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); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3fb85f7f..7dd3b0ad 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -765,18 +765,21 @@ 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 voiceViewArray; VoiceStealing stealer; - VoiceViewVector voiceViewArray; std::array noteActivationLists; std::array ccActivationLists; From ff1be61dc1142ef469888eb3a6a4cf777934acd2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 19:36:49 +0200 Subject: [PATCH 04/15] Use the same idiom for finding free voices --- src/sfizz/Synth.cpp | 50 +++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 09fa9736..2e9f1f41 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -922,14 +922,13 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex continue; } - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + if (Voice* selectedVoice = findFreeVoice()) { + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); + } } } } @@ -1007,17 +1006,13 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc parent = parent->getParent(); } - Voice* selectedVoice = findFreeVoice(); - // 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; - - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); + if (Voice* selectedVoice = findFreeVoice()) { + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); + } } } } @@ -1090,16 +1085,13 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept } 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); + if (Voice* selectedVoice = findFreeVoice()) { + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); + } } } } From afae38e6c3ab05b8148ee34a15b949c99e2cc075 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 20:09:45 +0200 Subject: [PATCH 05/15] Move the polyphony checks in separate functions --- src/sfizz/Synth.cpp | 153 ++++++++++++++++++++++++++------------------ src/sfizz/Synth.h | 7 +- 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 2e9f1f41..f334136a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -933,6 +933,92 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex } } +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, int number, float value, Voice::TriggerType triggerType) noexcept +{ + if (!region->notePolyphony) + return; + + unsigned notePolyphonyCounter { 0 }; + Voice* selfMaskCandidate { nullptr }; + + for (Voice* voice : voiceViewArray) { + if (!voice->releasedOrFree() + && voice->getRegion()->group == region->group + && voice->getTriggerNumber() == number + && voice->getTriggerType() ==triggerType) { + notePolyphonyCounter += 1; + switch (region->selfMask) { + case SfzSelfMask::mask: + if (voice->getTriggerValue() <= value) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) + 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); @@ -940,71 +1026,16 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - unsigned notePolyphonyCounter { 0 }; - Voice* selfMaskCandidate { nullptr }; - regionPolyphonyArray.clear(); for (auto& voice : voices) { - 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); - } - - - // 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 - auto parent = region->parent; - while (parent != nullptr) { - if (parent->numPlayingVoices() >= parent->getPolyphonyLimit()) { - const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); - SisterVoiceRing::offAllSisters(stealer.steal(activeVoices), delay); - } - parent = parent->getParent(); - } + checkNotePolyphony(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + checkRegionPolyphony(region, delay); + checkGroupPolyphony(region, delay); + checkSetPolyphony(region, delay); if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); @@ -1417,8 +1448,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 7dd3b0ad..a458e6c8 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -776,10 +776,15 @@ private: // 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; + void checkRegionPolyphony(const Region* region, int delay) noexcept; + void checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept; + void checkGroupPolyphony(const Region* region, int delay) noexcept; + void checkSetPolyphony(const Region* region, int delay) noexcept; + std::array noteActivationLists; std::array ccActivationLists; From dda10e8530174fa79fd3685e21c25fcd790d29e4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 20:47:51 +0200 Subject: [PATCH 06/15] Use a trigger event type instead of passing 3 parameters --- src/sfizz/Synth.cpp | 42 ++++++++++++++++++++------------- src/sfizz/Synth.h | 2 +- src/sfizz/TriggerEvent.h | 24 +++++++++++++++++++ src/sfizz/Voice.cpp | 38 ++++++++++++++---------------- src/sfizz/Voice.h | 51 +++++++++++++++++----------------------- tests/PolyphonyT.cpp | 46 ++++++++++++++++++------------------ tests/SynthT.cpp | 10 ++++---- 7 files changed, 117 insertions(+), 96 deletions(-) create mode 100644 src/sfizz/TriggerEvent.h diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f334136a..850456b2 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" @@ -892,12 +893,14 @@ 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) { +bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) +{ + const sfz::TriggerEvent& event = voice.getTriggerEvent(); return ( !voice.isFree() - && voice.getTriggerType() == sfz::Voice::TriggerType::NoteOn - && region.keyRange.containsWithEnd(voice.getTriggerNumber()) - && region.velocityRange.containsWithEnd(voice.getTriggerValue()) + && event.type == sfz::TriggerEventType::NoteOn + && region.keyRange.containsWithEnd(event.number) + && region.velocityRange.containsWithEnd(event.value) ); } @@ -905,6 +908,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex { 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)) { @@ -924,7 +928,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); + selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); @@ -949,7 +953,7 @@ void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept } } -void sfz::Synth::checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept +void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept { if (!region->notePolyphony) return; @@ -958,15 +962,16 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, int number, Voice* selfMaskCandidate { nullptr }; for (Voice* voice : voiceViewArray) { + const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); if (!voice->releasedOrFree() && voice->getRegion()->group == region->group - && voice->getTriggerNumber() == number - && voice->getTriggerType() ==triggerType) { + && voiceTriggerEvent.number == triggerEvent.number + && voiceTriggerEvent.type == triggerEvent.type) { notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: - if (voice->getTriggerValue() <= value) { - if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) + if (voiceTriggerEvent.value <= triggerEvent.value) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) selfMaskCandidate = voice; } break; @@ -1023,23 +1028,26 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc { 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)) { for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) - noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); + if (voice->checkOffGroup(delay, region->group)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } } - checkNotePolyphony(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + checkNotePolyphony(region, delay, triggerEvent); checkRegionPolyphony(region, delay); checkGroupPolyphony(region, delay); checkSetPolyphony(region, delay); if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); @@ -1082,6 +1090,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept voice->registerCC(delay, ccNumber, normValue); SisterVoiceRingBuilder ring; + const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, normValue }; for (auto& region : ccActivationLists[ccNumber]) { if (ccNumber == region->sustainCC) { @@ -1105,7 +1114,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept if (voice == nullptr) continue; - voice->startVoice(region, delay, note.first, note.second, Voice::TriggerType::NoteOff); + const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; + voice->startVoice(region, delay, noteOffEvent); ring.addVoiceToRing(voice); RegionSet::registerVoiceInHierarchy(region, voice); @@ -1118,7 +1128,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept if (region->registerCC(ccNumber, normValue)) { if (Voice* selectedVoice = findFreeVoice()) { ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); + selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index a458e6c8..1d2f1f5f 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -781,7 +781,7 @@ private: VoiceStealing stealer; void checkRegionPolyphony(const Region* region, int delay) noexcept; - void checkNotePolyphony(const Region* region, int delay, int number, float value, Voice::TriggerType triggerType) noexcept; + void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; 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..9687ebc4 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) { noteIsOff = true; if (region->loopMode == SfzLoopMode::one_shot) @@ -717,7 +713,7 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept if (region == nullptr) return false; - if (triggerType == TriggerType::NoteOn && region->offBy == group) { + if (triggerEvent.type == TriggerEventType::NoteOn && region->offBy == group) { off(delay); return true; } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 0aec391e..59ed0a46 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" @@ -113,11 +114,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. @@ -198,23 +197,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 +419,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 +481,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 +501,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/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index b45b2e4c..1610d7b3 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,9 @@ 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( 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 +305,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 +333,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 +353,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 +381,8 @@ 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()); } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 78f017b6..3cbc68fe 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 ); From 944373ea83749d4580f58e6fab500ca24e625286 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 21:34:42 +0200 Subject: [PATCH 07/15] Move common voice starting logic in a separate method --- src/sfizz/Synth.cpp | 61 ++++++++++++++++----------------------------- src/sfizz/Synth.h | 1 + 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 850456b2..890f990b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -893,6 +893,24 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept noteOffDispatch(delay, noteNumber, replacedVelocity); } +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 matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) { const sfz::TriggerEvent& event = voice.getTriggerEvent(); @@ -926,13 +944,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex continue; } - if (Voice* selectedVoice = findFreeVoice()) { - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, triggerEvent); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); - } + startVoice(region, delay, triggerEvent, ring); } } } @@ -1040,18 +1052,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } } - checkNotePolyphony(region, delay, triggerEvent); - checkRegionPolyphony(region, delay); - checkGroupPolyphony(region, delay); - checkSetPolyphony(region, delay); - - if (Voice* selectedVoice = findFreeVoice()) { - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, triggerEvent); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); - } + startVoice(region, delay, triggerEvent, ring); } } } @@ -1096,10 +1097,6 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept 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); }; @@ -1110,29 +1107,15 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept 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; - const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; - voice->startVoice(region, delay, noteOffEvent); - - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + startVoice(region, delay, noteOffEvent, ring); } region->delayedReleases.clear(); } if (region->registerCC(ccNumber, normValue)) { - if (Voice* selectedVoice = findFreeVoice()) { - ASSERT(selectedVoice->isFree()); - selectedVoice->startVoice(region, delay, triggerEvent); - ring.addVoiceToRing(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups[region->group].registerVoice(selectedVoice); - } + startVoice(region, delay, triggerEvent, ring); } } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 1d2f1f5f..73403913 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -784,6 +784,7 @@ private: void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; + void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; std::array noteActivationLists; std::array ccActivationLists; From 01c43f03d4a828d6639448d5732bc5f4d6441051 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 21:39:33 +0200 Subject: [PATCH 08/15] Move the delayed release check in a separate method --- src/sfizz/Synth.cpp | 48 +++++++++++++++++++++++---------------------- src/sfizz/Synth.h | 1 + 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 890f990b..6b558dbb 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1044,7 +1044,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - for (auto& voice : voices) { if (voice->checkOffGroup(delay, region->group)) { const TriggerEvent& event = voice->getTriggerEvent(); @@ -1057,6 +1056,28 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } } +void sfz::Synth::checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +{ + if (!region->rtDead) { + // check that a voice with compatible trigger is playing + 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... + 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); @@ -1092,31 +1113,12 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept SisterVoiceRingBuilder ring; const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, normValue }; - for (auto& region : ccActivationLists[ccNumber]) { - if (ccNumber == region->sustainCC) { - if (!region->rtDead) { - // check that a voice with compatible trigger is playing - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; + if (ccNumber == region->sustainCC) + checkDelayedReleases(region, delay, ring); - 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... - const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; - startVoice(region, delay, noteOffEvent, ring); - } - - region->delayedReleases.clear(); - } - - if (region->registerCC(ccNumber, normValue)) { + if (region->registerCC(ccNumber, normValue)) startVoice(region, delay, triggerEvent, ring); - } } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 73403913..d7d9543a 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -785,6 +785,7 @@ private: void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; + void checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; std::array noteActivationLists; std::array ccActivationLists; From e32348190f80e74313e49d7cf1e3c0c72405e2a0 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 21:59:55 +0200 Subject: [PATCH 09/15] Modularize the cc dispatch and the matching between a playing voice and release regions --- src/sfizz/Synth.cpp | 93 ++++++++++++++++++++++----------------------- src/sfizz/Synth.h | 5 ++- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 6b558dbb..3226d447 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -911,15 +911,22 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg polyphonyGroups[region->group].registerVoice(selectedVoice); } -bool matchReleaseRegionAndVoice(const sfz::Region& region, const sfz::Voice& voice) +bool sfz::Synth::matchAttackRegion(const Region* releaseRegion) noexcept { - const sfz::TriggerEvent& event = voice.getTriggerEvent(); - return ( - !voice.isFree() - && event.type == sfz::TriggerEventType::NoteOn - && region.keyRange.containsWithEnd(event.number) - && region.velocityRange.containsWithEnd(event.value) - ); + 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 @@ -930,19 +937,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex 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; - } + if (region->trigger == SfzTrigger::release && !region->rtDead && !matchAttackRegion(region)) + continue; startVoice(region, delay, triggerEvent, ring); } @@ -1036,6 +1032,16 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept } } +void sfz::Synth::checkOffGroups(Region* region, int delay) noexcept +{ + for (auto& voice : voices) { + if (voice->checkOffGroup(delay, region->group)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } + } +} + void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -1044,28 +1050,17 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) { - const TriggerEvent& event = voice->getTriggerEvent(); - noteOffDispatch(delay, event.number, event.value); - } - } - + checkOffGroups(region, delay); startVoice(region, delay, triggerEvent, ring); } } } -void sfz::Synth::checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept +void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept { - if (!region->rtDead) { - // check that a voice with compatible trigger is playing - const auto compatibleVoice = [region](const VoicePtr& v) -> bool { - return matchReleaseRegionAndVoice(*region, *v); - }; - - if (absl::c_find_if(voices, compatibleVoice) == voices.end()) - region->delayedReleases.clear(); + if (!region->rtDead && !matchAttackRegion(region)) { + region->delayedReleases.clear(); + return; } for (auto& note: region->delayedReleases) { @@ -1073,7 +1068,6 @@ void sfz::Synth::checkDelayedReleases(Region* region, int delay, SisterVoiceRing const TriggerEvent noteOffEvent { TriggerEventType::NoteOff, note.first, note.second }; startVoice(region, delay, noteOffEvent, ring); } - region->delayedReleases.clear(); } @@ -1084,6 +1078,19 @@ void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept 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); @@ -1111,15 +1118,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept for (auto& voice : voices) voice->registerCC(delay, ccNumber, normValue); - SisterVoiceRingBuilder ring; - const TriggerEvent triggerEvent { TriggerEventType::CC, ccNumber, normValue }; - for (auto& region : ccActivationLists[ccNumber]) { - if (ccNumber == region->sustainCC) - checkDelayedReleases(region, delay, ring); - - if (region->registerCC(ccNumber, normValue)) - startVoice(region, delay, triggerEvent, ring); - } + ccDispatch(delay, ccNumber, normValue); } void sfz::Synth::pitchWheel(int delay, int pitch) noexcept diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index d7d9543a..3a12c5bb 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -727,6 +727,7 @@ private: void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + void ccDispatch(int delay, int ccNumber, float value) noexcept; template static void updateUsedCCsFromCCMap(std::bitset& usedCCs, const CCMap map) @@ -785,7 +786,9 @@ private: void checkGroupPolyphony(const Region* region, int delay) noexcept; void checkSetPolyphony(const Region* region, int delay) noexcept; void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; - void checkDelayedReleases(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + void checkOffGroups(Region* region, int delay) noexcept; + void startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept; + bool matchAttackRegion(const Region* region) noexcept; std::array noteActivationLists; std::array ccActivationLists; From 025e87a0d1f22df3dc7ecef3d7119f9b3f6ba788 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 22:01:44 +0200 Subject: [PATCH 10/15] Remove the Voice::TriggerType enum --- src/sfizz/Voice.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 59ed0a46..682132ae 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -44,12 +44,6 @@ public: ~Voice(); - enum class TriggerType { - NoteOn, - NoteOff, - CC - }; - /** * @brief Get the unique identifier of this voice in a synth */ From 6750fb93a2ce8d14a0fc21d0a9417f7289840010 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 22:09:32 +0200 Subject: [PATCH 11/15] Comments and renaming --- src/sfizz/Synth.cpp | 6 ++-- src/sfizz/Synth.h | 86 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3226d447..f32b47fa 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -911,7 +911,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg polyphonyGroups[region->group].registerVoice(selectedVoice); } -bool sfz::Synth::matchAttackRegion(const Region* releaseRegion) noexcept +bool sfz::Synth::playingAttackVoice(const Region* releaseRegion) noexcept { const auto compatibleVoice = [releaseRegion](const Voice* v) -> bool { const sfz::TriggerEvent& event = v->getTriggerEvent(); @@ -937,7 +937,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { - if (region->trigger == SfzTrigger::release && !region->rtDead && !matchAttackRegion(region)) + if (region->trigger == SfzTrigger::release && !region->rtDead && !playingAttackVoice(region)) continue; startVoice(region, delay, triggerEvent, ring); @@ -1058,7 +1058,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc void sfz::Synth::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept { - if (!region->rtDead && !matchAttackRegion(region)) { + if (!region->rtDead && !playingAttackVoice(region)) { region->delayedReleases.clear(); return; } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3a12c5bb..6c22fdeb 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -725,8 +725,31 @@ 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 @@ -781,14 +804,75 @@ private: VoiceViewVector voiceViewArray; VoiceStealing stealer; + /** + * @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 Check the off groups of all playing voices, releasing if necessary + * + * @param region + * @param delay + */ void checkOffGroups(Region* region, int delay) 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; - bool matchAttackRegion(const Region* region) 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; From 1f65c135bcdb6f4f938105199337c17cabc7d98f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 25 Aug 2020 23:23:14 +0200 Subject: [PATCH 12/15] Don't release age 0 voices --- src/sfizz/Synth.cpp | 8 +++++++- src/sfizz/VoiceStealing.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f32b47fa..d141eda1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -665,6 +665,11 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept 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); @@ -991,8 +996,9 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg } } - if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) + if (notePolyphonyCounter >= *region->notePolyphony && selfMaskCandidate) { SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); + } } void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept 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; From fa37669e083c2669d57cae8c505fc17c824848fa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 26 Aug 2020 01:04:10 +0200 Subject: [PATCH 13/15] Add checks for release voices and note_polyphony --- src/sfizz/Synth.cpp | 6 ++++-- src/sfizz/Voice.cpp | 2 +- tests/PolyphonyT.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index d141eda1..64505a62 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -976,7 +976,8 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg for (Voice* voice : voiceViewArray) { const sfz::TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent(); - if (!voice->releasedOrFree() + 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) { @@ -984,8 +985,9 @@ void sfz::Synth::checkNotePolyphony(const Region* region, int delay, const Trigg switch (region->selfMask) { case SfzSelfMask::mask: if (voiceTriggerEvent.value <= triggerEvent.value) { - if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) + if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) { selfMaskCandidate = voice; + } } break; case SfzSelfMask::dontMask: diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 9687ebc4..117af317 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -193,7 +193,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, float velocity) noex if (state != State::playing) return; - if (triggerEvent.number == noteNumber) { + if (triggerEvent.number == noteNumber && triggerEvent.type == TriggerEventType::NoteOn) { noteIsOff = true; if (region->loopMode == SfzLoopMode::one_shot) diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 1610d7b3..1ff525cf 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -286,6 +286,7 @@ 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( numPlayingVoices(synth) == 2 ); // id REQUIRE( synth.getVoiceView(0)->getTriggerEvent().value == 63_norm); REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); REQUIRE( synth.getVoiceView(1)->getTriggerEvent().value == 62_norm); @@ -386,3 +387,45 @@ TEST_CASE("[Polyphony] Note polyphony do not operate across polyphony groups (wi 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()); +} From bb621282c9b13e4316fa4a2514a49b0fc23f6be6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 26 Aug 2020 01:27:57 +0200 Subject: [PATCH 14/15] Note polyphony tests and bugs --- src/sfizz/SisterVoiceRing.h | 11 ++---- tests/PolyphonyT.cpp | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 8 deletions(-) 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/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 1ff525cf..b7de5c7a 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -429,3 +429,71 @@ TEST_CASE("[Polyphony] Note polyphony operates on release voices (masking works 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()); +} From 863a08c421d563b11ae425a3ece59f89b8dc1c30 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 7 Sep 2020 23:18:02 +0200 Subject: [PATCH 15/15] Use Cakewalk/RGC behavior for self-choking notes Basically if group=off_by, a note will not choke other voices with the same note number --- src/sfizz/Synth.cpp | 18 +++++++----------- src/sfizz/Synth.h | 8 -------- src/sfizz/Voice.cpp | 8 +++++--- src/sfizz/Voice.h | 3 ++- tests/SynthT.cpp | 23 +++++++++++++++++++---- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 64505a62..b5f7298d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1040,16 +1040,6 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept } } -void sfz::Synth::checkOffGroups(Region* region, int delay) noexcept -{ - for (auto& voice : voices) { - if (voice->checkOffGroup(delay, region->group)) { - const TriggerEvent& event = voice->getTriggerEvent(); - noteOffDispatch(delay, event.number, event.value); - } - } -} - void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); @@ -1058,7 +1048,13 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - checkOffGroups(region, delay); + for (auto& voice : voices) { + if (voice->checkOffGroup(region, delay, noteNumber)) { + const TriggerEvent& event = voice->getTriggerEvent(); + noteOffDispatch(delay, event.number, event.value); + } + } + startVoice(region, delay, triggerEvent, ring); } } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 6c22fdeb..995bb5e6 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -848,14 +848,6 @@ private: */ void startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept; - /** - * @brief Check the off groups of all playing voices, releasing if necessary - * - * @param region - * @param delay - */ - void checkOffGroups(Region* region, int delay) noexcept; - /** * @brief Start all delayed release voices of the region if necessary * diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 117af317..4f120348 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -708,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 (triggerEvent.type == TriggerEventType::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 682132ae..a010aaa8 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -163,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 diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 3cbc68fe..69f722ae 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -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); }