From 8071cd0131d49e49858cab04d7db541acef80171 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 18:43:09 +0200 Subject: [PATCH 1/6] Add more voices than necessary These will naturally be used for "dying" voices beyond the engine polyphony --- src/sfizz/Config.h | 7 +++++++ src/sfizz/RegionSet.cpp | 5 +++++ src/sfizz/RegionSet.h | 5 +++++ src/sfizz/Synth.cpp | 44 ++++++++++++++++++----------------------- src/sfizz/Synth.h | 6 +++++- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 477c3ce3..eede94f2 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -131,6 +131,13 @@ namespace config { static constexpr int loopXfadeCurve = 2; // 0: linear // 1: use curves 5 & 6 // 2: use S-shaped curve + /** + * @brief Overflow voices in the engine, relative to the required voices. + * These are additional voices that more or less hold the "dying" voices + * due to engine polyphony being reached. + */ + static constexpr float overflowVoiceMultiplier { 1.5f }; + static_assert(overflowVoiceMultiplier >= 1.0f); } // namespace config } // namespace sfz diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp index 1387438a..bfceb4e2 100644 --- a/src/sfizz/RegionSet.cpp +++ b/src/sfizz/RegionSet.cpp @@ -53,3 +53,8 @@ unsigned sfz::RegionSet::numPlayingVoices() const noexcept return !v->releasedOrFree(); }); } + +void sfz::RegionSet::removeAllVoices() noexcept +{ + voices.clear(); +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 4424a030..4077fbe7 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -122,6 +122,11 @@ public: * @return const std::vector& */ const std::vector& getSubsets() const noexcept { return subsets; } + + /** + * @brief Remove all voices from the set + */ + void removeAllVoices() noexcept; private: RegionSet* parent { nullptr }; OpcodeScope level { kOpcodeScopeGeneric }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 498a9f62..039b967b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -41,6 +41,7 @@ sfz::Synth::Synth(int numVoices) initializeSIMDDispatchers(); const std::lock_guard disableCallback { callbackGuard }; + engineSet = absl::make_unique(nullptr, OpcodeScope::kOpcodeScopeGeneric); parser.setListener(this); effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 @@ -387,6 +388,7 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) default: DBG("Unsupported value for hint_stealing: " << member.value); } + break; default: // Unsupported control opcode DBG("Unsupported control opcode: " << member.opcode); @@ -742,23 +744,8 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - // Engine polyphony reached - Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); - if (stolenVoice == nullptr) - return {}; - - // Never kill age 0 voices - if (stolenVoice->getAge() == 0) - return {}; - - - auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); - SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); - - return stolenVoice; + DBG("Engine polyphony reached"); + return {}; } int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept @@ -1511,7 +1498,7 @@ void sfz::Synth::setVolume(float volume) noexcept int sfz::Synth::getNumVoices() const noexcept { - return numVoices; + return numRequiredVoices; } void sfz::Synth::setNumVoices(int numVoices) noexcept @@ -1520,7 +1507,7 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept const std::lock_guard disableCallback { callbackGuard }; // fast path - if (numVoices == this->numVoices) + if (numVoices == this->numRequiredVoices) return; resetVoices(numVoices); @@ -1528,16 +1515,25 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept void sfz::Synth::resetVoices(int numVoices) { + numActualVoices = + static_cast(config::overflowVoiceMultiplier * numVoices); + numRequiredVoices = numVoices; + + for (auto& set : sets) + set->removeAllVoices(); + engineSet->removeAllVoices(); + engineSet->setPolyphonyLimit(numRequiredVoices); + voices.clear(); - voices.reserve(numVoices); + voices.reserve(numActualVoices); voiceViewArray.clear(); - voiceViewArray.reserve(numVoices); + voiceViewArray.reserve(numActualVoices); tempPolyphonyArray.clear(); - tempPolyphonyArray.reserve(numVoices); + tempPolyphonyArray.reserve(numActualVoices); - for (int i = 0; i < numVoices; ++i) { + for (int i = 0; i < numActualVoices; ++i) { auto voice = absl::make_unique(i, resources); voice->setStateListener(this); voiceViewArray.push_back(voice.get()); @@ -1549,8 +1545,6 @@ void sfz::Synth::resetVoices(int numVoices) voice->setSamplesPerBlock(this->samplesPerBlock); } - this->numVoices = numVoices; - applySettingsPerVoice(); } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index c0d32c2e..2663afcc 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -819,6 +819,9 @@ private: // These are more general "groups" than sfz and encapsulates the full hierarchy RegionSet* currentSet { nullptr }; std::vector sets; + // This region set holds the engine set of voices, which tries to respect the required + // engine polyphony + RegionSetPtr engineSet; // These are the `group=` groups where you can off voices std::vector polyphonyGroups; @@ -902,7 +905,8 @@ private: int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; float volume { Default::globalVolume }; - int numVoices { config::numVoices }; + int numRequiredVoices { config::numVoices }; + int numActualVoices { static_cast(config::numVoices * config::overflowVoiceMultiplier) }; int activeVoices { 0 }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; From 480a7d4628cc1087b21dbac19dd60379dda1d435 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 21:49:28 +0200 Subject: [PATCH 2/6] Use the soft engine polyphony limit --- src/sfizz/SisterVoiceRing.h | 5 +++-- src/sfizz/Synth.cpp | 18 +++++++++++++++++- src/sfizz/Synth.h | 7 +++++++ src/sfizz/Voice.cpp | 6 +++--- src/sfizz/Voice.h | 3 ++- 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 4e7cc743..bb6137bb 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -61,13 +61,14 @@ struct SisterVoiceRing { * * @param voice * @param delay + * @param fast whether to apply a fast release */ template>::value, int> = 0> - static void offAllSisters(T* voice, int delay) { + static void offAllSisters(T* voice, int delay, bool fast = false) { if (voice != nullptr) { SisterVoiceRing::applyToRing(voice, [&] (Voice* v) { - v->off(delay); + v->off(delay, fast); }); } } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 039b967b..245a7240 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -71,6 +71,7 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) if (state == Voice::State::idle) { auto voice = getVoiceById(id); RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); + engineSet->removeVoice(voice); polyphonyGroups[voice->getRegion()->group].removeVoice(voice); } @@ -744,7 +745,7 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - DBG("Engine polyphony reached"); + DBG("Engine hard polyphony reached"); return {}; } @@ -972,6 +973,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg checkRegionPolyphony(region, delay); checkGroupPolyphony(region, delay); checkSetPolyphony(region, delay); + checkEnginePolyphony(delay); Voice* selectedVoice = findFreeVoice(); if (selectedVoice == nullptr) @@ -980,6 +982,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg ASSERT(selectedVoice->isFree()); selectedVoice->startVoice(region, delay, triggerEvent); ring.addVoiceToRing(selectedVoice); + engineSet->registerVoice(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice); } @@ -1108,6 +1111,19 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept } } +void sfz::Synth::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 sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept { const auto randValue = randNoteDistribution(Random::randomGenerator); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 2663afcc..af3aa469 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -865,6 +865,13 @@ private: */ 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. diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7dd2f948..96eb9ada 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -167,11 +167,11 @@ void sfz::Voice::release(int delay) noexcept resources.modMatrix.releaseVoice(id, region->getId(), delay); } -void sfz::Voice::off(int delay) noexcept +void sfz::Voice::off(int delay, bool fast) noexcept { if (!region->flexAmpEG) { - if (region->offMode == SfzOffMode::fast) { - egAmplitude.setReleaseTime( Default::offTime ); + if (region->offMode == SfzOffMode::fast || fast) { + egAmplitude.setReleaseTime(Default::offTime); } else if (region->offMode == SfzOffMode::time) { egAmplitude.setReleaseTime(region->offTime); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index dbcae7d7..54fd16e5 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -328,8 +328,9 @@ public: * and set the envelopes if necessary. * * @param delay + * @param fast whether to apply a fast release regardless of the off mode */ - void off(int delay) noexcept; + void off(int delay, bool fast = false) noexcept; /** * @brief gets the age of the Voice From 02a1615e2d131b26456fef704e4da73e00a3f8d7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 23:03:25 +0200 Subject: [PATCH 3/6] Use the back inserter idiom in all stealing methods --- src/sfizz/Synth.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 245a7240..2348dcb4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -1024,12 +1024,9 @@ 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); - } - } + 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)); @@ -1078,11 +1075,8 @@ 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); - } - } + 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)); @@ -1096,11 +1090,8 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept while (parent != nullptr) { const auto& activeVoices = parent->getActiveVoices(); tempPolyphonyArray.clear(); - for (Voice* voice : activeVoices) { - if (!voice->releasedOrFree()) { - tempPolyphonyArray.push_back(voice); - } - } + 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)); From 8212eb4966fbbe24a4a7fcecea8d3f95b663f4bd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 23:58:27 +0200 Subject: [PATCH 4/6] cpp11 static assert needs a message --- src/sfizz/Config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index eede94f2..378f4476 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -137,7 +137,7 @@ namespace config { * due to engine polyphony being reached. */ static constexpr float overflowVoiceMultiplier { 1.5f }; - static_assert(overflowVoiceMultiplier >= 1.0f); + static_assert(overflowVoiceMultiplier >= 1.0f, "This needs to add voices"); } // namespace config } // namespace sfz From 81fe043bb3cb2bf97f7812228ab663a21252d903 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 7 Oct 2020 09:01:48 +0200 Subject: [PATCH 5/6] No need to ramp out killed voice data --- src/sfizz/Synth.cpp | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 2348dcb4..f4caa9a4 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -844,20 +844,20 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept ModMatrix& mm = resources.modMatrix; mm.beginCycle(numFrames); + { // Clear effect busses + ScopedTiming logger { callbackBreakdown.effects }; + for (auto& bus : effectBuses) { + if (bus) + bus->clearInputs(numFrames); + } + } + activeVoices = 0; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); - // Ramp out whatever is in the buffer at this point; should only be killed voice data - linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(numFrames)); - for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { - if (auto& bus = effectBuses[i]) { - bus->applyGain(rampSpan->data(), numFrames); - } - } - for (auto& voice : voices) { if (voice->isFree()) continue; @@ -914,14 +914,6 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept // Reset the dispatch counter dispatchDuration = Duration(0); - { // Clear for the next run - ScopedTiming logger { callbackBreakdown.effects }; - for (auto& bus : effectBuses) { - if (bus) - bus->clearInputs(numFrames); - } - } - ASSERT(!hasNanInf(buffer.getConstSpan(0))); ASSERT(!hasNanInf(buffer.getConstSpan(1))); SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0))); From f9178d41aaf85a6b9757724632b41f7a1a88da91 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 7 Oct 2020 09:08:01 +0200 Subject: [PATCH 6/6] Remove the renderVoiceToOutput method and add a missing comment --- src/sfizz/Synth.cpp | 26 +++++++++++--------------- src/sfizz/Synth.h | 7 ++----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f4caa9a4..0b37df6d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -801,20 +801,6 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept } } -void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept -{ - const Region* region = voice.getRegion(); - ASSERT(region != nullptr); - - voice.renderBlock(tempSpan); - for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { - if (auto& bus = effectBuses[i]) { - float addGain = region->getGainToEffectBus(i); - bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames()); - } - } -} - void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { ScopedFTZ ftz; @@ -865,7 +851,17 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value); activeVoices++; - renderVoiceToOutputs(*voice, *tempSpan); + + const Region* region = voice->getRegion(); + ASSERT(region != nullptr); + + voice->renderBlock(*tempSpan); + for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { + if (auto& bus = effectBuses[i]) { + float addGain = region->getGainToEffectBus(i); + bus->addToInputs(*tempSpan, addGain, numFrames); + } + } callbackBreakdown.data += voice->getLastDataDuration(); callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); callbackBreakdown.filters += voice->getLastFilterDuration(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index af3aa469..48aa69f7 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -742,13 +742,10 @@ private: void setupModMatrix(); /** - * @brief Render the voice to its designated outputs and effect busses. + * @brief Get the modification time of all included sfz files * - * @param voice - * @param tempSpan a temporary span used for rendering + * @return fs::file_time_type */ - void renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept; - fs::file_time_type checkModificationTime(); /**