diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 67c3b36f6..554915feb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,9 @@ set (SFIZZ_SOURCES sfizz/Smoothers.cpp sfizz/Wavetables.cpp sfizz/Tuning.cpp + sfizz/RegionSet.cpp + sfizz/PolyphonyGroup.cpp + sfizz/VoiceStealing.cpp sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index 754c0d9e9..cd31c8c67 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -47,6 +47,8 @@ enum OpcodeScope { kOpcodeScopeGlobal, //! control scope kOpcodeScopeControl, + //! Master scope + kOpcodeScopeMaster, //! group scope kOpcodeScopeGroup, //! region scope diff --git a/src/sfizz/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp new file mode 100644 index 000000000..7ac1e3d64 --- /dev/null +++ b/src/sfizz/PolyphonyGroup.cpp @@ -0,0 +1,18 @@ +#include "PolyphonyGroup.h" + +void sfz::PolyphonyGroup::setPolyphonyLimit(unsigned limit) noexcept +{ + polyphonyLimit = limit; + voices.reserve(limit); +} + +void sfz::PolyphonyGroup::registerVoice(Voice* voice) noexcept +{ + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); +} + +void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept +{ + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); +} diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h new file mode 100644 index 000000000..d3e1413bf --- /dev/null +++ b/src/sfizz/PolyphonyGroup.h @@ -0,0 +1,60 @@ +// 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 "Region.h" +#include "Voice.h" +#include "SwapAndPop.h" +#include "absl/algorithm/container.h" + +namespace sfz +{ +class PolyphonyGroup { +public: + /** + * @brief Set the polyphony limit for this polyphony group. + * + * @param limit + */ + void setPolyphonyLimit(unsigned limit) noexcept; + /** + * @brief Register an active voice in this polyphony group. + * + * @param voice + */ + void registerVoice(Voice* voice) noexcept; + /** + * @brief Remove a voice from this polyphony group. + * If the voice was not registered before, this has no effect. + * + * @param voice + */ + void removeVoice(const Voice* voice) noexcept; + /** + * @brief Get the polyphony limit for this group + * + * @return unsigned + */ + unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; } + /** + * @brief Get the active voices + * + * @return const std::vector& + */ + const std::vector& getActiveVoices() const noexcept { return voices; } + /** + * @brief Get the active voices + * + * @return std::vector& + */ + std::vector& getActiveVoices() noexcept { return voices; } +private: + unsigned polyphonyLimit { config::maxVoices }; + std::vector voices; +}; + +} diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 5d6bbe555..424269f7c 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -164,6 +164,10 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) DBG("Unkown off mode:" << std::string(opcode.value)); } break; + case hash("polyphony"): + if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) + polyphony = *value; + break; case hash("note_polyphony"): if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) notePolyphony = *value; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index de80f53e5..e96ebe0f3 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -24,6 +24,9 @@ #include namespace sfz { + +class RegionSet; + /** * @brief Regions are the basic building blocks for the SFZ parsing and handling code. * All SFZ files are made of regions that are activated when a key is pressed or a CC @@ -282,7 +285,8 @@ struct Region { uint32_t group { Default::group }; // group absl::optional offBy {}; // off_by SfzOffMode offMode { Default::offMode }; // off_mode - absl::optional notePolyphony {}; + absl::optional notePolyphony {}; // note_polyphony + unsigned polyphony { config::maxVoices }; // polyphony SfzSelfMask selfMask { Default::selfMask }; // Region logic: key mapping @@ -365,6 +369,8 @@ struct Region { // Modifiers ModifierArray> modifiers; + // Parent + RegionSet* parent { nullptr }; private: const MidiState& midiState; bool keySwitched { true }; diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp new file mode 100644 index 000000000..d6f8a5852 --- /dev/null +++ b/src/sfizz/RegionSet.cpp @@ -0,0 +1,48 @@ +#include "RegionSet.h" + +void sfz::RegionSet::setPolyphonyLimit(unsigned limit) noexcept +{ + polyphonyLimit = limit; + voices.reserve(limit); +} + +void sfz::RegionSet::addRegion(Region* region) noexcept +{ + if (absl::c_find(regions, region) == regions.end()) + regions.push_back(region); +} + +void sfz::RegionSet::addSubset(RegionSet* group) noexcept +{ + if (absl::c_find(subsets, group) == subsets.end()) + subsets.push_back(group); +} + +void sfz::RegionSet::registerVoice(Voice* voice) noexcept +{ + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); +} + +void sfz::RegionSet::removeVoice(const Voice* voice) noexcept +{ + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); +} + +void sfz::RegionSet::registerVoiceInHierarchy(const Region* region, Voice* voice) noexcept +{ + auto* parent = region->parent; + while (parent != nullptr) { + parent->registerVoice(voice); + parent = parent->getParent(); + } +} + +void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice* voice) noexcept +{ + auto* parent = region->parent; + while (parent != nullptr) { + parent->removeVoice(voice); + parent = parent->getParent(); + } +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h new file mode 100644 index 000000000..24120173a --- /dev/null +++ b/src/sfizz/RegionSet.h @@ -0,0 +1,114 @@ +// 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 "Region.h" +#include "Voice.h" +#include "SwapAndPop.h" +#include + +namespace sfz +{ + +class RegionSet { +public: + /** + * @brief Set the polyphony limit for the set + * + * @param limit + */ + void setPolyphonyLimit(unsigned limit) noexcept; + /** + * @brief Add a region to the set + * + * @param region + */ + void addRegion(Region* region) noexcept; + /** + * @brief Add a subset to the set + * + * @param group + */ + void addSubset(RegionSet* group) noexcept; + /** + * @brief Register a voice as active in this set + * + * @param voice + */ + void registerVoice(Voice* voice) noexcept; + /** + * @brief Remove an active voice for this set. + * If the voice was not registered this has no effect. + * + * @param voice + */ + void removeVoice(const Voice* voice) noexcept; + /** + * @brief Register a voice in the whole parent hierarchy of the region + * + * @param region + * @param voice + */ + static void registerVoiceInHierarchy(const Region* region, Voice* voice) noexcept; + /** + * @brief Remove an active voice from the whole parent hierarchy of the region. + * + * @param region + * @param voice + */ + static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) noexcept; + /** + * @brief Get the polyphony limit + * + * @return unsigned + */ + unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; } + /** + * @brief Get the parent set + * + * @return RegionSet* + */ + RegionSet* getParent() const noexcept { return parent; } + /** + * @brief Set the parent set + * + * @param parent + */ + void setParent(RegionSet* parent) noexcept { this->parent = parent; } + /** + * @brief Get the active voices + * + * @return const std::vector& + */ + const std::vector& getActiveVoices() const noexcept { return voices; } + /** + * @brief Get the active voices + * + * @return std::vector& + */ + std::vector& getActiveVoices() noexcept { return voices; } + /** + * @brief Get the regions in the set + * + * @return const std::vector& + */ + const std::vector& getRegions() const noexcept { return regions; } + /** + * @brief Get the region subsets in this set + * + * @return const std::vector& + */ + const std::vector& getSubsets() const noexcept { return subsets; } +private: + RegionSet* parent { nullptr }; + std::vector regions; + std::vector subsets; + std::vector voices; + unsigned polyphonyLimit { config::maxVoices }; +}; + +} diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 2f8fac167..789353353 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -3,6 +3,7 @@ // 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 "absl/meta/type_traits.h" @@ -11,9 +12,17 @@ namespace sfz { struct SisterVoiceRing { + /** + * @brief Apply a lambda function to all sisters in a ring. + * This function should be robust enough to be able to kill the voice + * in the lambda. + * + * @param voice + * @param lambda + */ template>::value, int> = 0> - static void applyToRing(T* voice, F&& lambda) + static void applyToRing(T* voice, F&& lambda) noexcept { auto v = voice->getNextSisterVoice(); while (v != voice) { @@ -24,7 +33,13 @@ struct SisterVoiceRing { lambda(voice); } - static unsigned countSisterVoices(const Voice* start) + /** + * @brief Count the number of sister voices in a ring + * + * @param start + * @return unsigned + */ + static unsigned countSisterVoices(const Voice* start) noexcept { if (!start) return 0; @@ -40,6 +55,54 @@ struct SisterVoiceRing { ASSERT(count < config::maxVoices); return count; } + + /** + * @brief Check if a sister voice ring is well formed + * + * @param start + * @return true + * @return false + */ + static bool checkRingValidity(const Voice* start) noexcept + { + if (start == nullptr) + return true; + + unsigned idx { 0 }; + const Voice* ring[config::maxVoices]; + ring[idx] = start; + while (idx < config::maxVoices) { + const auto* newVoice = ring[idx]->getNextSisterVoice(); + + if (newVoice == nullptr) { + DBG("Error in ring: " << static_cast(ring[idx]) + << " next sister is null"); + return false; + } + + if (newVoice->getPreviousSisterVoice() != ring[idx]) { + DBG("Error in ring: " << static_cast(newVoice) + << " refers " << static_cast(newVoice->getPreviousSisterVoice()) + << " as previous sister voice instead of " + << static_cast(ring[idx])); + return false; + } + + if (newVoice == start) + break; + + for (unsigned i = 1; i < idx; ++i) { + if (ring[i] == newVoice) { + DBG("Error in ring: " << static_cast(newVoice) + << " already present in ring at index " << i); + return false; + } + } + ring[++idx] = newVoice; + } + + return true; + } }; /** @@ -49,7 +112,7 @@ struct SisterVoiceRing { */ class SisterVoiceRingBuilder { public: - ~SisterVoiceRingBuilder() { + ~SisterVoiceRingBuilder() noexcept { if (lastStartedVoice != nullptr) { ASSERT(firstStartedVoice); lastStartedVoice->setNextSisterVoice(firstStartedVoice); @@ -62,7 +125,7 @@ class SisterVoiceRingBuilder { * * @param voice */ - void addVoiceToRing(Voice* voice) { + void addVoiceToRing(Voice* voice) noexcept { if (firstStartedVoice == nullptr) firstStartedVoice = voice; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 766c1786d..ded6eeaa8 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -51,14 +51,30 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) { (void)id; (void)state; - DBG("Voice " << id.number << ": state " << static_cast(state)); + if (state == Voice::State::idle) { + auto voice = getVoiceById(id); + RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); + polyphonyGroups[voice->getRegion()->group].removeVoice(voice); + } + } void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector& members) { + const auto newRegionSet = [&](RegionSet* parentSet) { + ASSERT(parentSet != nullptr); + sets.emplace_back(new RegionSet); + auto newSet = sets.back().get(); + parentSet->addSubset(newSet); + newSet->setParent(parentSet); + currentSet = newSet; + }; + switch (hash(header)) { case hash("global"): globalOpcodes = members; + currentSet = sets.front().get(); + lastHeader = OpcodeScope::kOpcodeScopeGlobal; groupOpcodes.clear(); masterOpcodes.clear(); handleGlobalOpcodes(members); @@ -69,11 +85,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); + else + newRegionSet(currentSet); + lastHeader = OpcodeScope::kOpcodeScopeGroup; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -105,6 +129,8 @@ void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& mes void sfz::Synth::buildRegion(const std::vector& regionOpcodes) { + ASSERT(currentSet != nullptr); + int regionNumber = static_cast(regions.size()); auto lastRegion = absl::make_unique(regionNumber, resources.midiState, defaultPath); @@ -128,6 +154,13 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) if (octaveOffset != 0 || noteOffset != 0) lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset); + // 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); + + lastRegion->parent = currentSet; + currentSet->addRegion(lastRegion.get()); + regions.push_back(std::move(lastRegion)); } @@ -142,6 +175,10 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); + lastHeader = OpcodeScope::kOpcodeScopeGlobal; + sets.clear(); + sets.emplace_back(new RegionSet); + currentSet = sets.front().get(); regions.clear(); effectBuses.clear(); effectBuses.emplace_back(new EffectBus); @@ -162,17 +199,38 @@ void sfz::Synth::clear() masterOpcodes.clear(); groupOpcodes.clear(); unknownOpcodes.clear(); - groupMaxPolyphony.clear(); - groupMaxPolyphony.push_back(config::maxVoices); + polyphonyGroups.clear(); + polyphonyGroups.emplace_back(); + polyphonyGroups.back().setPolyphonyLimit(config::maxVoices); modificationTime = fs::file_time_type::min(); } +void sfz::Synth::handleMasterOpcodes(const std::vector& members) +{ + for (auto& rawMember : members) { + const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal); + + switch (member.lettersOnlyHash) { + case hash("polyphony"): + ASSERT(currentSet != nullptr); + if (auto value = readOpcode(member.value, Default::polyphonyRange)) + currentSet->setPolyphonyLimit(*value); + break; + } + } +} + void sfz::Synth::handleGlobalOpcodes(const std::vector& members) { for (auto& rawMember : members) { const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal); switch (member.lettersOnlyHash) { + case hash("polyphony"): + ASSERT(currentSet != nullptr); + if (auto value = readOpcode(member.value, Default::polyphonyRange)) + currentSet->setPolyphonyLimit(*value); + break; case hash("sw_default"): setValueFromOpcode(member, defaultSwitch, Default::keyRange); break; @@ -187,7 +245,7 @@ void sfz::Synth::handleGlobalOpcodes(const std::vector& members) void sfz::Synth::handleGroupOpcodes(const std::vector& members, const std::vector& masterMembers) { absl::optional groupIdx; - unsigned maxPolyphony { config::maxVoices }; + absl::optional maxPolyphony; const auto parseOpcode = [&](const Opcode& rawMember) { const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup); @@ -197,7 +255,7 @@ void sfz::Synth::handleGroupOpcodes(const std::vector& members, const st setValueFromOpcode(member, groupIdx, Default::groupRange); break; case hash("polyphony"): - setValueFromOpcode(member, maxPolyphony, Range(0, config::maxVoices)); + setValueFromOpcode(member, maxPolyphony, Default::polyphonyRange); break; } }; @@ -208,8 +266,14 @@ void sfz::Synth::handleGroupOpcodes(const std::vector& members, const st for (auto& member : members) parseOpcode(member); - if (groupIdx) - setGroupPolyphony(*groupIdx, maxPolyphony); + if (groupIdx && maxPolyphony) { + setGroupPolyphony(*groupIdx, *maxPolyphony); + } else if (maxPolyphony) { + ASSERT(currentSet != nullptr); + currentSet->setPolyphonyLimit(*maxPolyphony); + } else if (groupIdx && *groupIdx > polyphonyGroups.size()) { + setGroupPolyphony(*groupIdx, config::maxVoices); + } } void sfz::Synth::handleControlOpcodes(const std::vector& members) @@ -437,8 +501,10 @@ void sfz::Synth::finalizeSfzLoad() keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel }); // Some regions had group number but no "group-level" opcodes handled the polyphony - while (groupMaxPolyphony.size() <= region->group) - groupMaxPolyphony.push_back(config::maxVoices); + 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) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note))) @@ -544,52 +610,11 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& voice) { return voice->isFree(); }); + if (freeVoice != voices.end()) return freeVoice->get(); - // Start of the voice stealing algorithm - absl::c_sort(voiceViewArray, voiceOrdering); - - const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [](float sum, const Voice* v) { - return sum + v->getAverageEnvelope(); - }); - const auto envThreshold = sumEnvelope - / static_cast(voiceViewArray.size()) * config::stealingEnvelopeCoeff; - const auto ageThreshold = voiceViewArray.front()->getAge() * config::stealingAgeCoeff; - - Voice* returnedVoice = voiceViewArray.front(); - unsigned idx = 0; - while (idx < voiceViewArray.size()) { - const auto ref = voiceViewArray[idx]; - - if (ref->getAge() < ageThreshold) { - // Went too far, we'll kill the oldest note. - break; - } - - float maxEnvelope { 0.0f }; - SisterVoiceRing::applyToRing(ref, [&](Voice* v) { - maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); - }); - - if (maxEnvelope < envThreshold) { - returnedVoice = ref; - break; - } - - // Jump over the sister voices in the set - do { idx++; } - while (idx < voiceViewArray.size() && sisterVoices(ref, voiceViewArray[idx])); - } - - auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); - SisterVoiceRing::applyToRing(returnedVoice, [&] (Voice* v) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); - ASSERT(returnedVoice->isFree()); - - return returnedVoice; + return {}; } int sfz::Synth::getNumActiveVoices() const noexcept @@ -650,7 +675,6 @@ void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames()); } } - } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept @@ -706,7 +730,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept callbackBreakdown.panning += voice->getLastPanningDuration(); if (voice->toBeCleanedUp()) - voice->reset(); + voice->reset(); } } @@ -809,6 +833,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -820,21 +846,25 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - unsigned activeNotesInGroup { 0 }; - unsigned activeNotes { 0 }; + unsigned notePolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; + Voice* selectedVoice { nullptr }; + regionPolyphonyArray.clear(); for (auto& voice : voices) { - const auto voiceRegion = voice->getRegion(); - if (voiceRegion == nullptr) + if (voice->isFree()) { + if (selectedVoice == nullptr) + selectedVoice = voice.get(); continue; + } - if (voiceRegion->group == region->group) - activeNotesInGroup += 1; + if (voice->getRegion() == region) { + regionPolyphonyArray.push_back(voice.get()); + } if (region->notePolyphony) { if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { - activeNotes += 1; + notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: if (voice->getTriggerValue() < velocity) { @@ -854,22 +884,62 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - if (activeNotesInGroup >= groupMaxPolyphony[region->group]) - continue; - - if (region->notePolyphony && activeNotes >= *region->notePolyphony) { + // Polyphony reached on note_polyphony + if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { if (selfMaskCandidate != nullptr) selfMaskCandidate->release(delay); else // We're the lowest velocity guy here continue; } - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; + auto parent = region->parent; - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(voice); + // Polyphony reached on region + if (regionPolyphonyArray.size() >= region->polyphony) { + selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray)); + goto render; + } + + // Polyphony reached on polyphony group + if (polyphonyGroups[region->group].getActiveVoices().size() + == polyphonyGroups[region->group].getPolyphonyLimit()) { + const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; + } + + // Polyphony reached some parent group/master/etc + while (parent != nullptr) { + if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { + const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; + } + parent = parent->getParent(); + } + + // Engine polyphony reached, we're stealing something + if (selectedVoice == nullptr) { + selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); + } + + render: + // Kill voice if necessary, pre-rendering it into the output buffers + ASSERT(selectedVoice); + if (!selectedVoice->isFree()) { + auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); + SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) { + renderVoiceToOutputs(*v, *tempSpan); + v->reset(); + }); + } + + // Voice should be free now + ASSERT(selectedVoice->isFree()); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); } } } @@ -917,6 +987,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -1079,6 +1151,16 @@ const sfz::EffectBus* sfz::Synth::getEffectBusView(int idx) const noexcept return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr; } +const sfz::RegionSet* sfz::Synth::getRegionSetView(int idx) const noexcept +{ + return (size_t)idx < sets.size() ? sets[idx].get() : nullptr; +} + +const sfz::PolyphonyGroup* sfz::Synth::getPolyphonyGroupView(int idx) const noexcept +{ + return (size_t)idx < polyphonyGroups.size() ? &polyphonyGroups[idx] : nullptr; +} + const sfz::Region* sfz::Synth::getRegionById(NumericId id) const noexcept { const size_t size = regions.size(); @@ -1118,6 +1200,11 @@ const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept return (size_t)idx < voices.size() ? voices[idx].get() : nullptr; } +unsigned sfz::Synth::getNumPolyphonyGroups() const noexcept +{ + return polyphonyGroups.size(); +} + const std::vector& sfz::Synth::getUnknownOpcodes() const noexcept { return unknownOpcodes; @@ -1198,6 +1285,9 @@ void sfz::Synth::resetVoices(int numVoices) voiceViewArray.clear(); voiceViewArray.reserve(numVoices); + regionPolyphonyArray.clear(); + regionPolyphonyArray.reserve(numVoices); + for (auto& voice : voices) { voice->setSampleRate(this->sampleRate); voice->setSamplesPerBlock(this->samplesPerBlock); @@ -1226,8 +1316,10 @@ void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept if (factor == oversamplingFactor) return; - for (auto& voice : voices) + for (auto& voice : voices) { + voice->reset(); + } resources.filePool.emptyFileLoadingQueues(); resources.filePool.setOversamplingFactor(factor); @@ -1339,8 +1431,8 @@ void sfz::Synth::allSoundOff() noexcept void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept { - while (groupMaxPolyphony.size() <= groupIdx) - groupMaxPolyphony.push_back(config::maxVoices); + while (polyphonyGroups.size() <= groupIdx) + polyphonyGroups.emplace_back(); - groupMaxPolyphony[groupIdx] = polyphony; + polyphonyGroups[groupIdx].setPolyphonyLimit(polyphony); } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 9835b593f..bcd795427 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -9,11 +9,14 @@ #include "Parser.h" #include "Voice.h" #include "Region.h" +#include "RegionSet.h" +#include "PolyphonyGroup.h" #include "Effects.h" #include "LeakDetector.h" #include "MidiState.h" #include "AudioSpan.h" #include "parser/Parser.h" +#include "VoiceStealing.h" #include "absl/types/span.h" #include #include @@ -220,17 +223,39 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * for testing. * * @param idx - * @return const Region* + * @return const Voice* */ const Voice* getVoiceView(int idx) const noexcept; /** - * @brief Get a raw view into a specific voice. This is mostly used + * @brief Get a raw view into a specific effect bus. This is mostly used * for testing. * * @param idx - * @return const Region* + * @return const EffectBus* */ const EffectBus* getEffectBusView(int idx) const noexcept; + /** + * @brief Get a raw view into a specific set of regions. This is mostly used + * for testing. + * + * @param idx + * @return const RegionSet* + */ + const RegionSet* getRegionSetView(int idx) const noexcept; + /** + * @brief Get a raw view into a specific polyphony group. This is mostly used + * for testing. + * + * @param idx + * @return const PolyphonyGroup* + */ + const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept; + /** + * @brief Get the number of polyphony groups + * + * @return unsigned + */ + unsigned getNumPolyphonyGroups() const noexcept; /** * @brief Get a list of unknown opcodes. The lifetime of the * string views in the code are linked to the currently loaded @@ -572,7 +597,6 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * @param polyphone the max polyphony */ void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept; - std::vector groupMaxPolyphony { config::maxVoices }; /** * @brief Reset all CCs; to be used on CC 121 @@ -598,6 +622,12 @@ class Synth final : public Voice::StateListener, public Parser::Listener { * @param members the opcodes of the block */ void handleGlobalOpcodes(const std::vector& members); + /** + * @brief Helper function to dispatch opcodes + * + * @param members the opcodes of the block + */ + void handleMasterOpcodes(const std::vector& members); /** * @brief Helper function to dispatch opcodes * @@ -649,8 +679,6 @@ class Synth final : public Voice::StateListener, public Parser::Listener { void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; - unsigned killSisterVoices(const Voice* voiceToKill) noexcept; - // Opcode memory; these are used to build regions, as a new region // will integrate opcodes from the group, master and global block std::vector globalOpcodes; @@ -672,15 +700,27 @@ class Synth final : public Voice::StateListener, public Parser::Listener { // Default active switch if multiple keyswitchable regions are present absl::optional defaultSwitch; std::vector unknownOpcodes; - using RegionPtrVector = std::vector; - using VoicePtrVector = std::vector; - std::vector> regions; - std::vector> voices; + using RegionViewVector = std::vector; + using VoiceViewVector = std::vector; + using VoicePtr = std::unique_ptr; + using RegionPtr = std::unique_ptr; + using RegionSetPtr = std::unique_ptr; + std::vector regions; + std::vector voices; + // These are more general "groups" than sfz and encapsulates the full hierarchy + RegionSet* currentSet; + OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; + std::vector sets; + // 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 - VoicePtrVector voiceViewArray; - std::array noteActivationLists; - std::array ccActivationLists; + VoiceViewVector regionPolyphonyArray; + VoiceStealing stealer; + + VoiceViewVector voiceViewArray; + std::array noteActivationLists; + std::array ccActivationLists; // Effect factory and buses EffectFactory effectFactory; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp new file mode 100644 index 000000000..b4eec3aaa --- /dev/null +++ b/src/sfizz/VoiceStealing.cpp @@ -0,0 +1,53 @@ +#include "VoiceStealing.h" + +sfz::VoiceStealing::VoiceStealing() +{ + voiceScores.reserve(config::maxVoices); +} + +sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept +{ + // Start of the voice stealing algorithm + absl::c_sort(voices, voiceOrdering); + + const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { + return sum + v->getAverageEnvelope(); + }); + // We are checking the envelope to try and kill voices with relative low contribution + // to the output compared to the rest. + const auto envThreshold = sumEnvelope + / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + // 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 = voices.front()->getAge() * config::stealingAgeCoeff; + // This needs to be positive + ASSERT(ageThreshold >= 0); + + Voice* returnedVoice = voices.front(); + unsigned idx = 0; + while (idx < voices.size()) { + const auto ref = voices[idx]; + + if (ref->getAge() <= ageThreshold) { + // Went too far, we'll kill the oldest note. + break; + } + + float maxEnvelope { 0.0f }; + SisterVoiceRing::applyToRing(ref, [&](Voice* v) { + maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); + }); + + if (maxEnvelope < envThreshold) { + returnedVoice = ref; + break; + } + + // Jump over the sister voices in the set + do { idx++; } + while (idx < voices.size() && sisterVoices(ref, voices[idx])); + } + return returnedVoice; +} diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h new file mode 100644 index 000000000..7251f37c4 --- /dev/null +++ b/src/sfizz/VoiceStealing.h @@ -0,0 +1,54 @@ +// 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 "Config.h" +#include "Voice.h" +#include "SisterVoiceRing.h" +#include +#include "absl/types/span.h" + +namespace sfz +{ +class VoiceStealing +{ +public: + VoiceStealing(); + /** + * @brief Propose a voice to steal from a set of voices + * + * @param voices + * @return Voice* + */ + Voice* steal(absl::Span voices) noexcept; +private: + 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; +}; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 43b734713..31e134c80 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ set(SFIZZ_TEST_SOURCES MidiStateT.cpp InterpolatorsT.cpp SmoothersT.cpp + PolyphonyT.cpp RegionActivationT.cpp RegionValueComputationsT.cpp # If we're tweaking the curves this kind of tests does not make sense diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp new file mode 100644 index 000000000..23716cab9 --- /dev/null +++ b/tests/PolyphonyT.cpp @@ -0,0 +1,226 @@ +// 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 "sfizz/Synth.h" +#include "sfizz/SfzHelpers.h" +#include "catch2/catch.hpp" + +using namespace Catch::literals; +using namespace sfz::literals; + +constexpr int blockSize { 256 }; + + +TEST_CASE("[Polyphony] Polyphony in hierarchy") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=61 sample=*sine polyphony=2 + polyphony=2 + key=62 sample=*sine + polyphony=3 + key=63 sample=*sine + key=63 sample=*sine + key=63 sample=*sine + polyphony=4 + key=64 sample=*sine polyphony=5 + key=64 sample=*sine + key=64 sample=*sine + key=64 sample=*sine + )"); + REQUIRE( synth.getRegionView(0)->polyphony == 2 ); + REQUIRE( synth.getRegionSetView(1)->getPolyphonyLimit() == 2 ); + REQUIRE( synth.getRegionView(1)->polyphony == 2 ); + REQUIRE( synth.getRegionSetView(2)->getPolyphonyLimit() == 3 ); + REQUIRE( synth.getRegionSetView(2)->getRegions()[0]->polyphony == 3 ); + REQUIRE( synth.getRegionSetView(3)->getPolyphonyLimit() == 4 ); + REQUIRE( synth.getRegionSetView(3)->getRegions()[0]->polyphony == 5 ); + REQUIRE( synth.getRegionSetView(3)->getRegions()[1]->polyphony == 4 ); +} + +TEST_CASE("[Polyphony] Polyphony groups") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + key=62 sample=*sine + group=1 polyphony=3 + key=63 sample=*sine + key=63 sample=*sine group=2 polyphony=4 + key=63 sample=*sine group=4 polyphony=5 + group=4 + key=62 sample=*sine + )"); + REQUIRE( synth.getNumPolyphonyGroups() == 5 ); + REQUIRE( synth.getNumRegions() == 5 ); + REQUIRE( synth.getRegionView(0)->group == 0 ); + REQUIRE( synth.getRegionView(1)->group == 1 ); + REQUIRE( synth.getRegionView(2)->group == 2 ); + REQUIRE( synth.getRegionView(3)->group == 4 ); + REQUIRE( synth.getRegionView(3)->polyphony == 5 ); + REQUIRE( synth.getRegionView(4)->group == 4 ); + REQUIRE( synth.getPolyphonyGroupView(1)->getPolyphonyLimit() == 3 ); + REQUIRE( synth.getPolyphonyGroupView(2)->getPolyphonyLimit() == 4 ); + REQUIRE( synth.getPolyphonyGroupView(3)->getPolyphonyLimit() == sfz::config::maxVoices ); + REQUIRE( synth.getPolyphonyGroupView(4)->getPolyphonyLimit() == 5 ); +} + +TEST_CASE("[Polyphony] group polyphony limits") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + group=1 polyphony=2 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + polyphony=5 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + sample=*saw key=65 + + polyphony=5 + sample=*sine key=66 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 66, 64); + synth.noteOn(0, 66, 64); + synth.noteOn(0, 66, 64); + REQUIRE(synth.getNumActiveVoices() == 5); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + polyphony=5 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Polyphony in master") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(blockSize); + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + group=2 + sample=*sine key=65 + group=3 + sample=*sine key=63 + // Empty master resets the polyphony + sample=*sine key=61 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + synth.allSoundOff(); + synth.renderBlock(buffer); + REQUIRE(synth.getNumActiveVoices() == 0); + synth.noteOn(0, 63, 64); + synth.noteOn(0, 63, 64); + synth.noteOn(0, 63, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + synth.allSoundOff(); + synth.renderBlock(buffer); + REQUIRE(synth.getNumActiveVoices() == 0); + synth.noteOn(0, 61, 64); + synth.noteOn(0, 61, 64); + synth.noteOn(0, 61, 64); + REQUIRE(synth.getNumActiveVoices() == 3); +} + + +TEST_CASE("[Polyphony] Self-masking") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sample=*sine key=64 note_polyphony=2 + )"); + synth.noteOn(0, 64, 63); + synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 64); + REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Not self-masking") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sample=*sine key=66 note_polyphony=2 note_selfmask=off + )"); + synth.noteOn(0, 66, 63); + synth.noteOn(0, 66, 62); + synth.noteOn(0, 66, 64); + REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); +}