From 5ef921485b6990d304cf85ae8b04264e6b55cc70 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 02:03:04 +0200 Subject: [PATCH 01/11] Add the region sets (hierarchy) Link the voice lifecycle into region sets and polyphony groups Respect crudely the polyphony limits without stealing for now --- src/sfizz/PolyphonyGroup.h | 40 +++++++ src/sfizz/Region.cpp | 4 + src/sfizz/Region.h | 8 +- src/sfizz/RegionSet.h | 74 ++++++++++++ src/sfizz/Synth.cpp | 149 +++++++++++++++++++++---- src/sfizz/Synth.h | 63 ++++++++--- tests/CMakeLists.txt | 1 + tests/PolyphonyT.cpp | 223 +++++++++++++++++++++++++++++++++++++ 8 files changed, 528 insertions(+), 34 deletions(-) create mode 100644 src/sfizz/PolyphonyGroup.h create mode 100644 src/sfizz/RegionSet.h create mode 100644 tests/PolyphonyT.cpp diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h new file mode 100644 index 000000000..8f6a8c409 --- /dev/null +++ b/src/sfizz/PolyphonyGroup.h @@ -0,0 +1,40 @@ +#pragma once +#include "Region.h" +#include "Voice.h" +#include "absl/algorithm/container.h" + +namespace sfz +{ +class PolyphonyGroup { +public: + void setPolyphonyLimit(unsigned limit) + { + polyphonyLimit = limit; + voices.reserve(limit); + } + unsigned getPolyphonyLimit() const { return polyphonyLimit; } + void registerVoice(Voice* voice) + { + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); + } + void removeVoice(const Voice* voice) + { + auto it = absl::c_find(voices, voice); + if (it == voices.end()) + return; + + auto last = voices.end() - 1; + if (it != last) + std::iter_swap(it, last); + + voices.pop_back(); + } + const std::vector& getActiveVoices() const { return voices; } + std::vector& getActiveVoices() { 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.h b/src/sfizz/RegionSet.h new file mode 100644 index 000000000..539e3eca4 --- /dev/null +++ b/src/sfizz/RegionSet.h @@ -0,0 +1,74 @@ +#pragma once +#include "Region.h" +#include "Voice.h" +#include + +namespace sfz +{ + +class RegionSet { +public: + void setPolyphonyLimit(unsigned limit) + { + polyphonyLimit = limit; + voices.reserve(limit); + } + unsigned getPolyphonyLimit() const { return polyphonyLimit; } + void addRegion(Region* region) + { + if (absl::c_find(regions, region) == regions.end()) + regions.push_back(region); + } + void addSubset(RegionSet* group) + { + if (absl::c_find(subsets, group) == subsets.end()) + subsets.push_back(group); + } + void registerVoice(Voice* voice) + { + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); + } + void removeVoice(const Voice* voice) + { + auto it = absl::c_find(voices, voice); + if (it == voices.end()) + return; + + auto last = voices.end() - 1; + if (it != last) + std::iter_swap(it, last); + + voices.pop_back(); + DBG("Active voices size " << voices.size()); + } + static void registerVoiceInHierarchy(const Region* region, Voice* voice) + { + auto parent = region->parent; + while (parent != nullptr) { + parent->registerVoice(voice); + parent = parent->getParent(); + } + } + static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) + { + auto parent = region->parent; + while (parent != nullptr) { + parent->removeVoice(voice); + parent = parent->getParent(); + } + } + RegionSet* getParent() const { return parent; } + void setParent(RegionSet* parent) { this->parent = parent; } + const std::vector& getActiveVoices() const { return voices; } + const std::vector& getRegions() const { return regions; } + const std::vector& getSubsets() const { return subsets; } +private: + RegionSet* parent { nullptr }; + std::vector regions; + std::vector subsets; + std::vector voices; + unsigned polyphonyLimit { config::maxVoices }; +}; + +} diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 766c1786d..8d1d6222d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -51,14 +51,31 @@ 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); + DBG("Removing voice " << id.number << " from hierarchies"); + 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 = Header::Global; groupOpcodes.clear(); masterOpcodes.clear(); handleGlobalOpcodes(members); @@ -69,11 +86,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); + else + newRegionSet(currentSet); + lastHeader = Header::Group; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -105,6 +130,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 +155,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 +176,9 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); + 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))) @@ -809,6 +875,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,8 +888,8 @@ 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 }; + unsigned regionPolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; for (auto& voice : voices) { @@ -829,12 +897,12 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (voiceRegion == nullptr) continue; - if (voiceRegion->group == region->group) - activeNotesInGroup += 1; + if (voiceRegion == region) + regionPolyphonyCounter += 1; 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,10 +922,30 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - if (activeNotesInGroup >= groupMaxPolyphony[region->group]) + // FIXME: Do something for the polyphony limit + if (polyphonyGroups[region->group].getActiveVoices().size() + == polyphonyGroups[region->group].getPolyphonyLimit()) + continue; + + // FIXME: Do something for the polyphony limit + if (regionPolyphonyCounter >= region->polyphony) continue; - if (region->notePolyphony && activeNotes >= *region->notePolyphony) { + // FIXME: Do something for the polyphony limit + auto parent = region->parent; + bool polyphonyReached { false }; + while (parent != nullptr) { + if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { + polyphonyReached = true; + break; + } + + parent = parent->getParent(); + } + if (polyphonyReached) + continue; + + if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { if (selfMaskCandidate != nullptr) selfMaskCandidate->release(delay); else // We're the lowest velocity guy here @@ -870,6 +958,8 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -917,6 +1007,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 +1171,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 +1220,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; @@ -1226,8 +1333,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 +1448,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..28bd72ad8 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -9,6 +9,8 @@ #include "Parser.h" #include "Voice.h" #include "Region.h" +#include "RegionSet.h" +#include "PolyphonyGroup.h" #include "Effects.h" #include "LeakDetector.h" #include "MidiState.h" @@ -220,17 +222,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 +596,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 +621,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 +678,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 +699,25 @@ 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 + enum class Header { Global, Master, Group }; + RegionSet* currentSet; + Header lastHeader { Header::Global }; + 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 voiceViewArray; + std::array noteActivationLists; + std::array ccActivationLists; // Effect factory and buses EffectFactory effectFactory; 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..815b6c23d --- /dev/null +++ b/tests/PolyphonyT.cpp @@ -0,0 +1,223 @@ +// 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=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 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()); +} From 2195466055dac887f0772ab188e107dad5f24334 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 14:18:26 +0200 Subject: [PATCH 02/11] Rebase and use the swap and pop helper --- src/sfizz/PolyphonyGroup.h | 11 ++--------- src/sfizz/RegionSet.h | 12 ++---------- src/sfizz/Synth.cpp | 1 - 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index 8f6a8c409..ed5d61597 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -1,6 +1,7 @@ #pragma once #include "Region.h" #include "Voice.h" +#include "SwapAndPop.h" #include "absl/algorithm/container.h" namespace sfz @@ -20,15 +21,7 @@ class PolyphonyGroup { } void removeVoice(const Voice* voice) { - auto it = absl::c_find(voices, voice); - if (it == voices.end()) - return; - - auto last = voices.end() - 1; - if (it != last) - std::iter_swap(it, last); - - voices.pop_back(); + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); } const std::vector& getActiveVoices() const { return voices; } std::vector& getActiveVoices() { return voices; } diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 539e3eca4..87f8490b3 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -1,6 +1,7 @@ #pragma once #include "Region.h" #include "Voice.h" +#include "SwapAndPop.h" #include namespace sfz @@ -31,16 +32,7 @@ class RegionSet { } void removeVoice(const Voice* voice) { - auto it = absl::c_find(voices, voice); - if (it == voices.end()) - return; - - auto last = voices.end() - 1; - if (it != last) - std::iter_swap(it, last); - - voices.pop_back(); - DBG("Active voices size " << voices.size()); + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); } static void registerVoiceInHierarchy(const Region* region, Voice* voice) { diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 8d1d6222d..0eec9944a 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -53,7 +53,6 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) (void)state; if (state == Voice::State::idle) { auto voice = getVoiceById(id); - DBG("Removing voice " << id.number << " from hierarchies"); RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); polyphonyGroups[voice->getRegion()->group].removeVoice(voice); } From 89093b18263bd97e36543b6a15468f1d47e1a9c5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 22:27:43 +0200 Subject: [PATCH 03/11] Use explicit pointers --- src/sfizz/RegionSet.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 87f8490b3..3cb461969 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -36,7 +36,7 @@ class RegionSet { } static void registerVoiceInHierarchy(const Region* region, Voice* voice) { - auto parent = region->parent; + auto* parent = region->parent; while (parent != nullptr) { parent->registerVoice(voice); parent = parent->getParent(); @@ -44,7 +44,7 @@ class RegionSet { } static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) { - auto parent = region->parent; + auto* parent = region->parent; while (parent != nullptr) { parent->removeVoice(voice); parent = parent->getParent(); From 7559dfdd7b482c3cba3c130ea7eeb315eb3c0d6b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 22:28:09 +0200 Subject: [PATCH 04/11] Use the existing OpcodeScope and ass the "" level --- src/sfizz/Opcode.h | 2 ++ src/sfizz/Synth.cpp | 9 +++++---- src/sfizz/Synth.h | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) 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/Synth.cpp b/src/sfizz/Synth.cpp index 0eec9944a..bc2177b5f 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -74,7 +74,7 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); else newRegionSet(currentSet); - lastHeader = Header::Group; + lastHeader = OpcodeScope::kOpcodeScopeGroup; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -175,6 +175,7 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); + lastHeader = OpcodeScope::kOpcodeScopeGlobal; sets.clear(); sets.emplace_back(new RegionSet); currentSet = sets.front().get(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 28bd72ad8..3c08e9549 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -707,9 +707,8 @@ class Synth final : public Voice::StateListener, public Parser::Listener { std::vector regions; std::vector voices; // These are more general "groups" than sfz and encapsulates the full hierarchy - enum class Header { Global, Master, Group }; RegionSet* currentSet; - Header lastHeader { Header::Global }; + OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; std::vector sets; // These are the `group=` groups where you can off voices std::vector polyphonyGroups; From 396fb2199a91f72f7615d49605d9e236f9835eeb Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 22 Jun 2020 22:14:03 +0200 Subject: [PATCH 05/11] Add the voice stealing helper --- src/sfizz/VoiceStealing.h | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/sfizz/VoiceStealing.h diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h new file mode 100644 index 000000000..8cf4362b5 --- /dev/null +++ b/src/sfizz/VoiceStealing.h @@ -0,0 +1,87 @@ +// 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 "Config.h" +#include "Voice.h" +#include "SisterVoiceRing.h" +#include +#include "absl/types/span.h" + +namespace sfz +{ +class VoiceStealing +{ +public: + VoiceStealing() + { + voiceScores.reserve(config::maxVoices); + } + + Voice* 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(); + }); + const auto envThreshold = sumEnvelope + / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; + + 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; + } + +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; +}; +} From 50ab6910e1212ad259d04efa7c721e6e3417bf5a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 22 Jun 2020 23:02:12 +0200 Subject: [PATCH 06/11] Find a free voice on the fly --- src/sfizz/Synth.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index bc2177b5f..f808200ce 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -891,13 +891,16 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc unsigned notePolyphonyCounter { 0 }; unsigned regionPolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; + Voice* selectedVoice { nullptr }; for (auto& voice : voices) { - const auto voiceRegion = voice->getRegion(); - if (voiceRegion == nullptr) + if (voice->isFree()) { + if (selectedVoice == nullptr) + selectedVoice = voice.get(); continue; + } - if (voiceRegion == region) + if (voice->getRegion() == region) regionPolyphonyCounter += 1; if (region->notePolyphony) { @@ -923,12 +926,12 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } // FIXME: Do something for the polyphony limit - if (polyphonyGroups[region->group].getActiveVoices().size() - == polyphonyGroups[region->group].getPolyphonyLimit()) + if (regionPolyphonyCounter >= region->polyphony) continue; // FIXME: Do something for the polyphony limit - if (regionPolyphonyCounter >= region->polyphony) + if (polyphonyGroups[region->group].getActiveVoices().size() + == polyphonyGroups[region->group].getPolyphonyLimit()) continue; // FIXME: Do something for the polyphony limit @@ -952,14 +955,10 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; } - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); } } } From 32fe0cacf17a8d574d541b4b3042414895a743aa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 22 Jun 2020 23:51:23 +0200 Subject: [PATCH 07/11] Use the voice stealer for all polyphony limits --- src/sfizz/PolyphonyGroup.h | 7 +++ src/sfizz/RegionSet.h | 8 +++ src/sfizz/SisterVoiceRing.h | 1 + src/sfizz/Synth.cpp | 114 ++++++++++++++++-------------------- src/sfizz/Synth.h | 4 ++ src/sfizz/VoiceStealing.h | 3 + 6 files changed, 72 insertions(+), 65 deletions(-) diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index ed5d61597..74314da71 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -1,4 +1,11 @@ +// 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" diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 3cb461969..5203442bf 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -1,4 +1,11 @@ +// 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" @@ -53,6 +60,7 @@ class RegionSet { RegionSet* getParent() const { return parent; } void setParent(RegionSet* parent) { this->parent = parent; } const std::vector& getActiveVoices() const { return voices; } + std::vector& getActiveVoices() { return voices; } const std::vector& getRegions() const { return regions; } const std::vector& getSubsets() const { return subsets; } private: diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 2f8fac167..7bd4c0ef7 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" diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f808200ce..500e4edc1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -610,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 @@ -716,7 +675,6 @@ void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames()); } } - } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept @@ -889,9 +847,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { unsigned notePolyphonyCounter { 0 }; - unsigned regionPolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; Voice* selectedVoice { nullptr }; + regionPolyphonyArray.clear(); for (auto& voice : voices) { if (voice->isFree()) { @@ -900,8 +858,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; } - if (voice->getRegion() == region) - regionPolyphonyCounter += 1; + if (voice->getRegion() == region) { + regionPolyphonyArray.push_back(voice.get()); + } if (region->notePolyphony) { if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { @@ -925,36 +884,58 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - // FIXME: Do something for the polyphony limit - if (regionPolyphonyCounter >= region->polyphony) - continue; + auto parent = region->parent; + + // Polyphony reached on region + if (regionPolyphonyArray.size() >= region->polyphony) { + selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray)); + goto render; + } - // FIXME: Do something for the polyphony limit + // Polyphony reached on polyphony group if (polyphonyGroups[region->group].getActiveVoices().size() - == polyphonyGroups[region->group].getPolyphonyLimit()) - continue; + == polyphonyGroups[region->group].getPolyphonyLimit()) { + const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; + } - // FIXME: Do something for the polyphony limit - auto parent = region->parent; - bool polyphonyReached { false }; + // Polyphony reached some parent group/master/etc while (parent != nullptr) { if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { - polyphonyReached = true; - break; + const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; } - parent = parent->getParent(); } - if (polyphonyReached) - continue; + // 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; + if (selfMaskCandidate == nullptr) + continue; // We're the lowest velocity guy here + selectedVoice = selfMaskCandidate; + goto render; + } + + // 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); @@ -1304,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); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3c08e9549..bcd795427 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -16,6 +16,7 @@ #include "MidiState.h" #include "AudioSpan.h" #include "parser/Parser.h" +#include "VoiceStealing.h" #include "absl/types/span.h" #include #include @@ -714,6 +715,9 @@ class Synth final : public Voice::StateListener, public Parser::Listener { std::vector polyphonyGroups; // Views to speed up iteration over the regions and voices when events // occur in the audio callback + VoiceViewVector regionPolyphonyArray; + VoiceStealing stealer; + VoiceViewVector voiceViewArray; std::array noteActivationLists; std::array ccActivationLists; diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h index 8cf4362b5..6914d1b0d 100644 --- a/src/sfizz/VoiceStealing.h +++ b/src/sfizz/VoiceStealing.h @@ -3,6 +3,9 @@ // 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" From 825116063386782b7ea19ffd56900ac0d971377b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 23 Jun 2020 00:14:15 +0200 Subject: [PATCH 08/11] Put stuff into cpp files and add docs --- src/CMakeLists.txt | 3 + src/sfizz/PolyphonyGroup.cpp | 18 +++++ src/sfizz/PolyphonyGroup.h | 54 +++++++++----- src/sfizz/RegionSet.cpp | 48 +++++++++++++ src/sfizz/RegionSet.h | 134 +++++++++++++++++++++++------------ src/sfizz/SisterVoiceRing.h | 8 +-- src/sfizz/VoiceStealing.cpp | 45 ++++++++++++ src/sfizz/VoiceStealing.h | 52 +++----------- 8 files changed, 250 insertions(+), 112 deletions(-) create mode 100644 src/sfizz/PolyphonyGroup.cpp create mode 100644 src/sfizz/RegionSet.cpp create mode 100644 src/sfizz/VoiceStealing.cpp 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/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 index 74314da71..d3e1413bf 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -15,23 +15,43 @@ namespace sfz { class PolyphonyGroup { public: - void setPolyphonyLimit(unsigned limit) - { - polyphonyLimit = limit; - voices.reserve(limit); - } - unsigned getPolyphonyLimit() const { return polyphonyLimit; } - void registerVoice(Voice* voice) - { - if (absl::c_find(voices, voice) == voices.end()) - voices.push_back(voice); - } - void removeVoice(const Voice* voice) - { - swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); - } - const std::vector& getActiveVoices() const { return voices; } - std::vector& getActiveVoices() { return voices; } + /** + * @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/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 index 5203442bf..24120173a 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -16,53 +16,93 @@ namespace sfz class RegionSet { public: - void setPolyphonyLimit(unsigned limit) - { - polyphonyLimit = limit; - voices.reserve(limit); - } - unsigned getPolyphonyLimit() const { return polyphonyLimit; } - void addRegion(Region* region) - { - if (absl::c_find(regions, region) == regions.end()) - regions.push_back(region); - } - void addSubset(RegionSet* group) - { - if (absl::c_find(subsets, group) == subsets.end()) - subsets.push_back(group); - } - void registerVoice(Voice* voice) - { - if (absl::c_find(voices, voice) == voices.end()) - voices.push_back(voice); - } - void removeVoice(const Voice* voice) - { - swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); - } - static void registerVoiceInHierarchy(const Region* region, Voice* voice) - { - auto* parent = region->parent; - while (parent != nullptr) { - parent->registerVoice(voice); - parent = parent->getParent(); - } - } - static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) - { - auto* parent = region->parent; - while (parent != nullptr) { - parent->removeVoice(voice); - parent = parent->getParent(); - } - } - RegionSet* getParent() const { return parent; } - void setParent(RegionSet* parent) { this->parent = parent; } - const std::vector& getActiveVoices() const { return voices; } - std::vector& getActiveVoices() { return voices; } - const std::vector& getRegions() const { return regions; } - const std::vector& getSubsets() const { return subsets; } + /** + * @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; diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 7bd4c0ef7..4df6ec4dd 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -14,7 +14,7 @@ namespace sfz struct SisterVoiceRing { 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) { @@ -25,7 +25,7 @@ struct SisterVoiceRing { lambda(voice); } - static unsigned countSisterVoices(const Voice* start) + static unsigned countSisterVoices(const Voice* start) noexcept { if (!start) return 0; @@ -50,7 +50,7 @@ struct SisterVoiceRing { */ class SisterVoiceRingBuilder { public: - ~SisterVoiceRingBuilder() { + ~SisterVoiceRingBuilder() noexcept { if (lastStartedVoice != nullptr) { ASSERT(firstStartedVoice); lastStartedVoice->setNextSisterVoice(firstStartedVoice); @@ -63,7 +63,7 @@ class SisterVoiceRingBuilder { * * @param voice */ - void addVoiceToRing(Voice* voice) { + void addVoiceToRing(Voice* voice) noexcept { if (firstStartedVoice == nullptr) firstStartedVoice = voice; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp new file mode 100644 index 000000000..a90e4950b --- /dev/null +++ b/src/sfizz/VoiceStealing.cpp @@ -0,0 +1,45 @@ +#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(); + }); + const auto envThreshold = sumEnvelope + / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; + + 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 index 6914d1b0d..7251f37c4 100644 --- a/src/sfizz/VoiceStealing.h +++ b/src/sfizz/VoiceStealing.h @@ -17,50 +17,14 @@ namespace sfz class VoiceStealing { public: - VoiceStealing() - { - voiceScores.reserve(config::maxVoices); - } - - Voice* 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(); - }); - const auto envThreshold = sumEnvelope - / static_cast(voices.size()) * config::stealingEnvelopeCoeff; - const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; - - 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; - } - + 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 { From 498e1b95bbaab306c14136e5ce699ff9e0d0f0bc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 23 Jun 2020 00:21:30 +0200 Subject: [PATCH 09/11] Correct the self-masking behavior --- src/sfizz/Synth.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 500e4edc1..90542333c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -884,6 +884,14 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } + // 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 parent = region->parent; // Polyphony reached on region @@ -910,14 +918,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc parent = parent->getParent(); } - // Polyphony reached on note_polyphony - if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { - if (selfMaskCandidate == nullptr) - continue; // We're the lowest velocity guy here - selectedVoice = selfMaskCandidate; - goto render; - } - // Engine polyphony reached, we're stealing something if (selectedVoice == nullptr) { selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); From 1e6cb8245e56fb297eb885726e60e35d6a7bc78d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 23 Jun 2020 00:21:53 +0200 Subject: [PATCH 10/11] Updated a test that did not account for sister voices --- tests/PolyphonyT.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 815b6c23d..23716cab9 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -129,11 +129,14 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") sample=*saw key=65 polyphony=5 - sample=*sine key=65 + 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); } From b8a23ee205937f1e9a68d62c3677decc83970fc7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 2 Jul 2020 10:08:58 +0200 Subject: [PATCH 11/11] Add comments and a helper function to test ring validity --- src/sfizz/SisterVoiceRing.h | 62 +++++++++++++++++++++++++++++++++++++ src/sfizz/Synth.cpp | 2 +- src/sfizz/VoiceStealing.cpp | 10 +++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 4df6ec4dd..789353353 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -12,6 +12,14 @@ 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) noexcept @@ -25,6 +33,12 @@ struct SisterVoiceRing { lambda(voice); } + /** + * @brief Count the number of sister voices in a ring + * + * @param start + * @return unsigned + */ static unsigned countSisterVoices(const Voice* start) noexcept { if (!start) @@ -41,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; + } }; /** diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 90542333c..ded6eeaa8 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -730,7 +730,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept callbackBreakdown.panning += voice->getLastPanningDuration(); if (voice->toBeCleanedUp()) - voice->reset(); + voice->reset(); } } diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index a90e4950b..b4eec3aaa 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -13,16 +13,24 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept 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) { + if (ref->getAge() <= ageThreshold) { // Went too far, we'll kill the oldest note. break; }