Merge pull request #275 from paulfd/group-hierarchy

Region sets
This commit is contained in:
Paul Ferrand 2020-07-02 11:09:29 +02:00 committed by GitHub
commit 87be03832e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 876 additions and 92 deletions

View file

@ -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

View file

@ -47,6 +47,8 @@ enum OpcodeScope {
kOpcodeScopeGlobal,
//! control scope
kOpcodeScopeControl,
//! Master scope
kOpcodeScopeMaster,
//! group scope
kOpcodeScopeGroup,
//! region scope

View file

@ -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; });
}

View file

@ -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<Voice*>&
*/
const std::vector<Voice*>& getActiveVoices() const noexcept { return voices; }
/**
* @brief Get the active voices
*
* @return std::vector<Voice*>&
*/
std::vector<Voice*>& getActiveVoices() noexcept { return voices; }
private:
unsigned polyphonyLimit { config::maxVoices };
std::vector<Voice*> voices;
};
}

View file

@ -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;

View file

@ -24,6 +24,9 @@
#include <vector>
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<uint32_t> offBy {}; // off_by
SfzOffMode offMode { Default::offMode }; // off_mode
absl::optional<uint32_t> notePolyphony {};
absl::optional<uint32_t> notePolyphony {}; // note_polyphony
unsigned polyphony { config::maxVoices }; // polyphony
SfzSelfMask selfMask { Default::selfMask };
// Region logic: key mapping
@ -365,6 +369,8 @@ struct Region {
// Modifiers
ModifierArray<CCMap<Modifier>> modifiers;
// Parent
RegionSet* parent { nullptr };
private:
const MidiState& midiState;
bool keySwitched { true };

48
src/sfizz/RegionSet.cpp Normal file
View file

@ -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();
}
}

114
src/sfizz/RegionSet.h Normal file
View file

@ -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 <vector>
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<Voice*>&
*/
const std::vector<Voice*>& getActiveVoices() const noexcept { return voices; }
/**
* @brief Get the active voices
*
* @return std::vector<Voice*>&
*/
std::vector<Voice*>& getActiveVoices() noexcept { return voices; }
/**
* @brief Get the regions in the set
*
* @return const std::vector<Region*>&
*/
const std::vector<Region*>& getRegions() const noexcept { return regions; }
/**
* @brief Get the region subsets in this set
*
* @return const std::vector<RegionSet*>&
*/
const std::vector<RegionSet*>& getSubsets() const noexcept { return subsets; }
private:
RegionSet* parent { nullptr };
std::vector<Region*> regions;
std::vector<RegionSet*> subsets;
std::vector<Voice*> voices;
unsigned polyphonyLimit { config::maxVoices };
};
}

View file

