From 646b71cb43b1265545ec8b0d062b230ae585e696 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 5 May 2020 23:18:08 +0200 Subject: [PATCH 01/24] Set a default polyphony of 256 --- lv2/sfizz.c | 2 +- lv2/sfizz.ttl.in | 7 ++++++- src/sfizz/Config.h | 6 +++--- vst/SfizzVstState.h | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 67501d85b..bad88439d 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -74,7 +74,7 @@ #define PITCH_BUILD_AND_CENTER(first_byte, last_byte) (int)(((unsigned int)last_byte << 7) + (unsigned int)first_byte) - 8192 #define MAX_BLOCK_SIZE 8192 #define MAX_PATH_SIZE 1024 -#define MAX_VOICES 256 +#define MAX_VOICES 512 #define DEFAULT_VOICES 64 #define DEFAULT_OVERSAMPLING SFIZZ_OVERSAMPLING_X1 #define DEFAULT_PRELOAD 8192 diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 3ebbc2e78..3b07c0cfc 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -114,7 +114,7 @@ midnam:update a lv2:Feature . lv2:portProperty pprop:expensive ; lv2:portProperty lv2:integer ; lv2:portProperty lv2:enumeration ; - lv2:default 64 ; + lv2:default 256 ; lv2:minimum 8 ; lv2:maximum 256 ; lv2:scalePoint [ rdfs:label "8 voices", @@ -147,6 +147,11 @@ midnam:update a lv2:Feature . "256 Voci"@it; rdf:value 256 ] ; + lv2:scalePoint [ rdfs:label "512 voices", + "512 voix"@fr , + "512 Voci"@it; + rdf:value 512 + ] ; ] , [ a lv2:InputPort, lv2:ControlPort ; lv2:index 6 ; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 48aeb70f6..c73600ecb 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -37,9 +37,9 @@ namespace config { constexpr bool loggingEnabled { false }; constexpr size_t numChannels { 2 }; constexpr int numBackgroundThreads { 4 }; - constexpr int numVoices { 64 }; - constexpr unsigned maxVoices { 256 }; - constexpr int maxFilePromises { maxVoices * 2 }; + constexpr int numVoices { 256 }; + constexpr unsigned maxVoices { 512 }; + constexpr int maxFilePromises { maxVoices }; constexpr int sustainCC { 64 }; constexpr int allSoundOffCC { 120 }; constexpr int resetCC { 121 }; diff --git a/vst/SfizzVstState.h b/vst/SfizzVstState.h index eed212e8b..020e0fe8f 100644 --- a/vst/SfizzVstState.h +++ b/vst/SfizzVstState.h @@ -80,6 +80,6 @@ struct SfizzParameterRange { }; static constexpr SfizzParameterRange kParamVolumeRange(0.0, -60.0, +6.0); -static constexpr SfizzParameterRange kParamNumVoicesRange(64.0, 1.0, 256.0); +static constexpr SfizzParameterRange kParamNumVoicesRange(256.0, 1.0, 512.0); static constexpr SfizzParameterRange kParamOversamplingRange(0.0, 0.0, 3.0); static constexpr SfizzParameterRange kParamPreloadSizeRange(8192.0, 1024.0, 65536.0); From 13dcadc3217b9489934f90c363f15e16d71612fd Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 5 May 2020 23:40:06 +0200 Subject: [PATCH 02/24] Memoize the mean power --- src/sfizz/HistoricalBuffer.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sfizz/HistoricalBuffer.h b/src/sfizz/HistoricalBuffer.h index 1ed04ebe3..e2b3b8e52 100644 --- a/src/sfizz/HistoricalBuffer.h +++ b/src/sfizz/HistoricalBuffer.h @@ -37,6 +37,7 @@ class HistoricalBuffer { buffer.resize(size); fill(absl::MakeSpan(buffer), 0.0); index = 0; + validMean = false; } /** @@ -46,6 +47,7 @@ class HistoricalBuffer { */ void push(ValueType value) { + validMean = false; if (size > 0) { buffer[index] = value; if (++index == size) @@ -60,11 +62,18 @@ class HistoricalBuffer { */ ValueType getAverage() const { - return mean(buffer); + if (!validMean) { + mean = sfz::mean(buffer); + validMean = true; + } + + return mean; } private: Buffer buffer; size_t size { 0 }; size_t index { 0 }; + mutable bool validMean { true }; + mutable ValueType mean { 0.0 }; }; } From 87e4b4ed900548ac1e36803aab255e7d058f84d6 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 5 May 2020 23:43:32 +0200 Subject: [PATCH 03/24] Voice are unilateraly stolen now depending on their average power over the last 10 blocks. --- src/sfizz/Config.h | 1 - src/sfizz/Synth.cpp | 21 +++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index c73600ecb..8a1443ac7 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -54,7 +54,6 @@ namespace config { constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; - constexpr float voiceStealingThreshold { 0.00001f }; constexpr uint16_t numCCs { 512 }; constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index e9797e8a6..90c23c705 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -480,20 +480,12 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept return freeVoice->get(); // Find voices that can be stolen - voiceViewArray.clear(); - for (auto& voice : voices) - if (voice->canBeStolen()) - voiceViewArray.push_back(voice.get()); - absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { return lhs->getSourcePosition() > rhs->getSourcePosition(); }); - - for (auto* voice : voiceViewArray) { - if (voice->getMeanSquaredAverage() < config::voiceStealingThreshold) { - voice->reset(); - return voice; - } - } + absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { + return lhs->getMeanSquaredAverage() < rhs->getMeanSquaredAverage(); + }); - return {}; + voiceViewArray.front()->reset(); + return voiceViewArray.front(); } int sfz::Synth::getNumActiveVoices() const noexcept @@ -992,12 +984,13 @@ void sfz::Synth::resetVoices(int numVoices) for (int i = 0; i < numVoices; ++i) voices.push_back(absl::make_unique(resources)); + voiceViewArray.clear(); for (auto& voice : voices) { voice->setSampleRate(this->sampleRate); voice->setSamplesPerBlock(this->samplesPerBlock); + voiceViewArray.push_back(voice.get()); } - voiceViewArray.reserve(numVoices); this->numVoices = numVoices; } From ac993741d11fb06abc3ee8b3945d2278ca07279a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 5 May 2020 23:45:06 +0200 Subject: [PATCH 04/24] Rename the "canBeStolen" function --- src/sfizz/Voice.cpp | 2 +- src/sfizz/Voice.h | 4 ++-- tests/FilesT.cpp | 4 ++-- tests/SynthT.cpp | 18 +++++++++--------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7eea837af..131c3670b 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -638,7 +638,7 @@ float sfz::Voice::getMeanSquaredAverage() const noexcept return powerHistory.getAverage(); } -bool sfz::Voice::canBeStolen() const noexcept +bool sfz::Voice::releasedOrFree() const noexcept { return state == State::idle || egEnvelope.isReleased(); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index bc1e537c2..462cdfd70 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -144,12 +144,12 @@ class Voice { */ bool isFree() const noexcept; /** - * @brief Can the voice be "stolen" and reused (i.e. is it releasing) + * @brief Can the voice be reused (i.e. is it releasing or free) * * @return true * @return false */ - bool canBeStolen() const noexcept; + bool releasedOrFree() const noexcept; /** * @brief Get the number that triggered the voice (note number or cc number) * diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 2f22dea5e..1288638bb 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -455,7 +455,7 @@ TEST_CASE("[Files] Off by with different delays") REQUIRE( group1Voice->getRegion()->offBy == 2ul ); synth.noteOn(100, 64, 63); synth.renderBlock(buffer); - REQUIRE( group1Voice->canBeStolen() ); + REQUIRE( group1Voice->releasedOrFree() ); } TEST_CASE("[Files] Off by with the same delays") @@ -470,7 +470,7 @@ TEST_CASE("[Files] Off by with the same delays") REQUIRE( group1Voice->getRegion()->group == 1ul ); REQUIRE( group1Voice->getRegion()->offBy == 2ul ); synth.noteOn(0, 64, 63); - REQUIRE( !group1Voice->canBeStolen() ); + REQUIRE( !group1Voice->releasedOrFree() ); } TEST_CASE("[Files] Off by with the same notes at the same time") diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 6d7022baf..40a6b1363 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -195,7 +195,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE( synth.getVoiceView(0)->canBeStolen() ); + REQUIRE( synth.getVoiceView(0)->releasedOrFree() ); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -218,7 +218,7 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE( synth.getVoiceView(0)->canBeStolen() ); + REQUIRE( synth.getVoiceView(0)->releasedOrFree() ); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -241,7 +241,7 @@ TEST_CASE("[Synth] loopmode=one_shot and an envelope properly kills the voice at synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE( synth.getVoiceView(0)->canBeStolen() ); + REQUIRE( synth.getVoiceView(0)->releasedOrFree() ); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -376,11 +376,11 @@ TEST_CASE("[Synth] Self-masking") 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)->canBeStolen()); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); - REQUIRE(synth.getVoiceView(1)->canBeStolen()); // The lowest velocity voice is the masking candidate + REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); - REQUIRE(!synth.getVoiceView(2)->canBeStolen()); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Synth] Not self-masking") @@ -392,11 +392,11 @@ TEST_CASE("[Synth] Not self-masking") 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)->canBeStolen()); // The first encountered voice is the masking candidate + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); - REQUIRE(!synth.getVoiceView(1)->canBeStolen()); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); - REQUIRE(!synth.getVoiceView(2)->canBeStolen()); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); } TEST_CASE("[Synth] Polyphony in master") From 68abedae361bc46a1e4902397320ee4063ee38dc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 00:49:05 +0200 Subject: [PATCH 05/24] Updated tests --- tests/FilesT.cpp | 4 ++-- tests/SynthT.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 1288638bb..79dc839cd 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -455,7 +455,7 @@ TEST_CASE("[Files] Off by with different delays") REQUIRE( group1Voice->getRegion()->offBy == 2ul ); synth.noteOn(100, 64, 63); synth.renderBlock(buffer); - REQUIRE( group1Voice->releasedOrFree() ); + REQUIRE(group1Voice->releasedOrFree()); } TEST_CASE("[Files] Off by with the same delays") @@ -470,7 +470,7 @@ TEST_CASE("[Files] Off by with the same delays") REQUIRE( group1Voice->getRegion()->group == 1ul ); REQUIRE( group1Voice->getRegion()->offBy == 2ul ); synth.noteOn(0, 64, 63); - REQUIRE( !group1Voice->releasedOrFree() ); + REQUIRE(!group1Voice->releasedOrFree()); } TEST_CASE("[Files] Off by with the same notes at the same time") diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 40a6b1363..e89ef65e6 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -195,7 +195,7 @@ TEST_CASE("[Synth] Trigger=release and an envelope properly kills the voice at t synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE( synth.getVoiceView(0)->releasedOrFree() ); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -218,7 +218,7 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE( synth.getVoiceView(0)->releasedOrFree() ); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); @@ -241,7 +241,7 @@ TEST_CASE("[Synth] loopmode=one_shot and an envelope properly kills the voice at synth.renderBlock(buffer); // Decay (0.02) synth.renderBlock(buffer); synth.renderBlock(buffer); // Release (0.1) - REQUIRE( synth.getVoiceView(0)->releasedOrFree() ); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // Release is 0.1s for (int i = 0; i < 10; ++i) synth.renderBlock(buffer); From d740f1c32b079ac271822a9c68bad518522619ad Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 01:02:35 +0200 Subject: [PATCH 06/24] Cherry pick the OPF refactor from the smoothers --- benchmarks/BM_filterModulation.cpp | 3 +- src/sfizz/MathHelpers.h | 60 ++++++++++++++ src/sfizz/OnePoleFilter.h | 126 ++++++++++++----------------- src/sfizz/SfzHelpers.h | 6 ++ tests/OnePoleFilterT.cpp | 7 +- 5 files changed, 123 insertions(+), 79 deletions(-) diff --git a/benchmarks/BM_filterModulation.cpp b/benchmarks/BM_filterModulation.cpp index 9326e21f9..f97a9b54b 100644 --- a/benchmarks/BM_filterModulation.cpp +++ b/benchmarks/BM_filterModulation.cpp @@ -8,6 +8,7 @@ #include "OnePoleFilter.h" #include "SfzFilter.h" #include "ScopedFTZ.h" +#include "SfzHelpers.h" #include #include #include @@ -57,7 +58,7 @@ BENCHMARK_DEFINE_F(FilterFixture, OnePole_VA)(benchmark::State& state) { const auto sentinel = cutoff.data() + blockSize; while (cutoffPtr < sentinel) { - const auto gain = sfz::OnePoleFilter::normalizedGain(*cutoffPtr, sampleRate); + const auto gain = sfz::vaGain(*cutoffPtr, sampleRate); filter.setGain(gain); filter.processLowpass({ inputPtr, step }, { outputPtr, step } ); cutoffPtr += step; diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index a70446ca5..6567d5b5b 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -340,6 +340,66 @@ bool isValidAudio(absl::Span span) return true; } +/** + * @brief Finds the minimum size of 2 spans + * + * @tparam T + * @tparam U + * @param span1 + * @param span2 + * @return constexpr size_t + */ +template +constexpr size_t minSpanSize(absl::Span& span1, absl::Span& span2) +{ + return min(span1.size(), span2.size()); +} + +/** + * @brief Finds the minimum size of a list of spans. + * + * @tparam T + * @tparam Others + * @param first + * @param others + * @return constexpr size_t + */ +template +constexpr size_t minSpanSize(absl::Span& first, Others... others) +{ + return min(first.size(), minSpanSize(others...)); +} + +template +constexpr bool _checkSpanSizes(size_t size, absl::Span& span1) +{ + return span1.size() == size; +} + +template +constexpr bool _checkSpanSizes(size_t size, absl::Span& span1, Others... others) +{ + return span1.size() == size && _checkSpanSizes(size, others...); +} + +/** + * @brief Check that all spans of a compile time list have the same size + * + * @tparam T + * @tparam Others + * @param first + * @param others + * @return constexpr size_t + */ +template +constexpr bool checkSpanSizes(const absl::Span& span1, Others... others) +{ + return _checkSpanSizes(span1.size(), others...); +} + +#define CHECK_SPAN_SIZES(...) ASSERT(checkSpanSizes(__VA_ARGS__)) + + class ScopedRoundingMode { public: ScopedRoundingMode() = delete; diff --git a/src/sfizz/OnePoleFilter.h b/src/sfizz/OnePoleFilter.h index 99ddc7541..3e0ea5fcb 100644 --- a/src/sfizz/OnePoleFilter.h +++ b/src/sfizz/OnePoleFilter.h @@ -6,12 +6,14 @@ #pragma once #include "Config.h" +#include "Debug.h" #include "MathHelpers.h" +#include "Macros.h" #include #include -namespace sfz -{ +namespace sfz { + /** * @brief An implementation of a one pole filter. This is a scalar * implementation. @@ -22,108 +24,82 @@ template class OnePoleFilter { public: OnePoleFilter() = default; - // Normalized cutoff with respect to the sampling rate - template - static Type normalizedGain(Type cutoff, C sampleRate) + + void setGain(Type gain) { - return std::tan(cutoff / static_cast(sampleRate) * pi()); + G = gain / (1 + gain); } - OnePoleFilter(Type gain) + void processLowpass(absl::Span input, absl::Span output) { - setGain(gain); + CHECK_SPAN_SIZES(input, output); + processLowpass(input.data(), output.data(), minSpanSize(input, output)); } - void setGain(Type gain) + void processHighpass(absl::Span input, absl::Span output) { - this->gain = gain; - G = gain / (1 + gain); + CHECK_SPAN_SIZES(input, output); + processHighpass(input.data(), output.data(), minSpanSize(input, output)); } - Type getGain() const { return gain; } + void processLowpass(absl::Span input, absl::Span output, absl::Span gain) + { + CHECK_SPAN_SIZES(input, output, gain); + processLowpass(input.data(), output.data(), gain.data(), minSpanSize(input, output, gain)); + } - size_t processLowpass(absl::Span input, absl::Span lowpass) + void processHighpass(absl::Span input, absl::Span output, absl::Span gain) { - auto in = input.begin(); - auto out = lowpass.begin(); - auto size = std::min(input.size(), lowpass.size()); - auto sentinel = in + size; - while (in < sentinel) { - oneLowpass(in, out); - in++; - out++; - } - return size; + CHECK_SPAN_SIZES(input, output, gain); + processHighpass(input.data(), output.data(), gain.data(), minSpanSize(input, output, gain)); } - size_t processHighpass(absl::Span input, absl::Span highpass) + void processLowpass(const Type* input, Type* output, unsigned size) { - auto in = input.begin(); - auto out = highpass.begin(); - auto size = std::min(input.size(), highpass.size()); - auto sentinel = in + size; - while (in < sentinel) { - oneHighpass(in, out); - in++; - out++; + for (unsigned i = 0; i < size; ++i) { + const Type intermediate = G * (input[i] - state); + output[i] = intermediate + state; + state = output[i] + intermediate; } - return size; } - size_t processLowpassVariableGain(absl::Span input, absl::Span lowpass, absl::Span gain) + void processHighpass(const Type* input, Type* output, unsigned size) { - auto in = input.begin(); - auto out = lowpass.begin(); - auto g = gain.begin(); - auto size = min(input.size(), lowpass.size(), gain.size()); - auto sentinel = in + size; - while (in < sentinel) { - setGain(*g); - oneLowpass(in, out); - in++; - out++; - g++; + for (unsigned i = 0; i < size; ++i) { + const Type intermediate = G * (input[i] - state); + output[i] = input[i] - intermediate - state; + state += 2 * intermediate; } - return size; } - size_t processHighpassVariableGain(absl::Span input, absl::Span highpass, absl::Span gain) + void processLowpass(const Type* input, Type* output, const Type* gain, unsigned size) { - auto in = input.begin(); - auto out = highpass.begin(); - auto g = gain.begin(); - auto size = min(input.size(), highpass.size(), gain.size()); - auto sentinel = in + size; - while (in < sentinel) { - setGain(*g); - oneHighpass(in, out); - in++; - out++; - g++; + for (unsigned i = 0; i < size; ++i) { + setGain(gain[i]); + const Type intermediate = G * (input[i] - state); + output[i] = intermediate + state; + state = output[i] + intermediate; } - return size; } - void reset() { state = 0.0; } - -private: - Type state { 0.0 }; - Type gain { 0.25 }; - Type intermediate { 0.0 }; - Type G { gain / (1 + gain) }; - - inline void oneLowpass(const Type* in, Type* out) + void processHighpass(const Type* input, Type* output, const Type* gain, unsigned size) { - intermediate = G * (*in - state); - *out = intermediate + state; - state = *out + intermediate; + for (unsigned i = 0; i < size; ++i) { + setGain(gain[i]); + const Type intermediate = G * (input[i] - state); + output[i] = input[i] - intermediate - state; + state += 2 * intermediate; + } } - inline void oneHighpass(const Type* in, Type* out) + void reset(Type value = 0.0) { - intermediate = G * (*in - state); - *out = *in - intermediate - state; - state += 2 * intermediate; + state = value; } + +private: + Type state { 0.0 }; + Type G { 0.5 }; }; + } diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 5d581a743..5cd49aad1 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -204,6 +204,12 @@ namespace literals { */ absl::optional readNoteValue(const absl::string_view& value); +template +inline CXX14_CONSTEXPR Type vaGain(Type cutoff, Type sampleRate) +{ + return std::tan(cutoff / sampleRate * pi()); +} + /** * @brief From a source view, find the next sfz header and its members and * return them, while updating the source by removing this header diff --git a/tests/OnePoleFilterT.cpp b/tests/OnePoleFilterT.cpp index 3350fa7d3..f44e65cc7 100644 --- a/tests/OnePoleFilterT.cpp +++ b/tests/OnePoleFilterT.cpp @@ -40,12 +40,13 @@ void testFilter(const std::array& input, const std::array& exp std::array gains; std::fill(gains.begin(), gains.end(), gain); - sfz::OnePoleFilter filter { gain }; + sfz::OnePoleFilter filter; + filter.setGain(gain); filter.processLowpass(input, outputSpan); REQUIRE(approxEqual(output, expectedLow)); filter.reset(); - filter.processLowpassVariableGain(input, outputSpan, gains); + filter.processLowpass(input, outputSpan, gains); REQUIRE(approxEqual(output, expectedLow)); filter.reset(); @@ -53,7 +54,7 @@ void testFilter(const std::array& input, const std::array& exp REQUIRE(approxEqual(output, expectedHigh)); filter.reset(); - filter.processHighpassVariableGain(input, outputSpan, gains); + filter.processHighpass(input, outputSpan, gains); REQUIRE(approxEqual(output, expectedHigh)); } From a9980f163f03f8e306a4bdd957bd969652c9d493 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 01:27:20 +0200 Subject: [PATCH 07/24] Changed the historical buffer to a smoothed envelope tracker --- src/sfizz/Config.h | 1 + src/sfizz/Voice.cpp | 44 +++++++++++++++++++++++++++++++++++++------- src/sfizz/Voice.h | 5 ++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 8a1443ac7..fadba005f 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -54,6 +54,7 @@ namespace config { constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; constexpr float A440 { 440.0 }; constexpr size_t powerHistoryLength { 16 }; + constexpr float filteredEnvelopeCutoff { 5 }; constexpr uint16_t numCCs { 512 }; constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 131c3670b..992d97e77 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -21,6 +21,9 @@ sfz::Voice::Voice(sfz::Resources& resources) for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); + + for (auto & filter : channelPowerFilters) + filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); } void sfz::Voice::startVoice(Region* region, int delay, int number, float value, sfz::Voice::TriggerType triggerType) noexcept @@ -192,6 +195,9 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept { this->sampleRate = sampleRate; + for (auto & filter : channelPowerFilters) + filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); + for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); } @@ -207,11 +213,6 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept ASSERT(static_cast(buffer.getNumFrames()) <= samplesPerBlock); buffer.fill(0.0f); - if (state == State::idle || region == nullptr) { - powerHistory.push(0.0f); - return; - } - const auto delay = min(static_cast(initialDelay), buffer.getNumFrames()); auto delayed_buffer = buffer.subspan(delay); initialDelay -= static_cast(delay); @@ -237,7 +238,8 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept if (!egEnvelope.isSmoothing()) reset(); - powerHistory.push(buffer.meanSquared()); + updateChannelPowers(buffer); + this->triggerDelay = absl::nullopt; #if 0 ASSERT(!hasNanInf(buffer.getConstSpan(0))); @@ -629,13 +631,20 @@ void sfz::Voice::reset() noexcept sourcePosition = 0; floatPositionOffset = 0.0f; noteIsOff = false; + + for (auto& f : channelPowerFilters) + f.reset(); + + for (auto& p : channelPowers) + p = 0.0f; + filters.clear(); equalizers.clear(); } float sfz::Voice::getMeanSquaredAverage() const noexcept { - return powerHistory.getAverage(); + return max(channelPowers[0], channelPowers[1]); } bool sfz::Voice::releasedOrFree() const noexcept @@ -718,3 +727,24 @@ void sfz::Voice::setupOscillatorUnison() } #endif } + + +void sfz::Voice::updateChannelPowers(AudioSpan buffer) +{ + assert(channelPowers.size() == channelPowerFilters.size()); + assert(buffer.getNumFrames() <= channelPowerFilters.size()); + if (buffer.getNumFrames() == 0) + return; + + auto tempSpan = resources.bufferPool.getBuffer(buffer.getNumFrames()); + if (!tempSpan) + return; + + for (unsigned i = 0; i < channelPowers.size(); ++i) { + for (unsigned s = 0; s < buffer.getNumFrames(); ++s) + (*tempSpan)[s] = std::abs(buffer.getConstSpan(i)[s]); + + channelPowerFilters[i].processLowpass(*tempSpan, *tempSpan); + channelPowers[i] = tempSpan->back(); + } +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 462cdfd70..a8d30cbf6 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -13,6 +13,7 @@ #include "Resources.h" #include "AudioSpan.h" #include "LeakDetector.h" +#include "OnePoleFilter.h" #include "absl/types/span.h" #include #include @@ -245,6 +246,7 @@ class Voice { * @brief Initialize frequency and gain coefficients for the oscillators. */ void setupOscillatorUnison(); + void updateChannelPowers(AudioSpan buffer); Region* region { nullptr }; @@ -299,7 +301,8 @@ class Voice { std::normal_distribution noiseDist { 0, config::noiseVariance }; - HistoricalBuffer powerHistory { config::powerHistoryLength }; + std::array, 2> channelPowerFilters; + std::array channelPowers; LEAK_DETECTOR(Voice); }; From b78a7945abc44447f0a0390f8c20c5084c182b6b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 12:22:45 +0200 Subject: [PATCH 08/24] Add aging in the voice --- src/sfizz/Synth.cpp | 2 +- src/sfizz/Voice.cpp | 11 +++++++++-- src/sfizz/Voice.h | 10 +++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 90c23c705..823a86533 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -481,7 +481,7 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept // Find voices that can be stolen absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { - return lhs->getMeanSquaredAverage() < rhs->getMeanSquaredAverage(); + return lhs->getAverageEnvelope() < rhs->getAverageEnvelope(); }); voiceViewArray.front()->reset(); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 992d97e77..fc7bd4f06 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -240,7 +240,13 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept updateChannelPowers(buffer); - this->triggerDelay = absl::nullopt; + age += buffer.getNumFrames(); + if (triggerDelay) { + // Should be OK but just in case; + age = min(age - *triggerDelay, 0); + triggerDelay = absl::nullopt; + } + #if 0 ASSERT(!hasNanInf(buffer.getConstSpan(0))); ASSERT(!hasNanInf(buffer.getConstSpan(1))); @@ -629,6 +635,7 @@ void sfz::Voice::reset() noexcept region = nullptr; currentPromise.reset(); sourcePosition = 0; + age = 0; floatPositionOffset = 0.0f; noteIsOff = false; @@ -642,7 +649,7 @@ void sfz::Voice::reset() noexcept equalizers.clear(); } -float sfz::Voice::getMeanSquaredAverage() const noexcept +float sfz::Voice::getAverageEnvelope() const noexcept { return max(channelPowers[0], channelPowers[1]); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index a8d30cbf6..e3572c655 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -182,7 +182,7 @@ class Voice { * * @return float */ - float getMeanSquaredAverage() const noexcept; + float getAverageEnvelope() const noexcept; /** * @brief Get the position of the voice in the source, in samples * @@ -215,6 +215,13 @@ class Voice { */ void release(int delay, bool fastRelease = false) noexcept; + /** + * @brief gets the age of the Voice + * + * @return + */ + int getAge() const noexcept { return age; } + Duration getLastDataDuration() const noexcept { return dataDuration; } Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; } Duration getLastFilterDuration() const noexcept { return filterDuration; } @@ -271,6 +278,7 @@ class Voice { float floatPositionOffset { 0.0f }; int sourcePosition { 0 }; int initialDelay { 0 }; + int age { 0 }; FilePromisePtr currentPromise { nullptr }; From 36e4c69fc24376b6e8ec16b5b3de54a8cd42cf2c Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 12:54:30 +0200 Subject: [PATCH 09/24] Use age and average envelope for stealing --- src/sfizz/Synth.cpp | 47 +++++++++++++++++++++++++++++++++++++++++---- src/sfizz/Synth.h | 2 ++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 823a86533..1d8ccb49b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -473,19 +473,58 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) return true; } +unsigned sfz::Synth::killSisterVoices(const Voice* voiceToKill) noexcept +{ + const auto age = voiceToKill->getAge(); + const auto type = voiceToKill->getTriggerType(); + const auto number = voiceToKill->getTriggerNumber(); + const auto value = voiceToKill->getTriggerValue(); + + unsigned killedVoices = 0; + for (auto & voice : voiceViewArray) { + if (voice->getAge() == age + && voice->getTriggerType() == type + && voice->getTriggerNumber() == number + && voice->getTriggerValue() == value) { + killedVoices++; + voice->reset(); + } + } + return killedVoices; +} + sfz::Voice* sfz::Synth::findFreeVoice() noexcept { - auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& voice) { return voice->isFree(); }); + auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& voice) { + return voice->isFree(); + }); if (freeVoice != voices.end()) return freeVoice->get(); // Find voices that can be stolen absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { - return lhs->getAverageEnvelope() < rhs->getAverageEnvelope(); + return lhs->getAge() < rhs->getAge(); }); - voiceViewArray.front()->reset(); - return voiceViewArray.front(); + const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [] (float sum, const Voice* v) { + return sum + v->getAverageEnvelope(); + }); + const auto threshold = sumEnvelope / static_cast(voiceViewArray.size()) / 2; + + Voice* returnedVoice = voiceViewArray.front(); + for (auto & voice : voiceViewArray) { + if (voice->getAverageEnvelope() < threshold) { + returnedVoice = voice; + break; + } + } + const auto killedVoices = killSisterVoices(returnedVoice); + UNUSED(killedVoices); // only in debug + assert(killedVoices > 0); + assert(returnedVoice->isFree()); + std::cout << "Killed " << killedVoices << " voices"; + + return returnedVoice; } int sfz::Synth::getNumActiveVoices() const noexcept diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 0eddea0b1..9d2583ec2 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -495,6 +495,8 @@ class Synth final : public Parser::Listener { void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; void noteOffDispatch(int delay, int noteNumber, float velocity) noexcept; + unsigned killSisterVoices(const Voice* voiceToKill) noexcept; + // Opcode memory; these are used to build regions, as a new region // will integrate opcodes from the group, master and global block std::vector globalOpcodes; From 1601ff6afe24b4d269cf9f006a57e6fad2000035 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 13:25:47 +0200 Subject: [PATCH 10/24] Do not change parameters unnecessarily --- lv2/sfizz.c | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index bad88439d..16736ecf0 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -906,8 +906,7 @@ work(LV2_Handle instance, const void *data) { sfizz_plugin_t *self = (sfizz_plugin_t *)instance; - if (!data) - { + if (!data) { lv2_log_error(&self->logger, "[sfizz] Ignoring empty data in the worker thread\n"); return LV2_WORKER_ERR_UNKNOWN; } @@ -922,12 +921,9 @@ work(LV2_Handle instance, const char *sfz_file_path = LV2_ATOM_BODY_CONST(atom); self->changing_state = true; - if (sfizz_load_file(self->synth, sfz_file_path)) - { + if (sfizz_load_file(self->synth, sfz_file_path)) { sfizz_lv2_update_file_info(self, sfz_file_path); - } - else - { + } else { lv2_log_error(&self->logger, "[sfizz] Error with %s; no file should be loaded\n", sfz_file_path); } self->changing_state = false; @@ -939,12 +935,18 @@ work(LV2_Handle instance, return LV2_WORKER_SUCCESS; } - self->changing_state = true; const int num_voices = *(const int *)LV2_ATOM_BODY_CONST(atom); + if (sfizz_get_num_voices(self->synth) == num_voices) { + return LV2_WORKER_SUCCESS; // Nothing to do + } + + self->changing_state = true; sfizz_set_num_voices(self->synth, num_voices); if (sfizz_get_num_voices(self->synth) == num_voices) { self->num_voices = num_voices; lv2_log_note(&self->logger, "[sfizz] Number of voices changed to: %d\n", num_voices); + } else { + lv2_log_error(&self->logger, "[sfizz] Error changing the number of voices\n"); } self->changing_state = false; } @@ -955,12 +957,18 @@ work(LV2_Handle instance, return LV2_WORKER_SUCCESS; } - self->changing_state = true; const unsigned int preload_size = *(const unsigned int *)LV2_ATOM_BODY_CONST(atom); + if (sfizz_get_preload_size(self->synth) == preload_size) { + return LV2_WORKER_SUCCESS; // Nothing to do + } + + self->changing_state = true; sfizz_set_preload_size(self->synth, preload_size); if (sfizz_get_preload_size(self->synth) == preload_size) { self->preload_size = preload_size; lv2_log_note(&self->logger, "[sfizz] Preload size changed to: %d\n", preload_size); + } else { + lv2_log_error(&self->logger, "[sfizz] Error changing the preload size\n"); } self->changing_state = false; } @@ -970,13 +978,20 @@ work(LV2_Handle instance, respond(handle, size, data); // send back so that we reschedule the check return LV2_WORKER_SUCCESS; } - self->changing_state = true; + const sfizz_oversampling_factor_t oversampling = *(const sfizz_oversampling_factor_t *)LV2_ATOM_BODY_CONST(atom); + if (sfizz_get_oversampling_factor(self->synth) == oversampling) { + return LV2_WORKER_SUCCESS; // Nothing to do + } + + self->changing_state = true; sfizz_set_oversampling_factor(self->synth, oversampling); if (sfizz_get_oversampling_factor(self->synth) == oversampling) { self->oversampling = oversampling; lv2_log_note(&self->logger, "[sfizz] Oversampling changed to: %d\n", oversampling); + } else { + lv2_log_error(&self->logger, "[sfizz] Error changing the oversampling\n"); } self->changing_state = false; } @@ -992,13 +1007,12 @@ work(LV2_Handle instance, if (self->changing_state) return LV2_WORKER_SUCCESS; - lv2_log_note(&self->logger, "[sfizz] File %s seems to have been updated, reloading\n", self->sfz_file_path); - if (sfizz_load_file(self->synth, self->sfz_file_path)) - { + lv2_log_note(&self->logger, + "[sfizz] File %s seems to have been updated, reloading\n", + self->sfz_file_path); + if (sfizz_load_file(self->synth, self->sfz_file_path)) { sfizz_lv2_update_file_info(self, self->sfz_file_path); - } - else - { + } else { lv2_log_error(&self->logger, "[sfizz] Error with %s; no file should be loaded\n", self->sfz_file_path); } } @@ -1012,7 +1026,6 @@ work(LV2_Handle instance, self->unmap->unmap(self->unmap->handle, atom->type)); return LV2_WORKER_ERR_UNKNOWN; } - return LV2_WORKER_SUCCESS; } From 725bda2af8bb417f95d7d54aea051eee7d8e1717 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 13:26:05 +0200 Subject: [PATCH 11/24] Corrected a false error message when changing the number of voices --- lv2/sfizz.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 16736ecf0..104f713d2 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -584,7 +584,7 @@ sfizz_lv2_check_num_voices(sfizz_plugin_t* self) num_voices_atom.body = num_voices; if (self->worker->schedule_work(self->worker->handle, lv2_atom_total_size((LV2_Atom *)&num_voices_atom), - &num_voices_atom) == LV2_WORKER_SUCCESS) + &num_voices_atom) != LV2_WORKER_SUCCESS) { lv2_log_error(&self->logger, "[sfizz] There was an issue changing the number of voices\n"); } From 0c9c188e4b7d2ef3e9d30de9cd8327fb625955b9 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 13:49:13 +0200 Subject: [PATCH 12/24] Refine the stealing algorithm --- src/sfizz/Synth.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 1d8ccb49b..d59fc85e3 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -503,26 +503,33 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept // Find voices that can be stolen absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { - return lhs->getAge() < rhs->getAge(); + return lhs->getAge() > rhs->getAge(); }); const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [] (float sum, const Voice* v) { return sum + v->getAverageEnvelope(); }); - const auto threshold = sumEnvelope / static_cast(voiceViewArray.size()) / 2; + const auto envThreshold = sumEnvelope / static_cast(voiceViewArray.size()) * 0.25f; + const auto ageThreshold = voiceViewArray.front()->getAge() * 0.5f; Voice* returnedVoice = voiceViewArray.front(); for (auto & voice : voiceViewArray) { - if (voice->getAverageEnvelope() < threshold) { + if (voice->getAge() < ageThreshold) { + // std::cout << "Went too far, picking the oldest note..." << '\n'; + break; + } + if (voice->getAverageEnvelope() < envThreshold) { + // std::cout << "Found a better candidate!" << '\n'; returnedVoice = voice; break; } } + const auto killedVoices = killSisterVoices(returnedVoice); UNUSED(killedVoices); // only in debug assert(killedVoices > 0); assert(returnedVoice->isFree()); - std::cout << "Killed " << killedVoices << " voices"; + // std::cout << "Killed " << killedVoices << " voices" << '\n'; return returnedVoice; } From 81ee1f2e590d854447bf27527c2025b9fde2c309 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 17:52:29 +0200 Subject: [PATCH 13/24] Add a tick method to the OPF --- src/sfizz/OnePoleFilter.h | 32 ++++++++++++++++++++------------ src/sfizz/Voice.cpp | 25 ++----------------------- src/sfizz/Voice.h | 16 ++++++++++++---- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/sfizz/OnePoleFilter.h b/src/sfizz/OnePoleFilter.h index 3e0ea5fcb..9781b39e5 100644 --- a/src/sfizz/OnePoleFilter.h +++ b/src/sfizz/OnePoleFilter.h @@ -57,18 +57,14 @@ class OnePoleFilter { void processLowpass(const Type* input, Type* output, unsigned size) { for (unsigned i = 0; i < size; ++i) { - const Type intermediate = G * (input[i] - state); - output[i] = intermediate + state; - state = output[i] + intermediate; + output[i] = tickLowpass(input[i]); } } void processHighpass(const Type* input, Type* output, unsigned size) { for (unsigned i = 0; i < size; ++i) { - const Type intermediate = G * (input[i] - state); - output[i] = input[i] - intermediate - state; - state += 2 * intermediate; + output[i] = tickHighpass(input[i]); } } @@ -76,9 +72,7 @@ class OnePoleFilter { { for (unsigned i = 0; i < size; ++i) { setGain(gain[i]); - const Type intermediate = G * (input[i] - state); - output[i] = intermediate + state; - state = output[i] + intermediate; + output[i] = tickLowpass(input[i]); } } @@ -86,12 +80,26 @@ class OnePoleFilter { { for (unsigned i = 0; i < size; ++i) { setGain(gain[i]); - const Type intermediate = G * (input[i] - state); - output[i] = input[i] - intermediate - state; - state += 2 * intermediate; + output[i] = tickHighpass(input[i]); } } + Type tickHighpass(const Type& input) + { + const Type intermediate = G * (input - state); + const Type output = input - intermediate - state; + state += 2 * intermediate; + return output; + } + + Type tickLowpass(const Type& input) + { + const Type intermediate = G * (input - state); + const Type output = intermediate + state; + state = output + intermediate; + return output; + } + void reset(Type value = 0.0) { state = value; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index fc7bd4f06..4692930e9 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -614,21 +614,6 @@ bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept return false; } -int sfz::Voice::getTriggerNumber() const noexcept -{ - return triggerNumber; -} - -float sfz::Voice::getTriggerValue() const noexcept -{ - return triggerValue; -} - -sfz::Voice::TriggerType sfz::Voice::getTriggerType() const noexcept -{ - return triggerType; -} - void sfz::Voice::reset() noexcept { state = State::idle; @@ -743,15 +728,9 @@ void sfz::Voice::updateChannelPowers(AudioSpan buffer) if (buffer.getNumFrames() == 0) return; - auto tempSpan = resources.bufferPool.getBuffer(buffer.getNumFrames()); - if (!tempSpan) - return; - for (unsigned i = 0; i < channelPowers.size(); ++i) { + const auto input = buffer.getConstSpan(i); for (unsigned s = 0; s < buffer.getNumFrames(); ++s) - (*tempSpan)[s] = std::abs(buffer.getConstSpan(i)[s]); - - channelPowerFilters[i].processLowpass(*tempSpan, *tempSpan); - channelPowers[i] = tempSpan->back(); + channelPowers[i] = channelPowerFilters[i].tickLowpass(std::abs(input[s])); } } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index e3572c655..0a617a827 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -156,19 +156,19 @@ class Voice { * * @return int */ - int getTriggerNumber() const noexcept; + constexpr int getTriggerNumber() const noexcept { return triggerNumber; } /** * @brief Get the value that triggered the voice (note velocity or cc value) * * @return float */ - float getTriggerValue() const noexcept; + constexpr float getTriggerValue() const noexcept { return triggerValue; } /** * @brief Get the type of trigger * * @return TriggerType */ - TriggerType getTriggerType() const noexcept; + constexpr TriggerType getTriggerType() const noexcept { return triggerType; } /** * @brief Reset the voice to its initial values @@ -220,7 +220,7 @@ class Voice { * * @return */ - int getAge() const noexcept { return age; } + constexpr int getAge() const noexcept { return age; } Duration getLastDataDuration() const noexcept { return dataDuration; } Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; } @@ -314,4 +314,12 @@ class Voice { LEAK_DETECTOR(Voice); }; +constexpr bool sisterVoices(const Voice* lhs, const Voice* rhs) +{ + return lhs->getAge() == rhs->getAge() + && lhs->getTriggerNumber() == rhs->getTriggerNumber() + && lhs->getTriggerValue() == rhs->getTriggerValue() + && lhs->getTriggerType() == rhs->getTriggerType(); +} + } // namespace sfz From cd455c435ececcffc585586302ece6ff41965398 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 18:24:49 +0200 Subject: [PATCH 14/24] Consider all sister voices when killing a note --- src/sfizz/Synth.cpp | 92 +++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index d59fc85e3..71ef5822b 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -389,15 +389,14 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) // TODO: adjust with LFO targets const auto maxOffset = [region]() { uint64_t sumOffsetCC = region->offset + region->offsetRandom; - for (const auto& offsets: region->offsetCC) + for (const auto& offsets : region->offsetCC) sumOffsetCC += offsets.data; return Default::offsetCCRange.clamp(sumOffsetCC); }(); if (!resources.filePool.preloadFile(region->sampleId, maxOffset)) removeCurrentRegion(); - } - else if (region->oscillator && !region->isGenerator()) { + } else if (region->oscillator && !region->isGenerator()) { if (!resources.filePool.checkSampleId(region->sampleId)) { removeCurrentRegion(); continue; @@ -412,7 +411,6 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) if (region->keyswitchLabel && region->keyswitch) keyswitchLabels.push_back({ *region->keyswitch, *region->keyswitchLabel }); - // Some regions had group number but no "group-level" opcodes handled the polyphony while (groupMaxPolyphony.size() <= region->group) groupMaxPolyphony.push_back(config::maxVoices); @@ -473,26 +471,6 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) return true; } -unsigned sfz::Synth::killSisterVoices(const Voice* voiceToKill) noexcept -{ - const auto age = voiceToKill->getAge(); - const auto type = voiceToKill->getTriggerType(); - const auto number = voiceToKill->getTriggerNumber(); - const auto value = voiceToKill->getTriggerValue(); - - unsigned killedVoices = 0; - for (auto & voice : voiceViewArray) { - if (voice->getAge() == age - && voice->getTriggerType() == type - && voice->getTriggerNumber() == number - && voice->getTriggerValue() == value) { - killedVoices++; - voice->reset(); - } - } - return killedVoices; -} - sfz::Voice* sfz::Synth::findFreeVoice() noexcept { auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& voice) { @@ -503,34 +481,69 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept // Find voices that can be stolen absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { - return lhs->getAge() > rhs->getAge(); + if (lhs->getAge() > rhs->getAge()) + return true; + if (lhs->getAge() < rhs->getAge()) + return false; + + if (lhs->getTriggerNumber() > rhs->getTriggerNumber()) + return true; + if (lhs->getTriggerNumber() < rhs->getTriggerNumber()) + return false; + + if (lhs->getTriggerValue() > rhs->getTriggerValue()) + return true; + if (lhs->getTriggerValue() < rhs->getTriggerValue()) + return false; + + if (lhs->getTriggerType() > rhs->getTriggerType()) + return true; + + return false; }); - const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [] (float sum, const Voice* v) { + const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [](float sum, const Voice* v) { return sum + v->getAverageEnvelope(); }); - const auto envThreshold = sumEnvelope / static_cast(voiceViewArray.size()) * 0.25f; + const auto envThreshold = sumEnvelope / static_cast(voiceViewArray.size()) * 0.5f; const auto ageThreshold = voiceViewArray.front()->getAge() * 0.5f; Voice* returnedVoice = voiceViewArray.front(); - for (auto & voice : voiceViewArray) { - if (voice->getAge() < ageThreshold) { - // std::cout << "Went too far, picking the oldest note..." << '\n'; + unsigned idx = 0; + while (idx < voiceViewArray.size()) { + const auto refIdx = idx; + const auto ref = voiceViewArray[idx]; + idx++; + + if (ref->getAge() < ageThreshold) { + unsigned killIdx = 1; + while (killIdx < voiceViewArray.size() + && sisterVoices(returnedVoice, voiceViewArray[killIdx])) { + voiceViewArray[killIdx]->reset(); + killIdx++; + } + // std::cout << "Went too far, picking the oldest voice and killing " + // << killIdx << " voices" << '\n'; + returnedVoice->reset(); break; } - if (voice->getAverageEnvelope() < envThreshold) { - // std::cout << "Found a better candidate!" << '\n'; - returnedVoice = voice; + + float sumEnvelope = ref->getAverageEnvelope(); + while (idx < voiceViewArray.size() && sisterVoices(ref, voiceViewArray[idx])) { + sumEnvelope += voiceViewArray[idx]->getAverageEnvelope(); + idx++; + } + + if (sumEnvelope < envThreshold) { + returnedVoice = ref; + // std::cout << "Killing " << idx - refIdx << " voices" << '\n'; + for (unsigned j = refIdx; j < idx; j++) + voiceViewArray[j]->reset(); break; } } - const auto killedVoices = killSisterVoices(returnedVoice); - UNUSED(killedVoices); // only in debug - assert(killedVoices > 0); assert(returnedVoice->isFree()); - // std::cout << "Killed " << killedVoices << " voices" << '\n'; - return returnedVoice; } @@ -599,7 +612,6 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (!lock.owns_lock()) return; - size_t numFrames = buffer.getNumFrames(); auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames); auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames); @@ -1107,7 +1119,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept fs::file_time_type sfz::Synth::checkModificationTime() { auto returnedTime = modificationTime; - for (const auto& file: parser.getIncludedFiles()) { + for (const auto& file : parser.getIncludedFiles()) { std::error_code ec; const auto fileTime = fs::last_write_time(file, ec); if (!ec && returnedTime < fileTime) From 298ec7fc0c7c13d86e6d8a23d79d7fa5cf0c1dd7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 18:26:29 +0200 Subject: [PATCH 15/24] Change the name of the envelope variables and filters --- src/sfizz/Voice.cpp | 19 ++++++++++--------- src/sfizz/Voice.h | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4692930e9..8b69592a9 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -22,7 +22,7 @@ sfz::Voice::Voice(sfz::Resources& resources) for (WavetableOscillator& osc : waveOscillators) osc.init(sampleRate); - for (auto & filter : channelPowerFilters) + for (auto & filter : channelEnvelopeFilters) filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); } @@ -195,7 +195,7 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept { this->sampleRate = sampleRate; - for (auto & filter : channelPowerFilters) + for (auto & filter : channelEnvelopeFilters) filter.setGain(vaGain(config::filteredEnvelopeCutoff, sampleRate)); for (WavetableOscillator& osc : waveOscillators) @@ -624,10 +624,10 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; - for (auto& f : channelPowerFilters) + for (auto& f : channelEnvelopeFilters) f.reset(); - for (auto& p : channelPowers) + for (auto& p : smoothedChannelEnvelopes) p = 0.0f; filters.clear(); @@ -636,7 +636,7 @@ void sfz::Voice::reset() noexcept float sfz::Voice::getAverageEnvelope() const noexcept { - return max(channelPowers[0], channelPowers[1]); + return max(smoothedChannelEnvelopes[0], smoothedChannelEnvelopes[1]); } bool sfz::Voice::releasedOrFree() const noexcept @@ -723,14 +723,15 @@ void sfz::Voice::setupOscillatorUnison() void sfz::Voice::updateChannelPowers(AudioSpan buffer) { - assert(channelPowers.size() == channelPowerFilters.size()); - assert(buffer.getNumFrames() <= channelPowerFilters.size()); + assert(smoothedChannelEnvelopes.size() == channelEnvelopeFilters.size()); + assert(buffer.getNumFrames() <= channelEnvelopeFilters.size()); if (buffer.getNumFrames() == 0) return; - for (unsigned i = 0; i < channelPowers.size(); ++i) { + for (unsigned i = 0; i < smoothedChannelEnvelopes.size(); ++i) { const auto input = buffer.getConstSpan(i); for (unsigned s = 0; s < buffer.getNumFrames(); ++s) - channelPowers[i] = channelPowerFilters[i].tickLowpass(std::abs(input[s])); + smoothedChannelEnvelopes[i] = + channelEnvelopeFilters[i].tickLowpass(std::abs(input[s])); } } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 0a617a827..a6fbc1872 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -309,8 +309,8 @@ class Voice { std::normal_distribution noiseDist { 0, config::noiseVariance }; - std::array, 2> channelPowerFilters; - std::array channelPowers; + std::array, 2> channelEnvelopeFilters; + std::array smoothedChannelEnvelopes; LEAK_DETECTOR(Voice); }; From 2fcd2310624982abb21a230e00a288023655fec2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 18:45:59 +0200 Subject: [PATCH 16/24] Consider the max of the sister voices --- src/sfizz/Synth.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 71ef5822b..acfc371d1 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -528,13 +528,13 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept break; } - float sumEnvelope = ref->getAverageEnvelope(); + float maxEnvelope = ref->getAverageEnvelope(); while (idx < voiceViewArray.size() && sisterVoices(ref, voiceViewArray[idx])) { - sumEnvelope += voiceViewArray[idx]->getAverageEnvelope(); + maxEnvelope = max(maxEnvelope, voiceViewArray[idx]->getAverageEnvelope()); idx++; } - if (sumEnvelope < envThreshold) { + if (maxEnvelope < envThreshold) { returnedVoice = ref; // std::cout << "Killing " << idx - refIdx << " voices" << '\n'; for (unsigned j = refIdx; j < idx; j++) From f0b527525fb8280f8964e3e6fee28646beb21ad2 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 20:48:08 +0200 Subject: [PATCH 17/24] Add implicit conversions to AudioBuffer --- src/sfizz/AudioBuffer.h | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/sfizz/AudioBuffer.h b/src/sfizz/AudioBuffer.h index 931402dee..51500d0a9 100644 --- a/src/sfizz/AudioBuffer.h +++ b/src/sfizz/AudioBuffer.h @@ -9,6 +9,7 @@ #include "Config.h" #include "Debug.h" #include "LeakDetector.h" +#include "SIMDHelpers.h" #include "absl/types/span.h" #include "absl/memory/memory.h" #include @@ -259,6 +260,15 @@ class AudioBuffer { numChannels = 0; } + /** + * Writes zeros in the buffer + */ + void clear() + { + for (size_t i = 0; i < numChannels; ++i) + fill(getSpan(i), Type{ 0.0 }); + } + /** * @brief Add a positive number of channels to the buffer * @@ -271,6 +281,22 @@ class AudioBuffer { addChannel(); } + /** + * @brief Convert implicitly to a pointer of channels + */ + operator const float* const*() const noexcept + { + return buffers.data(); + } + + /** + * @brief Convert implicitly to a pointer of channels + */ + operator float* const*() noexcept + { + return buffers.data(); + } + private: using buffer_type = Buffer; using buffer_ptr = std::unique_ptr; From 91fc783b49312c2cef9b64706e37673c394fec00 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 20:48:18 +0200 Subject: [PATCH 18/24] Increase the buffer pool size --- src/sfizz/Config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index fadba005f..a26d36df1 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -28,7 +28,7 @@ namespace config { constexpr float defaultSampleRate { 48000 }; constexpr int defaultSamplesPerBlock { 1024 }; constexpr int maxBlockSize { 8192 }; - constexpr int bufferPoolSize { 4 }; + constexpr int bufferPoolSize { 6 }; constexpr int stereoBufferPoolSize { 4 }; constexpr int indexBufferPoolSize { 2 }; constexpr int preloadSize { 8192 }; From 691c25362b6362c741c72fc0030636547dce31fc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 20:48:48 +0200 Subject: [PATCH 19/24] Add a helper to apply a gain span to effect inputs Used for ramping out voice data --- src/sfizz/Effects.cpp | 11 +++++++++++ src/sfizz/Effects.h | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/sfizz/Effects.cpp b/src/sfizz/Effects.cpp index e6dd469d8..de6e96920 100644 --- a/src/sfizz/Effects.cpp +++ b/src/sfizz/Effects.cpp @@ -109,6 +109,17 @@ void EffectBus::addToInputs(const float* const addInput[], float addGain, unsign } } +void EffectBus::applyGain(const float* gain, unsigned nframes) +{ + if (!gain) + return; + + absl::Span gainSpan { gain, nframes }; + for (unsigned c = 0; c < EffectChannels; ++c) { + sfz::applyGain(gainSpan, _inputs.getSpan(c)); + } +} + void EffectBus::setSampleRate(double sampleRate) { for (const auto& effectPtr : _effects) diff --git a/src/sfizz/Effects.h b/src/sfizz/Effects.h index 1c1edfebf..2056cac2a 100644 --- a/src/sfizz/Effects.h +++ b/src/sfizz/Effects.h @@ -136,6 +136,11 @@ class EffectBus { */ void addToInputs(const float* const addInput[], float addGain, unsigned nframes); + /** + @brief Apply a gain to the inputs + */ + void applyGain(const float* gain, unsigned nframes); + /** @brief Initializes all effects in the bus with the given sample rate. */ From dbb75398cc389809e8b027b603b7de2feb984dd5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 20:49:42 +0200 Subject: [PATCH 20/24] Killed voices pre-render their next block This data is ramped out before the main rendering loop, to smooth the voice killings --- src/sfizz/Synth.cpp | 52 +++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index acfc371d1..533837787 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -137,6 +137,7 @@ void sfz::Synth::clear() effectBuses[0]->setGainToMain(1.0); effectBuses[0]->setSamplesPerBlock(samplesPerBlock); effectBuses[0]->setSampleRate(sampleRate); + effectBuses[0]->clearInputs(samplesPerBlock); resources.clear(); numGroups = 0; numMasters = 0; @@ -248,6 +249,7 @@ void sfz::Synth::handleEffectOpcodes(const std::vector& rawMembers) bus.reset(new EffectBus); bus->setSampleRate(sampleRate); bus->setSamplesPerBlock(samplesPerBlock); + bus->clearInputs(samplesPerBlock); } return *bus; }; @@ -508,6 +510,19 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept const auto envThreshold = sumEnvelope / static_cast(voiceViewArray.size()) * 0.5f; const auto ageThreshold = voiceViewArray.front()->getAge() * 0.5f; + auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); + const auto killVoice = [&] (Voice* v) { + const auto region = v->getRegion(); // voice can die after rendering, so save this + v->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, samplesPerBlock); + } + } + v->reset(); + }; + Voice* returnedVoice = voiceViewArray.front(); unsigned idx = 0; while (idx < voiceViewArray.size()) { @@ -519,12 +534,12 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept unsigned killIdx = 1; while (killIdx < voiceViewArray.size() && sisterVoices(returnedVoice, voiceViewArray[killIdx])) { - voiceViewArray[killIdx]->reset(); + killVoice(voiceViewArray[killIdx]); killIdx++; } // std::cout << "Went too far, picking the oldest voice and killing " // << killIdx << " voices" << '\n'; - returnedVoice->reset(); + killVoice(returnedVoice); break; } @@ -537,12 +552,12 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (maxEnvelope < envThreshold) { returnedVoice = ref; // std::cout << "Killing " << idx - refIdx << " voices" << '\n'; - for (unsigned j = refIdx; j < idx; j++) - voiceViewArray[j]->reset(); + for (unsigned j = refIdx; j < idx; j++) { + killVoice(voiceViewArray[j]); + } break; } } - assert(returnedVoice->isFree()); return returnedVoice; } @@ -615,19 +630,12 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept size_t numFrames = buffer.getNumFrames(); auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames); auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames); - if (!tempSpan || !tempMixSpan) { + auto rampSpan = resources.bufferPool.getBuffer(numFrames); + if (!tempSpan || !tempMixSpan || !rampSpan) { DBG("[sfizz] Could not get a temporary buffer; exiting callback... "); return; } - { // Prepare the effect inputs. They are mixes of per-region outputs. - ScopedTiming logger { callbackBreakdown.effects }; - for (auto& bus : effectBuses) { - if (bus) - bus->clearInputs(numFrames); - } - } - int numActiveVoices { 0 }; { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod, ScopedTiming::Operation::addToDuration }; @@ -635,6 +643,14 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept tempMixSpan->fill(0.0f); resources.filePool.cleanupPromises(); + // Ramp out whatever is in the buffer at this point; should only be killed voice data + linearRamp(*rampSpan, 1.0f, -1.0f / static_cast(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; @@ -694,6 +710,14 @@ void sfz::Synth::renderBlock(AudioSpan 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))); ASSERT(isValidAudio(buffer.getConstSpan(0))); From ec13c70b8286befc4def3675af4d4cbdf59f7707 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 7 May 2020 23:16:07 +0200 Subject: [PATCH 21/24] C++11 does not like constexpr --- src/sfizz/Voice.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index a6fbc1872..4f2b880c9 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -156,19 +156,19 @@ class Voice { * * @return int */ - constexpr int getTriggerNumber() const noexcept { return triggerNumber; } + int getTriggerNumber() const noexcept { return triggerNumber; } /** * @brief Get the value that triggered the voice (note velocity or cc value) * * @return float */ - constexpr float getTriggerValue() const noexcept { return triggerValue; } + float getTriggerValue() const noexcept { return triggerValue; } /** * @brief Get the type of trigger * * @return TriggerType */ - constexpr TriggerType getTriggerType() const noexcept { return triggerType; } + TriggerType getTriggerType() const noexcept { return triggerType; } /** * @brief Reset the voice to its initial values @@ -220,7 +220,7 @@ class Voice { * * @return */ - constexpr int getAge() const noexcept { return age; } + int getAge() const noexcept { return age; } Duration getLastDataDuration() const noexcept { return dataDuration; } Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; } @@ -314,7 +314,7 @@ class Voice { LEAK_DETECTOR(Voice); }; -constexpr bool sisterVoices(const Voice* lhs, const Voice* rhs) +inline bool sisterVoices(const Voice* lhs, const Voice* rhs) { return lhs->getAge() == rhs->getAge() && lhs->getTriggerNumber() == rhs->getTriggerNumber() From 6df5b217c9285f7b8fd6ca7e40bae77ea3834f54 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 8 May 2020 00:16:28 +0200 Subject: [PATCH 22/24] Move the voice rendering into a private method --- src/sfizz/Synth.cpp | 37 +++++++++++++++---------------------- src/sfizz/Synth.h | 8 ++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 533837787..3f9218e50 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -512,14 +512,7 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); const auto killVoice = [&] (Voice* v) { - const auto region = v->getRegion(); // voice can die after rendering, so save this - v->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, samplesPerBlock); - } - } + renderVoiceToOutputs(*v, *tempSpan); v->reset(); }; @@ -610,6 +603,19 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept } } +void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept +{ + const Region* region = voice.getRegion(); + 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 buffer) noexcept { ScopedFTZ ftz; @@ -655,21 +661,8 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (voice->isFree()) continue; - const Region* region = voice->getRegion(); - numActiveVoices++; - voice->renderBlock(*tempSpan); - - { // Add the output into the effects linked to this region - ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration }; - 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); - } - } - } - + renderVoiceToOutputs(*voice, *tempSpan); callbackBreakdown.data += voice->getLastDataDuration(); callbackBreakdown.amplitude += voice->getLastAmplitudeDuration(); callbackBreakdown.filters += voice->getLastFilterDuration(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 9d2583ec2..f99b5b833 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -490,6 +490,14 @@ class Synth final : public Parser::Listener { */ void resetVoices(int numVoices); + /** + * @brief Render the voice to its designated outputs and effect busses. + * + * @param voice + * @param tempSpan a temporary span used for rendering + */ + void renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) noexcept; + fs::file_time_type checkModificationTime(); void noteOnDispatch(int delay, int noteNumber, float velocity) noexcept; From 59c25dccf12ee0b46c9cd096811892a79f332d3e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 8 May 2020 00:16:50 +0200 Subject: [PATCH 23/24] Move the voice ordering as a free function --- src/sfizz/Synth.cpp | 22 +--------------------- src/sfizz/Voice.h | 35 +++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 3f9218e50..7380053c5 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -482,27 +482,7 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept return freeVoice->get(); // Find voices that can be stolen - absl::c_sort(voiceViewArray, [](Voice* lhs, Voice* rhs) { - if (lhs->getAge() > rhs->getAge()) - return true; - if (lhs->getAge() < rhs->getAge()) - return false; - - if (lhs->getTriggerNumber() > rhs->getTriggerNumber()) - return true; - if (lhs->getTriggerNumber() < rhs->getTriggerNumber()) - return false; - - if (lhs->getTriggerValue() > rhs->getTriggerValue()) - return true; - if (lhs->getTriggerValue() < rhs->getTriggerValue()) - return false; - - if (lhs->getTriggerType() > rhs->getTriggerType()) - return true; - - return false; - }); + absl::c_sort(voiceViewArray, voiceOrdering); const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [](float sum, const Voice* v) { return sum + v->getAverageEnvelope(); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 4f2b880c9..46b3a3f0b 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -162,13 +162,13 @@ class Voice { * * @return float */ - float getTriggerValue() const noexcept { return triggerValue; } + float getTriggerValue() const noexcept { return triggerValue; } /** * @brief Get the type of trigger * * @return TriggerType */ - TriggerType getTriggerType() const noexcept { return triggerType; } + TriggerType getTriggerType() const noexcept { return triggerType; } /** * @brief Reset the voice to its initial values @@ -271,7 +271,7 @@ class Voice { float speedRatio { 1.0 }; float pitchRatio { 1.0 }; - float baseVolumedB{ 0.0 }; + float baseVolumedB { 0.0 }; float baseGain { 1.0 }; float baseFrequency { 440.0 }; @@ -298,9 +298,9 @@ class Voice { // unison of oscillators unsigned waveUnisonSize { 0 }; - float waveDetuneRatio[config::oscillatorsPerVoice] { }; - float waveLeftGain[config::oscillatorsPerVoice] { }; - float waveRightGain[config::oscillatorsPerVoice] { }; + float waveDetuneRatio[config::oscillatorsPerVoice] {}; + float waveLeftGain[config::oscillatorsPerVoice] {}; + float waveRightGain[config::oscillatorsPerVoice] {}; Duration dataDuration; Duration amplitudeDuration; @@ -322,4 +322,27 @@ inline bool sisterVoices(const Voice* lhs, const Voice* rhs) && lhs->getTriggerType() == rhs->getTriggerType(); } +inline bool voiceOrdering(const Voice* lhs, const Voice* rhs) +{ + if (lhs->getAge() > rhs->getAge()) + return true; + if (lhs->getAge() < rhs->getAge()) + return false; + + if (lhs->getTriggerNumber() > rhs->getTriggerNumber()) + return true; + if (lhs->getTriggerNumber() < rhs->getTriggerNumber()) + return false; + + if (lhs->getTriggerValue() > rhs->getTriggerValue()) + return true; + if (lhs->getTriggerValue() < rhs->getTriggerValue()) + return false; + + if (lhs->getTriggerType() > rhs->getTriggerType()) + return true; + + return false; +} + } // namespace sfz From e4f0b0c7b066915b36ade93dd35883d0a02e5f97 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 8 May 2020 00:20:28 +0200 Subject: [PATCH 24/24] Move things around --- src/sfizz/Config.h | 10 ++++++++++ src/sfizz/Synth.cpp | 12 +++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index a26d36df1..b8f9d4ffe 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -59,6 +59,16 @@ namespace config { constexpr int maxCurves { 256 }; constexpr int chunkSize { 1024 }; constexpr int filtersInPool { maxVoices * 2 }; + /** + * @brief The threshold for age stealing. + * In percentage of the voice's max age. + */ + constexpr float stealingAgeCoeff { 0.5f }; + /** + * @brief The threshold for envelope stealing. + * In percentage of the sum of all envelopes. + */ + constexpr float stealingEnvelopeCoeff { 0.5f }; constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; constexpr int oscillatorsPerVoice { 9 }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 7380053c5..bb939560f 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -481,14 +481,15 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (freeVoice != voices.end()) return freeVoice->get(); - // Find voices that can be stolen + // Start of the voice stealing algorithm absl::c_sort(voiceViewArray, voiceOrdering); const auto sumEnvelope = absl::c_accumulate(voiceViewArray, 0.0f, [](float sum, const Voice* v) { return sum + v->getAverageEnvelope(); }); - const auto envThreshold = sumEnvelope / static_cast(voiceViewArray.size()) * 0.5f; - const auto ageThreshold = voiceViewArray.front()->getAge() * 0.5f; + const auto envThreshold = sumEnvelope + / static_cast(voiceViewArray.size()) * config::stealingEnvelopeCoeff; + const auto ageThreshold = voiceViewArray.front()->getAge() * config::stealingAgeCoeff; auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); const auto killVoice = [&] (Voice* v) { @@ -506,7 +507,7 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept if (ref->getAge() < ageThreshold) { unsigned killIdx = 1; while (killIdx < voiceViewArray.size() - && sisterVoices(returnedVoice, voiceViewArray[killIdx])) { + && sisterVoices(returnedVoice, voiceViewArray[killIdx])) { killVoice(voiceViewArray[killIdx]); killIdx++; } @@ -517,7 +518,8 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept } float maxEnvelope = ref->getAverageEnvelope(); - while (idx < voiceViewArray.size() && sisterVoices(ref, voiceViewArray[idx])) { + while (idx < voiceViewArray.size() + && sisterVoices(ref, voiceViewArray[idx])) { maxEnvelope = max(maxEnvelope, voiceViewArray[idx]->getAverageEnvelope()); idx++; }