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
// 1: use curves 5 & 6
// 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 sfz

View file

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

View file

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

View file

@ -61,13 +61,14 @@ struct SisterVoiceRing {
*
* @param voice
* @param delay
* @param fast whether to apply a fast release
*/
template<class T,
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) {
SisterVoiceRing::applyToRing(voice, [&] (Voice* v) {
v->off(delay);
v->off(delay, fast);
});
}
}

View file

@ -41,6 +41,7 @@ sfz::Synth::Synth(int numVoices)
initializeSIMDDispatchers();
const std::lock_guard<SpinMutex> disableCallback { callbackGuard };
engineSet = absl::make_unique<RegionSet>(nullptr, OpcodeScope::kOpcodeScopeGeneric);
parser.setListener(this);
effectFactory.registerStandardEffectTypes();
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) {
auto voice = getVoiceById(id);
RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice);
engineSet->removeVoice(voice);
polyphonyGroups[voice->getRegion()->group].removeVoice(voice);
}
@ -387,6 +389,7 @@ void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
default:
DBG("Unsupported value for hint_stealing: " << member.value);
}
break;
default:
// Unsupported control opcode
DBG("Unsupported control opcode: " << member.opcode);
@ -742,23 +745,8 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept
if (freeVoice != voices.end())
return freeVoice->get();
// Engine polyphony reached
Voice* stolenVoice = stealer.steal(absl::MakeSpan(voiceViewArray));
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;
DBG("Engine hard polyphony reached");
return {};
}
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
{
ScopedFTZ ftz;
@ -865,19 +839,19 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
ModMatrix& mm = resources.modMatrix;
mm.beginCycle(numFrames);
{ // Clear effect busses
ScopedTiming logger { callbackBreakdown.effects };
for (auto& bus : effectBuses) {
if (bus)
bus->clearInputs(numFrames);
}
}
activeVoices = 0;
{ // Main render block
ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration };
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) {
if (voice->isFree())
continue;
@ -885,7 +859,17 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
mm.beginVoice(voice->getId(), voice->getRegion()->getId(), voice->getTriggerEvent().value);
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.amplitude += voice->getLastAmplitudeDuration();
callbackBreakdown.filters += voice->getLastFilterDuration();
@ -934,14 +918,6 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
// Reset the dispatch counter
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(1)));
SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0)));
@ -993,6 +969,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg
checkRegionPolyphony(region, delay);
checkGroupPolyphony(region, delay);
checkSetPolyphony(region, delay);
checkEnginePolyphony(delay);
Voice* selectedVoice = findFreeVoice();
if (selectedVoice == nullptr)
@ -1001,6 +978,7 @@ void sfz::Synth::startVoice(Region* region, int delay, const TriggerEvent& trigg
ASSERT(selectedVoice->isFree());
selectedVoice->startVoice(region, delay, triggerEvent);
ring.addVoiceToRing(selectedVoice);
engineSet->registerVoice(selectedVoice);
RegionSet::registerVoiceInHierarchy(region, 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
{
tempPolyphonyArray.clear();
for (Voice* voice : voiceViewArray) {
if (voice->getRegion() == region && !voice->releasedOrFree()) {
tempPolyphonyArray.push_back(voice);
}
}
absl::c_copy_if(voiceViewArray,
std::back_inserter(tempPolyphonyArray),
[region](Voice* v) { return v->getRegion() == region && !v->releasedOrFree(); });
if (tempPolyphonyArray.size() >= region->polyphony) {
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();
tempPolyphonyArray.clear();
for (Voice* voice : activeVoices) {
if (!voice->releasedOrFree()) {
tempPolyphonyArray.push_back(voice);
}
}
absl::c_copy_if(activeVoices,
std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); });
if (tempPolyphonyArray.size() >= polyphonyGroups[region->group].getPolyphonyLimit()) {
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) {
const auto& activeVoices = parent->getActiveVoices();
tempPolyphonyArray.clear();
for (Voice* voice : activeVoices) {
if (!voice->releasedOrFree()) {
tempPolyphonyArray.push_back(voice);
}
}
absl::c_copy_if(activeVoices,
std::back_inserter(tempPolyphonyArray), [](Voice* v) { return !v->releasedOrFree(); });
if (tempPolyphonyArray.size() >= parent->getPolyphonyLimit()) {
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
{
const auto randValue = randNoteDistribution(Random::randomGenerator);
@ -1519,7 +1501,7 @@ void sfz::Synth::setVolume(float volume) noexcept
int sfz::Synth::getNumVoices() const noexcept
{
return numVoices;
return numRequiredVoices;
}
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 };
// fast path
if (numVoices == this->numVoices)
if (numVoices == this->numRequiredVoices)
return;
resetVoices(numVoices);
@ -1536,16 +1518,25 @@ void sfz::Synth::setNumVoices(int numVoices) noexcept
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.reserve(numVoices);
voices.reserve(numActualVoices);
voiceViewArray.clear();
voiceViewArray.reserve(numVoices);
voiceViewArray.reserve(numActualVoices);
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);
voice->setStateListener(this);
voiceViewArray.push_back(voice.get());
@ -1557,8 +1548,6 @@ void sfz::Synth::resetVoices(int numVoices)
voice->setSamplesPerBlock(this->samplesPerBlock);
}
this->numVoices = numVoices;
applySettingsPerVoice();
}

View file

@ -742,13 +742,10 @@ private:
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
* @param tempSpan a temporary span used for rendering
* @return fs::file_time_type
*/
void renderVoiceToOutputs(Voice& voice, AudioSpan<float>& tempSpan) noexcept;
fs::file_time_type checkModificationTime();
/**
@ -819,6 +816,9 @@ private:
// These are more general "groups" than sfz and encapsulates the full hierarchy
RegionSet* currentSet { nullptr };
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
std::vector<PolyphonyGroup> polyphonyGroups;
@ -862,6 +862,13 @@ private:
*/
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.
* This will do the needed polyphony checks and voice stealing.
@ -902,7 +909,8 @@ private:
int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate };
float volume { Default::globalVolume };
int numVoices { config::numVoices };
int numRequiredVoices { config::numVoices };
int numActualVoices { static_cast<int>(config::numVoices * config::overflowVoiceMultiplier) };
int activeVoices { 0 };
Oversampling oversamplingFactor { config::defaultOversamplingFactor };

View file

@ -167,11 +167,11 @@ void sfz::Voice::release(int delay) noexcept
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->offMode == SfzOffMode::fast) {
egAmplitude.setReleaseTime( Default::offTime );
if (region->offMode == SfzOffMode::fast || fast) {
egAmplitude.setReleaseTime(Default::offTime);
} else if (region->offMode == SfzOffMode::time) {
egAmplitude.setReleaseTime(region->offTime);
}

View file

@ -328,8 +328,9 @@ public:
* and set the envelopes if necessary.
*
* @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