@ -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<class F, class T,
absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::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<const void*>(ring[idx])
<< " next sister is null");
return false;
}
if (newVoice->getPreviousSisterVoice() != ring[idx]) {
DBG("Error in ring: " << static_cast<const void*>(newVoice)
<< " refers " << static_cast<const void*>(newVoice->getPreviousSisterVoice())
<< " as previous sister voice instead of "
<< static_cast<const void*>(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<const void*>(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 @@ public:
*
* @param voice
*/
void addVoiceToRing(Voice* voice) {
void addVoiceToRing(Voice* voice) noexcept {
if (firstStartedVoice == nullptr)
firstStartedVoice = voice;

View file

@ -51,14 +51,30 @@ void sfz::Synth::onVoiceStateChanged(NumericId<Voice> id, Voice::State state)
{
(void)id;
(void)state;
DBG("Voice " << id.number << ": state " << static_cast<int>(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<Opcode>& 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::vector<O
break;
case hash("master"):
masterOpcodes = members;
newRegionSet(sets.front().get());
groupOpcodes.clear();
lastHeader = OpcodeScope::kOpcodeScopeMaster;
handleMasterOpcodes(members);
numMasters++;
break;
case hash("group"):
groupOpcodes = members;
if (lastHeader == OpcodeScope::kOpcodeScopeGroup)
newRegionSet(currentSet->getParent());
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<Opcode>& regionOpcodes)
{
ASSERT(currentSet != nullptr);
int regionNumber = static_cast<int>(regions.size());
auto lastRegion = absl::make_unique<Region>(regionNumber, resources.midiState, defaultPath);
@ -128,6 +154,13 @@ void sfz::Synth::buildRegion(const std::vector<Opcode>& 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<Opcode>& 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<Opcode>& 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<Opcode>& members)
void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const std::vector<Opcode>& masterMembers)
{
absl::optional<unsigned> groupIdx;
unsigned maxPolyphony { config::maxVoices };
absl::optional<unsigned> maxPolyphony;
const auto parseOpcode = [&](const Opcode& rawMember) {
const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup);
@ -197,7 +255,7 @@ void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const st
setValueFromOpcode(member, groupIdx, Default::groupRange);
break;
case hash("polyphony"):
setValueFromOpcode(member, maxPolyphony, Range<unsigned>(0, config::maxVoices));
setValueFromOpcode(member, maxPolyphony, Default::polyphonyRange);
break;
}
};
@ -208,8 +266,14 @@ void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& 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<Opcode>& 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>& 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<float>(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<float>& tempSpan)
bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames());
}
}
}
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
@ -706,7 +730,7 @@ void sfz::Synth::renderBlock(AudioSpan<float> 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<Region> 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<std::string>& 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);
}

View file

@ -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 <absl/types/optional.h>
#include <random>
@ -220,17 +223,39 @@ public:
* 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 @@ private:
* @param polyphone the max polyphony
*/
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
std::vector<unsigned> groupMaxPolyphony { config::maxVoices };
/**
* @brief Reset all CCs; to be used on CC 121
@ -598,6 +622,12 @@ private:
* @param members the opcodes of the <global> block
*/
void handleGlobalOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <master> opcodes
*
* @param members the opcodes of the <master> block
*/
void handleMasterOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <group> opcodes
*
@ -649,8 +679,6 @@ private:
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<Opcode> globalOpcodes;
@ -672,15 +700,27 @@ private:
// Default active switch if multiple keyswitchable regions are present
absl::optional<uint8_t> defaultSwitch;
std::vector<std::string> unknownOpcodes;
using RegionPtrVector = std::vector<Region*>;
using VoicePtrVector = std::vector<Voice*>;
std::vector<std::unique_ptr<Region>> regions;
std::vector<std::unique_ptr<Voice>> voices;
using RegionViewVector = std::vector<Region*>;
using VoiceViewVector = std::vector<Voice*>;
using VoicePtr = std::unique_ptr<Voice>;
using RegionPtr = std::unique_ptr<Region>;
using RegionSetPtr = std::unique_ptr<RegionSet>;
std::vector<RegionPtr> regions;
std::vector<VoicePtr> voices;
// These are more general "groups" than sfz and encapsulates the full hierarchy
RegionSet* currentSet;
OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal };
std::vector<RegionSetPtr> sets;
// These are the `group=` groups where you can off voices
std::vector<PolyphonyGroup> polyphonyGroups;
// Views to speed up iteration over the regions and voices when events
// occur in the audio callback
VoicePtrVector voiceViewArray;
std::array<RegionPtrVector, 128> noteActivationLists;
std::array<RegionPtrVector, config::numCCs> ccActivationLists;
VoiceViewVector regionPolyphonyArray;
VoiceStealing stealer;
VoiceViewVector voiceViewArray;
std::array<RegionViewVector, 128> noteActivationLists;
std::array<RegionViewVector, config::numCCs> ccActivationLists;
// Effect factory and buses
EffectFactory effectFactory;

View file

@ -0,0 +1,53 @@
#include "VoiceStealing.h"
sfz::VoiceStealing::VoiceStealing()
{
voiceScores.reserve(config::maxVoices);
}
sfz::Voice* sfz::VoiceStealing::steal(absl::Span<sfz::Voice*> 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<float>(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;
}

54
src/sfizz/VoiceStealing.h Normal file
View file

@ -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 <vector>
#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<Voice*> 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<VoiceScore> voiceScores;
};
}

View file

@ -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

226
tests/PolyphonyT.cpp Normal file
View file

@ -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"(
<region> key=61 sample=*sine polyphony=2
<group> polyphony=2
<region> key=62 sample=*sine
<master> polyphony=3
<region> key=63 sample=*sine
<region> key=63 sample=*sine
<region> key=63 sample=*sine
<group> polyphony=4
<region> key=64 sample=*sine polyphony=5
<region> key=64 sample=*sine
<region> key=64 sample=*sine
<region> 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"(
<group> polyphony=2
<region> key=62 sample=*sine
<group> group=1 polyphony=3
<region> key=63 sample=*sine
<region> key=63 sample=*sine group=2 polyphony=4
<region> key=63 sample=*sine group=4 polyphony=5
<group> group=4
<region> 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> group=1 polyphony=2
<region> 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"(
<group> polyphony=2
<region> 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"(
<group> polyphony=2
<region> 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"(
<master> polyphony=2
<group> polyphony=5
<region> 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"(
<master> polyphony=2
<region> sample=*saw key=65
<master>
<group> polyphony=5
<region> 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"(
<global> polyphony=2
<group> polyphony=5
<region> 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<float> buffer { 2, blockSize };
synth.loadSfzString(fs::current_path(), R"(
<master> polyphony=2
<group> group=2
<region> sample=*sine key=65
<group> group=3
<region> sample=*sine key=63
<master> // Empty master resets the polyphony
<region> 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"(
<region> 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"(
<region> 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());
}