From 81a4ed22ebbd305a716116881a412517398ff29e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 31 Oct 2020 20:11:23 +0100 Subject: [PATCH] Final refactor into VoiceManager Cleanups.. --- common.mk | 1 + src/CMakeLists.txt | 2 + src/sfizz/Synth.cpp | 72 ++-- src/sfizz/VoiceList.h | 322 ------------------ src/sfizz/VoiceManager.cpp | 242 +++++++++++++ src/sfizz/VoiceManager.h | 193 +++++++++++ .../modulations/sources/ADSREnvelope.cpp | 10 +- src/sfizz/modulations/sources/ADSREnvelope.h | 6 +- .../modulations/sources/FlexEnvelope.cpp | 10 +- src/sfizz/modulations/sources/FlexEnvelope.h | 6 +- src/sfizz/modulations/sources/LFO.cpp | 8 +- src/sfizz/modulations/sources/LFO.h | 6 +- 12 files changed, 497 insertions(+), 381 deletions(-) delete mode 100644 src/sfizz/VoiceList.h create mode 100644 src/sfizz/VoiceManager.cpp create mode 100644 src/sfizz/VoiceManager.h diff --git a/common.mk b/common.mk index 27acfbc2..40abb575 100644 --- a/common.mk +++ b/common.mk @@ -115,6 +115,7 @@ SFIZZ_SOURCES = \ src/sfizz/Tuning.cpp \ src/sfizz/utility/SpinMutex.cpp \ src/sfizz/Voice.cpp \ + src/sfizz/VoiceManager.cpp \ src/sfizz/VoiceStealing.cpp \ src/sfizz/Wavetables.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d005c794..539a2ec0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -109,6 +109,7 @@ set (SFIZZ_HEADERS sfizz/SynthConfig.h sfizz/Tuning.h sfizz/Voice.h + sfizz/VoiceManager.h sfizz/VoiceStealing.h sfizz/Wavetables.h sfizz.h @@ -137,6 +138,7 @@ set (SFIZZ_SOURCES sfizz/Tuning.cpp sfizz/RegionSet.cpp sfizz/PolyphonyGroup.cpp + sfizz/VoiceManager.cpp sfizz/VoiceStealing.cpp sfizz/RTSemaphore.cpp sfizz/Panning.cpp diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7f0d7b91..f40823ea 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -30,7 +30,7 @@ #include "TriggerEvent.h" #include "utility/SpinMutex.h" #include "utility/XmlHelpers.h" -#include "VoiceList.h" +#include "VoiceManager.h" #include #include #include @@ -236,7 +236,7 @@ struct Synth::Impl: public Parser::Listener { using RegionPtr = std::unique_ptr; using RegionSetPtr = std::unique_ptr; std::vector regions_; - VoiceList voiceList_; + VoiceManager voiceManager_; // These are more general "groups" than sfz and encapsulates the full hierarchy RegionSet* currentSet_ { nullptr }; @@ -327,16 +327,16 @@ Synth::Impl::Impl() // modulation sources genController_.reset(new ControllerSource(resources_)); - genLFO_.reset(new LFOSource(voiceList_)); - genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceList_)); - genADSREnvelope_.reset(new ADSREnvelopeSource(voiceList_, resources_.midiState)); + genLFO_.reset(new LFOSource(voiceManager_)); + genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_)); + genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState)); } Synth::Impl::~Impl() { const std::lock_guard disableCallback { callbackGuard_ }; - voiceList_.reset(); + voiceManager_.reset(); resources_.filePool.emptyFileLoadingQueues(); } @@ -461,10 +461,10 @@ void Synth::Impl::buildRegion(const std::vector& regionOpcodes) // There was a combination of group= and polyphony= on a region, so set the group polyphony if (lastRegion->group != Default::group && lastRegion->polyphony != config::maxVoices) { - voiceList_.setGroupPolyphony(lastRegion->group, lastRegion->polyphony); + voiceManager_.setGroupPolyphony(lastRegion->group, lastRegion->polyphony); } else { // Just check that there are enough polyphony groups - voiceList_.ensureNumPolyphonyGroups(lastRegion->group); + voiceManager_.ensureNumPolyphonyGroups(lastRegion->group); } if (currentSet_ != nullptr) { @@ -483,7 +483,7 @@ void Synth::Impl::clear() // Clear the background queues before removing everyone resources_.filePool.waitForBackgroundLoading(); - voiceList_.reset(); + voiceManager_.reset(); for (auto& list : lastKeyswitchLists_) list.clear(); for (auto& list : downKeyswitchLists_) @@ -603,12 +603,12 @@ void Synth::Impl::handleGroupOpcodes(const std::vector& members, const s parseOpcode(member); if (groupIdx && maxPolyphony) { - voiceList_.setGroupPolyphony(*groupIdx, *maxPolyphony); + voiceManager_.setGroupPolyphony(*groupIdx, *maxPolyphony); } else if (maxPolyphony) { ASSERT(currentSet_ != nullptr); currentSet_->setPolyphonyLimit(*maxPolyphony); } else if (groupIdx) { - voiceList_.ensureNumPolyphonyGroups(*groupIdx); + voiceManager_.ensureNumPolyphonyGroups(*groupIdx); } } @@ -663,13 +663,13 @@ void Synth::Impl::handleControlOpcodes(const std::vector& members) case hash("hint_stealing"): switch(hash(member.value)) { case hash("first"): - voiceList_.setStealingAlgorithm(StealingAlgorithm::First); + voiceManager_.setStealingAlgorithm(StealingAlgorithm::First); break; case hash("oldest"): - voiceList_.setStealingAlgorithm(StealingAlgorithm::Oldest); + voiceManager_.setStealingAlgorithm(StealingAlgorithm::Oldest); break; case hash("envelope_and_age"): - voiceList_.setStealingAlgorithm(StealingAlgorithm::EnvelopeAndAge); + voiceManager_.setStealingAlgorithm(StealingAlgorithm::EnvelopeAndAge); break; default: DBG("Unsupported value for hint_stealing: " << member.value); @@ -1045,7 +1045,7 @@ void Synth::loadStretchTuningByRatio(float ratio) int Synth::getNumActiveVoices() const noexcept { Impl& impl = *impl_; - return static_cast(impl.voiceList_.getNumActiveVoices()); + return static_cast(impl.voiceManager_.getNumActiveVoices()); } void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept @@ -1056,7 +1056,7 @@ void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept const std::lock_guard disableCallback { impl.callbackGuard_ }; impl.samplesPerBlock_ = samplesPerBlock; - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.setSamplesPerBlock(samplesPerBlock); impl.resources_.setSamplesPerBlock(samplesPerBlock); @@ -1073,7 +1073,7 @@ void Synth::setSampleRate(float sampleRate) noexcept const std::lock_guard disableCallback { impl.callbackGuard_ }; impl.sampleRate_ = sampleRate; - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.setSampleRate(sampleRate); impl.resources_.setSampleRate(sampleRate); @@ -1136,7 +1136,7 @@ void Synth::renderBlock(AudioSpan buffer) noexcept ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; tempMixSpan->fill(0.0f); - for (auto& voice : impl.voiceList_) { + for (auto& voice : impl.voiceManager_) { if (voice.isFree()) continue; @@ -1243,7 +1243,7 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept // auto replacedVelocity = (velocity == 0 ? getNoteVelocity(noteNumber) : velocity); const auto replacedVelocity = impl.resources_.midiState.getNoteVelocity(noteNumber); - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.registerNoteOff(delay, noteNumber, replacedVelocity); impl.noteOffDispatch(delay, noteNumber, replacedVelocity); @@ -1251,8 +1251,8 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept void Synth::Impl::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept { - voiceList_.checkPolyphony(region, delay, triggerEvent); - Voice* selectedVoice = voiceList_.findFreeVoice(); + voiceManager_.checkPolyphony(region, delay, triggerEvent); + Voice* selectedVoice = voiceManager_.findFreeVoice(); if (selectedVoice == nullptr) return; @@ -1275,7 +1275,7 @@ void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noe for (auto& region : noteActivationLists_[noteNumber]) { if (region->registerNoteOff(noteNumber, velocity, randValue)) { - if (region->trigger == SfzTrigger::release && !region->rtDead && !voiceList_.playingAttackVoice(region)) + if (region->trigger == SfzTrigger::release && !region->rtDead && !voiceManager_.playingAttackVoice(region)) continue; startVoice(region, delay, triggerEvent, ring); @@ -1308,7 +1308,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex for (auto& region : noteActivationLists_[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - for (auto& voice : voiceList_) { + for (auto& voice : voiceManager_) { if (voice.checkOffGroup(region, delay, noteNumber)) { const TriggerEvent& event = voice.getTriggerEvent(); noteOffDispatch(delay, event.number, event.value); @@ -1325,7 +1325,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex void Synth::Impl::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept { - if (!region->rtDead && !voiceList_.playingAttackVoice(region)) { + if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) { region->delayedReleases.clear(); return; } @@ -1377,13 +1377,13 @@ void Synth::hdcc(int delay, int ccNumber, float normValue) noexcept } if (ccNumber == config::allNotesOffCC || ccNumber == config::allSoundOffCC) { - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.reset(); impl.resources_.midiState.allNotesOff(delay); return; } - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.registerCC(delay, ccNumber, normValue); impl.ccDispatch(delay, ccNumber, normValue); @@ -1427,7 +1427,7 @@ void Synth::pitchWheel(int delay, int pitch) noexcept region->registerPitchWheel(normalizedPitch); } - for (auto& voice : impl.voiceList_) { + for (auto& voice : impl.voiceManager_) { voice.registerPitchWheel(delay, normalizedPitch); } } @@ -1614,7 +1614,7 @@ const RegionSet* Synth::getRegionSetView(int idx) const noexcept const PolyphonyGroup* Synth::getPolyphonyGroupView(int idx) const noexcept { Impl& impl = *impl_; - return impl.voiceList_.getPolyphonyGroupView(idx); + return impl.voiceManager_.getPolyphonyGroupView(idx); } const Region* Synth::getRegionById(NumericId id) const noexcept @@ -1638,13 +1638,13 @@ const Region* Synth::getRegionById(NumericId id) const noexcept const Voice* Synth::getVoiceView(int idx) const noexcept { Impl& impl = *impl_; - return idx < impl.numVoices_ ? &impl.voiceList_[idx] : nullptr; + return idx < impl.numVoices_ ? &impl.voiceManager_[idx] : nullptr; } unsigned Synth::getNumPolyphonyGroups() const noexcept { Impl& impl = *impl_; - return impl.voiceList_.getNumPolyphonyGroups(); + return impl.voiceManager_.getNumPolyphonyGroups(); } const std::vector& Synth::getUnknownOpcodes() const noexcept @@ -1730,9 +1730,9 @@ void Synth::Impl::resetVoices(int numVoices) engineSet_->removeAllVoices(); engineSet_->setPolyphonyLimit(numVoices_); - voiceList_.requireNumVoices(numVoices_, resources_); + voiceManager_.requireNumVoices(numVoices_, resources_); - for (auto& voice : voiceList_) { + for (auto& voice : voiceManager_) { voice.setSampleRate(this->sampleRate_); voice.setSamplesPerBlock(this->samplesPerBlock_); } @@ -1742,7 +1742,7 @@ void Synth::Impl::resetVoices(int numVoices) void Synth::Impl::applySettingsPerVoice() { - for (auto& voice : voiceList_) { + for (auto& voice : voiceManager_) { voice.setMaxFiltersPerVoice(settingsPerVoice_.maxFilters); voice.setMaxEQsPerVoice(settingsPerVoice_.maxEQs); voice.setMaxLFOsPerVoice(settingsPerVoice_.maxLFOs); @@ -1829,7 +1829,7 @@ void Synth::setOversamplingFactor(Oversampling factor) noexcept if (factor == impl.oversamplingFactor_) return; - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.reset(); impl.resources_.filePool.emptyFileLoadingQueues(); @@ -1886,7 +1886,7 @@ void Synth::Impl::resetAllControllers(int delay) noexcept if (!lock.owns_lock()) return; - for (auto& voice : voiceList_) { + for (auto& voice : voiceManager_) { voice.registerPitchWheel(delay, 0); for (int cc = 0; cc < config::numCCs; ++cc) voice.registerCC(delay, cc, 0.0f); @@ -1945,7 +1945,7 @@ void Synth::allSoundOff() noexcept Impl& impl = *impl_; const std::lock_guard disableCallback { impl.callbackGuard_ }; - for (auto& voice : impl.voiceList_) + for (auto& voice : impl.voiceManager_) voice.reset(); for (auto& effectBus : impl.effectBuses_) effectBus->clear(); diff --git a/src/sfizz/VoiceList.h b/src/sfizz/VoiceList.h deleted file mode 100644 index 5e9c572e..00000000 --- a/src/sfizz/VoiceList.h +++ /dev/null @@ -1,322 +0,0 @@ -// 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 - -#include "Voice.h" -#include "Resources.h" -#include "Config.h" -#include "Region.h" -#include "SisterVoiceRing.h" -#include "PolyphonyGroup.h" -#include "RegionSet.h" -#include "VoiceStealing.h" -#include -#include - -namespace sfz { - -struct VoiceList : public Voice::StateListener -{ - /** - * @brief The voice callback which is called during a change of state. - */ - void onVoiceStateChanging(NumericId id, Voice::State state) final - { - (void)id; - if (state == Voice::State::idle) { - auto voice = getVoiceById(id); - RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); - swapAndPopFirst(activeVoices_, [voice](const Voice* v) { return v == voice; }); - polyphonyGroups_[voice->getRegion()->group].removeVoice(voice); - } else if (state == Voice::State::playing) { - auto voice = getVoiceById(id); - activeVoices_.push_back(voice); - RegionSet::registerVoiceInHierarchy(voice->getRegion(), voice); - polyphonyGroups_[voice->getRegion()->group].registerVoice(voice); - } - } - /** - * @brief Find the voice which is associated with the given identifier. - * - * @param id - * @return const Voice* - */ - const Voice* getVoiceById(NumericId id) const noexcept - { - const size_t size = list_.size(); - - if (size == 0 || !id.valid()) - return nullptr; - - // search a sequence of ordered identifiers with potential gaps - size_t index = static_cast(id.number()); - index = std::min(index, size - 1); - - while (index > 0 && list_[index].getId().number() > id.number()) - --index; - - return (list_[index].getId() == id) ? &list_[index] : nullptr; - } - - Voice* getVoiceById(NumericId id) noexcept - { - return const_cast( - const_cast(this)->getVoiceById(id)); - } - - void reset() - { - for (auto& voice : list_) - voice.reset(); - - polyphonyGroups_.clear(); - polyphonyGroups_.emplace_back(); - polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices); - setStealingAlgorithm(StealingAlgorithm::Oldest); - } - - bool playingAttackVoice(const Region* releaseRegion) noexcept - { - const auto compatibleVoice = [releaseRegion](const Voice& v) -> bool { - const TriggerEvent& event = v.getTriggerEvent(); - return ( - !v.isFree() - && event.type == TriggerEventType::NoteOn - && releaseRegion->keyRange.containsWithEnd(event.number) - && releaseRegion->velocityRange.containsWithEnd(event.value) - ); - }; - - if (absl::c_find_if(list_, compatibleVoice) == list_.end()) - return false; - else - return true; - } - - void ensureNumPolyphonyGroups(unsigned groupIdx) noexcept - { - while (polyphonyGroups_.size() <= groupIdx) - polyphonyGroups_.emplace_back(); - } - - void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept - { - ensureNumPolyphonyGroups(groupIdx); - polyphonyGroups_[groupIdx].setPolyphonyLimit(polyphony); - } - - size_t getNumPolyphonyGroups() const noexcept { return polyphonyGroups_.size(); } - - const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept - { - return (size_t)idx < polyphonyGroups_.size() ? &polyphonyGroups_[idx] : nullptr; - } - - void clear() - { - reset(); - list_.clear(); - activeVoices_.clear(); - } - - void setStealingAlgorithm(StealingAlgorithm algorithm) - { - switch(algorithm){ - case StealingAlgorithm::First: // fallthrough - for (auto& voice : list_) - voice.disablePowerFollower(); - - stealer_ = absl::make_unique(); - break; - case StealingAlgorithm::Oldest: - for (auto& voice : list_) - voice.disablePowerFollower(); - - stealer_ = absl::make_unique(); - break; - case StealingAlgorithm::EnvelopeAndAge: - for (auto& voice : list_) - voice.enablePowerFollower(); - - stealer_ = absl::make_unique(); - break; - } - } - - void checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept - { - checkNotePolyphony(region, delay, triggerEvent); - checkRegionPolyphony(region, delay); - checkGroupPolyphony(region, delay); - checkSetPolyphony(region, delay); - checkEnginePolyphony(delay); - } - - /** - * @brief Get the number of active voices - * - * @return unsigned - */ - unsigned getNumActiveVoices() const - { - return activeVoices_.size(); - } - - /** - * @brief Find a voice that is not currently playing - * - * @return Voice* - */ - Voice* findFreeVoice() noexcept - { - auto freeVoice = absl::c_find_if(list_, [](const Voice& voice) { - return voice.isFree(); - }); - - if (freeVoice != list_.end()) - return &*freeVoice; - - DBG("Engine hard polyphony reached"); - return {}; - } - - void requireNumVoices(int numVoices, Resources& resources) - { - numActualVoices_ = - static_cast(config::overflowVoiceMultiplier * numVoices); - numRequiredVoices_ = numVoices; - - clear(); - list_.reserve(numActualVoices_); - activeVoices_.reserve(numActualVoices_); - - for (int i = 0; i < numActualVoices_; ++i) { - list_.emplace_back(i, resources); - Voice& lastVoice = list_.back(); - lastVoice.setStateListener(this); - } - } - -private: - int numRequiredVoices_ { config::numVoices }; - int numActualVoices_ { static_cast(config::numVoices * config::overflowVoiceMultiplier) }; - std::vector list_; - std::vector activeVoices_; - // These are the `group=` groups where you can off voices - std::vector polyphonyGroups_; - std::unique_ptr stealer_ { absl::make_unique() }; - - /** - * @brief Check the region polyphony, releasing voices if necessary - * - * @param region - * @param delay - */ - void checkRegionPolyphony(const Region* region, int delay) noexcept - { - Voice* candidate = stealer_->checkRegionPolyphony(region, absl::MakeSpan(activeVoices_)); - SisterVoiceRing::offAllSisters(candidate, delay); - } - - /** - * @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 - { - if (!region->notePolyphony) - return; - - unsigned notePolyphonyCounter { 0 }; - Voice* selfMaskCandidate { nullptr }; - - for (Voice* voice : activeVoices_) { - const 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->getAge() < voice->getAge()) - selfMaskCandidate = voice; - break; - } - } - } - - if (notePolyphonyCounter >= *region->notePolyphony) { - SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); - } - } - - /** - * @brief Check the group polyphony, releasing voices if necessary - * - * @param region - * @param delay - */ - void checkGroupPolyphony(const Region* region, int delay) noexcept - { - auto& group = polyphonyGroups_[region->group]; - Voice* candidate = stealer_->checkPolyphony( - absl::MakeSpan(group.getActiveVoices()), group.getPolyphonyLimit()); - SisterVoiceRing::offAllSisters(candidate, delay); - } - - /** - * @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 - { - auto parent = region->parent; - while (parent != nullptr) { - Voice* candidate = stealer_->checkPolyphony( - absl::MakeSpan(parent->getActiveVoices()), parent->getPolyphonyLimit()); - SisterVoiceRing::offAllSisters(candidate, delay); - parent = parent->getParent(); - } - } - - /** - * @brief Check the engine polyphony, fast releasing voices if necessary - * - * @param delay - */ - void checkEnginePolyphony(int delay) noexcept - { - Voice* candidate = stealer_->checkPolyphony( - absl::MakeSpan(activeVoices_), numRequiredVoices_); - SisterVoiceRing::offAllSisters(candidate, delay); - } - -public: - // Vector shortcuts - typename decltype(list_)::iterator begin() { return list_.begin(); } - typename decltype(list_)::const_iterator cbegin() const { return list_.cbegin(); } - typename decltype(list_)::iterator end() { return list_.end(); } - typename decltype(list_)::const_iterator cend() const { return list_.cend(); } - typename decltype(list_)::reference operator[] (size_t n) { return list_[n]; } - typename decltype(list_)::const_reference operator[] (size_t n) const { return list_[n]; } -}; - -} // namespace sfz diff --git a/src/sfizz/VoiceManager.cpp b/src/sfizz/VoiceManager.cpp new file mode 100644 index 00000000..f8bf71f8 --- /dev/null +++ b/src/sfizz/VoiceManager.cpp @@ -0,0 +1,242 @@ +// 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 "VoiceManager.h" +#include "SisterVoiceRing.h" +#include "RegionSet.h" +#include + +namespace sfz { + +void VoiceManager::onVoiceStateChanging(NumericId id, Voice::State state) +{ + (void)id; + if (state == Voice::State::idle) { + auto voice = getVoiceById(id); + RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); + swapAndPopFirst(activeVoices_, [voice](const Voice* v) { return v == voice; }); + polyphonyGroups_[voice->getRegion()->group].removeVoice(voice); + } else if (state == Voice::State::playing) { + auto voice = getVoiceById(id); + activeVoices_.push_back(voice); + RegionSet::registerVoiceInHierarchy(voice->getRegion(), voice); + polyphonyGroups_[voice->getRegion()->group].registerVoice(voice); + } +} + +const Voice* VoiceManager::getVoiceById(NumericId id) const noexcept +{ + const size_t size = list_.size(); + + if (size == 0 || !id.valid()) + return nullptr; + + // search a sequence of ordered identifiers with potential gaps + size_t index = static_cast(id.number()); + index = std::min(index, size - 1); + + while (index > 0 && list_[index].getId().number() > id.number()) + --index; + + return (list_[index].getId() == id) ? &list_[index] : nullptr; +} + +Voice* VoiceManager::getVoiceById(NumericId id) noexcept +{ + return const_cast( + const_cast(this)->getVoiceById(id)); +} + +void VoiceManager::reset() +{ + for (auto& voice : list_) + voice.reset(); + + polyphonyGroups_.clear(); + polyphonyGroups_.emplace_back(); + polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices); + setStealingAlgorithm(StealingAlgorithm::Oldest); +} + +bool VoiceManager::playingAttackVoice(const Region* releaseRegion) noexcept +{ + const auto compatibleVoice = [releaseRegion](const Voice& v) -> bool { + const TriggerEvent& event = v.getTriggerEvent(); + return ( + !v.isFree() + && event.type == TriggerEventType::NoteOn + && releaseRegion->keyRange.containsWithEnd(event.number) + && releaseRegion->velocityRange.containsWithEnd(event.value) + ); + }; + + if (absl::c_find_if(list_, compatibleVoice) == list_.end()) + return false; + else + return true; +} + +void VoiceManager::ensureNumPolyphonyGroups(unsigned groupIdx) noexcept +{ + while (polyphonyGroups_.size() <= groupIdx) + polyphonyGroups_.emplace_back(); +} + +void VoiceManager::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept +{ + ensureNumPolyphonyGroups(groupIdx); + polyphonyGroups_[groupIdx].setPolyphonyLimit(polyphony); +} + + +const PolyphonyGroup* VoiceManager::getPolyphonyGroupView(int idx) const noexcept +{ + return (size_t)idx < polyphonyGroups_.size() ? &polyphonyGroups_[idx] : nullptr; +} + +void VoiceManager::clear() +{ + reset(); + list_.clear(); + activeVoices_.clear(); +} + +void VoiceManager::setStealingAlgorithm(StealingAlgorithm algorithm) +{ + switch(algorithm){ + case StealingAlgorithm::First: // fallthrough + for (auto& voice : list_) + voice.disablePowerFollower(); + + stealer_ = absl::make_unique(); + break; + case StealingAlgorithm::Oldest: + for (auto& voice : list_) + voice.disablePowerFollower(); + + stealer_ = absl::make_unique(); + break; + case StealingAlgorithm::EnvelopeAndAge: + for (auto& voice : list_) + voice.enablePowerFollower(); + + stealer_ = absl::make_unique(); + break; + } +} + +void VoiceManager::checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept +{ + checkNotePolyphony(region, delay, triggerEvent); + checkRegionPolyphony(region, delay); + checkGroupPolyphony(region, delay); + checkSetPolyphony(region, delay); + checkEnginePolyphony(delay); +} + +Voice* VoiceManager::findFreeVoice() noexcept +{ + auto freeVoice = absl::c_find_if(list_, [](const Voice& voice) { + return voice.isFree(); + }); + + if (freeVoice != list_.end()) + return &*freeVoice; + + DBG("Engine hard polyphony reached"); + return {}; +} + +void VoiceManager::requireNumVoices(int numVoices, Resources& resources) +{ + numActualVoices_ = + static_cast(config::overflowVoiceMultiplier * numVoices); + numRequiredVoices_ = numVoices; + + clear(); + list_.reserve(numActualVoices_); + activeVoices_.reserve(numActualVoices_); + + for (int i = 0; i < numActualVoices_; ++i) { + list_.emplace_back(i, resources); + Voice& lastVoice = list_.back(); + lastVoice.setStateListener(this); + } +} + +void VoiceManager::checkRegionPolyphony(const Region* region, int delay) noexcept +{ + Voice* candidate = stealer_->checkRegionPolyphony(region, absl::MakeSpan(activeVoices_)); + SisterVoiceRing::offAllSisters(candidate, delay); +} + +void VoiceManager::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept +{ + if (!region->notePolyphony) + return; + + unsigned notePolyphonyCounter { 0 }; + Voice* selfMaskCandidate { nullptr }; + + for (Voice* voice : activeVoices_) { + const 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->getAge() < voice->getAge()) + selfMaskCandidate = voice; + break; + } + } + } + + if (notePolyphonyCounter >= *region->notePolyphony) { + SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); + } +} + +void VoiceManager::checkGroupPolyphony(const Region* region, int delay) noexcept +{ + auto& group = polyphonyGroups_[region->group]; + Voice* candidate = stealer_->checkPolyphony( + absl::MakeSpan(group.getActiveVoices()), group.getPolyphonyLimit()); + SisterVoiceRing::offAllSisters(candidate, delay); +} + +void VoiceManager::checkSetPolyphony(const Region* region, int delay) noexcept +{ + auto parent = region->parent; + while (parent != nullptr) { + Voice* candidate = stealer_->checkPolyphony( + absl::MakeSpan(parent->getActiveVoices()), parent->getPolyphonyLimit()); + SisterVoiceRing::offAllSisters(candidate, delay); + parent = parent->getParent(); + } +} + +void VoiceManager::checkEnginePolyphony(int delay) noexcept +{ + Voice* candidate = stealer_->checkPolyphony( + absl::MakeSpan(activeVoices_), numRequiredVoices_); + SisterVoiceRing::offAllSisters(candidate, delay); +} + +} // namespace sfz diff --git a/src/sfizz/VoiceManager.h b/src/sfizz/VoiceManager.h new file mode 100644 index 00000000..b9e8fff0 --- /dev/null +++ b/src/sfizz/VoiceManager.h @@ -0,0 +1,193 @@ +// 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 + +#include "Voice.h" +#include "Resources.h" +#include "Config.h" +#include "Region.h" +#include "PolyphonyGroup.h" +#include "VoiceStealing.h" +#include + +namespace sfz { + +struct VoiceManager : public Voice::StateListener +{ + /** + * @brief The voice callback which is called during a change of state. + */ + void onVoiceStateChanging(NumericId id, Voice::State state) final; + + /** + * @brief Find the voice which is associated with the given identifier. + * + * @param id + * @return const Voice* + */ + const Voice* getVoiceById(NumericId id) const noexcept; + + /** + * @brief Find the voice which is associated with the given identifier. + * + * @param id + * @return Voice* + */ + Voice* getVoiceById(NumericId id) noexcept; + + /** + * @brief Reset all voices and clear the polyphony groups + */ + void reset(); + + /** + * @brief Check if a compatible attack voice is playing for the release region. + * + * @param releaseRegion + * @return true + * @return false + */ + bool playingAttackVoice(const Region* releaseRegion) noexcept; + + /** + * @brief Ensures that the polyphony groups are at least this size. + * Call this each time a new `group=N` is given in an sfz file. + * + * @param groupIdx + */ + void ensureNumPolyphonyGroups(unsigned groupIdx) noexcept; + + /** + * @brief Set the polyphony for a given group + * If the number of polyphony groups is too small, it will + * be increased. + * + * @param groupIdx + * @param polyphony + */ + void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept; + + /** + * @brief Get a view into a given polyphony group + * + * @param idx + * @return const PolyphonyGroup* + */ + const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept; + + /** + * @brief Clear all voices and polyphony groups. + * Also resets the stealing algorithm to default. + */ + void clear(); + + /** + * @brief Set the stealing algorithm + * + * @param algorithm + */ + void setStealingAlgorithm(StealingAlgorithm algorithm); + + /** + * @brief Off voices as necessary depending on the trigger event and started region + * + * @param region + * @param delay + * @param triggerEvent + */ + void checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept; + + /** + * @brief Get the number of active voices + * + * @return size_t + */ + size_t getNumActiveVoices() const { return activeVoices_.size(); } + + /** + * @brief Get the number of polyphony groups + * + * @return size_t + */ + size_t getNumPolyphonyGroups() const noexcept { return polyphonyGroups_.size(); } + + /** + * @brief Find a voice that is not currently playing + * + * @return Voice* + */ + Voice* findFreeVoice() noexcept; + /** + * @brief Require a number of voices from this manager. + * In practice, the manager will handle slightly more, in order to + * allow voices to die off upon reaching higher polyphony count. + * + * @param numVoices + * @param resources + */ + void requireNumVoices(int numVoices, Resources& resources); + +private: + int numRequiredVoices_ { config::numVoices }; + int numActualVoices_ { static_cast(config::numVoices * config::overflowVoiceMultiplier) }; + std::vector list_; + std::vector activeVoices_; + // These are the `group=` groups where you can off voices + std::vector polyphonyGroups_; + std::unique_ptr stealer_ { absl::make_unique() }; + + /** + * @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 Check the engine polyphony, fast releasing voices if necessary + * + * @param delay + */ + void checkEnginePolyphony(int delay) noexcept; + +public: + // Vector shortcuts + typename decltype(list_)::iterator begin() { return list_.begin(); } + typename decltype(list_)::const_iterator cbegin() const { return list_.cbegin(); } + typename decltype(list_)::iterator end() { return list_.end(); } + typename decltype(list_)::const_iterator cend() const { return list_.cend(); } + typename decltype(list_)::reference operator[] (size_t n) { return list_[n]; } + typename decltype(list_)::const_reference operator[] (size_t n) const { return list_[n]; } +}; + +} // namespace sfz diff --git a/src/sfizz/modulations/sources/ADSREnvelope.cpp b/src/sfizz/modulations/sources/ADSREnvelope.cpp index dfe67317..2f589282 100644 --- a/src/sfizz/modulations/sources/ADSREnvelope.cpp +++ b/src/sfizz/modulations/sources/ADSREnvelope.cpp @@ -15,14 +15,14 @@ namespace sfz { -ADSREnvelopeSource::ADSREnvelopeSource(VoiceList& list, MidiState& state) - : voiceList_(list), midiState_(state) +ADSREnvelopeSource::ADSREnvelopeSource(VoiceManager& manager, MidiState& state) + : voiceManager_(manager), midiState_(state) { } void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) { - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; @@ -60,7 +60,7 @@ void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) { - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; @@ -91,7 +91,7 @@ void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId voice void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) { - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; diff --git a/src/sfizz/modulations/sources/ADSREnvelope.h b/src/sfizz/modulations/sources/ADSREnvelope.h index a8835467..d52a9ca0 100644 --- a/src/sfizz/modulations/sources/ADSREnvelope.h +++ b/src/sfizz/modulations/sources/ADSREnvelope.h @@ -6,7 +6,7 @@ #pragma once #include "../ModGenerator.h" -#include "../../VoiceList.h" +#include "../../VoiceManager.h" #include "../../MidiState.h" namespace sfz { @@ -14,13 +14,13 @@ class Synth; class ADSREnvelopeSource : public ModGenerator { public: - explicit ADSREnvelopeSource(VoiceList &synth, MidiState& state); + explicit ADSREnvelopeSource(VoiceManager &manager, MidiState& state); void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; private: - VoiceList& voiceList_; + VoiceManager& voiceManager_; MidiState& midiState_; }; diff --git a/src/sfizz/modulations/sources/FlexEnvelope.cpp b/src/sfizz/modulations/sources/FlexEnvelope.cpp index db471ed1..5cea2ad8 100644 --- a/src/sfizz/modulations/sources/FlexEnvelope.cpp +++ b/src/sfizz/modulations/sources/FlexEnvelope.cpp @@ -14,8 +14,8 @@ namespace sfz { -FlexEnvelopeSource::FlexEnvelopeSource(VoiceList& list) - : voiceList_(list) +FlexEnvelopeSource::FlexEnvelopeSource(VoiceManager& manager) + : voiceManager_(manager) { } @@ -23,7 +23,7 @@ void FlexEnvelopeSource::init(const ModKey& sourceKey, NumericId voiceId, { unsigned egIndex = sourceKey.parameters().N; - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; @@ -50,7 +50,7 @@ void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId voice { unsigned egIndex = sourceKey.parameters().N; - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; @@ -70,7 +70,7 @@ void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId voic { unsigned egIndex = sourceKey.parameters().N; - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; diff --git a/src/sfizz/modulations/sources/FlexEnvelope.h b/src/sfizz/modulations/sources/FlexEnvelope.h index 487efdba..1868ab10 100644 --- a/src/sfizz/modulations/sources/FlexEnvelope.h +++ b/src/sfizz/modulations/sources/FlexEnvelope.h @@ -6,20 +6,20 @@ #pragma once #include "../ModGenerator.h" -#include "../../VoiceList.h" +#include "../../VoiceManager.h" namespace sfz { class Synth; class FlexEnvelopeSource : public ModGenerator { public: - explicit FlexEnvelopeSource(VoiceList& list); + explicit FlexEnvelopeSource(VoiceManager& manager); void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void release(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; private: - VoiceList& voiceList_; + VoiceManager& voiceManager_; }; } // namespace sfz diff --git a/src/sfizz/modulations/sources/LFO.cpp b/src/sfizz/modulations/sources/LFO.cpp index 8a603e43..a485833d 100644 --- a/src/sfizz/modulations/sources/LFO.cpp +++ b/src/sfizz/modulations/sources/LFO.cpp @@ -14,8 +14,8 @@ namespace sfz { -LFOSource::LFOSource(VoiceList& list) - : voiceList_(list) +LFOSource::LFOSource(VoiceManager& manager) + : voiceManager_(manager) { } @@ -23,7 +23,7 @@ void LFOSource::init(const ModKey& sourceKey, NumericId voiceId, unsigned { unsigned lfoIndex = sourceKey.parameters().N; - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; return; @@ -44,7 +44,7 @@ void LFOSource::generate(const ModKey& sourceKey, NumericId voiceId, absl { const unsigned lfoIndex = sourceKey.parameters().N; - Voice* voice = voiceList_.getVoiceById(voiceId); + Voice* voice = voiceManager_.getVoiceById(voiceId); if (!voice) { ASSERTFALSE; fill(buffer, 0.0f); diff --git a/src/sfizz/modulations/sources/LFO.h b/src/sfizz/modulations/sources/LFO.h index 02c696c9..430f483a 100644 --- a/src/sfizz/modulations/sources/LFO.h +++ b/src/sfizz/modulations/sources/LFO.h @@ -6,18 +6,18 @@ #pragma once #include "../ModGenerator.h" -#include "../../VoiceList.h" +#include "../../VoiceManager.h" namespace sfz { class Synth; class LFOSource : public ModGenerator { public: - explicit LFOSource(VoiceList &list); + explicit LFOSource(VoiceManager &manager); void init(const ModKey& sourceKey, NumericId voiceId, unsigned delay) override; void generate(const ModKey& sourceKey, NumericId voiceId, absl::Span buffer) override; private: - VoiceList& voiceList_; + VoiceManager& voiceManager_; }; } // namespace sfz