Final refactor into VoiceManager
Cleanups..
This commit is contained in:
parent
1b87fa3764
commit
81a4ed22eb
12 changed files with 497 additions and 381 deletions
|
|
@ -115,6 +115,7 @@ SFIZZ_SOURCES = \
|
||||||
src/sfizz/Tuning.cpp \
|
src/sfizz/Tuning.cpp \
|
||||||
src/sfizz/utility/SpinMutex.cpp \
|
src/sfizz/utility/SpinMutex.cpp \
|
||||||
src/sfizz/Voice.cpp \
|
src/sfizz/Voice.cpp \
|
||||||
|
src/sfizz/VoiceManager.cpp \
|
||||||
src/sfizz/VoiceStealing.cpp \
|
src/sfizz/VoiceStealing.cpp \
|
||||||
src/sfizz/Wavetables.cpp
|
src/sfizz/Wavetables.cpp
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ set (SFIZZ_HEADERS
|
||||||
sfizz/SynthConfig.h
|
sfizz/SynthConfig.h
|
||||||
sfizz/Tuning.h
|
sfizz/Tuning.h
|
||||||
sfizz/Voice.h
|
sfizz/Voice.h
|
||||||
|
sfizz/VoiceManager.h
|
||||||
sfizz/VoiceStealing.h
|
sfizz/VoiceStealing.h
|
||||||
sfizz/Wavetables.h
|
sfizz/Wavetables.h
|
||||||
sfizz.h
|
sfizz.h
|
||||||
|
|
@ -137,6 +138,7 @@ set (SFIZZ_SOURCES
|
||||||
sfizz/Tuning.cpp
|
sfizz/Tuning.cpp
|
||||||
sfizz/RegionSet.cpp
|
sfizz/RegionSet.cpp
|
||||||
sfizz/PolyphonyGroup.cpp
|
sfizz/PolyphonyGroup.cpp
|
||||||
|
sfizz/VoiceManager.cpp
|
||||||
sfizz/VoiceStealing.cpp
|
sfizz/VoiceStealing.cpp
|
||||||
sfizz/RTSemaphore.cpp
|
sfizz/RTSemaphore.cpp
|
||||||
sfizz/Panning.cpp
|
sfizz/Panning.cpp
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
#include "TriggerEvent.h"
|
#include "TriggerEvent.h"
|
||||||
#include "utility/SpinMutex.h"
|
#include "utility/SpinMutex.h"
|
||||||
#include "utility/XmlHelpers.h"
|
#include "utility/XmlHelpers.h"
|
||||||
#include "VoiceList.h"
|
#include "VoiceManager.h"
|
||||||
#include <absl/types/optional.h>
|
#include <absl/types/optional.h>
|
||||||
#include <absl/types/span.h>
|
#include <absl/types/span.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -236,7 +236,7 @@ struct Synth::Impl: public Parser::Listener {
|
||||||
using RegionPtr = std::unique_ptr<Region>;
|
using RegionPtr = std::unique_ptr<Region>;
|
||||||
using RegionSetPtr = std::unique_ptr<RegionSet>;
|
using RegionSetPtr = std::unique_ptr<RegionSet>;
|
||||||
std::vector<RegionPtr> regions_;
|
std::vector<RegionPtr> regions_;
|
||||||
VoiceList voiceList_;
|
VoiceManager voiceManager_;
|
||||||
|
|
||||||
// These are more general "groups" than sfz and encapsulates the full hierarchy
|
// These are more general "groups" than sfz and encapsulates the full hierarchy
|
||||||
RegionSet* currentSet_ { nullptr };
|
RegionSet* currentSet_ { nullptr };
|
||||||
|
|
@ -327,16 +327,16 @@ Synth::Impl::Impl()
|
||||||
|
|
||||||
// modulation sources
|
// modulation sources
|
||||||
genController_.reset(new ControllerSource(resources_));
|
genController_.reset(new ControllerSource(resources_));
|
||||||
genLFO_.reset(new LFOSource(voiceList_));
|
genLFO_.reset(new LFOSource(voiceManager_));
|
||||||
genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceList_));
|
genFlexEnvelope_.reset(new FlexEnvelopeSource(voiceManager_));
|
||||||
genADSREnvelope_.reset(new ADSREnvelopeSource(voiceList_, resources_.midiState));
|
genADSREnvelope_.reset(new ADSREnvelopeSource(voiceManager_, resources_.midiState));
|
||||||
}
|
}
|
||||||
|
|
||||||
Synth::Impl::~Impl()
|
Synth::Impl::~Impl()
|
||||||
{
|
{
|
||||||
const std::lock_guard<SpinMutex> disableCallback { callbackGuard_ };
|
const std::lock_guard<SpinMutex> disableCallback { callbackGuard_ };
|
||||||
|
|
||||||
voiceList_.reset();
|
voiceManager_.reset();
|
||||||
resources_.filePool.emptyFileLoadingQueues();
|
resources_.filePool.emptyFileLoadingQueues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,10 +461,10 @@ void Synth::Impl::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||||
|
|
||||||
// There was a combination of group= and polyphony= on a region, so set the group polyphony
|
// 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) {
|
if (lastRegion->group != Default::group && lastRegion->polyphony != config::maxVoices) {
|
||||||
voiceList_.setGroupPolyphony(lastRegion->group, lastRegion->polyphony);
|
voiceManager_.setGroupPolyphony(lastRegion->group, lastRegion->polyphony);
|
||||||
} else {
|
} else {
|
||||||
// Just check that there are enough polyphony groups
|
// Just check that there are enough polyphony groups
|
||||||
voiceList_.ensureNumPolyphonyGroups(lastRegion->group);
|
voiceManager_.ensureNumPolyphonyGroups(lastRegion->group);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSet_ != nullptr) {
|
if (currentSet_ != nullptr) {
|
||||||
|
|
@ -483,7 +483,7 @@ void Synth::Impl::clear()
|
||||||
// Clear the background queues before removing everyone
|
// Clear the background queues before removing everyone
|
||||||
resources_.filePool.waitForBackgroundLoading();
|
resources_.filePool.waitForBackgroundLoading();
|
||||||
|
|
||||||
voiceList_.reset();
|
voiceManager_.reset();
|
||||||
for (auto& list : lastKeyswitchLists_)
|
for (auto& list : lastKeyswitchLists_)
|
||||||
list.clear();
|
list.clear();
|
||||||
for (auto& list : downKeyswitchLists_)
|
for (auto& list : downKeyswitchLists_)
|
||||||
|
|
@ -603,12 +603,12 @@ void Synth::Impl::handleGroupOpcodes(const std::vector<Opcode>& members, const s
|
||||||
parseOpcode(member);
|
parseOpcode(member);
|
||||||
|
|
||||||
if (groupIdx && maxPolyphony) {
|
if (groupIdx && maxPolyphony) {
|
||||||
voiceList_.setGroupPolyphony(*groupIdx, *maxPolyphony);
|
voiceManager_.setGroupPolyphony(*groupIdx, *maxPolyphony);
|
||||||
} else if (maxPolyphony) {
|
} else if (maxPolyphony) {
|
||||||
ASSERT(currentSet_ != nullptr);
|
ASSERT(currentSet_ != nullptr);
|
||||||
currentSet_->setPolyphonyLimit(*maxPolyphony);
|
currentSet_->setPolyphonyLimit(*maxPolyphony);
|
||||||
} else if (groupIdx) {
|
} else if (groupIdx) {
|
||||||
voiceList_.ensureNumPolyphonyGroups(*groupIdx);
|
voiceManager_.ensureNumPolyphonyGroups(*groupIdx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -663,13 +663,13 @@ void Synth::Impl::handleControlOpcodes(const std::vector<Opcode>& members)
|
||||||
case hash("hint_stealing"):
|
case hash("hint_stealing"):
|
||||||
switch(hash(member.value)) {
|
switch(hash(member.value)) {
|
||||||
case hash("first"):
|
case hash("first"):
|
||||||
voiceList_.setStealingAlgorithm(StealingAlgorithm::First);
|
voiceManager_.setStealingAlgorithm(StealingAlgorithm::First);
|
||||||
break;
|
break;
|
||||||
case hash("oldest"):
|
case hash("oldest"):
|
||||||
voiceList_.setStealingAlgorithm(StealingAlgorithm::Oldest);
|
voiceManager_.setStealingAlgorithm(StealingAlgorithm::Oldest);
|
||||||
break;
|
break;
|
||||||
case hash("envelope_and_age"):
|
case hash("envelope_and_age"):
|
||||||
voiceList_.setStealingAlgorithm(StealingAlgorithm::EnvelopeAndAge);
|
voiceManager_.setStealingAlgorithm(StealingAlgorithm::EnvelopeAndAge);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
DBG("Unsupported value for hint_stealing: " << member.value);
|
DBG("Unsupported value for hint_stealing: " << member.value);
|
||||||
|
|
@ -1045,7 +1045,7 @@ void Synth::loadStretchTuningByRatio(float ratio)
|
||||||
int Synth::getNumActiveVoices() const noexcept
|
int Synth::getNumActiveVoices() const noexcept
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
return static_cast<int>(impl.voiceList_.getNumActiveVoices());
|
return static_cast<int>(impl.voiceManager_.getNumActiveVoices());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
|
void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
|
||||||
|
|
@ -1056,7 +1056,7 @@ void Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
|
||||||
const std::lock_guard<SpinMutex> disableCallback { impl.callbackGuard_ };
|
const std::lock_guard<SpinMutex> disableCallback { impl.callbackGuard_ };
|
||||||
|
|
||||||
impl.samplesPerBlock_ = samplesPerBlock;
|
impl.samplesPerBlock_ = samplesPerBlock;
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.setSamplesPerBlock(samplesPerBlock);
|
voice.setSamplesPerBlock(samplesPerBlock);
|
||||||
|
|
||||||
impl.resources_.setSamplesPerBlock(samplesPerBlock);
|
impl.resources_.setSamplesPerBlock(samplesPerBlock);
|
||||||
|
|
@ -1073,7 +1073,7 @@ void Synth::setSampleRate(float sampleRate) noexcept
|
||||||
const std::lock_guard<SpinMutex> disableCallback { impl.callbackGuard_ };
|
const std::lock_guard<SpinMutex> disableCallback { impl.callbackGuard_ };
|
||||||
|
|
||||||
impl.sampleRate_ = sampleRate;
|
impl.sampleRate_ = sampleRate;
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.setSampleRate(sampleRate);
|
voice.setSampleRate(sampleRate);
|
||||||
|
|
||||||
impl.resources_.setSampleRate(sampleRate);
|
impl.resources_.setSampleRate(sampleRate);
|
||||||
|
|
@ -1136,7 +1136,7 @@ void Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
|
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
|
||||||
tempMixSpan->fill(0.0f);
|
tempMixSpan->fill(0.0f);
|
||||||
|
|
||||||
for (auto& voice : impl.voiceList_) {
|
for (auto& voice : impl.voiceManager_) {
|
||||||
if (voice.isFree())
|
if (voice.isFree())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -1243,7 +1243,7 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept
|
||||||
// auto replacedVelocity = (velocity == 0 ? getNoteVelocity(noteNumber) : velocity);
|
// auto replacedVelocity = (velocity == 0 ? getNoteVelocity(noteNumber) : velocity);
|
||||||
const auto replacedVelocity = impl.resources_.midiState.getNoteVelocity(noteNumber);
|
const auto replacedVelocity = impl.resources_.midiState.getNoteVelocity(noteNumber);
|
||||||
|
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.registerNoteOff(delay, noteNumber, replacedVelocity);
|
voice.registerNoteOff(delay, noteNumber, replacedVelocity);
|
||||||
|
|
||||||
impl.noteOffDispatch(delay, noteNumber, replacedVelocity);
|
impl.noteOffDispatch(delay, noteNumber, replacedVelocity);
|
||||||
|
|
@ -1251,8 +1251,8 @@ void Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept
|
||||||
|
|
||||||
void Synth::Impl::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept
|
void Synth::Impl::startVoice(Region* region, int delay, const TriggerEvent& triggerEvent, SisterVoiceRingBuilder& ring) noexcept
|
||||||
{
|
{
|
||||||
voiceList_.checkPolyphony(region, delay, triggerEvent);
|
voiceManager_.checkPolyphony(region, delay, triggerEvent);
|
||||||
Voice* selectedVoice = voiceList_.findFreeVoice();
|
Voice* selectedVoice = voiceManager_.findFreeVoice();
|
||||||
if (selectedVoice == nullptr)
|
if (selectedVoice == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -1275,7 +1275,7 @@ void Synth::Impl::noteOffDispatch(int delay, int noteNumber, float velocity) noe
|
||||||
|
|
||||||
for (auto& region : noteActivationLists_[noteNumber]) {
|
for (auto& region : noteActivationLists_[noteNumber]) {
|
||||||
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
|
if (region->registerNoteOff(noteNumber, velocity, randValue)) {
|
||||||
if (region->trigger == SfzTrigger::release && !region->rtDead && !voiceList_.playingAttackVoice(region))
|
if (region->trigger == SfzTrigger::release && !region->rtDead && !voiceManager_.playingAttackVoice(region))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
startVoice(region, delay, triggerEvent, ring);
|
startVoice(region, delay, triggerEvent, ring);
|
||||||
|
|
@ -1308,7 +1308,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
|
|
||||||
for (auto& region : noteActivationLists_[noteNumber]) {
|
for (auto& region : noteActivationLists_[noteNumber]) {
|
||||||
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
||||||
for (auto& voice : voiceList_) {
|
for (auto& voice : voiceManager_) {
|
||||||
if (voice.checkOffGroup(region, delay, noteNumber)) {
|
if (voice.checkOffGroup(region, delay, noteNumber)) {
|
||||||
const TriggerEvent& event = voice.getTriggerEvent();
|
const TriggerEvent& event = voice.getTriggerEvent();
|
||||||
noteOffDispatch(delay, event.number, event.value);
|
noteOffDispatch(delay, event.number, event.value);
|
||||||
|
|
@ -1325,7 +1325,7 @@ void Synth::Impl::noteOnDispatch(int delay, int noteNumber, float velocity) noex
|
||||||
|
|
||||||
void Synth::Impl::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept
|
void Synth::Impl::startDelayedReleaseVoices(Region* region, int delay, SisterVoiceRingBuilder& ring) noexcept
|
||||||
{
|
{
|
||||||
if (!region->rtDead && !voiceList_.playingAttackVoice(region)) {
|
if (!region->rtDead && !voiceManager_.playingAttackVoice(region)) {
|
||||||
region->delayedReleases.clear();
|
region->delayedReleases.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1377,13 +1377,13 @@ void Synth::hdcc(int delay, int ccNumber, float normValue) noexcept
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ccNumber == config::allNotesOffCC || ccNumber == config::allSoundOffCC) {
|
if (ccNumber == config::allNotesOffCC || ccNumber == config::allSoundOffCC) {
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.reset();
|
voice.reset();
|
||||||
impl.resources_.midiState.allNotesOff(delay);
|
impl.resources_.midiState.allNotesOff(delay);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.registerCC(delay, ccNumber, normValue);
|
voice.registerCC(delay, ccNumber, normValue);
|
||||||
|
|
||||||
impl.ccDispatch(delay, ccNumber, normValue);
|
impl.ccDispatch(delay, ccNumber, normValue);
|
||||||
|
|
@ -1427,7 +1427,7 @@ void Synth::pitchWheel(int delay, int pitch) noexcept
|
||||||
region->registerPitchWheel(normalizedPitch);
|
region->registerPitchWheel(normalizedPitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& voice : impl.voiceList_) {
|
for (auto& voice : impl.voiceManager_) {
|
||||||
voice.registerPitchWheel(delay, normalizedPitch);
|
voice.registerPitchWheel(delay, normalizedPitch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1614,7 +1614,7 @@ const RegionSet* Synth::getRegionSetView(int idx) const noexcept
|
||||||
const PolyphonyGroup* Synth::getPolyphonyGroupView(int idx) const noexcept
|
const PolyphonyGroup* Synth::getPolyphonyGroupView(int idx) const noexcept
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
return impl.voiceList_.getPolyphonyGroupView(idx);
|
return impl.voiceManager_.getPolyphonyGroupView(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Region* Synth::getRegionById(NumericId<Region> id) const noexcept
|
const Region* Synth::getRegionById(NumericId<Region> id) const noexcept
|
||||||
|
|
@ -1638,13 +1638,13 @@ const Region* Synth::getRegionById(NumericId<Region> id) const noexcept
|
||||||
const Voice* Synth::getVoiceView(int idx) const noexcept
|
const Voice* Synth::getVoiceView(int idx) const noexcept
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
return idx < impl.numVoices_ ? &impl.voiceList_[idx] : nullptr;
|
return idx < impl.numVoices_ ? &impl.voiceManager_[idx] : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned Synth::getNumPolyphonyGroups() const noexcept
|
unsigned Synth::getNumPolyphonyGroups() const noexcept
|
||||||
{
|
{
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
return impl.voiceList_.getNumPolyphonyGroups();
|
return impl.voiceManager_.getNumPolyphonyGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<std::string>& Synth::getUnknownOpcodes() const noexcept
|
const std::vector<std::string>& Synth::getUnknownOpcodes() const noexcept
|
||||||
|
|
@ -1730,9 +1730,9 @@ void Synth::Impl::resetVoices(int numVoices)
|
||||||
engineSet_->removeAllVoices();
|
engineSet_->removeAllVoices();
|
||||||
engineSet_->setPolyphonyLimit(numVoices_);
|
engineSet_->setPolyphonyLimit(numVoices_);
|
||||||
|
|
||||||
voiceList_.requireNumVoices(numVoices_, resources_);
|
voiceManager_.requireNumVoices(numVoices_, resources_);
|
||||||
|
|
||||||
for (auto& voice : voiceList_) {
|
for (auto& voice : voiceManager_) {
|
||||||
voice.setSampleRate(this->sampleRate_);
|
voice.setSampleRate(this->sampleRate_);
|
||||||
voice.setSamplesPerBlock(this->samplesPerBlock_);
|
voice.setSamplesPerBlock(this->samplesPerBlock_);
|
||||||
}
|
}
|
||||||
|
|
@ -1742,7 +1742,7 @@ void Synth::Impl::resetVoices(int numVoices)
|
||||||
|
|
||||||
void Synth::Impl::applySettingsPerVoice()
|
void Synth::Impl::applySettingsPerVoice()
|
||||||
{
|
{
|
||||||
for (auto& voice : voiceList_) {
|
for (auto& voice : voiceManager_) {
|
||||||
voice.setMaxFiltersPerVoice(settingsPerVoice_.maxFilters);
|
voice.setMaxFiltersPerVoice(settingsPerVoice_.maxFilters);
|
||||||
voice.setMaxEQsPerVoice(settingsPerVoice_.maxEQs);
|
voice.setMaxEQsPerVoice(settingsPerVoice_.maxEQs);
|
||||||
voice.setMaxLFOsPerVoice(settingsPerVoice_.maxLFOs);
|
voice.setMaxLFOsPerVoice(settingsPerVoice_.maxLFOs);
|
||||||
|
|
@ -1829,7 +1829,7 @@ void Synth::setOversamplingFactor(Oversampling factor) noexcept
|
||||||
if (factor == impl.oversamplingFactor_)
|
if (factor == impl.oversamplingFactor_)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.reset();
|
voice.reset();
|
||||||
|
|
||||||
impl.resources_.filePool.emptyFileLoadingQueues();
|
impl.resources_.filePool.emptyFileLoadingQueues();
|
||||||
|
|
@ -1886,7 +1886,7 @@ void Synth::Impl::resetAllControllers(int delay) noexcept
|
||||||
if (!lock.owns_lock())
|
if (!lock.owns_lock())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (auto& voice : voiceList_) {
|
for (auto& voice : voiceManager_) {
|
||||||
voice.registerPitchWheel(delay, 0);
|
voice.registerPitchWheel(delay, 0);
|
||||||
for (int cc = 0; cc < config::numCCs; ++cc)
|
for (int cc = 0; cc < config::numCCs; ++cc)
|
||||||
voice.registerCC(delay, cc, 0.0f);
|
voice.registerCC(delay, cc, 0.0f);
|
||||||
|
|
@ -1945,7 +1945,7 @@ void Synth::allSoundOff() noexcept
|
||||||
Impl& impl = *impl_;
|
Impl& impl = *impl_;
|
||||||
const std::lock_guard<SpinMutex> disableCallback { impl.callbackGuard_ };
|
const std::lock_guard<SpinMutex> disableCallback { impl.callbackGuard_ };
|
||||||
|
|
||||||
for (auto& voice : impl.voiceList_)
|
for (auto& voice : impl.voiceManager_)
|
||||||
voice.reset();
|
voice.reset();
|
||||||
for (auto& effectBus : impl.effectBuses_)
|
for (auto& effectBus : impl.effectBuses_)
|
||||||
effectBus->clear();
|
effectBus->clear();
|
||||||
|
|
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
// 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 "Voice.h"
|
|
||||||
#include "Resources.h"
|
|
||||||
#include "Config.h"
|
|
||||||
#include "Region.h"
|
|
||||||
#include "SisterVoiceRing.h"
|
|
||||||
#include "PolyphonyGroup.h"
|
|
||||||
#include "RegionSet.h"
|
|
||||||
#include "VoiceStealing.h"
|
|
||||||
#include <vector>
|
|
||||||
#include <absl/algorithm/container.h>
|
|
||||||
|
|
||||||
namespace sfz {
|
|
||||||
|
|
||||||
struct VoiceList : public Voice::StateListener
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @brief The voice callback which is called during a change of state.
|
|
||||||
*/
|
|
||||||
void onVoiceStateChanging(NumericId<Voice> id, Voice::State state) final
|
|
||||||
{
|
|
||||||
(void)id;
|
|
||||||
if (state == Voice::State::idle) {
|
|
||||||
auto voice = getVoiceById(id);
|
|
||||||
RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice);
|
|
||||||
swapAndPopFirst(activeVoices_, [voice](const Voice* v) { return v == voice; });
|
|
||||||
polyphonyGroups_[voice->getRegion()->group].removeVoice(voice);
|
|
||||||
} else if (state == Voice::State::playing) {
|
|
||||||
auto voice = getVoiceById(id);
|
|
||||||
activeVoices_.push_back(voice);
|
|
||||||
RegionSet::registerVoiceInHierarchy(voice->getRegion(), voice);
|
|
||||||
polyphonyGroups_[voice->getRegion()->group].registerVoice(voice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief Find the voice which is associated with the given identifier.
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @return const Voice*
|
|
||||||
*/
|
|
||||||
const Voice* getVoiceById(NumericId<Voice> id) const noexcept
|
|
||||||
{
|
|
||||||
const size_t size = list_.size();
|
|
||||||
|
|
||||||
if (size == 0 || !id.valid())
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
// search a sequence of ordered identifiers with potential gaps
|
|
||||||
size_t index = static_cast<size_t>(id.number());
|
|
||||||
index = std::min(index, size - 1);
|
|
||||||
|
|
||||||
while (index > 0 && list_[index].getId().number() > id.number())
|
|
||||||
--index;
|
|
||||||
|
|
||||||
return (list_[index].getId() == id) ? &list_[index] : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Voice* getVoiceById(NumericId<Voice> id) noexcept
|
|
||||||
{
|
|
||||||
return const_cast<Voice*>(
|
|
||||||
const_cast<const VoiceList*>(this)->getVoiceById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset()
|
|
||||||
{
|
|
||||||
for (auto& voice : list_)
|
|
||||||
voice.reset();
|
|
||||||
|
|
||||||
polyphonyGroups_.clear();
|
|
||||||
polyphonyGroups_.emplace_back();
|
|
||||||
polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices);
|
|
||||||
setStealingAlgorithm(StealingAlgorithm::Oldest);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool playingAttackVoice(const Region* releaseRegion) noexcept
|
|
||||||
{
|
|
||||||
const auto compatibleVoice = [releaseRegion](const Voice& v) -> bool {
|
|
||||||
const TriggerEvent& event = v.getTriggerEvent();
|
|
||||||
return (
|
|
||||||
!v.isFree()
|
|
||||||
&& event.type == TriggerEventType::NoteOn
|
|
||||||
&& releaseRegion->keyRange.containsWithEnd(event.number)
|
|
||||||
&& releaseRegion->velocityRange.containsWithEnd(event.value)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (absl::c_find_if(list_, compatibleVoice) == list_.end())
|
|
||||||
return false;
|
|
||||||
else
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ensureNumPolyphonyGroups(unsigned groupIdx) noexcept
|
|
||||||
{
|
|
||||||
while (polyphonyGroups_.size() <= groupIdx)
|
|
||||||
polyphonyGroups_.emplace_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
|
||||||
{
|
|
||||||
ensureNumPolyphonyGroups(groupIdx);
|
|
||||||
polyphonyGroups_[groupIdx].setPolyphonyLimit(polyphony);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t getNumPolyphonyGroups() const noexcept { return polyphonyGroups_.size(); }
|
|
||||||
|
|
||||||
const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept
|
|
||||||
{
|
|
||||||
return (size_t)idx < polyphonyGroups_.size() ? &polyphonyGroups_[idx] : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear()
|
|
||||||
{
|
|
||||||
reset();
|
|
||||||
list_.clear();
|
|
||||||
activeVoices_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setStealingAlgorithm(StealingAlgorithm algorithm)
|
|
||||||
{
|
|
||||||
switch(algorithm){
|
|
||||||
case StealingAlgorithm::First: // fallthrough
|
|
||||||
for (auto& voice : list_)
|
|
||||||
voice.disablePowerFollower();
|
|
||||||
|
|
||||||
stealer_ = absl::make_unique<FirstStealer>();
|
|
||||||
break;
|
|
||||||
case StealingAlgorithm::Oldest:
|
|
||||||
for (auto& voice : list_)
|
|
||||||
voice.disablePowerFollower();
|
|
||||||
|
|
||||||
stealer_ = absl::make_unique<OldestStealer>();
|
|
||||||
break;
|
|
||||||
case StealingAlgorithm::EnvelopeAndAge:
|
|
||||||
for (auto& voice : list_)
|
|
||||||
voice.enablePowerFollower();
|
|
||||||
|
|
||||||
stealer_ = absl::make_unique<EnvelopeAndAgeStealer>();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
|
|
||||||
{
|
|
||||||
checkNotePolyphony(region, delay, triggerEvent);
|
|
||||||
checkRegionPolyphony(region, delay);
|
|
||||||
checkGroupPolyphony(region, delay);
|
|
||||||
checkSetPolyphony(region, delay);
|
|
||||||
checkEnginePolyphony(delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the number of active voices
|
|
||||||
*
|
|
||||||
* @return unsigned
|
|
||||||
*/
|
|
||||||
unsigned getNumActiveVoices() const
|
|
||||||
{
|
|
||||||
return activeVoices_.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Find a voice that is not currently playing
|
|
||||||
*
|
|
||||||
* @return Voice*
|
|
||||||
*/
|
|
||||||
Voice* findFreeVoice() noexcept
|
|
||||||
{
|
|
||||||
auto freeVoice = absl::c_find_if(list_, [](const Voice& voice) {
|
|
||||||
return voice.isFree();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (freeVoice != list_.end())
|
|
||||||
return &*freeVoice;
|
|
||||||
|
|
||||||
DBG("Engine hard polyphony reached");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void requireNumVoices(int numVoices, Resources& resources)
|
|
||||||
{
|
|
||||||
numActualVoices_ =
|
|
||||||
static_cast<int>(config::overflowVoiceMultiplier * numVoices);
|
|
||||||
numRequiredVoices_ = numVoices;
|
|
||||||
|
|
||||||
clear();
|
|
||||||
list_.reserve(numActualVoices_);
|
|
||||||
activeVoices_.reserve(numActualVoices_);
|
|
||||||
|
|
||||||
for (int i = 0; i < numActualVoices_; ++i) {
|
|
||||||
list_.emplace_back(i, resources);
|
|
||||||
Voice& lastVoice = list_.back();
|
|
||||||
lastVoice.setStateListener(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
int numRequiredVoices_ { config::numVoices };
|
|
||||||
int numActualVoices_ { static_cast<int>(config::numVoices * config::overflowVoiceMultiplier) };
|
|
||||||
std::vector<Voice> list_;
|
|
||||||
std::vector<Voice*> activeVoices_;
|
|
||||||
// These are the `group=` groups where you can off voices
|
|
||||||
std::vector<PolyphonyGroup> polyphonyGroups_;
|
|
||||||
std::unique_ptr<VoiceStealer> stealer_ { absl::make_unique<OldestStealer>() };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check the region polyphony, releasing voices if necessary
|
|
||||||
*
|
|
||||||
* @param region
|
|
||||||
* @param delay
|
|
||||||
*/
|
|
||||||
void checkRegionPolyphony(const Region* region, int delay) noexcept
|
|
||||||
{
|
|
||||||
Voice* candidate = stealer_->checkRegionPolyphony(region, absl::MakeSpan(activeVoices_));
|
|
||||||
SisterVoiceRing::offAllSisters(candidate, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check the note polyphony, releasing voices if necessary
|
|
||||||
*
|
|
||||||
* @param region
|
|
||||||
* @param delay
|
|
||||||
* @param triggerEvent
|
|
||||||
*/
|
|
||||||
void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
|
|
||||||
{
|
|
||||||
if (!region->notePolyphony)
|
|
||||||
return;
|
|
||||||
|
|
||||||
unsigned notePolyphonyCounter { 0 };
|
|
||||||
Voice* selfMaskCandidate { nullptr };
|
|
||||||
|
|
||||||
for (Voice* voice : activeVoices_) {
|
|
||||||
const TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent();
|
|
||||||
const bool skipVoice = (triggerEvent.type == TriggerEventType::NoteOn && voice->releasedOrFree()) || voice->isFree();
|
|
||||||
if (!skipVoice
|
|
||||||
&& voice->getRegion()->group == region->group
|
|
||||||
&& voiceTriggerEvent.number == triggerEvent.number
|
|
||||||
&& voiceTriggerEvent.type == triggerEvent.type) {
|
|
||||||
notePolyphonyCounter += 1;
|
|
||||||
switch (region->selfMask) {
|
|
||||||
case SfzSelfMask::mask:
|
|
||||||
if (voiceTriggerEvent.value <= triggerEvent.value) {
|
|
||||||
if (!selfMaskCandidate || selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) {
|
|
||||||
selfMaskCandidate = voice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case SfzSelfMask::dontMask:
|
|
||||||
if (!selfMaskCandidate || selfMaskCandidate->getAge() < voice->getAge())
|
|
||||||
selfMaskCandidate = voice;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notePolyphonyCounter >= *region->notePolyphony) {
|
|
||||||
SisterVoiceRing::offAllSisters(selfMaskCandidate, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check the group polyphony, releasing voices if necessary
|
|
||||||
*
|
|
||||||
* @param region
|
|
||||||
* @param delay
|
|
||||||
*/
|
|
||||||
void checkGroupPolyphony(const Region* region, int delay) noexcept
|
|
||||||
{
|
|
||||||
auto& group = polyphonyGroups_[region->group];
|
|
||||||
Voice* candidate = stealer_->checkPolyphony(
|
|
||||||
absl::MakeSpan(group.getActiveVoices()), group.getPolyphonyLimit());
|
|
||||||
SisterVoiceRing::offAllSisters(candidate, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check the region set polyphony at all levels, releasing voices if necessary
|
|
||||||
*
|
|
||||||
* @param region
|
|
||||||
* @param delay
|
|
||||||
*/
|
|
||||||
void checkSetPolyphony(const Region* region, int delay) noexcept
|
|
||||||
{
|
|
||||||
auto parent = region->parent;
|
|
||||||
while (parent != nullptr) {
|
|
||||||
Voice* candidate = stealer_->checkPolyphony(
|
|
||||||
absl::MakeSpan(parent->getActiveVoices()), parent->getPolyphonyLimit());
|
|
||||||
SisterVoiceRing::offAllSisters(candidate, delay);
|
|
||||||
parent = parent->getParent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check the engine polyphony, fast releasing voices if necessary
|
|
||||||
*
|
|
||||||
* @param delay
|
|
||||||
*/
|
|
||||||
void checkEnginePolyphony(int delay) noexcept
|
|
||||||
{
|
|
||||||
Voice* candidate = stealer_->checkPolyphony(
|
|
||||||
absl::MakeSpan(activeVoices_), numRequiredVoices_);
|
|
||||||
SisterVoiceRing::offAllSisters(candidate, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Vector shortcuts
|
|
||||||
typename decltype(list_)::iterator begin() { return list_.begin(); }
|
|
||||||
typename decltype(list_)::const_iterator cbegin() const { return list_.cbegin(); }
|
|
||||||
typename decltype(list_)::iterator end() { return list_.end(); }
|
|
||||||
typename decltype(list_)::const_iterator cend() const { return list_.cend(); }
|
|
||||||
typename decltype(list_)::reference operator[] (size_t n) { return list_[n]; }
|
|
||||||
typename decltype(list_)::const_reference operator[] (size_t n) const { return list_[n]; }
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sfz
|
|
||||||
242
src/sfizz/VoiceManager.cpp
Normal file
242
src/sfizz/VoiceManager.cpp
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
// 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 "VoiceManager.h"
|
||||||
|
#include "SisterVoiceRing.h"
|
||||||
|
#include "RegionSet.h"
|
||||||
|
#include <absl/algorithm/container.h>
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
|
||||||
|
void VoiceManager::onVoiceStateChanging(NumericId<Voice> id, Voice::State state)
|
||||||
|
{
|
||||||
|
(void)id;
|
||||||
|
if (state == Voice::State::idle) {
|
||||||
|
auto voice = getVoiceById(id);
|
||||||
|
RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice);
|
||||||
|
swapAndPopFirst(activeVoices_, [voice](const Voice* v) { return v == voice; });
|
||||||
|
polyphonyGroups_[voice->getRegion()->group].removeVoice(voice);
|
||||||
|
} else if (state == Voice::State::playing) {
|
||||||
|
auto voice = getVoiceById(id);
|
||||||
|
activeVoices_.push_back(voice);
|
||||||
|
RegionSet::registerVoiceInHierarchy(voice->getRegion(), voice);
|
||||||
|
polyphonyGroups_[voice->getRegion()->group].registerVoice(voice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Voice* VoiceManager::getVoiceById(NumericId<Voice> id) const noexcept
|
||||||
|
{
|
||||||
|
const size_t size = list_.size();
|
||||||
|
|
||||||
|
if (size == 0 || !id.valid())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// search a sequence of ordered identifiers with potential gaps
|
||||||
|
size_t index = static_cast<size_t>(id.number());
|
||||||
|
index = std::min(index, size - 1);
|
||||||
|
|
||||||
|
while (index > 0 && list_[index].getId().number() > id.number())
|
||||||
|
--index;
|
||||||
|
|
||||||
|
return (list_[index].getId() == id) ? &list_[index] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice* VoiceManager::getVoiceById(NumericId<Voice> id) noexcept
|
||||||
|
{
|
||||||
|
return const_cast<Voice*>(
|
||||||
|
const_cast<const VoiceManager*>(this)->getVoiceById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::reset()
|
||||||
|
{
|
||||||
|
for (auto& voice : list_)
|
||||||
|
voice.reset();
|
||||||
|
|
||||||
|
polyphonyGroups_.clear();
|
||||||
|
polyphonyGroups_.emplace_back();
|
||||||
|
polyphonyGroups_.back().setPolyphonyLimit(config::maxVoices);
|
||||||
|
setStealingAlgorithm(StealingAlgorithm::Oldest);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VoiceManager::playingAttackVoice(const Region* releaseRegion) noexcept
|
||||||
|
{
|
||||||
|
const auto compatibleVoice = [releaseRegion](const Voice& v) -> bool {
|
||||||
|
const TriggerEvent& event = v.getTriggerEvent();
|
||||||
|
return (
|
||||||
|
!v.isFree()
|
||||||
|
&& event.type == TriggerEventType::NoteOn
|
||||||
|
&& releaseRegion->keyRange.containsWithEnd(event.number)
|
||||||
|
&& releaseRegion->velocityRange.containsWithEnd(event.value)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (absl::c_find_if(list_, compatibleVoice) == list_.end())
|
||||||
|
return false;
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::ensureNumPolyphonyGroups(unsigned groupIdx) noexcept
|
||||||
|
{
|
||||||
|
while (polyphonyGroups_.size() <= groupIdx)
|
||||||
|
polyphonyGroups_.emplace_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
||||||
|
{
|
||||||
|
ensureNumPolyphonyGroups(groupIdx);
|
||||||
|
polyphonyGroups_[groupIdx].setPolyphonyLimit(polyphony);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PolyphonyGroup* VoiceManager::getPolyphonyGroupView(int idx) const noexcept
|
||||||
|
{
|
||||||
|
return (size_t)idx < polyphonyGroups_.size() ? &polyphonyGroups_[idx] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::clear()
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
list_.clear();
|
||||||
|
activeVoices_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::setStealingAlgorithm(StealingAlgorithm algorithm)
|
||||||
|
{
|
||||||
|
switch(algorithm){
|
||||||
|
case StealingAlgorithm::First: // fallthrough
|
||||||
|
for (auto& voice : list_)
|
||||||
|
voice.disablePowerFollower();
|
||||||
|
|
||||||
|
stealer_ = absl::make_unique<FirstStealer>();
|
||||||
|
break;
|
||||||
|
case StealingAlgorithm::Oldest:
|
||||||
|
for (auto& voice : list_)
|
||||||
|
voice.disablePowerFollower();
|
||||||
|
|
||||||
|
stealer_ = absl::make_unique<OldestStealer>();
|
||||||
|
break;
|
||||||
|
case StealingAlgorithm::EnvelopeAndAge:
|
||||||
|
for (auto& voice : list_)
|
||||||
|
voice.enablePowerFollower();
|
||||||
|
|
||||||
|
stealer_ = absl::make_unique<EnvelopeAndAgeStealer>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
|
||||||
|
{
|
||||||
|
checkNotePolyphony(region, delay, triggerEvent);
|
||||||
|
checkRegionPolyphony(region, delay);
|
||||||
|
checkGroupPolyphony(region, delay);
|
||||||
|
checkSetPolyphony(region, delay);
|
||||||
|
checkEnginePolyphony(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice* VoiceManager::findFreeVoice() noexcept
|
||||||
|
{
|
||||||
|
auto freeVoice = absl::c_find_if(list_, [](const Voice& voice) {
|
||||||
|
return voice.isFree();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (freeVoice != list_.end())
|
||||||
|
return &*freeVoice;
|
||||||
|
|
||||||
|
DBG("Engine hard polyphony reached");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::requireNumVoices(int numVoices, Resources& resources)
|
||||||
|
{
|
||||||
|
numActualVoices_ =
|
||||||
|
static_cast<int>(config::overflowVoiceMultiplier * numVoices);
|
||||||
|
numRequiredVoices_ = numVoices;
|
||||||
|
|
||||||
|
clear();
|
||||||
|
list_.reserve(numActualVoices_);
|
||||||
|
activeVoices_.reserve(numActualVoices_);
|
||||||
|
|
||||||
|
for (int i = 0; i < numActualVoices_; ++i) {
|
||||||
|
list_.emplace_back(i, resources);
|
||||||
|
Voice& lastVoice = list_.back();
|
||||||
|
lastVoice.setStateListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::checkRegionPolyphony(const Region* region, int delay) noexcept
|
||||||
|
{
|
||||||
|
Voice* candidate = stealer_->checkRegionPolyphony(region, absl::MakeSpan(activeVoices_));
|
||||||
|
SisterVoiceRing::offAllSisters(candidate, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept
|
||||||
|
{
|
||||||
|
if (!region->notePolyphony)
|
||||||
|
return;
|
||||||
|
|
||||||
|
unsigned notePolyphonyCounter { 0 };
|
||||||
|
Voice* selfMaskCandidate { nullptr };
|
||||||
|
|
||||||
|
for (Voice* voice : activeVoices_) {
|
||||||
|
const TriggerEvent& voiceTriggerEvent = voice->getTriggerEvent();
|
||||||
|
const bool skipVoice =
|
||||||
|
(triggerEvent.type == TriggerEventType::NoteOn && voice->releasedOrFree())
|
||||||
|
|| voice->isFree();
|
||||||
|
if (!skipVoice
|
||||||
|
&& voice->getRegion()->group == region->group
|
||||||
|
&& voiceTriggerEvent.number == triggerEvent.number
|
||||||
|
&& voiceTriggerEvent.type == triggerEvent.type) {
|
||||||
|
notePolyphonyCounter += 1;
|
||||||
|
switch (region->selfMask) {
|
||||||
|
case SfzSelfMask::mask:
|
||||||
|
if (voiceTriggerEvent.value <= triggerEvent.value) {
|
||||||
|
if (!selfMaskCandidate
|
||||||
|
|| selfMaskCandidate->getTriggerEvent().value > voiceTriggerEvent.value) {
|
||||||
|
selfMaskCandidate = voice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SfzSelfMask::dontMask:
|
||||||
|
if (!selfMaskCandidate || selfMaskCandidate->getAge() < voice->getAge())
|
||||||
|
selfMaskCandidate = voice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notePolyphonyCounter >= *region->notePolyphony) {
|
||||||
|
SisterVoiceRing::offAllSisters(selfMaskCandidate, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::checkGroupPolyphony(const Region* region, int delay) noexcept
|
||||||
|
{
|
||||||
|
auto& group = polyphonyGroups_[region->group];
|
||||||
|
Voice* candidate = stealer_->checkPolyphony(
|
||||||
|
absl::MakeSpan(group.getActiveVoices()), group.getPolyphonyLimit());
|
||||||
|
SisterVoiceRing::offAllSisters(candidate, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::checkSetPolyphony(const Region* region, int delay) noexcept
|
||||||
|
{
|
||||||
|
auto parent = region->parent;
|
||||||
|
while (parent != nullptr) {
|
||||||
|
Voice* candidate = stealer_->checkPolyphony(
|
||||||
|
absl::MakeSpan(parent->getActiveVoices()), parent->getPolyphonyLimit());
|
||||||
|
SisterVoiceRing::offAllSisters(candidate, delay);
|
||||||
|
parent = parent->getParent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceManager::checkEnginePolyphony(int delay) noexcept
|
||||||
|
{
|
||||||
|
Voice* candidate = stealer_->checkPolyphony(
|
||||||
|
absl::MakeSpan(activeVoices_), numRequiredVoices_);
|
||||||
|
SisterVoiceRing::offAllSisters(candidate, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sfz
|
||||||
193
src/sfizz/VoiceManager.h
Normal file
193
src/sfizz/VoiceManager.h
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
// 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 "Voice.h"
|
||||||
|
#include "Resources.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include "Region.h"
|
||||||
|
#include "PolyphonyGroup.h"
|
||||||
|
#include "VoiceStealing.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
|
||||||
|
struct VoiceManager : public Voice::StateListener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief The voice callback which is called during a change of state.
|
||||||
|
*/
|
||||||
|
void onVoiceStateChanging(NumericId<Voice> id, Voice::State state) final;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find the voice which is associated with the given identifier.
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return const Voice*
|
||||||
|
*/
|
||||||
|
const Voice* getVoiceById(NumericId<Voice> id) const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find the voice which is associated with the given identifier.
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return Voice*
|
||||||
|
*/
|
||||||
|
Voice* getVoiceById(NumericId<Voice> id) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset all voices and clear the polyphony groups
|
||||||
|
*/
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a compatible attack voice is playing for the release region.
|
||||||
|
*
|
||||||
|
* @param releaseRegion
|
||||||
|
* @return true
|
||||||
|
* @return false
|
||||||
|
*/
|
||||||
|
bool playingAttackVoice(const Region* releaseRegion) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ensures that the polyphony groups are at least this size.
|
||||||
|
* Call this each time a new `group=N` is given in an sfz file.
|
||||||
|
*
|
||||||
|
* @param groupIdx
|
||||||
|
*/
|
||||||
|
void ensureNumPolyphonyGroups(unsigned groupIdx) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the polyphony for a given group
|
||||||
|
* If the number of polyphony groups is too small, it will
|
||||||
|
* be increased.
|
||||||
|
*
|
||||||
|
* @param groupIdx
|
||||||
|
* @param polyphony
|
||||||
|
*/
|
||||||
|
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get a view into a given polyphony group
|
||||||
|
*
|
||||||
|
* @param idx
|
||||||
|
* @return const PolyphonyGroup*
|
||||||
|
*/
|
||||||
|
const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Clear all voices and polyphony groups.
|
||||||
|
* Also resets the stealing algorithm to default.
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the stealing algorithm
|
||||||
|
*
|
||||||
|
* @param algorithm
|
||||||
|
*/
|
||||||
|
void setStealingAlgorithm(StealingAlgorithm algorithm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Off voices as necessary depending on the trigger event and started region
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param delay
|
||||||
|
* @param triggerEvent
|
||||||
|
*/
|
||||||
|
void checkPolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the number of active voices
|
||||||
|
*
|
||||||
|
* @return size_t
|
||||||
|
*/
|
||||||
|
size_t getNumActiveVoices() const { return activeVoices_.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the number of polyphony groups
|
||||||
|
*
|
||||||
|
* @return size_t
|
||||||
|
*/
|
||||||
|
size_t getNumPolyphonyGroups() const noexcept { return polyphonyGroups_.size(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find a voice that is not currently playing
|
||||||
|
*
|
||||||
|
* @return Voice*
|
||||||
|
*/
|
||||||
|
Voice* findFreeVoice() noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Require a number of voices from this manager.
|
||||||
|
* In practice, the manager will handle slightly more, in order to
|
||||||
|
* allow voices to die off upon reaching higher polyphony count.
|
||||||
|
*
|
||||||
|
* @param numVoices
|
||||||
|
* @param resources
|
||||||
|
*/
|
||||||
|
void requireNumVoices(int numVoices, Resources& resources);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int numRequiredVoices_ { config::numVoices };
|
||||||
|
int numActualVoices_ { static_cast<int>(config::numVoices * config::overflowVoiceMultiplier) };
|
||||||
|
std::vector<Voice> list_;
|
||||||
|
std::vector<Voice*> activeVoices_;
|
||||||
|
// These are the `group=` groups where you can off voices
|
||||||
|
std::vector<PolyphonyGroup> polyphonyGroups_;
|
||||||
|
std::unique_ptr<VoiceStealer> stealer_ { absl::make_unique<OldestStealer>() };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check the region polyphony, releasing voices if necessary
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param delay
|
||||||
|
*/
|
||||||
|
void checkRegionPolyphony(const Region* region, int delay) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check the note polyphony, releasing voices if necessary
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param delay
|
||||||
|
* @param triggerEvent
|
||||||
|
*/
|
||||||
|
void checkNotePolyphony(const Region* region, int delay, const TriggerEvent& triggerEvent) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check the group polyphony, releasing voices if necessary
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param delay
|
||||||
|
*/
|
||||||
|
void checkGroupPolyphony(const Region* region, int delay) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check the region set polyphony at all levels, releasing voices if necessary
|
||||||
|
*
|
||||||
|
* @param region
|
||||||
|
* @param delay
|
||||||
|
*/
|
||||||
|
void checkSetPolyphony(const Region* region, int delay) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check the engine polyphony, fast releasing voices if necessary
|
||||||
|
*
|
||||||
|
* @param delay
|
||||||
|
*/
|
||||||
|
void checkEnginePolyphony(int delay) noexcept;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Vector shortcuts
|
||||||
|
typename decltype(list_)::iterator begin() { return list_.begin(); }
|
||||||
|
typename decltype(list_)::const_iterator cbegin() const { return list_.cbegin(); }
|
||||||
|
typename decltype(list_)::iterator end() { return list_.end(); }
|
||||||
|
typename decltype(list_)::const_iterator cend() const { return list_.cend(); }
|
||||||
|
typename decltype(list_)::reference operator[] (size_t n) { return list_[n]; }
|
||||||
|
typename decltype(list_)::const_reference operator[] (size_t n) const { return list_[n]; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sfz
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
||||||
ADSREnvelopeSource::ADSREnvelopeSource(VoiceList& list, MidiState& state)
|
ADSREnvelopeSource::ADSREnvelopeSource(VoiceManager& manager, MidiState& state)
|
||||||
: voiceList_(list), midiState_(state)
|
: voiceManager_(manager), midiState_(state)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
|
void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
|
||||||
{
|
{
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
@ -60,7 +60,7 @@ void ADSREnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId,
|
||||||
|
|
||||||
void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
|
void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay)
|
||||||
{
|
{
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
@ -91,7 +91,7 @@ void ADSREnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voice
|
||||||
|
|
||||||
void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
|
void ADSREnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer)
|
||||||
{
|
{
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../ModGenerator.h"
|
#include "../ModGenerator.h"
|
||||||
#include "../../VoiceList.h"
|
#include "../../VoiceManager.h"
|
||||||
#include "../../MidiState.h"
|
#include "../../MidiState.h"
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
@ -14,13 +14,13 @@ class Synth;
|
||||||
|
|
||||||
class ADSREnvelopeSource : public ModGenerator {
|
class ADSREnvelopeSource : public ModGenerator {
|
||||||
public:
|
public:
|
||||||
explicit ADSREnvelopeSource(VoiceList &synth, MidiState& state);
|
explicit ADSREnvelopeSource(VoiceManager &manager, MidiState& state);
|
||||||
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
||||||
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
||||||
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VoiceList& voiceList_;
|
VoiceManager& voiceManager_;
|
||||||
MidiState& midiState_;
|
MidiState& midiState_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
||||||
FlexEnvelopeSource::FlexEnvelopeSource(VoiceList& list)
|
FlexEnvelopeSource::FlexEnvelopeSource(VoiceManager& manager)
|
||||||
: voiceList_(list)
|
: voiceManager_(manager)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ void FlexEnvelopeSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId,
|
||||||
{
|
{
|
||||||
unsigned egIndex = sourceKey.parameters().N;
|
unsigned egIndex = sourceKey.parameters().N;
|
||||||
|
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
@ -50,7 +50,7 @@ void FlexEnvelopeSource::release(const ModKey& sourceKey, NumericId<Voice> voice
|
||||||
{
|
{
|
||||||
unsigned egIndex = sourceKey.parameters().N;
|
unsigned egIndex = sourceKey.parameters().N;
|
||||||
|
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
@ -70,7 +70,7 @@ void FlexEnvelopeSource::generate(const ModKey& sourceKey, NumericId<Voice> voic
|
||||||
{
|
{
|
||||||
unsigned egIndex = sourceKey.parameters().N;
|
unsigned egIndex = sourceKey.parameters().N;
|
||||||
|
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,20 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../ModGenerator.h"
|
#include "../ModGenerator.h"
|
||||||
#include "../../VoiceList.h"
|
#include "../../VoiceManager.h"
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
class Synth;
|
class Synth;
|
||||||
|
|
||||||
class FlexEnvelopeSource : public ModGenerator {
|
class FlexEnvelopeSource : public ModGenerator {
|
||||||
public:
|
public:
|
||||||
explicit FlexEnvelopeSource(VoiceList& list);
|
explicit FlexEnvelopeSource(VoiceManager& manager);
|
||||||
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
||||||
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
void release(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
||||||
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VoiceList& voiceList_;
|
VoiceManager& voiceManager_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sfz
|
} // namespace sfz
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
|
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
||||||
LFOSource::LFOSource(VoiceList& list)
|
LFOSource::LFOSource(VoiceManager& manager)
|
||||||
: voiceList_(list)
|
: voiceManager_(manager)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ void LFOSource::init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned
|
||||||
{
|
{
|
||||||
unsigned lfoIndex = sourceKey.parameters().N;
|
unsigned lfoIndex = sourceKey.parameters().N;
|
||||||
|
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
return;
|
return;
|
||||||
|
|
@ -44,7 +44,7 @@ void LFOSource::generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl
|
||||||
{
|
{
|
||||||
const unsigned lfoIndex = sourceKey.parameters().N;
|
const unsigned lfoIndex = sourceKey.parameters().N;
|
||||||
|
|
||||||
Voice* voice = voiceList_.getVoiceById(voiceId);
|
Voice* voice = voiceManager_.getVoiceById(voiceId);
|
||||||
if (!voice) {
|
if (!voice) {
|
||||||
ASSERTFALSE;
|
ASSERTFALSE;
|
||||||
fill(buffer, 0.0f);
|
fill(buffer, 0.0f);
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,18 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../ModGenerator.h"
|
#include "../ModGenerator.h"
|
||||||
#include "../../VoiceList.h"
|
#include "../../VoiceManager.h"
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
class Synth;
|
class Synth;
|
||||||
|
|
||||||
class LFOSource : public ModGenerator {
|
class LFOSource : public ModGenerator {
|
||||||
public:
|
public:
|
||||||
explicit LFOSource(VoiceList &list);
|
explicit LFOSource(VoiceManager &manager);
|
||||||
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
void init(const ModKey& sourceKey, NumericId<Voice> voiceId, unsigned delay) override;
|
||||||
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
void generate(const ModKey& sourceKey, NumericId<Voice> voiceId, absl::Span<float> buffer) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VoiceList& voiceList_;
|
VoiceManager& voiceManager_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sfz
|
} // namespace sfz
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue