diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h new file mode 100644 index 00000000..2f8fac16 --- /dev/null +++ b/src/sfizz/SisterVoiceRing.h @@ -0,0 +1,90 @@ +// 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 + +#include "Voice.h" +#include "absl/meta/type_traits.h" + +namespace sfz +{ + +struct SisterVoiceRing { + template>::value, int> = 0> + static void applyToRing(T* voice, F&& lambda) + { + auto v = voice->getNextSisterVoice(); + while (v != voice) { + const auto next = v->getNextSisterVoice(); + lambda(v); + v = next; + } + lambda(voice); + } + + static unsigned countSisterVoices(const Voice* start) + { + if (!start) + return 0; + + unsigned count = 0; + auto next = start; + do + { + count++; + next = next->getNextSisterVoice(); + } while (next != start && count < config::maxVoices); + + ASSERT(count < config::maxVoices); + return count; + } +}; + +/** + * @brief RAII helper to build sister voice rings. + * Closes the doubly-linked list on destruction. + * + */ +class SisterVoiceRingBuilder { +public: + ~SisterVoiceRingBuilder() { + if (lastStartedVoice != nullptr) { + ASSERT(firstStartedVoice); + lastStartedVoice->setNextSisterVoice(firstStartedVoice); + firstStartedVoice->setPreviousSisterVoice(lastStartedVoice); + } + } + + /** + * @brief Add a voice to the sister ring + * + * @param voice + */ + void addVoiceToRing(Voice* voice) { + if (firstStartedVoice == nullptr) + firstStartedVoice = voice; + + if (lastStartedVoice != nullptr) { + voice->setPreviousSisterVoice(lastStartedVoice); + lastStartedVoice->setNextSisterVoice(voice); + } + + lastStartedVoice = voice; + } + /** + * @brief Apply a function to the sister ring, including the current voice. + * This function should be safe enough to even reset the sister voices, but + * if you mutate the ring significantly you should probably roll your own + * iterator. + * + * @param lambda the function to apply. + * @param voice the starting voice + */ +private: + Voice* firstStartedVoice { nullptr }; + Voice* lastStartedVoice { nullptr }; +}; + +} diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3c35b6ee..95fcebda 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -15,6 +15,7 @@ #include "absl/algorithm/container.h" #include "absl/memory/memory.h" #include "absl/strings/str_replace.h" +#include "SisterVoiceRing.h" #include #include #include @@ -800,6 +801,8 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); + SisterVoiceRingBuilder ring; + for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { auto voice = findFreeVoice(); @@ -807,6 +810,7 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex continue; voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); + ring.addVoiceToRing(voice); } } } @@ -814,6 +818,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); + SisterVoiceRingBuilder ring; + for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { unsigned activeNotesInGroup { 0 }; @@ -865,6 +871,7 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + ring.addVoiceToRing(voice); } } } @@ -895,6 +902,8 @@ 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 (region->registerCC(ccNumber, normValue)) { auto voice = findFreeVoice(); @@ -902,6 +911,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept continue; voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); + ring.addVoiceToRing(voice); } } } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 768be01b..b4ac6c57 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -674,6 +674,30 @@ void sfz::Voice::reset() noexcept filters.clear(); equalizers.clear(); + + removeVoiceFromRing(); +} + +void sfz::Voice::setNextSisterVoice(Voice* voice) noexcept +{ + // Should never be null + ASSERT(voice); + nextSisterVoice = voice; +} + +void sfz::Voice::setPreviousSisterVoice(Voice* voice) noexcept +{ + // Should never be null + ASSERT(voice); + previousSisterVoice = voice; +} + +void sfz::Voice::removeVoiceFromRing() noexcept +{ + previousSisterVoice->setNextSisterVoice(nextSisterVoice); + nextSisterVoice->setPreviousSisterVoice(previousSisterVoice); + previousSisterVoice = this; + nextSisterVoice = this; } float sfz::Voice::getAverageEnvelope() const noexcept diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 0dd5557a..ca3797fb 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -210,6 +210,40 @@ public: */ void reset() noexcept; + /** + * @brief Set the next voice in the "sister voice" ring + * The sister voices are voices that started on the same event. + * This has to be set by the synth. A voice will remove itself from + * the ring upon reset. + * + * @param voice + */ + void setNextSisterVoice(Voice* voice) noexcept; + + /** + * @brief Set the previous voice in the "sister voice" ring + * The sister voices are voices that started on the same event. + * This has to be set by the synth. A voice will remove itself from + * the ring upon reset. + * + * @param voice + */ + void setPreviousSisterVoice(Voice* voice) noexcept; + + /** + * @brief Get the next sister voice in the ring + * + * @return Voice* + */ + Voice* getNextSisterVoice() const noexcept { return nextSisterVoice; }; + + /** + * @brief Get the previous sister voice in the ring + * + * @return Voice* + */ + Voice* getPreviousSisterVoice() const noexcept { return previousSisterVoice; }; + /** * @brief Get the mean squared power of the last rendered block. This is used * to determine which voice to steal if there are too many notes flying around. @@ -297,6 +331,12 @@ private: void panStageStereo(AudioSpan buffer) noexcept; void filterStageMono(AudioSpan buffer) noexcept; void filterStageStereo(AudioSpan buffer) noexcept; + + /** + * @brief Remove the voice from the sister ring + * + */ + void removeVoiceFromRing() noexcept; /** * @brief Initialize frequency and gain coefficients for the oscillators. */ @@ -359,6 +399,9 @@ private: Duration panningDuration; Duration filterDuration; + Voice* nextSisterVoice { this }; + Voice* previousSisterVoice { this }; + std::normal_distribution noiseDist { 0, config::noiseVariance }; std::array, 2> channelEnvelopeFilters; diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 08112bef..505b7d54 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/Synth.h" +#include "sfizz/SisterVoiceRing.h" #include "sfizz/SfzHelpers.h" #include "sfizz/NumericId.h" #include "catch2/catch.hpp" @@ -540,3 +541,79 @@ TEST_CASE("[Synth] sample quality") synth.allSoundOff(); synth.disableFreeWheeling(); } + + +TEST_CASE("[Synth] Sister voices") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=61 sample=*sine + key=62 sample=*sine + key=62 sample=*sine + key=63 sample=*saw + key=63 sample=*saw + key=63 sample=*saw + )"); + synth.noteOn(0, 61, 85); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); + REQUIRE( synth.getVoiceView(0)->getNextSisterVoice() == synth.getVoiceView(0) ); + REQUIRE( synth.getVoiceView(0)->getPreviousSisterVoice() == synth.getVoiceView(0) ); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 3 ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(1)) == 2 ); + REQUIRE( synth.getVoiceView(1)->getNextSisterVoice() == synth.getVoiceView(2) ); + REQUIRE( synth.getVoiceView(1)->getPreviousSisterVoice() == synth.getVoiceView(2) ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(2)) == 2 ); + REQUIRE( synth.getVoiceView(2)->getNextSisterVoice() == synth.getVoiceView(1) ); + REQUIRE( synth.getVoiceView(2)->getPreviousSisterVoice() == synth.getVoiceView(1) ); + synth.noteOn(0, 63, 85); + REQUIRE( synth.getNumActiveVoices() == 6 ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(3)) == 3 ); + REQUIRE( synth.getVoiceView(3)->getNextSisterVoice() == synth.getVoiceView(4) ); + REQUIRE( synth.getVoiceView(3)->getPreviousSisterVoice() == synth.getVoiceView(5) ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(4)) == 3 ); + REQUIRE( synth.getVoiceView(4)->getNextSisterVoice() == synth.getVoiceView(5) ); + REQUIRE( synth.getVoiceView(4)->getPreviousSisterVoice() == synth.getVoiceView(3) ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(5)) == 3 ); + REQUIRE( synth.getVoiceView(5)->getNextSisterVoice() == synth.getVoiceView(3) ); + REQUIRE( synth.getVoiceView(5)->getPreviousSisterVoice() == synth.getVoiceView(4) ); +} + +TEST_CASE("[Synth] Apply function on sisters") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + synth.loadSfzString(fs::current_path(), R"( + key=63 sample=*saw + key=63 sample=*saw + key=63 sample=*saw + )"); + synth.noteOn(0, 63, 85); + 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()); + }); + REQUIRE( start == 1.0f + 3.0f * 63.0f ); +} + +TEST_CASE("[Synth] Sisters and off-by") +{ + sfz::Synth synth; + sfz::AudioBuffer buffer { 2, 256 }; + synth.loadSfzString(fs::current_path(), R"( + key=62 sample=*sine + group=1 off_by=2 key=62 sample=*sine + group=2 key=63 sample=*saw + )"); + synth.noteOn(0, 62, 85); + REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 2 ); + synth.renderBlock(buffer); + REQUIRE( synth.getNumActiveVoices() == 2 ); + synth.noteOn(0, 63, 85); + REQUIRE( synth.getNumActiveVoices() == 3 ); + synth.renderBlock(buffer); + REQUIRE( synth.getNumActiveVoices() == 2 ); + REQUIRE( sfz::SisterVoiceRing::countSisterVoices(synth.getVoiceView(0)) == 1 ); +}