Add the region sets (hierarchy)
Link the voice lifecycle into region sets and polyphony groups Respect crudely the polyphony limits without stealing for now
This commit is contained in:
parent
8f61463269
commit
5ef921485b
8 changed files with 528 additions and 34 deletions
40
src/sfizz/PolyphonyGroup.h
Normal file
40
src/sfizz/PolyphonyGroup.h
Normal file
|
|
@ -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<Voice*>& getActiveVoices() const { return voices; }
|
||||||
|
std::vector<Voice*>& getActiveVoices() { return voices; }
|
||||||
|
private:
|
||||||
|
unsigned polyphonyLimit { config::maxVoices };
|
||||||
|
std::vector<Voice*> voices;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -164,6 +164,10 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
|
||||||
DBG("Unkown off mode:" << std::string(opcode.value));
|
DBG("Unkown off mode:" << std::string(opcode.value));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case hash("polyphony"):
|
||||||
|
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
||||||
|
polyphony = *value;
|
||||||
|
break;
|
||||||
case hash("note_polyphony"):
|
case hash("note_polyphony"):
|
||||||
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
||||||
notePolyphony = *value;
|
notePolyphony = *value;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
||||||
|
class RegionSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Regions are the basic building blocks for the SFZ parsing and handling code.
|
* @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
|
* 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
|
uint32_t group { Default::group }; // group
|
||||||
absl::optional<uint32_t> offBy {}; // off_by
|
absl::optional<uint32_t> offBy {}; // off_by
|
||||||
SfzOffMode offMode { Default::offMode }; // off_mode
|
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 };
|
SfzSelfMask selfMask { Default::selfMask };
|
||||||
|
|
||||||
// Region logic: key mapping
|
// Region logic: key mapping
|
||||||
|
|
@ -365,6 +369,8 @@ struct Region {
|
||||||
// Modifiers
|
// Modifiers
|
||||||
ModifierArray<CCMap<Modifier>> modifiers;
|
ModifierArray<CCMap<Modifier>> modifiers;
|
||||||
|
|
||||||
|
// Parent
|
||||||
|
RegionSet* parent { nullptr };
|
||||||
private:
|
private:
|
||||||
const MidiState& midiState;
|
const MidiState& midiState;
|
||||||
bool keySwitched { true };
|
bool keySwitched { true };
|
||||||
|
|
|
||||||
74
src/sfizz/RegionSet.h
Normal file
74
src/sfizz/RegionSet.h
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Region.h"
|
||||||
|
#include "Voice.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<Voice*>& getActiveVoices() const { return voices; }
|
||||||
|
const std::vector<Region*>& getRegions() const { return regions; }
|
||||||
|
const std::vector<RegionSet*>& getSubsets() const { return subsets; }
|
||||||
|
private:
|
||||||
|
RegionSet* parent { nullptr };
|
||||||
|
std::vector<Region*> regions;
|
||||||
|
std::vector<RegionSet*> subsets;
|
||||||
|
std::vector<Voice*> voices;
|
||||||
|
unsigned polyphonyLimit { config::maxVoices };
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -51,14 +51,31 @@ void sfz::Synth::onVoiceStateChanged(NumericId<Voice> id, Voice::State state)
|
||||||
{
|
{
|
||||||
(void)id;
|
(void)id;
|
||||||
(void)state;
|
(void)state;
|
||||||
DBG("Voice " << id.number << ": state " << static_cast<int>(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<Opcode>& members)
|
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)) {
|
switch (hash(header)) {
|
||||||
case hash("global"):
|
case hash("global"):
|
||||||
globalOpcodes = members;
|
globalOpcodes = members;
|
||||||
|
currentSet = sets.front().get();
|
||||||
|
lastHeader = Header::Global;
|
||||||
groupOpcodes.clear();
|
groupOpcodes.clear();
|
||||||
masterOpcodes.clear();
|
masterOpcodes.clear();
|
||||||
handleGlobalOpcodes(members);
|
handleGlobalOpcodes(members);
|
||||||
|
|
@ -69,11 +86,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<O
|
||||||
break;
|
break;
|
||||||
case hash("master"):
|
case hash("master"):
|
||||||
masterOpcodes = members;
|
masterOpcodes = members;
|
||||||
|
newRegionSet(sets.front().get());
|
||||||
groupOpcodes.clear();
|
groupOpcodes.clear();
|
||||||
|
lastHeader = Header::Master;
|
||||||
|
handleMasterOpcodes(members);
|
||||||
numMasters++;
|
numMasters++;
|
||||||
break;
|
break;
|
||||||
case hash("group"):
|
case hash("group"):
|
||||||
groupOpcodes = members;
|
groupOpcodes = members;
|
||||||
|
if (lastHeader == Header::Group)
|
||||||
|
newRegionSet(currentSet->getParent());
|
||||||
|
else
|
||||||
|
newRegionSet(currentSet);
|
||||||
|
lastHeader = Header::Group;
|
||||||
handleGroupOpcodes(members, masterOpcodes);
|
handleGroupOpcodes(members, masterOpcodes);
|
||||||
numGroups++;
|
numGroups++;
|
||||||
break;
|
break;
|
||||||
|
|
@ -105,6 +130,8 @@ void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& mes
|
||||||
|
|
||||||
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
{
|
{
|
||||||
|
ASSERT(currentSet != nullptr);
|
||||||
|
|
||||||
int regionNumber = static_cast<int>(regions.size());
|
int regionNumber = static_cast<int>(regions.size());
|
||||||
auto lastRegion = absl::make_unique<Region>(regionNumber, resources.midiState, defaultPath);
|
auto lastRegion = absl::make_unique<Region>(regionNumber, resources.midiState, defaultPath);
|
||||||
|
|
||||||
|
|
@ -128,6 +155,13 @@ void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
if (octaveOffset != 0 || noteOffset != 0)
|
if (octaveOffset != 0 || noteOffset != 0)
|
||||||
lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset);
|
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));
|
regions.push_back(std::move(lastRegion));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +176,9 @@ void sfz::Synth::clear()
|
||||||
for (auto& list : ccActivationLists)
|
for (auto& list : ccActivationLists)
|
||||||
list.clear();
|
list.clear();
|
||||||
|
|
||||||
|
sets.clear();
|
||||||
|
sets.emplace_back(new RegionSet);
|
||||||
|
currentSet = sets.front().get();
|
||||||
regions.clear();
|
regions.clear();
|
||||||
effectBuses.clear();
|
effectBuses.clear();
|
||||||
effectBuses.emplace_back(new EffectBus);
|
effectBuses.emplace_back(new EffectBus);
|
||||||
|
|
@ -162,17 +199,38 @@ void sfz::Synth::clear()
|
||||||
masterOpcodes.clear();
|
masterOpcodes.clear();
|
||||||
groupOpcodes.clear();
|
groupOpcodes.clear();
|
||||||
unknownOpcodes.clear();
|
unknownOpcodes.clear();
|
||||||
groupMaxPolyphony.clear();
|
polyphonyGroups.clear();
|
||||||
groupMaxPolyphony.push_back(config::maxVoices);
|
polyphonyGroups.emplace_back();
|
||||||
|
polyphonyGroups.back().setPolyphonyLimit(config::maxVoices);
|
||||||
modificationTime = fs::file_time_type::min();
|
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)
|
void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& members)
|
||||||
{
|
{
|
||||||
for (auto& rawMember : members) {
|
for (auto& rawMember : members) {
|
||||||
const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal);
|
const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal);
|
||||||
|
|
||||||
switch (member.lettersOnlyHash) {
|
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"):
|
case hash("sw_default"):
|
||||||
setValueFromOpcode(member, defaultSwitch, Default::keyRange);
|
setValueFromOpcode(member, defaultSwitch, Default::keyRange);
|
||||||
break;
|
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)
|
void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const std::vector<Opcode>& masterMembers)
|
||||||
{
|
{
|
||||||
absl::optional<unsigned> groupIdx;
|
absl::optional<unsigned> groupIdx;
|
||||||
unsigned maxPolyphony { config::maxVoices };
|
absl::optional<unsigned> maxPolyphony;
|
||||||
|
|
||||||
const auto parseOpcode = [&](const Opcode& rawMember) {
|
const auto parseOpcode = [&](const Opcode& rawMember) {
|
||||||
const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup);
|
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);
|
setValueFromOpcode(member, groupIdx, Default::groupRange);
|
||||||
break;
|
break;
|
||||||
case hash("polyphony"):
|
case hash("polyphony"):
|
||||||
setValueFromOpcode(member, maxPolyphony, Range<unsigned>(0, config::maxVoices));
|
setValueFromOpcode(member, maxPolyphony, Default::polyphonyRange);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -208,8 +266,14 @@ void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members, const st
|
||||||
for (auto& member : members)
|
for (auto& member : members)
|
||||||
parseOpcode(member);
|
parseOpcode(member);
|
||||||
|
|
||||||
if (groupIdx)
|
if (groupIdx && maxPolyphony) {
|
||||||
setGroupPolyphony(*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)
|
void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
|
||||||
|
|
@ -437,8 +501,10 @@ void sfz::Synth::finalizeSfzLoad()
|
||||||
keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel });
|
keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel });
|
||||||
|
|
||||||
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
||||||
while (groupMaxPolyphony.size() <= region->group)
|
while (polyphonyGroups.size() <= region->group) {
|
||||||
groupMaxPolyphony.push_back(config::maxVoices);
|
polyphonyGroups.emplace_back();
|
||||||
|
polyphonyGroups.back().setPolyphonyLimit(config::maxVoices);
|
||||||
|
}
|
||||||
|
|
||||||
for (auto note = 0; note < 128; note++) {
|
for (auto note = 0; note < 128; note++) {
|
||||||
if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(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);
|
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff);
|
||||||
ring.addVoiceToRing(voice);
|
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]) {
|
for (auto& region : noteActivationLists[noteNumber]) {
|
||||||
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
||||||
unsigned activeNotesInGroup { 0 };
|
unsigned notePolyphonyCounter { 0 };
|
||||||
unsigned activeNotes { 0 };
|
unsigned regionPolyphonyCounter { 0 };
|
||||||
Voice* selfMaskCandidate { nullptr };
|
Voice* selfMaskCandidate { nullptr };
|
||||||
|
|
||||||
for (auto& voice : voices) {
|
for (auto& voice : voices) {
|
||||||
|
|
@ -829,12 +897,12 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
|
||||||
if (voiceRegion == nullptr)
|
if (voiceRegion == nullptr)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (voiceRegion->group == region->group)
|
if (voiceRegion == region)
|
||||||
activeNotesInGroup += 1;
|
regionPolyphonyCounter += 1;
|
||||||
|
|
||||||
if (region->notePolyphony) {
|
if (region->notePolyphony) {
|
||||||
if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) {
|
if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) {
|
||||||
activeNotes += 1;
|
notePolyphonyCounter += 1;
|
||||||
switch (region->selfMask) {
|
switch (region->selfMask) {
|
||||||
case SfzSelfMask::mask:
|
case SfzSelfMask::mask:
|
||||||
if (voice->getTriggerValue() < velocity) {
|
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());
|
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;
|
continue;
|
||||||
|
|
||||||
if (region->notePolyphony && activeNotes >= *region->notePolyphony) {
|
// FIXME: Do something for the polyphony limit
|
||||||
|
if (regionPolyphonyCounter >= region->polyphony)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// 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)
|
if (selfMaskCandidate != nullptr)
|
||||||
selfMaskCandidate->release(delay);
|
selfMaskCandidate->release(delay);
|
||||||
else // We're the lowest velocity guy here
|
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);
|
voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn);
|
||||||
ring.addVoiceToRing(voice);
|
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);
|
voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC);
|
||||||
ring.addVoiceToRing(voice);
|
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;
|
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 sfz::Region* sfz::Synth::getRegionById(NumericId<Region> id) const noexcept
|
||||||
{
|
{
|
||||||
const size_t size = regions.size();
|
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;
|
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
|
const std::vector<std::string>& sfz::Synth::getUnknownOpcodes() const noexcept
|
||||||
{
|
{
|
||||||
return unknownOpcodes;
|
return unknownOpcodes;
|
||||||
|
|
@ -1226,8 +1333,10 @@ void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept
|
||||||
if (factor == oversamplingFactor)
|
if (factor == oversamplingFactor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (auto& voice : voices)
|
for (auto& voice : voices) {
|
||||||
|
|
||||||
voice->reset();
|
voice->reset();
|
||||||
|
}
|
||||||
|
|
||||||
resources.filePool.emptyFileLoadingQueues();
|
resources.filePool.emptyFileLoadingQueues();
|
||||||
resources.filePool.setOversamplingFactor(factor);
|
resources.filePool.setOversamplingFactor(factor);
|
||||||
|
|
@ -1339,8 +1448,8 @@ void sfz::Synth::allSoundOff() noexcept
|
||||||
|
|
||||||
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
||||||
{
|
{
|
||||||
while (groupMaxPolyphony.size() <= groupIdx)
|
while (polyphonyGroups.size() <= groupIdx)
|
||||||
groupMaxPolyphony.push_back(config::maxVoices);
|
polyphonyGroups.emplace_back();
|
||||||
|
|
||||||
groupMaxPolyphony[groupIdx] = polyphony;
|
polyphonyGroups[groupIdx].setPolyphonyLimit(polyphony);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
#include "Parser.h"
|
#include "Parser.h"
|
||||||
#include "Voice.h"
|
#include "Voice.h"
|
||||||
#include "Region.h"
|
#include "Region.h"
|
||||||
|
#include "RegionSet.h"
|
||||||
|
#include "PolyphonyGroup.h"
|
||||||
#include "Effects.h"
|
#include "Effects.h"
|
||||||
#include "LeakDetector.h"
|
#include "LeakDetector.h"
|
||||||
#include "MidiState.h"
|
#include "MidiState.h"
|
||||||
|
|
@ -220,17 +222,39 @@ public:
|
||||||
* for testing.
|
* for testing.
|
||||||
*
|
*
|
||||||
* @param idx
|
* @param idx
|
||||||
* @return const Region*
|
* @return const Voice*
|
||||||
*/
|
*/
|
||||||
const Voice* getVoiceView(int idx) const noexcept;
|
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.
|
* for testing.
|
||||||
*
|
*
|
||||||
* @param idx
|
* @param idx
|
||||||
* @return const Region*
|
* @return const EffectBus*
|
||||||
*/
|
*/
|
||||||
const EffectBus* getEffectBusView(int idx) const noexcept;
|
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
|
* @brief Get a list of unknown opcodes. The lifetime of the
|
||||||
* string views in the code are linked to the currently loaded
|
* string views in the code are linked to the currently loaded
|
||||||
|
|
@ -572,7 +596,6 @@ private:
|
||||||
* @param polyphone the max polyphony
|
* @param polyphone the max polyphony
|
||||||
*/
|
*/
|
||||||
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
|
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
|
||||||
std::vector<unsigned> groupMaxPolyphony { config::maxVoices };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reset all CCs; to be used on CC 121
|
* @brief Reset all CCs; to be used on CC 121
|
||||||
|
|
@ -598,6 +621,12 @@ private:
|
||||||
* @param members the opcodes of the <global> block
|
* @param members the opcodes of the <global> block
|
||||||
*/
|
*/
|
||||||
void handleGlobalOpcodes(const std::vector<Opcode>& members);
|
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
|
* @brief Helper function to dispatch <group> opcodes
|
||||||
*
|
*
|
||||||
|
|
@ -649,8 +678,6 @@ private:
|
||||||
void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept;
|
void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept;
|
||||||
void noteOffDispatch(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
|
// Opcode memory; these are used to build regions, as a new region
|
||||||
// will integrate opcodes from the group, master and global block
|
// will integrate opcodes from the group, master and global block
|
||||||
std::vector<Opcode> globalOpcodes;
|
std::vector<Opcode> globalOpcodes;
|
||||||
|
|
@ -672,15 +699,25 @@ private:
|
||||||
// Default active switch if multiple keyswitchable regions are present
|
// Default active switch if multiple keyswitchable regions are present
|
||||||
absl::optional<uint8_t> defaultSwitch;
|
absl::optional<uint8_t> defaultSwitch;
|
||||||
std::vector<std::string> unknownOpcodes;
|
std::vector<std::string> unknownOpcodes;
|
||||||
using RegionPtrVector = std::vector<Region*>;
|
using RegionViewVector = std::vector<Region*>;
|
||||||
using VoicePtrVector = std::vector<Voice*>;
|
using VoiceViewVector = std::vector<Voice*>;
|
||||||
std::vector<std::unique_ptr<Region>> regions;
|
using VoicePtr = std::unique_ptr<Voice>;
|
||||||
std::vector<std::unique_ptr<Voice>> voices;
|
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
|
||||||
|
enum class Header { Global, Master, Group };
|
||||||
|
RegionSet* currentSet;
|
||||||
|
Header lastHeader { Header::Global };
|
||||||
|
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
|
// Views to speed up iteration over the regions and voices when events
|
||||||
// occur in the audio callback
|
// occur in the audio callback
|
||||||
VoicePtrVector voiceViewArray;
|
VoiceViewVector voiceViewArray;
|
||||||
std::array<RegionPtrVector, 128> noteActivationLists;
|
std::array<RegionViewVector, 128> noteActivationLists;
|
||||||
std::array<RegionPtrVector, config::numCCs> ccActivationLists;
|
std::array<RegionViewVector, config::numCCs> ccActivationLists;
|
||||||
|
|
||||||
// Effect factory and buses
|
// Effect factory and buses
|
||||||
EffectFactory effectFactory;
|
EffectFactory effectFactory;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ set(SFIZZ_TEST_SOURCES
|
||||||
MidiStateT.cpp
|
MidiStateT.cpp
|
||||||
InterpolatorsT.cpp
|
InterpolatorsT.cpp
|
||||||
SmoothersT.cpp
|
SmoothersT.cpp
|
||||||
|
PolyphonyT.cpp
|
||||||
RegionActivationT.cpp
|
RegionActivationT.cpp
|
||||||
RegionValueComputationsT.cpp
|
RegionValueComputationsT.cpp
|
||||||
# If we're tweaking the curves this kind of tests does not make sense
|
# If we're tweaking the curves this kind of tests does not make sense
|
||||||
|
|
|
||||||
223
tests/PolyphonyT.cpp
Normal file
223
tests/PolyphonyT.cpp
Normal file
|
|
@ -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"(
|
||||||
|
<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=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"(
|
||||||
|
<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());
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue