diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 1c8b97a7..349a9c12 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -132,6 +132,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, "This needs to add voices"); } // 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/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 1a2f7183..352329ef 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 @@ -70,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); } @@ -387,6 +389,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 +745,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 hard polyphony reached"); + return {}; } int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept @@ -813,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,19 +839,19 @@ 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); - // 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; @@ -885,7 +859,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(); @@ -934,14 +918,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))); @@ -993,6 +969,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) @@ -1001,6 +978,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); } @@ -1042,12 +1020,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)); @@ -1096,11 +1071,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)); @@ -1114,11 +1086,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)); @@ -1129,6 +1098,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); @@ -1519,7 +1501,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 @@ -1528,7 +1510,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); @@ -1536,16 +1518,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()); @@ -1557,8 +1548,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 bea54281..350e98bf 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(); /** @@ -819,6 +816,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; @@ -862,6 +862,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. @@ -902,7 +909,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 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 92092404..2069d9b7 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 bc0b66ff..0503b6d7 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