Merge pull request #477 from paulfd/overflow-voices

Overflow voices
This commit is contained in:
Paul Ferrand 2020-10-07 14:32:05 +02:00 committed by GitHub
commit b9f58f1fe4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 100 additions and 84 deletions

View file

@ -132,6 +132,13 @@ namespace config {
static constexpr int loopXfadeCurve = 2; // 0: linear static constexpr int loopXfadeCurve = 2; // 0: linear
// 1: use curves 5 & 6 // 1: use curves 5 & 6
// 2: use S-shaped curve // 2: use S-shaped curve
/**
* @brief Overflow voices in the engine, relative to the required voices.
* These are additional voices that more or less hold the "dying" voices
* due to engine polyphony being reached.
*/
static constexpr float overflowVoiceMultiplier { 1.5f };
static_assert(overflowVoiceMultiplier >= 1.0f, "This needs to add voices");
} // namespace config } // namespace config
} // namespace sfz } // namespace sfz

View file

@ -53,3 +53,8 @@ unsigned sfz::RegionSet::numPlayingVoices() const noexcept
return !v->releasedOrFree(); return !v->releasedOrFree();
}); });
} }
void sfz::RegionSet::removeAllVoices() noexcept
{
voices.clear();
}

View file

@ -122,6 +122,11 @@ public:
* @return const std::vector<RegionSet*>& * @return const std::vector<RegionSet*>&
*/ */
const std::vector<RegionSet*>& getSubsets() const noexcept { return subsets; } const std::vector<RegionSet*>& getSubsets() const noexcept { return subsets; }
/**
* @brief Remove all voices from the set
*/
void removeAllVoices() noexcept;
private: private:
RegionSet* parent { nullptr }; RegionSet* parent { nullptr };
OpcodeScope level { kOpcodeScopeGeneric }; OpcodeScope level { kOpcodeScopeGeneric };

View file

