From ed151880d9f768108406c7f6f98d41513447a0c5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 31 Oct 2020 18:51:19 +0100 Subject: [PATCH] Move the polyphony logic in the voice list and refactor --- src/sfizz/PolyphonyGroup.h | 1 + src/sfizz/Synth.cpp | 246 +++--------------------------------- src/sfizz/Voice.cpp | 2 +- src/sfizz/Voice.h | 2 +- src/sfizz/VoiceList.h | 218 ++++++++++++++++++++++++++++++-- src/sfizz/VoiceStealing.cpp | 141 ++++++++++++++++----- src/sfizz/VoiceStealing.h | 95 +++++++------- 7 files changed, 384 insertions(+), 321 deletions(-) diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index 7a4281af..f3d47257 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -6,6 +6,7 @@ #pragma once +#include "Config.h" #include "Region.h" #include "Voice.h" #include "SwapAndPop.h" diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 585689d7..7c6fb08d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -31,7 +31,6 @@ #include "utility/SpinMutex.h" #include "utility/XmlHelpers.h" #include "VoiceList.h" -#include "VoiceStealing.h" #include #include #include @@ -42,15 +41,10 @@ namespace sfz { -struct Synth::Impl : public Voice::StateListener, public Parser::Listener { +struct Synth::Impl: public Parser::Listener { Impl(); ~Impl(); - /** - * @brief The voice callback which is called during a change of state. - */ - void onVoiceStateChanged(NumericId idNumber, Voice::State state) final; - /** * @brief The parser callback; this is called by the parent object each time * a new region, group, master, global, curve or control set of opcodes @@ -71,15 +65,6 @@ struct Synth::Impl : public Voice::StateListener, public Parser::Listener { */ void onParseWarning(const SourceRange& range, const std::string& message) final; - - /** - * @brief change the group maximum polyphony - * - * @param groupIdx the group index - * @param polyphone the max polyphony - */ - void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept; - /** * @brief Reset all CCs; to be used on CC 121 * @@ -193,46 +178,6 @@ struct Synth::Impl : public Voice::StateListener, public Parser::Listener { Voice* findFreeVoice() noexcept; - /** - * @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; - /** * @brief Start a voice for a specific region. * This will do the needed polyphony checks and voice stealing. @@ -308,14 +253,10 @@ struct Synth::Impl : public Voice::StateListener, public Parser::Listener { // engine polyphony RegionSetPtr engineSet_; - // 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 tempPolyphonyArray_; VoiceViewVector voiceViewArray_; - VoiceStealing stealer_; std::array lastKeyswitchLists_; std::array downKeyswitchLists_; @@ -413,19 +354,6 @@ Synth::Impl::~Impl() resources_.filePool.emptyFileLoadingQueues(); } -void Synth::Impl::onVoiceStateChanged(NumericId id, Voice::State state) -{ - (void)id; - (void)state; - if (state == Voice::State::idle) { - auto voice = voiceList_.getVoiceById(id); - RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); - engineSet_->removeVoice(voice); - polyphonyGroups_[voice->getRegion()->group].removeVoice(voice); - } - -} - void Synth::Impl::onParseFullBlock(const std::string& header, const std::vector& members) { const auto newRegionSet = [&](OpcodeScope level) { @@ -546,8 +474,12 @@ void Synth::Impl::buildRegion(const std::vector& regionOpcodes) currentSwitch_ = *lastRegion->defaultSwitch; // 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) - setGroupPolyphony(lastRegion->group, lastRegion->polyphony); + if (lastRegion->group != Default::group && lastRegion->polyphony != config::maxVoices) { + voiceList_.setGroupPolyphony(lastRegion->group, lastRegion->polyphony); + } else { + // Just check that there are enough polyphony groups + voiceList_.ensureNumPolyphonyGroups(lastRegion->group); + } if (currentSet_ != nullptr) { lastRegion->parent = currentSet_; @@ -595,7 +527,6 @@ void Synth::Impl::clear() resources_.midiState.reset(); resources_.filePool.clear(); resources_.filePool.setRamLoading(config::loadInRam); - stealer_.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::Oldest); ccLabels_.clear(); keyLabels_.clear(); keyswitchLabels_.clear(); @@ -603,9 +534,6 @@ void Synth::Impl::clear() masterOpcodes_.clear(); groupOpcodes_.clear(); unknownOpcodes_.clear(); - polyphonyGroups_.clear(); - polyphonyGroups_.emplace_back(); - polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices); modificationTime_ = fs::file_time_type::min(); // set default controllers @@ -689,12 +617,12 @@ void Synth::Impl::handleGroupOpcodes(const std::vector& members, const s parseOpcode(member); if (groupIdx && maxPolyphony) { - setGroupPolyphony(*groupIdx, *maxPolyphony); + voiceList_.setGroupPolyphony(*groupIdx, *maxPolyphony); } else if (maxPolyphony) { ASSERT(currentSet_ != nullptr); currentSet_->setPolyphonyLimit(*maxPolyphony); - } else if (groupIdx && *groupIdx > polyphonyGroups_.size()) { - setGroupPolyphony(*groupIdx, config::maxVoices); + } else if (groupIdx) { + voiceList_.ensureNumPolyphonyGroups(*groupIdx); } } @@ -749,22 +677,13 @@ void Synth::Impl::handleControlOpcodes(const std::vector& members) case hash("hint_stealing"): switch(hash(member.value)) { case hash("first"): - for (auto& voice : voiceList_) - voice.disablePowerFollower(); - - stealer_.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::First); + voiceList_.setStealingAlgorithm(StealingAlgorithm::First); break; case hash("oldest"): - for (auto& voice : voiceList_) - voice.disablePowerFollower(); - - stealer_.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::Oldest); + voiceList_.setStealingAlgorithm(StealingAlgorithm::Oldest); break; case hash("envelope_and_age"): - for (auto& voice : voiceList_) - voice.enablePowerFollower(); - - stealer_.setStealingAlgorithm(VoiceStealing::StealingAlgorithm::EnvelopeAndAge); + voiceList_.setStealingAlgorithm(StealingAlgorithm::EnvelopeAndAge); break; default: DBG("Unsupported value for hint_stealing: " << member.value); @@ -993,12 +912,6 @@ void Synth::Impl::finalizeSfzLoad() } } - // Some regions had group number but no "group-level" opcodes handled the polyphony - while (polyphonyGroups_.size() <= region->group) { - polyphonyGroups_.emplace_back(); - polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices); - } - for (auto note = 0; note < 128; note++) { if (region->keyRange.containsWithEnd(note)) noteActivationLists_[note].push_back(region); @@ -1374,12 +1287,7 @@ 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 { - checkNotePolyphony(region, delay, triggerEvent); - checkRegionPolyphony(region, delay); - checkGroupPolyphony(region, delay); - checkSetPolyphony(region, delay); - checkEnginePolyphony(delay); - + voiceList_.checkPolyphony(region, delay, triggerEvent); Voice* selectedVoice = findFreeVoice(); if (selectedVoice == nullptr) return; @@ -1387,9 +1295,6 @@ void Synth::Impl::startVoice(Region* region, int delay, const TriggerEvent& trig ASSERT(selectedVoice->isFree()); selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); - engineSet_->registerVoice(selectedVoice); - RegionSet::registerVoiceInHierarchy(region, selectedVoice); - polyphonyGroups_[region->group].registerVoice(selectedVoice); } void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noexcept @@ -1414,100 +1319,6 @@ void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noe } } -void Synth::Impl::checkRegionPolyphony(const Region* region, int delay) noexcept -{ - tempPolyphonyArray_.clear(); - absl::c_copy_if(voiceViewArray_, - std::back_inserter(tempPolyphonyArray_), - [region](Voice* v) { return v->getRegion() == region && !v->releasedOrFree(); }); - - if (tempPolyphonyArray_.size() >= region->polyphony) { - const auto voiceToSteal = stealer_.steal(absl::MakeSpan(tempPolyphonyArray_)); - SisterVoiceRing::offAllSisters(voiceToSteal, delay); - } -} - -void Synth::Impl::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept -{ - if (!region->notePolyphony) - return; - - unsigned notePolyphonyCounter { 0 }; - Voice* selfMaskCandidate { nullptr }; - - for (Voice* voice : voiceViewArray_) { - const 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 && selfMaskCandidate) { - SisterVoiceRing::offAllSisters(selfMaskCandidate, delay); - } -} - -void Synth::Impl::checkGroupPolyphony(const Region* region, int delay) noexcept -{ - const auto& activeVoices = polyphonyGroups_[region->group].getActiveVoices(); - tempPolyphonyArray_.clear(); - absl::c_copy_if(activeVoices, - std::back_inserter(tempPolyphonyArray_), [](Voice* v) { return !v->releasedOrFree(); }); - - if (tempPolyphonyArray_.size() >= polyphonyGroups_[region->group].getPolyphonyLimit()) { - const auto voiceToSteal = stealer_.steal(absl::MakeSpan(tempPolyphonyArray_)); - SisterVoiceRing::offAllSisters(voiceToSteal, delay); - } -} - -void Synth::Impl::checkSetPolyphony(const Region* region, int delay) noexcept -{ - auto parent = region->parent; - while (parent != nullptr) { - const auto& activeVoices = parent->getActiveVoices(); - tempPolyphonyArray_.clear(); - absl::c_copy_if(activeVoices, - std::back_inserter(tempPolyphonyArray_), [](Voice* v) { return !v->releasedOrFree(); }); - - if (tempPolyphonyArray_.size() >= parent->getPolyphonyLimit()) { - const auto voiceToSteal = stealer_.steal(absl::MakeSpan(tempPolyphonyArray_)); - SisterVoiceRing::offAllSisters(voiceToSteal, delay); - } - - parent = parent->getParent(); - } -} - -void Synth::Impl::checkEnginePolyphony(int delay) noexcept -{ - auto& activeVoices = engineSet_->getActiveVoices(); - - if (activeVoices.size() >= static_cast(numRequiredVoices_)) { - tempPolyphonyArray_.clear(); - absl::c_copy_if(activeVoices, - std::back_inserter(tempPolyphonyArray_), [](Voice* v) { return !v->releasedOrFree(); }); - const auto voiceToSteal = stealer_.steal(absl::MakeSpan(tempPolyphonyArray_)); - SisterVoiceRing::offAllSisters(voiceToSteal, delay, true); - } -} - void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution_(Random::randomGenerator); @@ -1839,7 +1650,7 @@ const RegionSet* Synth::getRegionSetView(int idx) const noexcept const PolyphonyGroup* Synth::getPolyphonyGroupView(int idx) const noexcept { Impl& impl = *impl_; - return (size_t)idx < impl.polyphonyGroups_.size() ? &impl.polyphonyGroups_[idx] : nullptr; + return impl.voiceList_.getPolyphonyGroupView(idx); } const Region* Synth::getRegionById(NumericId id) const noexcept @@ -1869,7 +1680,7 @@ const Voice* Synth::getVoiceView(int idx) const noexcept unsigned Synth::getNumPolyphonyGroups() const noexcept { Impl& impl = *impl_; - return impl.polyphonyGroups_.size(); + return impl.voiceList_.getNumPolyphonyGroups(); } const std::vector& Synth::getUnknownOpcodes() const noexcept @@ -1971,7 +1782,7 @@ void Synth::Impl::resetVoices(int numVoices) Voice& lastVoice = voiceList_.back(); lastVoice.setSampleRate(this->sampleRate_); lastVoice.setSamplesPerBlock(this->samplesPerBlock_); - lastVoice.setStateListener(this); + lastVoice.setStateListener(&voiceList_); voiceViewArray_.push_back(&lastVoice); } @@ -1988,15 +1799,6 @@ void Synth::Impl::applySettingsPerVoice() voice.setPitchEGEnabledPerVoice(settingsPerVoice_.havePitchEG); voice.setFilterEGEnabledPerVoice(settingsPerVoice_.haveFilterEG); } - - if (stealer_.getStealingAlgorithm() == - VoiceStealing::StealingAlgorithm::EnvelopeAndAge) { - for (auto& voice : voiceList_) - voice.enablePowerFollower(); - } else { - for (auto& voice : voiceList_) - voice.disablePowerFollower(); - } } void Synth::Impl::setupModMatrix() @@ -2076,7 +1878,8 @@ void Synth::setOversamplingFactor(Oversampling factor) noexcept if (factor == impl.oversamplingFactor_) return; - impl.voiceList_.reset(); + for (auto& voice : impl.voiceList_) + voice.reset(); impl.resources_.filePool.emptyFileLoadingQueues(); impl.resources_.filePool.setOversamplingFactor(factor); @@ -2191,19 +1994,12 @@ void Synth::allSoundOff() noexcept Impl& impl = *impl_; const std::lock_guard disableCallback { impl.callbackGuard_ }; - impl.voiceList_.reset(); + for (auto& voice : impl.voiceList_) + voice.reset(); for (auto& effectBus : impl.effectBuses_) effectBus->clear(); } -void Synth::Impl::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept -{ - while (polyphonyGroups_.size() <= groupIdx) - polyphonyGroups_.emplace_back(); - - polyphonyGroups_[groupIdx].setPolyphonyLimit(polyphony); -} - std::bitset Synth::getUsedCCs() const noexcept { Impl& impl = *impl_; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index e57dd627..dcdfccd2 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -1582,7 +1582,7 @@ void Voice::Impl::switchState(State s) if (s != state_) { state_ = s; if (stateListener_) - stateListener_->onVoiceStateChanged(id_, s); + stateListener_->onVoiceStateChanging(id_, s); } } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 2e79584a..35428a6b 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -53,7 +53,7 @@ public: class StateListener { public: - virtual void onVoiceStateChanged(NumericId /*id*/, State /*state*/) {} + virtual void onVoiceStateChanging(NumericId /*id*/, State /*state*/) {} }; /** diff --git a/src/sfizz/VoiceList.h b/src/sfizz/VoiceList.h index 0903c0bb..660c1090 100644 --- a/src/sfizz/VoiceList.h +++ b/src/sfizz/VoiceList.h @@ -7,14 +7,37 @@ #pragma once #include "Voice.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 +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. * @@ -48,6 +71,11 @@ struct VoiceList { 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 @@ -68,21 +96,187 @@ struct VoiceList return true; } - typename std::vector::iterator begin() { return list_.begin(); } - typename std::vector::const_iterator cbegin() const { return list_.cbegin(); } - typename std::vector::iterator end() { return list_.end(); } - typename std::vector::const_iterator cend() const { return list_.cend(); } - typename std::vector::reference operator[] (size_t n) { return list_[n]; } - typename std::vector::const_reference operator[] (size_t n) const { return list_[n]; } - typename std::vector::reference back() { return list_.back(); } - typename std::vector::const_reference back() const { return list_.back(); } + 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); + } + +private: + std::vector list_; + std::vector activeVoices_; + std::vector temp_; + // 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 + { + // TODO (paul): should have the "required" vs "actual" number of voices here + Voice* candidate = stealer_->checkPolyphony( + absl::MakeSpan(activeVoices_), list_.size()); + 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]; } + typename decltype(list_)::reference back() { return list_.back(); } + typename decltype(list_)::const_reference back() const { return list_.back(); } size_t size() const { return list_.size(); } - void clear() { list_.clear(); } void reserve(size_t n) { list_.reserve(n); } template< class... Args > void emplace_back(Args&&... args) { list_.emplace_back(std::forward(args)...); } -private: - std::vector list_; }; } // namespace sfz diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index 878f5fb8..6d06f6e6 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -1,57 +1,102 @@ #include "VoiceStealing.h" +#include "SisterVoiceRing.h" -sfz::VoiceStealing::VoiceStealing() +namespace sfz { + +/** + * @brief Generic polyphony checker + * A voice is counted as incrementing the voice count and "stealable" if voiceCond(voice) is true. + * For each stealable voice, the voice becomes the stealing candidate if candidateCont(voice, candidate) is true. + * + * @tparam F + * @tparam G + * @param candidates + * @param voiceCond a functor with signature bool(Voice* voice) + * @param candidateCond a functor with signature bool(Voice* voice, Voice* candidate) + * @return Voice* + */ +template +Voice* genericPolyphonyCheck(absl::Span candidates, unsigned polyphony, F&& voiceCond, G&& candidateCond) { - voiceScores.reserve(config::maxVoices); -} - -sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept -{ - if (voices.empty()) - return {}; - - switch(stealingAlgorithm) { - case StealingAlgorithm::First: - return stealFirst(voices); - case StealingAlgorithm::EnvelopeAndAge: - return stealEnvelopeAndAge(voices); - case StealingAlgorithm::Oldest: - default: - return stealOldest(voices); + Voice* candidate = nullptr; + unsigned numPlaying = 0; + for (const auto& voice : candidates) { + if (voiceCond(voice)) { + if (candidateCond(voice, candidate)) + candidate = voice; + numPlaying += 1; + } } + + if (numPlaying >= polyphony) + return candidate; + + return {}; } -void sfz::VoiceStealing::setStealingAlgorithm(StealingAlgorithm algorithm) noexcept +/** + * @brief Helper to ignore the voice if released depending on a boolean + * + * @param voice + * @param ignoreReleased + */ +constexpr bool ignoreVoice(const Voice* voice) { - stealingAlgorithm = algorithm; + return (voice == nullptr || voice->releasedOrFree()); } -sfz::Voice* sfz::VoiceStealing::stealFirst(absl::Span voices) noexcept +Voice* FirstStealer::checkRegionPolyphony(const Region* region, absl::Span candidates) { - return voices.front(); + ASSERT(region); + return genericPolyphonyCheck(candidates, region->polyphony, + [=](const Voice* v) { return (!ignoreVoice(v) && v->getRegion() == region); }, + [=](const Voice* v, const Voice* c) { return c == nullptr; }); } -sfz::Voice* sfz::VoiceStealing::stealOldest(absl::Span voices) noexcept +Voice* FirstStealer::checkPolyphony(absl::Span candidates, unsigned maxPolyphony) { - absl::c_sort(voices, voiceOrdering); - return voices.front(); + return genericPolyphonyCheck(candidates, maxPolyphony, + [=](const Voice* v) { return (!ignoreVoice(v)); }, + [=](const Voice* v, const Voice* c) { return c == nullptr; }); } -sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span voices) noexcept +Voice* OldestStealer::checkRegionPolyphony(const Region* region, absl::Span candidates) +{ + ASSERT(region); + return genericPolyphonyCheck(candidates, region->polyphony, + [=](const Voice* v) { return (!ignoreVoice(v) && v->getRegion() == region); }, + [=](const Voice* v, const Voice* c) { return (c == nullptr || v->getAge() > c->getAge()); }); +} + +Voice* OldestStealer::checkPolyphony(absl::Span candidates, unsigned maxPolyphony) +{ + return genericPolyphonyCheck(candidates, maxPolyphony, + [=](const Voice* v) { return (!ignoreVoice(v)); }, + [=](const Voice* v, const Voice* c) { return (c == nullptr || v->getAge() > c->getAge()); }); +} + +/** + * @brief Stealer on envelope and age. + * The stealer checks that the power to try and kill voices with relative low contribution + * to the output compared to the rest. + * The stealer also checks the age so that voices have the time to build up attack + * This is not perfect because pad-type voices will take a long time to output + * their sound, but it's reasonable for sounds with a quick attack and longer + * release. + * + * @param voices + * @return sfz::Voice* + */ +sfz::Voice* stealEnvelopeAndAge(absl::Span voices) noexcept { absl::c_sort(voices, voiceOrdering); const auto sumPower = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { return sum + v->getAveragePower(); }); - // We are checking the power to try and kill voices with relative low contribution - // to the output compared to the rest. + const auto powerThreshold = sumPower / static_cast(voices.size()) * config::stealingPowerCoeff; - // We are checking the age so that voices have the time to build up attack - // This is not perfect because pad-type voices will take a long time to output - // 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); @@ -82,3 +127,37 @@ sfz::Voice* sfz::VoiceStealing::stealEnvelopeAndAge(absl::Span voices) n return returnedVoice; } + +Voice* EnvelopeAndAgeStealer::checkRegionPolyphony(const Region* region, absl::Span candidates) +{ + ASSERT(region); + temp_.clear(); + absl::c_copy_if(candidates, std::back_inserter(temp_), [=](Voice* v) { + return (!ignoreVoice(v) && v->getRegion() == region); + }); + + if (temp_.size() >= region->polyphony) + return stealEnvelopeAndAge(absl::MakeSpan(temp_)); + + return {}; +} + +Voice* EnvelopeAndAgeStealer::checkPolyphony(absl::Span candidates, unsigned maxPolyphony) +{ + temp_.clear(); + absl::c_copy_if(candidates, std::back_inserter(temp_), [=](Voice* v) { + return !ignoreVoice(v); + }); + + if (temp_.size() >= maxPolyphony) + return stealEnvelopeAndAge(absl::MakeSpan(temp_)); + + return {}; +} + +EnvelopeAndAgeStealer::EnvelopeAndAgeStealer() +{ + temp_.reserve(config::maxVoices); +} + +} diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h index 47eb8692..78e5000c 100644 --- a/src/sfizz/VoiceStealing.h +++ b/src/sfizz/VoiceStealing.h @@ -7,6 +7,7 @@ #pragma once #include "Config.h" +#include "Region.h" #include "Voice.h" #include "SisterVoiceRing.h" #include @@ -14,64 +15,56 @@ namespace sfz { -class VoiceStealing + +enum class StealingAlgorithm { + First, + Oldest, + EnvelopeAndAge +}; + +class VoiceStealer { public: - enum class StealingAlgorithm { - First, - Oldest, - EnvelopeAndAge - }; - - VoiceStealing(); /** - * @brief Get the current stealing algorithm + * @brief Check that the region polyphony is respected. * - * @return StealingAlgorithm + * @param region + * @param candidates + * @return Voice* a non-null voice if the region polyphony is not respected */ - StealingAlgorithm getStealingAlgorithm() const noexcept { return stealingAlgorithm; } + virtual Voice* checkRegionPolyphony(const Region* region, absl::Span candidates) = 0; /** - * @brief Set a default stealing algorithm + * @brief Check that then polyphony is respected. * - * @param algorithm + * @param region + * @param candidates + * @return Voice* a non-null voice if the region polyphony is not respected */ - void setStealingAlgorithm(StealingAlgorithm algorithm) noexcept; - /** - * @brief Propose a voice to steal from a set of voices - * - * @param voices - * @return Voice* - */ - Voice* steal(absl::Span voices) noexcept; -private: - StealingAlgorithm stealingAlgorithm { StealingAlgorithm::Oldest }; - Voice* stealFirst(absl::Span voices) noexcept; - Voice* stealOldest(absl::Span voices) noexcept; - Voice* stealEnvelopeAndAge(absl::Span voices) noexcept; - - struct VoiceScore - { - Voice* voice; - double score; - }; - - struct VoiceScoreComparator - { - bool operator()(const VoiceScore& voiceScore, const double& score) - { - return (voiceScore.score < score); - } - - bool operator()(const double& score, const VoiceScore& voiceScore) - { - return (score < voiceScore.score); - } - - bool operator()(const VoiceScore& lhs, const VoiceScore& rhs) - { - return (lhs.score < rhs.score); - } - }; - std::vector voiceScores; + virtual Voice* checkPolyphony(absl::Span candidates, unsigned maxPolyphony) = 0; }; + +class FirstStealer : public VoiceStealer +{ +public: + Voice* checkRegionPolyphony(const Region* region, absl::Span candidates) final; + Voice* checkPolyphony(absl::Span candidates, unsigned maxPolyphony) final; +}; + +class OldestStealer : public VoiceStealer +{ +public: + Voice* checkRegionPolyphony(const Region* region, absl::Span candidates) final; + Voice* checkPolyphony(absl::Span candidates, unsigned maxPolyphony) final; +}; + +class EnvelopeAndAgeStealer : public VoiceStealer +{ +public: + EnvelopeAndAgeStealer(); + Voice* checkRegionPolyphony(const Region* region, absl::Span candidates) final; + Voice* checkPolyphony(absl::Span candidates, unsigned maxPolyphony) final; +private: + std::vector temp_; +}; + }