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 e772b734c..5d33d5ab3 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -158,6 +158,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 e363116b0..b5bf56f46 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -23,6 +23,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 @@ -272,7 +275,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 @@ -357,6 +361,8 @@ struct Region { // Effects std::vector gainToEffect; + // 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 5fa4827b3..be9e173bf 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -49,14 +49,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); @@ -67,11 +84,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); + else + newRegionSet(currentSet); + lastHeader = Header::Group; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -103,6 +128,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); @@ -126,6 +153,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)); } @@ -140,6 +174,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); @@ -160,17 +197,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; @@ -185,7 +243,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); @@ -195,7 +253,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; } }; @@ -206,8 +264,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) @@ -434,8 +498,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))) @@ -803,6 +869,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); } } } @@ -814,8 +882,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) { @@ -823,12 +891,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) { @@ -848,10 +916,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 @@ -864,6 +952,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); } } } @@ -911,6 +1001,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); } } } @@ -1073,6 +1165,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(); @@ -1112,6 +1214,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; @@ -1209,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); @@ -1322,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 00372288b..9dcfd0790 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 * @@ -645,8 +674,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; @@ -668,15 +695,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 c71cee5b5..00e0cf874 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ set(SFIZZ_TEST_SOURCES MidiStateT.cpp OnePoleFilterT.cpp InterpolatorsT.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()); +}