@ -61,13 +61,14 @@ struct SisterVoiceRing {
* *
* @param voice * @param voice
* @param delay * @param delay
* @param fast whether to apply a fast release
*/ */
template<class T, template<class T,
absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::value, int> = 0> absl::enable_if_t<std::is_same<Voice, absl::remove_const_t<T>>::value, int> = 0>
static void offAllSisters(T* voice, int delay) { static void offAllSisters(T* voice, int delay, bool fast = false) {
if (voice != nullptr) { if (voice != nullptr) {
SisterVoiceRing::applyToRing(voice, [&] (Voice* v) { SisterVoiceRing::applyToRing(voice, [&] (Voice* v) {
v->off(delay); v->off(delay, fast);
}); });
} }
} }

View file

@ -41,6 +41,7 @@ sfz::Synth::Synth(int numVoices)
initializeSIMDDispatchers(); initializeSIMDDispatchers();
const std::lock_guard<SpinMutex> disableCallback { callbackGuard }; const std::lock_guard<SpinMutex> disableCallback { callbackGuard };
engineSet = absl::make_unique<RegionSet>(nullptr, OpcodeScope::kOpcodeScopeGeneric);
parser.setListener(this); parser.setListener(this);
effectFactory.registerStandardEffectTypes(); effectFactory.registerStandardEffectTypes();
effectBuses.reserve(5); // sufficient room for main and fx1-4 effectBuses.reserve(5); // sufficient room for main and fx1-4
@ -70,6 +71,7 @@ void sfz::Synth::onVoiceStateChanged(NumericId<Voice> id, Voice::State state)
if (state == Voice::State::idle) { if (state == Voice::State::idle) {
auto voice = getVoiceById(id); auto voice = getVoiceById(id);
RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice);
engineSet->removeVoice(voice);
polyphonyGroups[voice->getRegion()->group].removeVoice(voice); polyphonyGroups[voice->getRegion()->group].removeVoice(voice);
} }
@ -387,6 +389,7 @@ void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
default: default:
DBG("Unsupported value for hint_stealing: " << member.value); DBG("Unsupported value for hint_stealing: " << member.value);
} }
break;
default: default:
// Unsupported control opcode // Unsupported control opcode
DBG("Unsupported control opcode: " << member.opcode); DBG("Unsupported control opcode: " << member.opcode);
@ -742,23 +745,8 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept
if (freeVoice != voices.end()) if (freeVoice != voices.end())
return freeVoice->get(); return freeVoice->get();
// Engine polyphony reached DBG("Engine hard polyphony reached");
Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); return {};
if (stolenVoice == nullptr)
return {};
// Never kill age 0 voices
if (stolenVoice->getAge() == 0)
return {};
auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock);
SisterVoiceRing::applyToRing(stolenVoice, [&] (Voice* v) {
renderVoiceToOutputs(*v, *tempSpan);
v->reset();
});
return stolenVoice;
} }
int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept int sfz::Synth::getNumActiveVoices(bool recompute) const noexcept
@ -813,20 +801,6 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept
} }
} }
void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan) noexcept
{
const Region* region = voice.getRegion();
ASSERT(region != nullptr);
voice.renderBlock(tempSpan);
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
if (auto& bus = effectBuses[i]) {
float addGain = region->getGainToEffectBus(i);
bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames());
}
}
}
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
{ {
ScopedFTZ ftz; ScopedFTZ ftz;
@ -865,19 +839,19 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
ModMatrix& mm = resources.modMatrix; ModMatrix& mm = resources.modMatrix;
mm.beginCycle(numFrames); mm.beginCycle(numFrames);
{ // Clear effect busses
ScopedTiming logger { callbackBreakdown.effects };
for (auto& bus : effectBuses) {
if (bus)
bus->clearInputs(numFrames);
}
}
activeVoices = 0; activeVoices = 0;
{ // Main render block { // Main render block
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
tempMixSpan->fill(0.0f); tempMixSpan->fill(0.0f);
// Ramp out whatever is in the buffer at this point; should only be killed voice data
linearRamp<float>(*rampSpan, 1.0f, -1.0f / static_cast<float>(numFrames));
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
if (auto& bus = effectBuses[i]) {
bus->applyGain(rampSpan->data(), numFrames);
}
}
for (auto& voice : voices) { for (auto& voice : voices) {
if (voice->isFree()) if (voice->isFree())
continue; continue;
@ -885,7 +859,17 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value); mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value);
activeVoices++; activeVoices++;
renderVoiceToOutputs(*voice, *tempSpan);
const Region* region = voice->getRegion();
ASSERT(region != nullptr);
voice->renderBlock(*tempSpan);
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
if (auto& bus = effectBuses[i]) {
float addGain = region->getGainToEffectBus(i);
bus->addToInputs(*tempSpan, addGain, numFrames);
}
}
callbackBreakdown.data += voice->getLastDataDuration(); callbackBreakdown.data += voice->getLastDataDuration();
callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); callbackBreakdown.amplitude += voice->getLastAmplitudeDuration();
callbackBreakdown.filters += voice->getLastFilterDuration(); callbackBreakdown.filters += voice->getLastFilterDuration();
@ -934,14 +918,6 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
// Reset the dispatch counter // Reset the dispatch counter
dispatchDuration = Duration(0); dispatchDuration = Duration(0);
{ // Clear for the next run
ScopedTiming logger { callbackBreakdown.effects };
for (auto& bus : effectBuses) {
if (bus)
bus->clearInputs(numFrames);
}
}
ASSERT(!hasNanInf(buffer.getConstSpan(0))); ASSERT(!hasNanInf(buffer.getConstSpan(0)));
ASSERT(!hasNanInf(buffer.getConstSpan(1))); ASSERT(!hasNanInf(buffer.getConstSpan(1)));
SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0))); SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0)));
@ -993,6 +969,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg
checkRegionPolyphony(region, delay); checkRegionPolyphony(region, delay);
checkGroupPolyphony(region, delay); checkGroupPolyphony(region, delay);
checkSetPolyphony(region, delay); checkSetPolyphony(region, delay);
checkEnginePolyphony(delay);
Voice* selectedVoice = findFreeVoice(); Voice* selectedVoice = findFreeVoice();
if (selectedVoice == nullptr) if (selectedVoice == nullptr)
@ -1001,6 +978,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg
ASSERT(selectedVoice->isFree()); ASSERT(selectedVoice->isFree());
selectedVoice->startVoice(region, delay, triggerEvent); selectedVoice->startVoice(region, delay, triggerEvent);
ring.addVoiceToRing(selectedVoice); ring.addVoiceToRing(selectedVoice);
engineSet->registerVoice(selectedVoice);
RegionSet::registerVoiceInHierarchy(region, selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice);
polyphonyGroups[region->group].registerVoice(selectedVoice); polyphonyGroups[region->group].registerVoice(selectedVoice);
} }
@ -1042,12 +1020,9 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex
void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept void sfz::Synth::checkRegionPolyphony(const Region* region, int delay) noexcept
{ {
tempPolyphonyArray.clear(); tempPolyphonyArray.clear();
absl::c_copy_if(voiceViewArray,
for (Voice* voice : voiceViewArray) { std::back_inserter(tempPolyphonyArray),
if (voice->getRegion() == region && !voice->releasedOrFree()) { [region](Voice* v) { return v->getRegion() == region && !v->releasedOrFree(); });
tempPolyphonyArray.push_back(voice);
}
}
if (tempPolyphonyArray.size() >= region->polyphony) { if (tempPolyphonyArray.size() >= region->polyphony) {
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
@ -1096,11 +1071,8 @@ void sfz::Synth::checkGroupPolyphony(const Region* region, int delay) noexcept
{ {
const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices(); const auto& activeVoices = polyphonyGroups[region->group].getActiveVoices();
tempPolyphonyArray.clear(); tempPolyphonyArray.clear();
for (Voice* voice : activeVoices) { absl::c_copy_if(activeVoices,
if (!voice->releasedOrFree()) { std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); });
tempPolyphonyArray.push_back(voice);
}
}
if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) { if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) {
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
@ -1114,11 +1086,8 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept
while (parent != nullptr) { while (parent != nullptr) {
const auto& activeVoices = parent->getActiveVoices(); const auto& activeVoices = parent->getActiveVoices();
tempPolyphonyArray.clear(); tempPolyphonyArray.clear();
for (Voice* voice : activeVoices) { absl::c_copy_if(activeVoices,
if (!voice->releasedOrFree()) { std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); });
tempPolyphonyArray.push_back(voice);
}
}
if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) { if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) {
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray)); const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
@ -1129,6 +1098,19 @@ void sfz::Synth::checkSetPolyphony(const Region* region, int delay) noexcept
} }
} }
void sfz::Synth::checkEnginePolyphony(int delay) noexcept
{
auto& activeVoices = engineSet->getActiveVoices();
if (activeVoices.size() >= static_cast<size_t>(numRequiredVoices)) {
tempPolyphonyArray.clear();
absl::c_copy_if(activeVoices,
std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); });
const auto voiceToSteal = stealer.steal(absl::MakeSpan(tempPolyphonyArray));
SisterVoiceRing::offAllSisters(voiceToSteal, delay, true);
}
}
void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexcept
{ {
const auto randValue = randNoteDistribution(Random::randomGenerator); const auto randValue = randNoteDistribution(Random::randomGenerator);
@ -1519,7 +1501,7 @@ void sfz::Synth::setVolume(float volume) noexcept
int sfz::Synth::getNumVoices() const noexcept int sfz::Synth::getNumVoices() const noexcept
{ {
return numVoices; return numRequiredVoices;
} }
void sfz::Synth::setNumVoices(int numVoices) noexcept void sfz::Synth::setNumVoices(int numVoices) noexcept
@ -1528,7 +1510,7 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept
const std::lock_guard<SpinMutex> disableCallback { callbackGuard }; const std::lock_guard<SpinMutex> disableCallback { callbackGuard };
// fast path // fast path
if (numVoices == this->numVoices) if (numVoices == this->numRequiredVoices)
return; return;
resetVoices(numVoices); resetVoices(numVoices);
@ -1536,16 +1518,25 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept
void sfz::Synth::resetVoices(int numVoices) void sfz::Synth::resetVoices(int numVoices)
{ {
numActualVoices =
static_cast<int>(config::overflowVoiceMultiplier * numVoices);
numRequiredVoices = numVoices;
for (auto& set : sets)
set->removeAllVoices();
engineSet->removeAllVoices();
engineSet->setPolyphonyLimit(numRequiredVoices);
voices.clear(); voices.clear();
voices.reserve(numVoices); voices.reserve(numActualVoices);
voiceViewArray.clear(); voiceViewArray.clear();
voiceViewArray.reserve(numVoices); voiceViewArray.reserve(numActualVoices);
tempPolyphonyArray.clear(); tempPolyphonyArray.clear();
tempPolyphonyArray.reserve(numVoices); tempPolyphonyArray.reserve(numActualVoices);
for (int i = 0; i < numVoices; ++i) { for (int i = 0; i < numActualVoices; ++i) {
auto voice = absl::make_unique<Voice>(i, resources); auto voice = absl::make_unique<Voice>(i, resources);
voice->setStateListener(this); voice->setStateListener(this);
voiceViewArray.push_back(voice.get()); voiceViewArray.push_back(voice.get());
@ -1557,8 +1548,6 @@ void sfz::Synth::resetVoices(int numVoices)
voice->setSamplesPerBlock(this->samplesPerBlock); voice->setSamplesPerBlock(this->samplesPerBlock);
} }
this->numVoices = numVoices;
applySettingsPerVoice(); applySettingsPerVoice();
} }

View file

@ -742,13 +742,10 @@ private:
void setupModMatrix(); void setupModMatrix();
/** /**
* @brief Render the voice to its designated outputs and effect busses. * @brief Get the modification time of all included sfz files
* *
* @param voice * @return fs::file_time_type
* @param tempSpan a temporary span used for rendering
*/ */
void renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan) noexcept;
fs::file_time_type checkModificationTime(); fs::file_time_type checkModificationTime();
/** /**
@ -819,6 +816,9 @@ private:
// 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 };
std::vector<RegionSetPtr> sets; std::vector<RegionSetPtr> sets;
// This region set holds the engine set of voices, which tries to respect the required
// engine polyphony
RegionSetPtr engineSet;
// These are the `group=` groups where you can off voices // These are the `group=` groups where you can off voices
std::vector<PolyphonyGroup> polyphonyGroups; std::vector<PolyphonyGroup> polyphonyGroups;
@ -862,6 +862,13 @@ private:
*/ */
void checkSetPolyphony(const Region* region, int delay) noexcept; 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;
/** /**
* @brief Start a voice for a specific region. * @brief Start a voice for a specific region.
* This will do the needed polyphony checks and voice stealing. * This will do the needed polyphony checks and voice stealing.
@ -902,7 +909,8 @@ private:
int samplesPerBlock { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate }; float sampleRate { config::defaultSampleRate };
float volume { Default::globalVolume }; float volume { Default::globalVolume };
int numVoices { config::numVoices }; int numRequiredVoices { config::numVoices };
int numActualVoices { static_cast<int>(config::numVoices * config::overflowVoiceMultiplier) };
int activeVoices { 0 }; int activeVoices { 0 };
Oversampling oversamplingFactor { config::defaultOversamplingFactor }; Oversampling oversamplingFactor { config::defaultOversamplingFactor };

View file

@ -167,11 +167,11 @@ void sfz::Voice::release(int delay) noexcept
resources.modMatrix.releaseVoice(id, region->getId(), delay); resources.modMatrix.releaseVoice(id, region->getId(), delay);
} }
void sfz::Voice::off(int delay) noexcept void sfz::Voice::off(int delay, bool fast) noexcept
{ {
if (!region->flexAmpEG) { if (!region->flexAmpEG) {
if (region->offMode == SfzOffMode::fast) { if (region->offMode == SfzOffMode::fast || fast) {
egAmplitude.setReleaseTime( Default::offTime ); egAmplitude.setReleaseTime(Default::offTime);
} else if (region->offMode == SfzOffMode::time) { } else if (region->offMode == SfzOffMode::time) {
egAmplitude.setReleaseTime(region->offTime); egAmplitude.setReleaseTime(region->offTime);
} }

View file

@ -328,8 +328,9 @@ public:
* and set the envelopes if necessary. * and set the envelopes if necessary.
* *
* @param delay * @param delay
* @param fast whether to apply a fast release regardless of the off mode
*/ */
void off(int delay) noexcept; void off(int delay, bool fast = false) noexcept;
/** /**
* @brief gets the age of the Voice * @brief gets the age of the Voice