From f91ee9cdfae31cd5e6841d32b566abd60618d88f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 23 Jun 2020 03:56:56 +0200 Subject: [PATCH 01/27] Replace the clearing thread busy wait with semaphore --- src/sfizz/FilePool.cpp | 19 ++++++++++++++----- src/sfizz/FilePool.h | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 95519ac6..b18d5254 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -125,11 +125,16 @@ sfz::FilePool::~FilePool() { quitThread = true; + std::error_code ec; + for (unsigned i = 0; i < threadPool.size(); ++i) { - std::error_code ec; + ec = std::error_code(); workerBarrier.post(ec); } + ec = std::error_code(); + semClearingRequest.post(ec); + for (auto& thread: threadPool) thread.join(); } @@ -360,10 +365,13 @@ void sfz::FilePool::tryToClearPromises() void sfz::FilePool::clearingThread() { - while (!quitThread) { + RTSemaphore& request = semClearingRequest; + do { + request.wait(); + if (quitThread) + return; tryToClearPromises(); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } + } while (1); } void sfz::FilePool::loadingThread() noexcept @@ -443,7 +451,8 @@ void sfz::FilePool::cleanupPromises() noexcept auto promiseUsedOnce = [](FilePromisePtr& p) { return p.use_count() == 1; }; auto moveToClear = [&](FilePromisePtr& p) { return promisesToClear.push_back(p); }; - swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear); + if (swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear) > 0) + semClearingRequest.post(); } void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 25eaf068..1325168c 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -275,6 +275,7 @@ private: volatile bool emptyQueue { false }; std::atomic threadsLoading { 0 }; RTSemaphore workerBarrier; + RTSemaphore semClearingRequest; // File promises data structures along with their guards. std::vector emptyPromises; From f25dd03db0caff9645d3bdf4d23083ccbce060b2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 23 Jun 2020 03:59:20 +0200 Subject: [PATCH 02/27] Replace the queue emptying busy wait with semaphore --- src/sfizz/FilePool.cpp | 16 +++++----------- src/sfizz/FilePool.h | 1 + 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index b18d5254..cc0ed7b8 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -378,19 +378,17 @@ void sfz::FilePool::loadingThread() noexcept { FilePromisePtr promise; while (!quitThread) { + workerBarrier.wait(); if (emptyQueue) { - while(promiseQueue.try_pop(promise)) { + while (promiseQueue.try_pop(promise)) { // We're just dequeuing } emptyQueue = false; + semEmptyQueueFinished.post(); continue; } - std::error_code ec; - workerBarrier.wait(ec); - ASSERT(!ec); - if (!promiseQueue.try_pop(promise)) { continue; } @@ -483,12 +481,8 @@ uint32_t sfz::FilePool::getPreloadSize() const noexcept void sfz::FilePool::emptyFileLoadingQueues() noexcept { emptyQueue = true; - std::error_code ec; - workerBarrier.post(ec); - ASSERT(!ec); - - while (emptyQueue) - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + workerBarrier.post(); + semEmptyQueueFinished.wait(); } void sfz::FilePool::waitForBackgroundLoading() noexcept diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 1325168c..80e7f8a2 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -273,6 +273,7 @@ private: // Signals volatile bool quitThread { false }; volatile bool emptyQueue { false }; + RTSemaphore semEmptyQueueFinished; std::atomic threadsLoading { 0 }; RTSemaphore workerBarrier; RTSemaphore semClearingRequest; From 26d63a92f0fdcf2c9af6a1e5f6634acbbedbb8c4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 23 Jun 2020 04:01:35 +0200 Subject: [PATCH 03/27] Rewrite the loading loop to check quit flag after waiting --- src/sfizz/FilePool.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index cc0ed7b8..65a3b357 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -377,7 +377,7 @@ void sfz::FilePool::clearingThread() void sfz::FilePool::loadingThread() noexcept { FilePromisePtr promise; - while (!quitThread) { + do { workerBarrier.wait(); if (emptyQueue) { @@ -389,6 +389,9 @@ void sfz::FilePool::loadingThread() noexcept continue; } + if (quitThread) + return; + if (!promiseQueue.try_pop(promise)) { continue; } @@ -418,7 +421,7 @@ void sfz::FilePool::loadingThread() noexcept } promise.reset(); - } + } while (1); } void sfz::FilePool::clear() From b0c8495f6c6b9ea5aeb34d4200e8fd3af82609b2 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 23 Jun 2020 04:03:35 +0200 Subject: [PATCH 04/27] Replace the filled promise queue busy wait with semaphore --- src/sfizz/FilePool.cpp | 10 +++++----- src/sfizz/FilePool.h | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 65a3b357..2abf3b45 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -415,10 +415,8 @@ void sfz::FilePool::loadingThread() noexcept threadsLoading--; - while (!filledPromiseQueue.try_push(promise)) { - DBG("[sfizz] Error enqueuing the promise for " << promise->fileId << " in the filledPromiseQueue"); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + semFilledPromiseQueueAvailable.wait(); + filledPromiseQueue.push(promise); promise.reset(); } while (1); @@ -447,8 +445,10 @@ void sfz::FilePool::cleanupPromises() noexcept // Remove the promises from the filled queue and put them in a linear // storage FilePromisePtr promise; - while (filledPromiseQueue.try_pop(promise)) + while (filledPromiseQueue.try_pop(promise)) { + semFilledPromiseQueueAvailable.post(); temporaryFilePromises.push_back(promise); + } auto promiseUsedOnce = [](FilePromisePtr& p) { return p.use_count() == 1; }; auto moveToClear = [&](FilePromisePtr& p) { return promisesToClear.push_back(p); }; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 80e7f8a2..ee81c268 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -268,6 +268,7 @@ private: atomic_queue::AtomicQueue2 promiseQueue; atomic_queue::AtomicQueue2 filledPromiseQueue; + RTSemaphore semFilledPromiseQueueAvailable { config::maxVoices }; uint32_t preloadSize { config::preloadSize }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; // Signals From 0f10991dd9171ec50e14cac019b21630e118df43 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 23 Jun 2020 05:00:37 +0200 Subject: [PATCH 05/27] Increase the background thread priority --- src/sfizz/Config.h | 4 ++++ src/sfizz/FilePool.cpp | 45 +++++++++++++++++++++++++++++++++++++++++- src/sfizz/FilePool.h | 5 +++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 918264ca..9268cb89 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -99,6 +99,10 @@ namespace config { static constexpr double amplitudeTriangle = 0.625; static constexpr double amplitudeSaw = 0.515; static constexpr double amplitudeSquare = 0.515; + /** + Background file loading + */ + static constexpr int backgroundLoaderPthreadPriority = 50; // expressed in % } // namespace config } // namespace sfz diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 2abf3b45..95647340 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -37,8 +37,14 @@ #include "absl/memory/memory.h" #include #include -#include #include +#include +#include +#if defined(_WIN32) +#include +#else +#include +#endif void readBaseFile(SndfileHandle& sndFile, sfz::FileAudioBuffer& output, uint32_t numFrames, bool reverse) { @@ -365,6 +371,8 @@ void sfz::FilePool::tryToClearPromises() void sfz::FilePool::clearingThread() { + raiseCurrentThreadPriority(); + RTSemaphore& request = semClearingRequest; do { request.wait(); @@ -376,6 +384,8 @@ void sfz::FilePool::clearingThread() void sfz::FilePool::loadingThread() noexcept { + raiseCurrentThreadPriority(); + FilePromisePtr promise; do { workerBarrier.wait(); @@ -502,3 +512,36 @@ void sfz::FilePool::waitForBackgroundLoading() noexcept std::this_thread::sleep_for(std::chrono::microseconds(100)); } } + +void sfz::FilePool::raiseCurrentThreadPriority() noexcept +{ +#if defined(_WIN32) + #pragma message("Implement Win32 thread background priority") + HANDLE thread = GetCurrentThread(); + const int priority = THREAD_PRIORITY_ABOVE_NORMAL; /*THREAD_PRIORITY_HIGHEST*/ + if (!SetThreadPriority(thread, priority)) { + std::system_error error(GetLastError(), std::system_category()); + DBG("[sfizz] Cannot set current thread priority: " << error.what()); + } +#else + pthread_t thread = pthread_self(); + int policy; + sched_param param; + + if (pthread_getschedparam(thread, &policy, ¶m) != 0) { + DBG("[sfizz] Cannot get current thread scheduling parameters"); + return; + } + + policy = SCHED_RR; + const int minprio = sched_get_priority_min(policy); + const int maxprio = sched_get_priority_max(policy); + param.sched_priority = minprio + + config::backgroundLoaderPthreadPriority * (maxprio - minprio) / 100; + + if (pthread_setschedparam(thread, policy, ¶m) != 0) { + DBG("[sfizz] Cannot set current thread scheduling parameters"); + return; + } +#endif +} diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index ee81c268..2547591f 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -259,6 +259,11 @@ public: * in the queue. */ void waitForBackgroundLoading() noexcept; + /** + * @brief Assign the current thread a priority which is appropriate + * for background sample file processing. + */ + static void raiseCurrentThreadPriority() noexcept; private: Logger& logger; fs::path rootDirectory; From 957d1b18649fb8b46db87eed1a41bf49c821ac7e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 23 Jun 2020 05:37:27 +0200 Subject: [PATCH 06/27] Remove irrelevant warning pragma --- src/sfizz/FilePool.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 95647340..9bec98ef 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -516,7 +516,6 @@ void sfz::FilePool::waitForBackgroundLoading() noexcept void sfz::FilePool::raiseCurrentThreadPriority() noexcept { #if defined(_WIN32) - #pragma message("Implement Win32 thread background priority") HANDLE thread = GetCurrentThread(); const int priority = THREAD_PRIORITY_ABOVE_NORMAL; /*THREAD_PRIORITY_HIGHEST*/ if (!SetThreadPriority(thread, priority)) { From d8d956562e7553790198c94760421ae8e07d0e24 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 1 Jul 2020 14:14:09 +0200 Subject: [PATCH 07/27] Unclamp the initial volume; we need to remove these clampings... --- src/sfizz/Defaults.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index f5edae7f..0846c370 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -111,7 +111,7 @@ namespace Default // Performance parameters: amplifier constexpr float globalVolume { -7.35f }; constexpr float volume { 0.0f }; - constexpr Range volumeRange { -144.0, 6.0 }; + constexpr Range volumeRange { -144.0, 48.0 }; constexpr Range volumeCCRange { -144.0, 48.0 }; constexpr float amplitude { 100.0 }; constexpr Range amplitudeRange { 0.0, 100.0 }; From ae0ac8d63243354299c3be3438c45115e21b9139 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 1 Jul 2020 14:14:45 +0200 Subject: [PATCH 08/27] Properly set the initial value for the smoothers Take into account the curve --- src/sfizz/Voice.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 66a46d99..df3a46b6 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -136,21 +136,24 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, for (auto& modId : allModifiers) { ASSERT(modifierSmoothers[modId].size() >= region->modifiers[modId].size()); forEachWithSmoother(modId, [modId, this](const CCData& mod, Smoother& smoother) { + const auto ccValue = resources.midiState.getCCValue(mod.cc); + const auto curve = resources.curves.getCurve(mod.data.curve); + const auto finalValue = curve.evalNormalized(ccValue) * mod.data.value; switch (modId) { case Mod::volume: - smoother.reset(db2mag(resources.midiState.getCCValue(mod.cc) * mod.data.value)); + smoother.reset(db2mag(finalValue)); break; case Mod::pitch: - smoother.reset(centsFactor(resources.midiState.getCCValue(mod.cc) * mod.data.value)); + smoother.reset(centsFactor(finalValue)); break; case Mod::amplitude: case Mod::pan: case Mod::width: case Mod::position: - smoother.reset(normalizePercents(resources.midiState.getCCValue(mod.cc) * mod.data.value)); + smoother.reset(normalizePercents(finalValue)); break; default: - smoother.reset(resources.midiState.getCCValue(mod.cc) * mod.data.value); + smoother.reset(finalValue); break; } smoother.setSmoothing(mod.data.smooth, sampleRate); From 5463f143e07440f3042f6ade265512373153952e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 1 Jul 2020 14:41:27 +0200 Subject: [PATCH 09/27] correct a bounds test --- tests/RegionT.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index e4a9c09d..b8470fec 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -522,8 +522,8 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.volume == -123.0f); region.parseOpcode({ "volume", "-185" }); REQUIRE(region.volume == -144.0f); - region.parseOpcode({ "volume", "19" }); - REQUIRE(region.volume == 6.0f); + region.parseOpcode({ "volume", "79" }); + REQUIRE(region.volume == 48.0f); } SECTION("pan") From 0e2ebe6d467ed6951c3721b4177c5edf9962a8b0 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Wed, 1 Jul 2020 17:37:44 +0200 Subject: [PATCH 10/27] ampeg_XX_oncc can now contain multiple modifiers --- src/sfizz/EGDescription.h | 56 +++++++++++++----- src/sfizz/Region.cpp | 49 +++++++++++++--- tests/EGDescriptionT.cpp | 79 ++++++++++++++++++++----- tests/RegionT.cpp | 120 +++++++++++++++++++++++++------------- 4 files changed, 227 insertions(+), 77 deletions(-) diff --git a/src/sfizz/EGDescription.h b/src/sfizz/EGDescription.h index e64fa642..683b4d51 100644 --- a/src/sfizz/EGDescription.h +++ b/src/sfizz/EGDescription.h @@ -80,13 +80,13 @@ struct EGDescription { float vel2sustain { Default::vel2sustain }; int vel2depth { Default::depth }; - absl::optional> ccAttack; - absl::optional> ccDecay; - absl::optional> ccDelay; - absl::optional> ccHold; - absl::optional> ccRelease; - absl::optional> ccStart; - absl::optional> ccSustain; + CCMap ccAttack; + CCMap ccDecay; + CCMap ccDelay; + CCMap ccHold; + CCMap ccRelease; + CCMap ccStart; + CCMap ccSustain; /** * @brief Get the attack with possibly a CC modifier and a velocity modifier @@ -98,7 +98,11 @@ struct EGDescription { float getAttack(const MidiState& state, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - return Default::egTimeRange.clamp(ccSwitchedValue(state, ccAttack, attack) + velocity * vel2attack); + float returnedValue { attack + velocity * vel2attack }; + for (auto& mod: ccAttack) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egTimeRange.clamp(returnedValue); } /** * @brief Get the decay with possibly a CC modifier and a velocity modifier @@ -110,7 +114,11 @@ struct EGDescription { float getDecay(const MidiState& state, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - return Default::egTimeRange.clamp(ccSwitchedValue(state, ccDecay, decay) + velocity * vel2decay); + float returnedValue { decay + velocity * vel2decay }; + for (auto& mod: ccDecay) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egTimeRange.clamp(returnedValue); } /** * @brief Get the delay with possibly a CC modifier and a velocity modifier @@ -122,7 +130,11 @@ struct EGDescription { float getDelay(const MidiState& state, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - return Default::egTimeRange.clamp(ccSwitchedValue(state, ccDelay, delay) + velocity * vel2delay); + float returnedValue { delay + velocity * vel2delay }; + for (auto& mod: ccDelay) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egTimeRange.clamp(returnedValue); } /** * @brief Get the holding duration with possibly a CC modifier and a velocity modifier @@ -134,7 +146,11 @@ struct EGDescription { float getHold(const MidiState& state, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - return Default::egTimeRange.clamp(ccSwitchedValue(state, ccHold, hold) + velocity * vel2hold); + float returnedValue { hold + velocity * vel2hold }; + for (auto& mod: ccHold) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egTimeRange.clamp(returnedValue); } /** * @brief Get the release duration with possibly a CC modifier and a velocity modifier @@ -146,7 +162,11 @@ struct EGDescription { float getRelease(const MidiState& state, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - return Default::egTimeRange.clamp(ccSwitchedValue(state, ccRelease, release) + velocity * vel2release); + float returnedValue { release + velocity * vel2release }; + for (auto& mod: ccRelease) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egTimeRange.clamp(returnedValue); } /** * @brief Get the starting level with possibly a CC modifier and a velocity modifier @@ -158,7 +178,11 @@ struct EGDescription { float getStart(const MidiState& state, float velocity) const noexcept { UNUSED(velocity); - return Default::egPercentRange.clamp(ccSwitchedValue(state, ccStart, start)); + float returnedValue { start }; + for (auto& mod: ccStart) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egPercentRange.clamp(returnedValue); } /** * @brief Get the sustain level with possibly a CC modifier and a velocity modifier @@ -170,7 +194,11 @@ struct EGDescription { float getSustain(const MidiState& state, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); - return Default::egPercentRange.clamp(ccSwitchedValue(state, ccSustain, sustain) + velocity * vel2sustain); + float returnedValue { sustain + velocity * vel2sustain }; + for (auto& mod: ccSustain) { + returnedValue += state.getCCValue(mod.cc) * mod.data; + } + return Default::egPercentRange.clamp(returnedValue); } LEAK_DETECTOR(EGDescription); }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 287fc7ad..5d6bbe55 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -816,25 +816,60 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) setValueFromOpcode(opcode, amplitudeEG.vel2sustain, Default::egOnCCPercentRange); break; case hash("ampeg_attack_oncc&"): // also ampeg_attackcc& - setCCPairFromOpcode(opcode, amplitudeEG.ccAttack, Default::egOnCCTimeRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + amplitudeEG.ccAttack[opcode.parameters.back()] = *value; + break; case hash("ampeg_decay_oncc&"): // also ampeg_decaycc& - setCCPairFromOpcode(opcode, amplitudeEG.ccDecay, Default::egOnCCTimeRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + amplitudeEG.ccDecay[opcode.parameters.back()] = *value; + break; case hash("ampeg_delay_oncc&"): // also ampeg_delaycc& - setCCPairFromOpcode(opcode, amplitudeEG.ccDelay, Default::egOnCCTimeRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + amplitudeEG.ccDelay[opcode.parameters.back()] = *value; + break; case hash("ampeg_hold_oncc&"): // also ampeg_holdcc& - setCCPairFromOpcode(opcode, amplitudeEG.ccHold, Default::egOnCCTimeRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + amplitudeEG.ccHold[opcode.parameters.back()] = *value; + break; case hash("ampeg_release_oncc&"): // also ampeg_releasecc& - setCCPairFromOpcode(opcode, amplitudeEG.ccRelease, Default::egOnCCTimeRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCTimeRange)) + amplitudeEG.ccRelease[opcode.parameters.back()] = *value; + break; case hash("ampeg_start_oncc&"): // also ampeg_startcc& - setCCPairFromOpcode(opcode, amplitudeEG.ccStart, Default::egOnCCPercentRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) + amplitudeEG.ccStart[opcode.parameters.back()] = *value; + break; case hash("ampeg_sustain_oncc&"): // also ampeg_sustaincc& - setCCPairFromOpcode(opcode, amplitudeEG.ccSustain, Default::egOnCCPercentRange); + if (opcode.parameters.back() >= config::numCCs) + return false; + + if (auto value = readOpcode(opcode.value, Default::egOnCCPercentRange)) + amplitudeEG.ccSustain[opcode.parameters.back()] = *value; + break; case hash("effect&"): diff --git a/tests/EGDescriptionT.cpp b/tests/EGDescriptionT.cpp index 25730dc9..950e7d3b 100644 --- a/tests/EGDescriptionT.cpp +++ b/tests/EGDescriptionT.cpp @@ -17,14 +17,21 @@ TEST_CASE("[EGDescription] Attack range") sfz::MidiState state; eg.attack = 1; eg.vel2attack = -1.27f; - eg.ccAttack = { 63, 1.27f }; + eg.ccAttack[63] = 1.27f; REQUIRE(eg.getAttack(state, 0_norm) == 1.0f); REQUIRE(eg.getAttack(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getAttack(state, 127_norm) == 1.0f); REQUIRE(eg.getAttack(state, 0_norm) == 2.27f); - eg.ccAttack = { 63, 127.0f }; + eg.ccAttack[63] = 127.0f; REQUIRE(eg.getAttack(state, 0_norm) == 100.0f); + eg.ccAttack[63] = 1.27f; + eg.ccAttack[65] = 1.0f; + REQUIRE(eg.getAttack(state, 0_norm) == 2.27f); + REQUIRE(eg.getAttack(state, 127_norm) == 1.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getAttack(state, 0_norm) == 3.27f); + REQUIRE(eg.getAttack(state, 127_norm) == 2.0f); } TEST_CASE("[EGDescription] Delay range") @@ -33,14 +40,21 @@ TEST_CASE("[EGDescription] Delay range") sfz::MidiState state; eg.delay = 1; eg.vel2delay = -1.27f; - eg.ccDelay = { 63, 1.27f }; + eg.ccDelay[63] = 1.27f; REQUIRE(eg.getDelay(state, 0_norm) == 1.0f); REQUIRE(eg.getDelay(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getDelay(state, 127_norm) == 1.0f); REQUIRE(eg.getDelay(state, 0_norm) == 2.27f); - eg.ccDelay = { 63, 127.0f }; + eg.ccDelay[63] = 127.0f; REQUIRE(eg.getDelay(state, 0_norm) == 100.0f); + eg.ccDelay[63] = 1.27f; + eg.ccDelay[65] = 1.0f; + REQUIRE(eg.getDelay(state, 0_norm) == 2.27f); + REQUIRE(eg.getDelay(state, 127_norm) == 1.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getDelay(state, 0_norm) == 3.27f); + REQUIRE(eg.getDelay(state, 127_norm) == 2.0f); } TEST_CASE("[EGDescription] Decay range") @@ -49,14 +63,21 @@ TEST_CASE("[EGDescription] Decay range") sfz::MidiState state; eg.decay = 1.0f; eg.vel2decay = -1.27f; - eg.ccDecay = { 63, 1.27f }; + eg.ccDecay[63] = 1.27f; REQUIRE(eg.getDecay(state, 0_norm) == 1.0f); REQUIRE(eg.getDecay(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getDecay(state, 127_norm) == 1.0f); REQUIRE(eg.getDecay(state, 0_norm) == 2.27f); - eg.ccDecay = { 63, 127.0f }; + eg.ccDecay[63] = 127.0f; REQUIRE(eg.getDecay(state, 0_norm) == 100.0f); + eg.ccDecay[63] = 1.27f; + eg.ccDecay[65] = 1.0f; + REQUIRE(eg.getDecay(state, 0_norm) == 2.27f); + REQUIRE(eg.getDecay(state, 127_norm) == 1.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getDecay(state, 0_norm) == 3.27f); + REQUIRE(eg.getDecay(state, 127_norm) == 2.0f); } TEST_CASE("[EGDescription] Release range") @@ -65,14 +86,21 @@ TEST_CASE("[EGDescription] Release range") sfz::MidiState state; eg.release = 1; eg.vel2release = -1.27f; - eg.ccRelease = { 63, 1.27f }; + eg.ccRelease[63] = 1.27f; REQUIRE(eg.getRelease(state, 0_norm) == 1.0f); REQUIRE(eg.getRelease(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getRelease(state, 127_norm) == 1.0f); REQUIRE(eg.getRelease(state, 0_norm) == 2.27f); - eg.ccRelease = { 63, 127.0f }; + eg.ccRelease[63] = 127.0f; REQUIRE(eg.getRelease(state, 0_norm) == 100.0f); + eg.ccRelease[63] = 1.27f; + eg.ccRelease[65] = 1.0f; + REQUIRE(eg.getRelease(state, 0_norm) == 2.27f); + REQUIRE(eg.getRelease(state, 127_norm) == 1.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getRelease(state, 0_norm) == 3.27f); + REQUIRE(eg.getRelease(state, 127_norm) == 2.0f); } TEST_CASE("[EGDescription] Hold range") @@ -81,14 +109,21 @@ TEST_CASE("[EGDescription] Hold range") sfz::MidiState state; eg.hold = 1; eg.vel2hold = -1.27f; - eg.ccHold = { 63, 1.27f }; + eg.ccHold[63] = 1.27f; REQUIRE(eg.getHold(state, 0_norm) == 1.0f); REQUIRE(eg.getHold(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getHold(state, 127_norm) == 1.0f); REQUIRE(eg.getHold(state, 0_norm) == 2.27f); - eg.ccHold = { 63, 127.0f }; + eg.ccHold[63] = 127.0f; REQUIRE(eg.getHold(state, 0_norm) == 100.0f); + eg.ccHold[63] = 1.27f; + eg.ccHold[65] = 1.0f; + REQUIRE(eg.getHold(state, 0_norm) == 2.27f); + REQUIRE(eg.getHold(state, 127_norm) == 1.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getHold(state, 0_norm) == 3.27f); + REQUIRE(eg.getHold(state, 127_norm) == 2.0f); } TEST_CASE("[EGDescription] Sustain level") @@ -97,13 +132,21 @@ TEST_CASE("[EGDescription] Sustain level") sfz::MidiState state; eg.sustain = 50; eg.vel2sustain = -100; - eg.ccSustain = { 63, 100.0f }; + eg.ccSustain[63] = 100.0f; REQUIRE(eg.getSustain(state, 0_norm) == 50.0f); REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getSustain(state, 127_norm) == 50.0f); - eg.ccSustain = { 63, 200.0f }; + eg.ccSustain[63] = 200.0f; REQUIRE(eg.getSustain(state, 0_norm) == 100.0f); + eg.sustain = 0; + eg.ccSustain[63] = 50.0f; + eg.ccSustain[65] = 50.0f; + REQUIRE(eg.getSustain(state, 0_norm) == 50.0f); + REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getSustain(state, 0_norm) == 100.0f); + REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); } TEST_CASE("[EGDescription] Start level") @@ -111,11 +154,19 @@ TEST_CASE("[EGDescription] Start level") sfz::EGDescription eg; sfz::MidiState state; eg.start = 0; - eg.ccStart = { 63, 127.0f }; + eg.ccStart[63] = 127.0f; REQUIRE(eg.getStart(state, 0_norm) == 0.0f); REQUIRE(eg.getStart(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); REQUIRE(eg.getStart(state, 0_norm) == 100.0f); - eg.ccStart = { 63, -127.0f }; + eg.ccStart[63] = -127.0f; REQUIRE(eg.getStart(state, 0_norm) == 0.0f); + eg.start = 0; + eg.ccStart[63] = 50.0f; + eg.ccStart[65] = 50.0f; + REQUIRE(eg.getStart(state, 0_norm) == 50.0f); + REQUIRE(eg.getStart(state, 127_norm) == 50.0f); + state.ccEvent(0, 65, 127_norm); + REQUIRE(eg.getStart(state, 0_norm) == 100.0f); + REQUIRE(eg.getStart(state, 127_norm) == 100.0f); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index e4a9c09d..eba8df05 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1083,13 +1083,13 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("ampeg_XX_onccNN") { // Defaults - REQUIRE(!region.amplitudeEG.ccAttack); - REQUIRE(!region.amplitudeEG.ccDecay); - REQUIRE(!region.amplitudeEG.ccDelay); - REQUIRE(!region.amplitudeEG.ccHold); - REQUIRE(!region.amplitudeEG.ccRelease); - REQUIRE(!region.amplitudeEG.ccStart); - REQUIRE(!region.amplitudeEG.ccSustain); + REQUIRE(region.amplitudeEG.ccAttack.empty()); + REQUIRE(region.amplitudeEG.ccDecay.empty()); + REQUIRE(region.amplitudeEG.ccDelay.empty()); + REQUIRE(region.amplitudeEG.ccHold.empty()); + REQUIRE(region.amplitudeEG.ccRelease.empty()); + REQUIRE(region.amplitudeEG.ccStart.empty()); + REQUIRE(region.amplitudeEG.ccSustain.empty()); // region.parseOpcode({ "ampeg_attack_oncc1", "1" }); region.parseOpcode({ "ampeg_decay_oncc2", "2" }); @@ -1098,27 +1098,20 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "ampeg_release_oncc5", "5" }); region.parseOpcode({ "ampeg_start_oncc6", "6" }); region.parseOpcode({ "ampeg_sustain_oncc7", "7" }); - REQUIRE(region.amplitudeEG.ccAttack); - REQUIRE(region.amplitudeEG.ccDecay); - REQUIRE(region.amplitudeEG.ccDelay); - REQUIRE(region.amplitudeEG.ccHold); - REQUIRE(region.amplitudeEG.ccRelease); - REQUIRE(region.amplitudeEG.ccStart); - REQUIRE(region.amplitudeEG.ccSustain); - REQUIRE(region.amplitudeEG.ccAttack->cc == 1); - REQUIRE(region.amplitudeEG.ccDecay->cc == 2); - REQUIRE(region.amplitudeEG.ccDelay->cc == 3); - REQUIRE(region.amplitudeEG.ccHold->cc == 4); - REQUIRE(region.amplitudeEG.ccRelease->cc == 5); - REQUIRE(region.amplitudeEG.ccStart->cc == 6); - REQUIRE(region.amplitudeEG.ccSustain->cc == 7); - REQUIRE(region.amplitudeEG.ccAttack->data == 1.0f); - REQUIRE(region.amplitudeEG.ccDecay->data == 2.0f); - REQUIRE(region.amplitudeEG.ccDelay->data == 3.0f); - REQUIRE(region.amplitudeEG.ccHold->data == 4.0f); - REQUIRE(region.amplitudeEG.ccRelease->data == 5.0f); - REQUIRE(region.amplitudeEG.ccStart->data == 6.0f); - REQUIRE(region.amplitudeEG.ccSustain->data == 7.0f); + REQUIRE(region.amplitudeEG.ccAttack.contains(1)); + REQUIRE(region.amplitudeEG.ccDecay.contains(2)); + REQUIRE(region.amplitudeEG.ccDelay.contains(3)); + REQUIRE(region.amplitudeEG.ccHold.contains(4)); + REQUIRE(region.amplitudeEG.ccRelease.contains(5)); + REQUIRE(region.amplitudeEG.ccStart.contains(6)); + REQUIRE(region.amplitudeEG.ccSustain.contains(7)); + REQUIRE(region.amplitudeEG.ccAttack[1] == 1.0f); + REQUIRE(region.amplitudeEG.ccDecay[2] == 2.0f); + REQUIRE(region.amplitudeEG.ccDelay[3] == 3.0f); + REQUIRE(region.amplitudeEG.ccHold[4] == 4.0f); + REQUIRE(region.amplitudeEG.ccRelease[5] == 5.0f); + REQUIRE(region.amplitudeEG.ccStart[6] == 6.0f); + REQUIRE(region.amplitudeEG.ccSustain[7] == 7.0f); // region.parseOpcode({ "ampeg_attack_oncc1", "101" }); region.parseOpcode({ "ampeg_decay_oncc2", "101" }); @@ -1127,13 +1120,13 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "ampeg_release_oncc5", "101" }); region.parseOpcode({ "ampeg_start_oncc6", "101" }); region.parseOpcode({ "ampeg_sustain_oncc7", "101" }); - REQUIRE(region.amplitudeEG.ccAttack->data == 100.0f); - REQUIRE(region.amplitudeEG.ccDecay->data == 100.0f); - REQUIRE(region.amplitudeEG.ccDelay->data == 100.0f); - REQUIRE(region.amplitudeEG.ccHold->data == 100.0f); - REQUIRE(region.amplitudeEG.ccRelease->data == 100.0f); - REQUIRE(region.amplitudeEG.ccStart->data == 100.0f); - REQUIRE(region.amplitudeEG.ccSustain->data == 100.0f); + REQUIRE(region.amplitudeEG.ccAttack[1] == 100.0f); + REQUIRE(region.amplitudeEG.ccDecay[2] == 100.0f); + REQUIRE(region.amplitudeEG.ccDelay[3] == 100.0f); + REQUIRE(region.amplitudeEG.ccHold[4] == 100.0f); + REQUIRE(region.amplitudeEG.ccRelease[5] == 100.0f); + REQUIRE(region.amplitudeEG.ccStart[6] == 100.0f); + REQUIRE(region.amplitudeEG.ccSustain[7] == 100.0f); // region.parseOpcode({ "ampeg_attack_oncc1", "-101" }); region.parseOpcode({ "ampeg_decay_oncc2", "-101" }); @@ -1142,13 +1135,56 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "ampeg_release_oncc5", "-101" }); region.parseOpcode({ "ampeg_start_oncc6", "-101" }); region.parseOpcode({ "ampeg_sustain_oncc7", "-101" }); - REQUIRE(region.amplitudeEG.ccAttack->data == -100.0f); - REQUIRE(region.amplitudeEG.ccDecay->data == -100.0f); - REQUIRE(region.amplitudeEG.ccDelay->data == -100.0f); - REQUIRE(region.amplitudeEG.ccHold->data == -100.0f); - REQUIRE(region.amplitudeEG.ccRelease->data == -100.0f); - REQUIRE(region.amplitudeEG.ccStart->data == -100.0f); - REQUIRE(region.amplitudeEG.ccSustain->data == -100.0f); + REQUIRE(region.amplitudeEG.ccAttack[1] == -100.0f); + REQUIRE(region.amplitudeEG.ccDecay[2] == -100.0f); + REQUIRE(region.amplitudeEG.ccDelay[3] == -100.0f); + REQUIRE(region.amplitudeEG.ccHold[4] == -100.0f); + REQUIRE(region.amplitudeEG.ccRelease[5] == -100.0f); + REQUIRE(region.amplitudeEG.ccStart[6] == -100.0f); + REQUIRE(region.amplitudeEG.ccSustain[7] == -100.0f); + // + region.parseOpcode({ "ampeg_attack_oncc1", "1" }); + region.parseOpcode({ "ampeg_decay_oncc2", "2" }); + region.parseOpcode({ "ampeg_delay_oncc3", "3" }); + region.parseOpcode({ "ampeg_hold_oncc4", "4" }); + region.parseOpcode({ "ampeg_release_oncc5", "5" }); + region.parseOpcode({ "ampeg_start_oncc6", "6" }); + region.parseOpcode({ "ampeg_sustain_oncc7", "7" }); + region.parseOpcode({ "ampeg_attack_oncc2", "2" }); + region.parseOpcode({ "ampeg_decay_oncc3", "3" }); + region.parseOpcode({ "ampeg_delay_oncc4", "4" }); + region.parseOpcode({ "ampeg_hold_oncc5", "5" }); + region.parseOpcode({ "ampeg_release_oncc6", "6" }); + region.parseOpcode({ "ampeg_start_oncc7", "7" }); + region.parseOpcode({ "ampeg_sustain_oncc8", "8" }); + REQUIRE(region.amplitudeEG.ccAttack.contains(1)); + REQUIRE(region.amplitudeEG.ccDecay.contains(2)); + REQUIRE(region.amplitudeEG.ccDelay.contains(3)); + REQUIRE(region.amplitudeEG.ccHold.contains(4)); + REQUIRE(region.amplitudeEG.ccRelease.contains(5)); + REQUIRE(region.amplitudeEG.ccStart.contains(6)); + REQUIRE(region.amplitudeEG.ccSustain.contains(7)); + REQUIRE(region.amplitudeEG.ccAttack.contains(2)); + REQUIRE(region.amplitudeEG.ccDecay.contains(3)); + REQUIRE(region.amplitudeEG.ccDelay.contains(4)); + REQUIRE(region.amplitudeEG.ccHold.contains(5)); + REQUIRE(region.amplitudeEG.ccRelease.contains(6)); + REQUIRE(region.amplitudeEG.ccStart.contains(7)); + REQUIRE(region.amplitudeEG.ccSustain.contains(8)); + REQUIRE(region.amplitudeEG.ccAttack[1] == 1.0f); + REQUIRE(region.amplitudeEG.ccDecay[2] == 2.0f); + REQUIRE(region.amplitudeEG.ccDelay[3] == 3.0f); + REQUIRE(region.amplitudeEG.ccHold[4] == 4.0f); + REQUIRE(region.amplitudeEG.ccRelease[5] == 5.0f); + REQUIRE(region.amplitudeEG.ccStart[6] == 6.0f); + REQUIRE(region.amplitudeEG.ccSustain[7] == 7.0f); + REQUIRE(region.amplitudeEG.ccAttack[2] == 2.0f); + REQUIRE(region.amplitudeEG.ccDecay[3] == 3.0f); + REQUIRE(region.amplitudeEG.ccDelay[4] == 4.0f); + REQUIRE(region.amplitudeEG.ccHold[5] == 5.0f); + REQUIRE(region.amplitudeEG.ccRelease[6] == 6.0f); + REQUIRE(region.amplitudeEG.ccStart[7] == 7.0f); + REQUIRE(region.amplitudeEG.ccSustain[8] == 8.0f); } SECTION("sustain_sw and sostenuto_sw") From 5ef921485b6990d304cf85ae8b04264e6b55cc70 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 02:03:04 +0200 Subject: [PATCH 11/27] Add the region sets (hierarchy) Link the voice lifecycle into region sets and polyphony groups Respect crudely the polyphony limits without stealing for now --- src/sfizz/PolyphonyGroup.h | 40 +++++++ src/sfizz/Region.cpp | 4 + src/sfizz/Region.h | 8 +- src/sfizz/RegionSet.h | 74 ++++++++++++ src/sfizz/Synth.cpp | 149 +++++++++++++++++++++---- src/sfizz/Synth.h | 63 ++++++++--- tests/CMakeLists.txt | 1 + tests/PolyphonyT.cpp | 223 +++++++++++++++++++++++++++++++++++++ 8 files changed, 528 insertions(+), 34 deletions(-) create mode 100644 src/sfizz/PolyphonyGroup.h create mode 100644 src/sfizz/RegionSet.h create mode 100644 tests/PolyphonyT.cpp diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h new file mode 100644 index 00000000..8f6a8c40 --- /dev/null +++ b/src/sfizz/PolyphonyGroup.h @@ -0,0 +1,40 @@ +#pragma once +#include "Region.h" +#include "Voice.h" +#include "absl/algorithm/container.h" + +namespace sfz +{ +class PolyphonyGroup { +public: + void setPolyphonyLimit(unsigned limit) + { + polyphonyLimit = limit; + voices.reserve(limit); + } + unsigned getPolyphonyLimit() const { return polyphonyLimit; } + void registerVoice(Voice* voice) + { + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); + } + void removeVoice(const Voice* voice) + { + auto it = absl::c_find(voices, voice); + if (it == voices.end()) + return; + + auto last = voices.end() - 1; + if (it != last) + std::iter_swap(it, last); + + voices.pop_back(); + } + const std::vector& getActiveVoices() const { return voices; } + std::vector& getActiveVoices() { return voices; } +private: + unsigned polyphonyLimit { config::maxVoices }; + std::vector voices; +}; + +} diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 5d6bbe55..424269f7 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -164,6 +164,10 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) DBG("Unkown off mode:" << std::string(opcode.value)); } break; + case hash("polyphony"): + if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) + polyphony = *value; + break; case hash("note_polyphony"): if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) notePolyphony = *value; diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index de80f53e..e96ebe0f 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -24,6 +24,9 @@ #include namespace sfz { + +class RegionSet; + /** * @brief Regions are the basic building blocks for the SFZ parsing and handling code. * All SFZ files are made of regions that are activated when a key is pressed or a CC @@ -282,7 +285,8 @@ struct Region { uint32_t group { Default::group }; // group absl::optional offBy {}; // off_by SfzOffMode offMode { Default::offMode }; // off_mode - absl::optional notePolyphony {}; + absl::optional notePolyphony {}; // note_polyphony + unsigned polyphony { config::maxVoices }; // polyphony SfzSelfMask selfMask { Default::selfMask }; // Region logic: key mapping @@ -365,6 +369,8 @@ struct Region { // Modifiers ModifierArray> modifiers; + // Parent + RegionSet* parent { nullptr }; private: const MidiState& midiState; bool keySwitched { true }; diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h new file mode 100644 index 00000000..539e3eca --- /dev/null +++ b/src/sfizz/RegionSet.h @@ -0,0 +1,74 @@ +#pragma once +#include "Region.h" +#include "Voice.h" +#include + +namespace sfz +{ + +class RegionSet { +public: + void setPolyphonyLimit(unsigned limit) + { + polyphonyLimit = limit; + voices.reserve(limit); + } + unsigned getPolyphonyLimit() const { return polyphonyLimit; } + void addRegion(Region* region) + { + if (absl::c_find(regions, region) == regions.end()) + regions.push_back(region); + } + void addSubset(RegionSet* group) + { + if (absl::c_find(subsets, group) == subsets.end()) + subsets.push_back(group); + } + void registerVoice(Voice* voice) + { + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); + } + void removeVoice(const Voice* voice) + { + auto it = absl::c_find(voices, voice); + if (it == voices.end()) + return; + + auto last = voices.end() - 1; + if (it != last) + std::iter_swap(it, last); + + voices.pop_back(); + DBG("Active voices size " << voices.size()); + } + static void registerVoiceInHierarchy(const Region* region, Voice* voice) + { + auto parent = region->parent; + while (parent != nullptr) { + parent->registerVoice(voice); + parent = parent->getParent(); + } + } + static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) + { + auto parent = region->parent; + while (parent != nullptr) { + parent->removeVoice(voice); + parent = parent->getParent(); + } + } + RegionSet* getParent() const { return parent; } + void setParent(RegionSet* parent) { this->parent = parent; } + const std::vector& getActiveVoices() const { return voices; } + const std::vector& getRegions() const { return regions; } + const std::vector& getSubsets() const { return subsets; } +private: + RegionSet* parent { nullptr }; + std::vector regions; + std::vector subsets; + std::vector voices; + unsigned polyphonyLimit { config::maxVoices }; +}; + +} diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 766c1786..8d1d6222 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -51,14 +51,31 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) { (void)id; (void)state; - DBG("Voice " << id.number << ": state " << static_cast(state)); + if (state == Voice::State::idle) { + auto voice = getVoiceById(id); + DBG("Removing voice " << id.number << " from hierarchies"); + RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); + polyphonyGroups[voice->getRegion()->group].removeVoice(voice); + } + } void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector& members) { + const auto newRegionSet = [&](RegionSet* parentSet) { + ASSERT(parentSet != nullptr); + sets.emplace_back(new RegionSet); + auto newSet = sets.back().get(); + parentSet->addSubset(newSet); + newSet->setParent(parentSet); + currentSet = newSet; + }; + switch (hash(header)) { case hash("global"): globalOpcodes = members; + currentSet = sets.front().get(); + lastHeader = Header::Global; groupOpcodes.clear(); masterOpcodes.clear(); handleGlobalOpcodes(members); @@ -69,11 +86,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); + else + newRegionSet(currentSet); + lastHeader = Header::Group; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -105,6 +130,8 @@ void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& mes void sfz::Synth::buildRegion(const std::vector& regionOpcodes) { + ASSERT(currentSet != nullptr); + int regionNumber = static_cast(regions.size()); auto lastRegion = absl::make_unique(regionNumber, resources.midiState, defaultPath); @@ -128,6 +155,13 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) if (octaveOffset != 0 || noteOffset != 0) lastRegion->offsetAllKeys(octaveOffset * 12 + noteOffset); + // There was a combination of group= and polyphony= on a region, so set the group polyphony + if (lastRegion->group != Default::group && lastRegion->polyphony != config::maxVoices) + setGroupPolyphony(lastRegion->group, lastRegion->polyphony); + + lastRegion->parent = currentSet; + currentSet->addRegion(lastRegion.get()); + regions.push_back(std::move(lastRegion)); } @@ -142,6 +176,9 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); + sets.clear(); + sets.emplace_back(new RegionSet); + currentSet = sets.front().get(); regions.clear(); effectBuses.clear(); effectBuses.emplace_back(new EffectBus); @@ -162,17 +199,38 @@ void sfz::Synth::clear() masterOpcodes.clear(); groupOpcodes.clear(); unknownOpcodes.clear(); - groupMaxPolyphony.clear(); - groupMaxPolyphony.push_back(config::maxVoices); + polyphonyGroups.clear(); + polyphonyGroups.emplace_back(); + polyphonyGroups.back().setPolyphonyLimit(config::maxVoices); modificationTime = fs::file_time_type::min(); } +void sfz::Synth::handleMasterOpcodes(const std::vector& members) +{ + for (auto& rawMember : members) { + const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal); + + switch (member.lettersOnlyHash) { + case hash("polyphony"): + ASSERT(currentSet != nullptr); + if (auto value = readOpcode(member.value, Default::polyphonyRange)) + currentSet->setPolyphonyLimit(*value); + break; + } + } +} + void sfz::Synth::handleGlobalOpcodes(const std::vector& members) { for (auto& rawMember : members) { const Opcode member = rawMember.cleanUp(kOpcodeScopeGlobal); switch (member.lettersOnlyHash) { + case hash("polyphony"): + ASSERT(currentSet != nullptr); + if (auto value = readOpcode(member.value, Default::polyphonyRange)) + currentSet->setPolyphonyLimit(*value); + break; case hash("sw_default"): setValueFromOpcode(member, defaultSwitch, Default::keyRange); break; @@ -187,7 +245,7 @@ void sfz::Synth::handleGlobalOpcodes(const std::vector& members) void sfz::Synth::handleGroupOpcodes(const std::vector& members, const std::vector& masterMembers) { absl::optional groupIdx; - unsigned maxPolyphony { config::maxVoices }; + absl::optional maxPolyphony; const auto parseOpcode = [&](const Opcode& rawMember) { const Opcode member = rawMember.cleanUp(kOpcodeScopeGroup); @@ -197,7 +255,7 @@ void sfz::Synth::handleGroupOpcodes(const std::vector& members, const st setValueFromOpcode(member, groupIdx, Default::groupRange); break; case hash("polyphony"): - setValueFromOpcode(member, maxPolyphony, Range(0, config::maxVoices)); + setValueFromOpcode(member, maxPolyphony, Default::polyphonyRange); break; } }; @@ -208,8 +266,14 @@ void sfz::Synth::handleGroupOpcodes(const std::vector& members, const st for (auto& member : members) parseOpcode(member); - if (groupIdx) - setGroupPolyphony(*groupIdx, maxPolyphony); + if (groupIdx && maxPolyphony) { + setGroupPolyphony(*groupIdx, *maxPolyphony); + } else if (maxPolyphony) { + ASSERT(currentSet != nullptr); + currentSet->setPolyphonyLimit(*maxPolyphony); + } else if (groupIdx && *groupIdx > polyphonyGroups.size()) { + setGroupPolyphony(*groupIdx, config::maxVoices); + } } void sfz::Synth::handleControlOpcodes(const std::vector& members) @@ -437,8 +501,10 @@ void sfz::Synth::finalizeSfzLoad() 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); + while (polyphonyGroups.size() <= region->group) { + polyphonyGroups.emplace_back(); + polyphonyGroups.back().setPolyphonyLimit(config::maxVoices); + } for (auto note = 0; note < 128; note++) { if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note))) @@ -809,6 +875,8 @@ void sfz::Synth::noteOffDispatch(int delay, int noteNumber, float velocity) noex voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOff); ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -820,8 +888,8 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { - unsigned activeNotesInGroup { 0 }; - unsigned activeNotes { 0 }; + unsigned notePolyphonyCounter { 0 }; + unsigned regionPolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; for (auto& voice : voices) { @@ -829,12 +897,12 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc if (voiceRegion == nullptr) continue; - if (voiceRegion->group == region->group) - activeNotesInGroup += 1; + if (voiceRegion == region) + regionPolyphonyCounter += 1; if (region->notePolyphony) { if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { - activeNotes += 1; + notePolyphonyCounter += 1; switch (region->selfMask) { case SfzSelfMask::mask: if (voice->getTriggerValue() < velocity) { @@ -854,10 +922,30 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - if (activeNotesInGroup >= groupMaxPolyphony[region->group]) + // FIXME: Do something for the polyphony limit + if (polyphonyGroups[region->group].getActiveVoices().size() + == polyphonyGroups[region->group].getPolyphonyLimit()) continue; - if (region->notePolyphony && activeNotes >= *region->notePolyphony) { + // FIXME: Do something for the polyphony limit + if (regionPolyphonyCounter >= region->polyphony) + continue; + + // FIXME: Do something for the polyphony limit + auto parent = region->parent; + bool polyphonyReached { false }; + while (parent != nullptr) { + if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { + polyphonyReached = true; + break; + } + + parent = parent->getParent(); + } + if (polyphonyReached) + continue; + + if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { if (selfMaskCandidate != nullptr) selfMaskCandidate->release(delay); else // We're the lowest velocity guy here @@ -870,6 +958,8 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -917,6 +1007,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept voice->startVoice(region, delay, ccNumber, normValue, Voice::TriggerType::CC); ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -1079,6 +1171,16 @@ const sfz::EffectBus* sfz::Synth::getEffectBusView(int idx) const noexcept return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr; } +const sfz::RegionSet* sfz::Synth::getRegionSetView(int idx) const noexcept +{ + return (size_t)idx < sets.size() ? sets[idx].get() : nullptr; +} + +const sfz::PolyphonyGroup* sfz::Synth::getPolyphonyGroupView(int idx) const noexcept +{ + return (size_t)idx < polyphonyGroups.size() ? &polyphonyGroups[idx] : nullptr; +} + const sfz::Region* sfz::Synth::getRegionById(NumericId id) const noexcept { const size_t size = regions.size(); @@ -1118,6 +1220,11 @@ const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept return (size_t)idx < voices.size() ? voices[idx].get() : nullptr; } +unsigned sfz::Synth::getNumPolyphonyGroups() const noexcept +{ + return polyphonyGroups.size(); +} + const std::vector& sfz::Synth::getUnknownOpcodes() const noexcept { return unknownOpcodes; @@ -1226,8 +1333,10 @@ void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept if (factor == oversamplingFactor) return; - for (auto& voice : voices) + for (auto& voice : voices) { + voice->reset(); + } resources.filePool.emptyFileLoadingQueues(); resources.filePool.setOversamplingFactor(factor); @@ -1339,8 +1448,8 @@ void sfz::Synth::allSoundOff() noexcept void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept { - while (groupMaxPolyphony.size() <= groupIdx) - groupMaxPolyphony.push_back(config::maxVoices); + while (polyphonyGroups.size() <= groupIdx) + polyphonyGroups.emplace_back(); - groupMaxPolyphony[groupIdx] = polyphony; + polyphonyGroups[groupIdx].setPolyphonyLimit(polyphony); } diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 9835b593..28bd72ad 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -9,6 +9,8 @@ #include "Parser.h" #include "Voice.h" #include "Region.h" +#include "RegionSet.h" +#include "PolyphonyGroup.h" #include "Effects.h" #include "LeakDetector.h" #include "MidiState.h" @@ -220,17 +222,39 @@ public: * for testing. * * @param idx - * @return const Region* + * @return const Voice* */ const Voice* getVoiceView(int idx) const noexcept; /** - * @brief Get a raw view into a specific voice. This is mostly used + * @brief Get a raw view into a specific effect bus. This is mostly used * for testing. * * @param idx - * @return const Region* + * @return const EffectBus* */ const EffectBus* getEffectBusView(int idx) const noexcept; + /** + * @brief Get a raw view into a specific set of regions. This is mostly used + * for testing. + * + * @param idx + * @return const RegionSet* + */ + const RegionSet* getRegionSetView(int idx) const noexcept; + /** + * @brief Get a raw view into a specific polyphony group. This is mostly used + * for testing. + * + * @param idx + * @return const PolyphonyGroup* + */ + const PolyphonyGroup* getPolyphonyGroupView(int idx) const noexcept; + /** + * @brief Get the number of polyphony groups + * + * @return unsigned + */ + unsigned getNumPolyphonyGroups() const noexcept; /** * @brief Get a list of unknown opcodes. The lifetime of the * string views in the code are linked to the currently loaded @@ -572,7 +596,6 @@ private: * @param polyphone the max polyphony */ void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept; - std::vector groupMaxPolyphony { config::maxVoices }; /** * @brief Reset all CCs; to be used on CC 121 @@ -598,6 +621,12 @@ private: * @param members the opcodes of the block */ void handleGlobalOpcodes(const std::vector& members); + /** + * @brief Helper function to dispatch opcodes + * + * @param members the opcodes of the block + */ + void handleMasterOpcodes(const std::vector& members); /** * @brief Helper function to dispatch opcodes * @@ -649,8 +678,6 @@ private: 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; @@ -672,15 +699,25 @@ private: // Default active switch if multiple keyswitchable regions are present absl::optional defaultSwitch; std::vector unknownOpcodes; - using RegionPtrVector = std::vector; - using VoicePtrVector = std::vector; - std::vector> regions; - std::vector> voices; + using RegionViewVector = std::vector; + using VoiceViewVector = std::vector; + using VoicePtr = std::unique_ptr; + using RegionPtr = std::unique_ptr; + using RegionSetPtr = std::unique_ptr; + std::vector regions; + std::vector voices; + // These are more general "groups" than sfz and encapsulates the full hierarchy + enum class Header { Global, Master, Group }; + RegionSet* currentSet; + Header lastHeader { Header::Global }; + std::vector sets; + // These are the `group=` groups where you can off voices + std::vector polyphonyGroups; // Views to speed up iteration over the regions and voices when events // occur in the audio callback - VoicePtrVector voiceViewArray; - std::array noteActivationLists; - std::array ccActivationLists; + VoiceViewVector voiceViewArray; + std::array noteActivationLists; + std::array ccActivationLists; // Effect factory and buses EffectFactory effectFactory; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 43b73471..31e134c8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ set(SFIZZ_TEST_SOURCES MidiStateT.cpp InterpolatorsT.cpp SmoothersT.cpp + PolyphonyT.cpp RegionActivationT.cpp RegionValueComputationsT.cpp # If we're tweaking the curves this kind of tests does not make sense diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp new file mode 100644 index 00000000..815b6c23 --- /dev/null +++ b/tests/PolyphonyT.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/Synth.h" +#include "sfizz/SfzHelpers.h" +#include "catch2/catch.hpp" + +using namespace Catch::literals; +using namespace sfz::literals; + +constexpr int blockSize { 256 }; + + +TEST_CASE("[Polyphony] Polyphony in hierarchy") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + key=61 sample=*sine polyphony=2 + polyphony=2 + key=62 sample=*sine + polyphony=3 + key=63 sample=*sine + key=63 sample=*sine + key=63 sample=*sine + polyphony=4 + key=64 sample=*sine polyphony=5 + key=64 sample=*sine + key=64 sample=*sine + key=64 sample=*sine + )"); + REQUIRE( synth.getRegionView(0)->polyphony == 2 ); + REQUIRE( synth.getRegionSetView(1)->getPolyphonyLimit() == 2 ); + REQUIRE( synth.getRegionView(1)->polyphony == 2 ); + REQUIRE( synth.getRegionSetView(2)->getPolyphonyLimit() == 3 ); + REQUIRE( synth.getRegionSetView(2)->getRegions()[0]->polyphony == 3 ); + REQUIRE( synth.getRegionSetView(3)->getPolyphonyLimit() == 4 ); + REQUIRE( synth.getRegionSetView(3)->getRegions()[0]->polyphony == 5 ); + REQUIRE( synth.getRegionSetView(3)->getRegions()[1]->polyphony == 4 ); +} + +TEST_CASE("[Polyphony] Polyphony groups") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + key=62 sample=*sine + group=1 polyphony=3 + key=63 sample=*sine + key=63 sample=*sine group=2 polyphony=4 + key=63 sample=*sine group=4 polyphony=5 + group=4 + key=62 sample=*sine + )"); + REQUIRE( synth.getNumPolyphonyGroups() == 5 ); + REQUIRE( synth.getNumRegions() == 5 ); + REQUIRE( synth.getRegionView(0)->group == 0 ); + REQUIRE( synth.getRegionView(1)->group == 1 ); + REQUIRE( synth.getRegionView(2)->group == 2 ); + REQUIRE( synth.getRegionView(3)->group == 4 ); + REQUIRE( synth.getRegionView(3)->polyphony == 5 ); + REQUIRE( synth.getRegionView(4)->group == 4 ); + REQUIRE( synth.getPolyphonyGroupView(1)->getPolyphonyLimit() == 3 ); + REQUIRE( synth.getPolyphonyGroupView(2)->getPolyphonyLimit() == 4 ); + REQUIRE( synth.getPolyphonyGroupView(3)->getPolyphonyLimit() == sfz::config::maxVoices ); + REQUIRE( synth.getPolyphonyGroupView(4)->getPolyphonyLimit() == 5 ); +} + +TEST_CASE("[Polyphony] group polyphony limits") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + group=1 polyphony=2 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (group)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (master)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + polyphony=5 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + sample=*saw key=65 + + polyphony=5 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 5); +} + +TEST_CASE("[Polyphony] Hierarchy polyphony limits (global)") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + polyphony=5 + sample=*sine key=65 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); +} + +TEST_CASE("[Polyphony] Polyphony in master") +{ + sfz::Synth synth; + synth.setSamplesPerBlock(blockSize); + sfz::AudioBuffer buffer { 2, blockSize }; + synth.loadSfzString(fs::current_path(), R"( + polyphony=2 + group=2 + sample=*sine key=65 + group=3 + sample=*sine key=63 + // Empty master resets the polyphony + sample=*sine key=61 + )"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + synth.allSoundOff(); + synth.renderBlock(buffer); + REQUIRE(synth.getNumActiveVoices() == 0); + synth.noteOn(0, 63, 64); + synth.noteOn(0, 63, 64); + synth.noteOn(0, 63, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note + synth.allSoundOff(); + synth.renderBlock(buffer); + REQUIRE(synth.getNumActiveVoices() == 0); + synth.noteOn(0, 61, 64); + synth.noteOn(0, 61, 64); + synth.noteOn(0, 61, 64); + REQUIRE(synth.getNumActiveVoices() == 3); +} + + +TEST_CASE("[Polyphony] Self-masking") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sample=*sine key=64 note_polyphony=2 + )"); + synth.noteOn(0, 64, 63); + synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 64); + REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->releasedOrFree()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(1)->releasedOrFree()); // The lowest velocity voice is the masking candidate + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); +} + +TEST_CASE("[Polyphony] Not self-masking") +{ + sfz::Synth synth; + synth.loadSfzString(fs::current_path(), R"( + sample=*sine key=66 note_polyphony=2 note_selfmask=off + )"); + synth.noteOn(0, 66, 63); + synth.noteOn(0, 66, 62); + synth.noteOn(0, 66, 64); + REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(synth.getVoiceView(0)->releasedOrFree()); // The first encountered voice is the masking candidate + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(!synth.getVoiceView(1)->releasedOrFree()); + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(2)->releasedOrFree()); +} From 2195466055dac887f0772ab188e107dad5f24334 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 14:18:26 +0200 Subject: [PATCH 12/27] Rebase and use the swap and pop helper --- src/sfizz/PolyphonyGroup.h | 11 ++--------- src/sfizz/RegionSet.h | 12 ++---------- src/sfizz/Synth.cpp | 1 - 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index 8f6a8c40..ed5d6159 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -1,6 +1,7 @@ #pragma once #include "Region.h" #include "Voice.h" +#include "SwapAndPop.h" #include "absl/algorithm/container.h" namespace sfz @@ -20,15 +21,7 @@ public: } void removeVoice(const Voice* voice) { - auto it = absl::c_find(voices, voice); - if (it == voices.end()) - return; - - auto last = voices.end() - 1; - if (it != last) - std::iter_swap(it, last); - - voices.pop_back(); + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); } const std::vector& getActiveVoices() const { return voices; } std::vector& getActiveVoices() { return voices; } diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 539e3eca..87f8490b 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -1,6 +1,7 @@ #pragma once #include "Region.h" #include "Voice.h" +#include "SwapAndPop.h" #include namespace sfz @@ -31,16 +32,7 @@ public: } void removeVoice(const Voice* voice) { - auto it = absl::c_find(voices, voice); - if (it == voices.end()) - return; - - auto last = voices.end() - 1; - if (it != last) - std::iter_swap(it, last); - - voices.pop_back(); - DBG("Active voices size " << voices.size()); + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); } static void registerVoiceInHierarchy(const Region* region, Voice* voice) { diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 8d1d6222..0eec9944 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -53,7 +53,6 @@ void sfz::Synth::onVoiceStateChanged(NumericId id, Voice::State state) (void)state; if (state == Voice::State::idle) { auto voice = getVoiceById(id); - DBG("Removing voice " << id.number << " from hierarchies"); RegionSet::removeVoiceFromHierarchy(voice->getRegion(), voice); polyphonyGroups[voice->getRegion()->group].removeVoice(voice); } From 89093b18263bd97e36543b6a15468f1d47e1a9c5 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 22:27:43 +0200 Subject: [PATCH 13/27] Use explicit pointers --- src/sfizz/RegionSet.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 87f8490b..3cb46196 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -36,7 +36,7 @@ public: } static void registerVoiceInHierarchy(const Region* region, Voice* voice) { - auto parent = region->parent; + auto* parent = region->parent; while (parent != nullptr) { parent->registerVoice(voice); parent = parent->getParent(); @@ -44,7 +44,7 @@ public: } static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) { - auto parent = region->parent; + auto* parent = region->parent; while (parent != nullptr) { parent->removeVoice(voice); parent = parent->getParent(); From 7559dfdd7b482c3cba3c130ea7eeb315eb3c0d6b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 15 Jun 2020 22:28:09 +0200 Subject: [PATCH 14/27] Use the existing OpcodeScope and ass the "" level --- src/sfizz/Opcode.h | 2 ++ src/sfizz/Synth.cpp | 9 +++++---- src/sfizz/Synth.h | 3 +-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index 754c0d9e..cd31c8c6 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -47,6 +47,8 @@ enum OpcodeScope { kOpcodeScopeGlobal, //! control scope kOpcodeScopeControl, + //! Master scope + kOpcodeScopeMaster, //! group scope kOpcodeScopeGroup, //! region scope diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 0eec9944..bc2177b5 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -74,7 +74,7 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); else newRegionSet(currentSet); - lastHeader = Header::Group; + lastHeader = OpcodeScope::kOpcodeScopeGroup; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -175,6 +175,7 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); + lastHeader = OpcodeScope::kOpcodeScopeGlobal; sets.clear(); sets.emplace_back(new RegionSet); currentSet = sets.front().get(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 28bd72ad..3c08e954 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -707,9 +707,8 @@ private: std::vector regions; std::vector voices; // These are more general "groups" than sfz and encapsulates the full hierarchy - enum class Header { Global, Master, Group }; RegionSet* currentSet; - Header lastHeader { Header::Global }; + OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; std::vector sets; // These are the `group=` groups where you can off voices std::vector polyphonyGroups; From 396fb2199a91f72f7615d49605d9e236f9835eeb Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 22 Jun 2020 22:14:03 +0200 Subject: [PATCH 15/27] Add the voice stealing helper --- src/sfizz/VoiceStealing.h | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/sfizz/VoiceStealing.h diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h new file mode 100644 index 00000000..8cf4362b --- /dev/null +++ b/src/sfizz/VoiceStealing.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#include "Config.h" +#include "Voice.h" +#include "SisterVoiceRing.h" +#include +#include "absl/types/span.h" + +namespace sfz +{ +class VoiceStealing +{ +public: + VoiceStealing() + { + voiceScores.reserve(config::maxVoices); + } + + Voice* steal(absl::Span voices) noexcept + { + // Start of the voice stealing algorithm + absl::c_sort(voices, voiceOrdering); + + const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { + return sum + v->getAverageEnvelope(); + }); + const auto envThreshold = sumEnvelope + / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; + + Voice* returnedVoice = voices.front(); + unsigned idx = 0; + while (idx < voices.size()) { + const auto ref = voices[idx]; + + if (ref->getAge() < ageThreshold) { + // Went too far, we'll kill the oldest note. + break; + } + + float maxEnvelope { 0.0f }; + SisterVoiceRing::applyToRing(ref, [&](Voice* v) { + maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); + }); + + if (maxEnvelope < envThreshold) { + returnedVoice = ref; + break; + } + + // Jump over the sister voices in the set + do { idx++; } + while (idx < voices.size() && sisterVoices(ref, voices[idx])); + } + return returnedVoice; + } + +private: + struct VoiceScore + { + Voice* voice; + double score; + }; + + struct VoiceScoreComparator + { + bool operator()(const VoiceScore& voiceScore, const double& score) + { + return (voiceScore.score < score); + } + + bool operator()(const double& score, const VoiceScore& voiceScore) + { + return (score < voiceScore.score); + } + + bool operator()(const VoiceScore& lhs, const VoiceScore& rhs) + { + return (lhs.score < rhs.score); + } + }; + std::vector voiceScores; +}; +} From 50ab6910e1212ad259d04efa7c721e6e3417bf5a Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 22 Jun 2020 23:02:12 +0200 Subject: [PATCH 16/27] Find a free voice on the fly --- src/sfizz/Synth.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index bc2177b5..f808200c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -891,13 +891,16 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc unsigned notePolyphonyCounter { 0 }; unsigned regionPolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; + Voice* selectedVoice { nullptr }; for (auto& voice : voices) { - const auto voiceRegion = voice->getRegion(); - if (voiceRegion == nullptr) + if (voice->isFree()) { + if (selectedVoice == nullptr) + selectedVoice = voice.get(); continue; + } - if (voiceRegion == region) + if (voice->getRegion() == region) regionPolyphonyCounter += 1; if (region->notePolyphony) { @@ -923,12 +926,12 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc } // FIXME: Do something for the polyphony limit - if (polyphonyGroups[region->group].getActiveVoices().size() - == polyphonyGroups[region->group].getPolyphonyLimit()) + if (regionPolyphonyCounter >= region->polyphony) continue; // FIXME: Do something for the polyphony limit - if (regionPolyphonyCounter >= region->polyphony) + if (polyphonyGroups[region->group].getActiveVoices().size() + == polyphonyGroups[region->group].getPolyphonyLimit()) continue; // FIXME: Do something for the polyphony limit @@ -952,14 +955,10 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; } - auto voice = findFreeVoice(); - if (voice == nullptr) - continue; - - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(voice); - RegionSet::registerVoiceInHierarchy(region, voice); - polyphonyGroups[region->group].registerVoice(voice); + selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); + ring.addVoiceToRing(selectedVoice); + RegionSet::registerVoiceInHierarchy(region, selectedVoice); + polyphonyGroups[region->group].registerVoice(selectedVoice); } } } From 32fe0cacf17a8d574d541b4b3042414895a743aa Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 22 Jun 2020 23:51:23 +0200 Subject: [PATCH 17/27] Use the voice stealer for all polyphony limits --- src/sfizz/PolyphonyGroup.h | 7 +++ src/sfizz/RegionSet.h | 8 +++ src/sfizz/SisterVoiceRing.h | 1 + src/sfizz/Synth.cpp | 118 ++++++++++++++++-------------------- src/sfizz/Synth.h | 4 ++ src/sfizz/VoiceStealing.h | 3 + 6 files changed, 74 insertions(+), 67 deletions(-) diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index ed5d6159..74314da7 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -1,4 +1,11 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + #pragma once + #include "Region.h" #include "Voice.h" #include "SwapAndPop.h" diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 3cb46196..5203442b 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -1,4 +1,11 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + #pragma once + #include "Region.h" #include "Voice.h" #include "SwapAndPop.h" @@ -53,6 +60,7 @@ public: RegionSet* getParent() const { return parent; } void setParent(RegionSet* parent) { this->parent = parent; } const std::vector& getActiveVoices() const { return voices; } + std::vector& getActiveVoices() { return voices; } const std::vector& getRegions() const { return regions; } const std::vector& getSubsets() const { return subsets; } private: diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 2f8fac16..7bd4c0ef 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -3,6 +3,7 @@ // This code is part of the sfizz library and is licensed under a BSD 2-clause // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz +#pragma once #include "Voice.h" #include "absl/meta/type_traits.h" diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index f808200c..500e4edc 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -610,52 +610,11 @@ sfz::Voice* sfz::Synth::findFreeVoice() noexcept auto freeVoice = absl::c_find_if(voices, [](const std::unique_ptr& voice) { return voice->isFree(); }); + if (freeVoice != voices.end()) return freeVoice->get(); - // 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()) * config::stealingEnvelopeCoeff; - const auto ageThreshold = voiceViewArray.front()->getAge() * config::stealingAgeCoeff; - - Voice* returnedVoice = voiceViewArray.front(); - unsigned idx = 0; - while (idx < voiceViewArray.size()) { - const auto ref = voiceViewArray[idx]; - - if (ref->getAge() < ageThreshold) { - // Went too far, we'll kill the oldest note. - break; - } - - float maxEnvelope { 0.0f }; - SisterVoiceRing::applyToRing(ref, [&](Voice* v) { - maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); - }); - - if (maxEnvelope < envThreshold) { - returnedVoice = ref; - break; - } - - // Jump over the sister voices in the set - do { idx++; } - while (idx < voiceViewArray.size() && sisterVoices(ref, voiceViewArray[idx])); - } - - auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); - SisterVoiceRing::applyToRing(returnedVoice, [&] (Voice* v) { - renderVoiceToOutputs(*v, *tempSpan); - v->reset(); - }); - ASSERT(returnedVoice->isFree()); - - return returnedVoice; + return {}; } int sfz::Synth::getNumActiveVoices() const noexcept @@ -716,7 +675,6 @@ void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames()); } } - } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept @@ -889,9 +847,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { unsigned notePolyphonyCounter { 0 }; - unsigned regionPolyphonyCounter { 0 }; Voice* selfMaskCandidate { nullptr }; Voice* selectedVoice { nullptr }; + regionPolyphonyArray.clear(); for (auto& voice : voices) { if (voice->isFree()) { @@ -900,8 +858,9 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc continue; } - if (voice->getRegion() == region) - regionPolyphonyCounter += 1; + if (voice->getRegion() == region) { + regionPolyphonyArray.push_back(voice.get()); + } if (region->notePolyphony) { if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { @@ -925,36 +884,58 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - // FIXME: Do something for the polyphony limit - if (regionPolyphonyCounter >= region->polyphony) - continue; - - // FIXME: Do something for the polyphony limit - if (polyphonyGroups[region->group].getActiveVoices().size() - == polyphonyGroups[region->group].getPolyphonyLimit()) - continue; - - // FIXME: Do something for the polyphony limit auto parent = region->parent; - bool polyphonyReached { false }; + + // Polyphony reached on region + if (regionPolyphonyArray.size() >= region->polyphony) { + selectedVoice = stealer.steal(absl::MakeSpan(regionPolyphonyArray)); + goto render; + } + + // Polyphony reached on polyphony group + if (polyphonyGroups[region->group].getActiveVoices().size() + == polyphonyGroups[region->group].getPolyphonyLimit()) { + const auto activeVoices = absl::MakeSpan(polyphonyGroups[region->group].getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; + } + + // Polyphony reached some parent group/master/etc while (parent != nullptr) { if (parent->getActiveVoices().size() >= parent->getPolyphonyLimit()) { - polyphonyReached = true; - break; + const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; } - parent = parent->getParent(); } - if (polyphonyReached) - continue; + // Polyphony reached on note_polyphony if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { - if (selfMaskCandidate != nullptr) - selfMaskCandidate->release(delay); - else // We're the lowest velocity guy here - continue; + if (selfMaskCandidate == nullptr) + continue; // We're the lowest velocity guy here + selectedVoice = selfMaskCandidate; + goto render; } + // Engine polyphony reached, we're stealing something + if (selectedVoice == nullptr) { + selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); + } + + render: + // Kill voice if necessary, pre-rendering it into the output buffers + ASSERT(selectedVoice); + if (!selectedVoice->isFree()) { + auto tempSpan = resources.bufferPool.getStereoBuffer(samplesPerBlock); + SisterVoiceRing::applyToRing(selectedVoice, [&] (Voice* v) { + renderVoiceToOutputs(*v, *tempSpan); + v->reset(); + }); + } + + // Voice should be free now + ASSERT(selectedVoice->isFree()); selectedVoice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); ring.addVoiceToRing(selectedVoice); RegionSet::registerVoiceInHierarchy(region, selectedVoice); @@ -1304,6 +1285,9 @@ void sfz::Synth::resetVoices(int numVoices) voiceViewArray.clear(); voiceViewArray.reserve(numVoices); + regionPolyphonyArray.clear(); + regionPolyphonyArray.reserve(numVoices); + for (auto& voice : voices) { voice->setSampleRate(this->sampleRate); voice->setSamplesPerBlock(this->samplesPerBlock); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 3c08e954..bcd79542 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -16,6 +16,7 @@ #include "MidiState.h" #include "AudioSpan.h" #include "parser/Parser.h" +#include "VoiceStealing.h" #include "absl/types/span.h" #include #include @@ -714,6 +715,9 @@ private: std::vector polyphonyGroups; // Views to speed up iteration over the regions and voices when events // occur in the audio callback + VoiceViewVector regionPolyphonyArray; + VoiceStealing stealer; + VoiceViewVector voiceViewArray; std::array noteActivationLists; std::array ccActivationLists; diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h index 8cf4362b..6914d1b0 100644 --- a/src/sfizz/VoiceStealing.h +++ b/src/sfizz/VoiceStealing.h @@ -3,6 +3,9 @@ // This code is part of the sfizz library and is licensed under a BSD 2-clause // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + #include "Config.h" #include "Voice.h" #include "SisterVoiceRing.h" From 825116063386782b7ea19ffd56900ac0d971377b Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 23 Jun 2020 00:14:15 +0200 Subject: [PATCH 18/27] Put stuff into cpp files and add docs --- src/CMakeLists.txt | 3 + src/sfizz/PolyphonyGroup.cpp | 18 +++++ src/sfizz/PolyphonyGroup.h | 54 +++++++++----- src/sfizz/RegionSet.cpp | 48 +++++++++++++ src/sfizz/RegionSet.h | 134 +++++++++++++++++++++++------------ src/sfizz/SisterVoiceRing.h | 8 +-- src/sfizz/VoiceStealing.cpp | 45 ++++++++++++ src/sfizz/VoiceStealing.h | 52 +++----------- 8 files changed, 250 insertions(+), 112 deletions(-) create mode 100644 src/sfizz/PolyphonyGroup.cpp create mode 100644 src/sfizz/RegionSet.cpp create mode 100644 src/sfizz/VoiceStealing.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 67c3b36f..554915fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,9 @@ set (SFIZZ_SOURCES sfizz/Smoothers.cpp sfizz/Wavetables.cpp sfizz/Tuning.cpp + sfizz/RegionSet.cpp + sfizz/PolyphonyGroup.cpp + sfizz/VoiceStealing.cpp sfizz/RTSemaphore.cpp sfizz/Panning.cpp sfizz/Effects.cpp diff --git a/src/sfizz/PolyphonyGroup.cpp b/src/sfizz/PolyphonyGroup.cpp new file mode 100644 index 00000000..7ac1e3d6 --- /dev/null +++ b/src/sfizz/PolyphonyGroup.cpp @@ -0,0 +1,18 @@ +#include "PolyphonyGroup.h" + +void sfz::PolyphonyGroup::setPolyphonyLimit(unsigned limit) noexcept +{ + polyphonyLimit = limit; + voices.reserve(limit); +} + +void sfz::PolyphonyGroup::registerVoice(Voice* voice) noexcept +{ + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); +} + +void sfz::PolyphonyGroup::removeVoice(const Voice* voice) noexcept +{ + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); +} diff --git a/src/sfizz/PolyphonyGroup.h b/src/sfizz/PolyphonyGroup.h index 74314da7..d3e1413b 100644 --- a/src/sfizz/PolyphonyGroup.h +++ b/src/sfizz/PolyphonyGroup.h @@ -15,23 +15,43 @@ namespace sfz { class PolyphonyGroup { public: - void setPolyphonyLimit(unsigned limit) - { - polyphonyLimit = limit; - voices.reserve(limit); - } - unsigned getPolyphonyLimit() const { return polyphonyLimit; } - void registerVoice(Voice* voice) - { - if (absl::c_find(voices, voice) == voices.end()) - voices.push_back(voice); - } - void removeVoice(const Voice* voice) - { - swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); - } - const std::vector& getActiveVoices() const { return voices; } - std::vector& getActiveVoices() { return voices; } + /** + * @brief Set the polyphony limit for this polyphony group. + * + * @param limit + */ + void setPolyphonyLimit(unsigned limit) noexcept; + /** + * @brief Register an active voice in this polyphony group. + * + * @param voice + */ + void registerVoice(Voice* voice) noexcept; + /** + * @brief Remove a voice from this polyphony group. + * If the voice was not registered before, this has no effect. + * + * @param voice + */ + void removeVoice(const Voice* voice) noexcept; + /** + * @brief Get the polyphony limit for this group + * + * @return unsigned + */ + unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; } + /** + * @brief Get the active voices + * + * @return const std::vector& + */ + const std::vector& getActiveVoices() const noexcept { return voices; } + /** + * @brief Get the active voices + * + * @return std::vector& + */ + std::vector& getActiveVoices() noexcept { return voices; } private: unsigned polyphonyLimit { config::maxVoices }; std::vector voices; diff --git a/src/sfizz/RegionSet.cpp b/src/sfizz/RegionSet.cpp new file mode 100644 index 00000000..d6f8a585 --- /dev/null +++ b/src/sfizz/RegionSet.cpp @@ -0,0 +1,48 @@ +#include "RegionSet.h" + +void sfz::RegionSet::setPolyphonyLimit(unsigned limit) noexcept +{ + polyphonyLimit = limit; + voices.reserve(limit); +} + +void sfz::RegionSet::addRegion(Region* region) noexcept +{ + if (absl::c_find(regions, region) == regions.end()) + regions.push_back(region); +} + +void sfz::RegionSet::addSubset(RegionSet* group) noexcept +{ + if (absl::c_find(subsets, group) == subsets.end()) + subsets.push_back(group); +} + +void sfz::RegionSet::registerVoice(Voice* voice) noexcept +{ + if (absl::c_find(voices, voice) == voices.end()) + voices.push_back(voice); +} + +void sfz::RegionSet::removeVoice(const Voice* voice) noexcept +{ + swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); +} + +void sfz::RegionSet::registerVoiceInHierarchy(const Region* region, Voice* voice) noexcept +{ + auto* parent = region->parent; + while (parent != nullptr) { + parent->registerVoice(voice); + parent = parent->getParent(); + } +} + +void sfz::RegionSet::removeVoiceFromHierarchy(const Region* region, const Voice* voice) noexcept +{ + auto* parent = region->parent; + while (parent != nullptr) { + parent->removeVoice(voice); + parent = parent->getParent(); + } +} diff --git a/src/sfizz/RegionSet.h b/src/sfizz/RegionSet.h index 5203442b..24120173 100644 --- a/src/sfizz/RegionSet.h +++ b/src/sfizz/RegionSet.h @@ -16,53 +16,93 @@ namespace sfz class RegionSet { public: - void setPolyphonyLimit(unsigned limit) - { - polyphonyLimit = limit; - voices.reserve(limit); - } - unsigned getPolyphonyLimit() const { return polyphonyLimit; } - void addRegion(Region* region) - { - if (absl::c_find(regions, region) == regions.end()) - regions.push_back(region); - } - void addSubset(RegionSet* group) - { - if (absl::c_find(subsets, group) == subsets.end()) - subsets.push_back(group); - } - void registerVoice(Voice* voice) - { - if (absl::c_find(voices, voice) == voices.end()) - voices.push_back(voice); - } - void removeVoice(const Voice* voice) - { - swapAndPopFirst(voices, [voice](const Voice* v) { return v == voice; }); - } - static void registerVoiceInHierarchy(const Region* region, Voice* voice) - { - auto* parent = region->parent; - while (parent != nullptr) { - parent->registerVoice(voice); - parent = parent->getParent(); - } - } - static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) - { - auto* parent = region->parent; - while (parent != nullptr) { - parent->removeVoice(voice); - parent = parent->getParent(); - } - } - RegionSet* getParent() const { return parent; } - void setParent(RegionSet* parent) { this->parent = parent; } - const std::vector& getActiveVoices() const { return voices; } - std::vector& getActiveVoices() { return voices; } - const std::vector& getRegions() const { return regions; } - const std::vector& getSubsets() const { return subsets; } + /** + * @brief Set the polyphony limit for the set + * + * @param limit + */ + void setPolyphonyLimit(unsigned limit) noexcept; + /** + * @brief Add a region to the set + * + * @param region + */ + void addRegion(Region* region) noexcept; + /** + * @brief Add a subset to the set + * + * @param group + */ + void addSubset(RegionSet* group) noexcept; + /** + * @brief Register a voice as active in this set + * + * @param voice + */ + void registerVoice(Voice* voice) noexcept; + /** + * @brief Remove an active voice for this set. + * If the voice was not registered this has no effect. + * + * @param voice + */ + void removeVoice(const Voice* voice) noexcept; + /** + * @brief Register a voice in the whole parent hierarchy of the region + * + * @param region + * @param voice + */ + static void registerVoiceInHierarchy(const Region* region, Voice* voice) noexcept; + /** + * @brief Remove an active voice from the whole parent hierarchy of the region. + * + * @param region + * @param voice + */ + static void removeVoiceFromHierarchy(const Region* region, const Voice* voice) noexcept; + /** + * @brief Get the polyphony limit + * + * @return unsigned + */ + unsigned getPolyphonyLimit() const noexcept { return polyphonyLimit; } + /** + * @brief Get the parent set + * + * @return RegionSet* + */ + RegionSet* getParent() const noexcept { return parent; } + /** + * @brief Set the parent set + * + * @param parent + */ + void setParent(RegionSet* parent) noexcept { this->parent = parent; } + /** + * @brief Get the active voices + * + * @return const std::vector& + */ + const std::vector& getActiveVoices() const noexcept { return voices; } + /** + * @brief Get the active voices + * + * @return std::vector& + */ + std::vector& getActiveVoices() noexcept { return voices; } + /** + * @brief Get the regions in the set + * + * @return const std::vector& + */ + const std::vector& getRegions() const noexcept { return regions; } + /** + * @brief Get the region subsets in this set + * + * @return const std::vector& + */ + const std::vector& getSubsets() const noexcept { return subsets; } private: RegionSet* parent { nullptr }; std::vector regions; diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 7bd4c0ef..4df6ec4d 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -14,7 +14,7 @@ namespace sfz struct SisterVoiceRing { template>::value, int> = 0> - static void applyToRing(T* voice, F&& lambda) + static void applyToRing(T* voice, F&& lambda) noexcept { auto v = voice->getNextSisterVoice(); while (v != voice) { @@ -25,7 +25,7 @@ struct SisterVoiceRing { lambda(voice); } - static unsigned countSisterVoices(const Voice* start) + static unsigned countSisterVoices(const Voice* start) noexcept { if (!start) return 0; @@ -50,7 +50,7 @@ struct SisterVoiceRing { */ class SisterVoiceRingBuilder { public: - ~SisterVoiceRingBuilder() { + ~SisterVoiceRingBuilder() noexcept { if (lastStartedVoice != nullptr) { ASSERT(firstStartedVoice); lastStartedVoice->setNextSisterVoice(firstStartedVoice); @@ -63,7 +63,7 @@ public: * * @param voice */ - void addVoiceToRing(Voice* voice) { + void addVoiceToRing(Voice* voice) noexcept { if (firstStartedVoice == nullptr) firstStartedVoice = voice; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp new file mode 100644 index 00000000..a90e4950 --- /dev/null +++ b/src/sfizz/VoiceStealing.cpp @@ -0,0 +1,45 @@ +#include "VoiceStealing.h" + +sfz::VoiceStealing::VoiceStealing() +{ + voiceScores.reserve(config::maxVoices); +} + +sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept +{ + // Start of the voice stealing algorithm + absl::c_sort(voices, voiceOrdering); + + const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { + return sum + v->getAverageEnvelope(); + }); + const auto envThreshold = sumEnvelope + / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; + + Voice* returnedVoice = voices.front(); + unsigned idx = 0; + while (idx < voices.size()) { + const auto ref = voices[idx]; + + if (ref->getAge() < ageThreshold) { + // Went too far, we'll kill the oldest note. + break; + } + + float maxEnvelope { 0.0f }; + SisterVoiceRing::applyToRing(ref, [&](Voice* v) { + maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); + }); + + if (maxEnvelope < envThreshold) { + returnedVoice = ref; + break; + } + + // Jump over the sister voices in the set + do { idx++; } + while (idx < voices.size() && sisterVoices(ref, voices[idx])); + } + return returnedVoice; +} diff --git a/src/sfizz/VoiceStealing.h b/src/sfizz/VoiceStealing.h index 6914d1b0..7251f37c 100644 --- a/src/sfizz/VoiceStealing.h +++ b/src/sfizz/VoiceStealing.h @@ -17,50 +17,14 @@ namespace sfz class VoiceStealing { public: - VoiceStealing() - { - voiceScores.reserve(config::maxVoices); - } - - Voice* steal(absl::Span voices) noexcept - { - // Start of the voice stealing algorithm - absl::c_sort(voices, voiceOrdering); - - const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { - return sum + v->getAverageEnvelope(); - }); - const auto envThreshold = sumEnvelope - / static_cast(voices.size()) * config::stealingEnvelopeCoeff; - const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; - - Voice* returnedVoice = voices.front(); - unsigned idx = 0; - while (idx < voices.size()) { - const auto ref = voices[idx]; - - if (ref->getAge() < ageThreshold) { - // Went too far, we'll kill the oldest note. - break; - } - - float maxEnvelope { 0.0f }; - SisterVoiceRing::applyToRing(ref, [&](Voice* v) { - maxEnvelope = max(maxEnvelope, v->getAverageEnvelope()); - }); - - if (maxEnvelope < envThreshold) { - returnedVoice = ref; - break; - } - - // Jump over the sister voices in the set - do { idx++; } - while (idx < voices.size() && sisterVoices(ref, voices[idx])); - } - return returnedVoice; - } - + VoiceStealing(); + /** + * @brief Propose a voice to steal from a set of voices + * + * @param voices + * @return Voice* + */ + Voice* steal(absl::Span voices) noexcept; private: struct VoiceScore { From 498e1b95bbaab306c14136e5ce699ff9e0d0f0bc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 23 Jun 2020 00:21:30 +0200 Subject: [PATCH 19/27] Correct the self-masking behavior --- src/sfizz/Synth.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 500e4edc..90542333 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -884,6 +884,14 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } + // Polyphony reached on note_polyphony + if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { + if (selfMaskCandidate != nullptr) + selfMaskCandidate->release(delay); + else // We're the lowest velocity guy here + continue; + } + auto parent = region->parent; // Polyphony reached on region @@ -910,14 +918,6 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc parent = parent->getParent(); } - // Polyphony reached on note_polyphony - if (region->notePolyphony && notePolyphonyCounter >= *region->notePolyphony) { - if (selfMaskCandidate == nullptr) - continue; // We're the lowest velocity guy here - selectedVoice = selfMaskCandidate; - goto render; - } - // Engine polyphony reached, we're stealing something if (selectedVoice == nullptr) { selectedVoice = stealer.steal(absl::MakeSpan(voiceViewArray)); From 1e6cb8245e56fb297eb885726e60e35d6a7bc78d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 23 Jun 2020 00:21:53 +0200 Subject: [PATCH 20/27] Updated a test that did not account for sister voices --- tests/PolyphonyT.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp index 815b6c23..23716cab 100644 --- a/tests/PolyphonyT.cpp +++ b/tests/PolyphonyT.cpp @@ -129,11 +129,14 @@ TEST_CASE("[Polyphony] Hierarchy polyphony limits (limit in another master)") sample=*saw key=65 polyphony=5 - sample=*sine key=65 + sample=*sine key=66 )"); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); synth.noteOn(0, 65, 64); + synth.noteOn(0, 66, 64); + synth.noteOn(0, 66, 64); + synth.noteOn(0, 66, 64); REQUIRE(synth.getNumActiveVoices() == 5); } From b8a23ee205937f1e9a68d62c3677decc83970fc7 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Thu, 2 Jul 2020 10:08:58 +0200 Subject: [PATCH 21/27] Add comments and a helper function to test ring validity --- src/sfizz/SisterVoiceRing.h | 62 +++++++++++++++++++++++++++++++++++++ src/sfizz/Synth.cpp | 2 +- src/sfizz/VoiceStealing.cpp | 10 +++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 4df6ec4d..78935335 100644 --- a/src/sfizz/SisterVoiceRing.h +++ b/src/sfizz/SisterVoiceRing.h @@ -12,6 +12,14 @@ namespace sfz { struct SisterVoiceRing { + /** + * @brief Apply a lambda function to all sisters in a ring. + * This function should be robust enough to be able to kill the voice + * in the lambda. + * + * @param voice + * @param lambda + */ template>::value, int> = 0> static void applyToRing(T* voice, F&& lambda) noexcept @@ -25,6 +33,12 @@ struct SisterVoiceRing { lambda(voice); } + /** + * @brief Count the number of sister voices in a ring + * + * @param start + * @return unsigned + */ static unsigned countSisterVoices(const Voice* start) noexcept { if (!start) @@ -41,6 +55,54 @@ struct SisterVoiceRing { ASSERT(count < config::maxVoices); return count; } + + /** + * @brief Check if a sister voice ring is well formed + * + * @param start + * @return true + * @return false + */ + static bool checkRingValidity(const Voice* start) noexcept + { + if (start == nullptr) + return true; + + unsigned idx { 0 }; + const Voice* ring[config::maxVoices]; + ring[idx] = start; + while (idx < config::maxVoices) { + const auto* newVoice = ring[idx]->getNextSisterVoice(); + + if (newVoice == nullptr) { + DBG("Error in ring: " << static_cast(ring[idx]) + << " next sister is null"); + return false; + } + + if (newVoice->getPreviousSisterVoice() != ring[idx]) { + DBG("Error in ring: " << static_cast(newVoice) + << " refers " << static_cast(newVoice->getPreviousSisterVoice()) + << " as previous sister voice instead of " + << static_cast(ring[idx])); + return false; + } + + if (newVoice == start) + break; + + for (unsigned i = 1; i < idx; ++i) { + if (ring[i] == newVoice) { + DBG("Error in ring: " << static_cast(newVoice) + << " already present in ring at index " << i); + return false; + } + } + ring[++idx] = newVoice; + } + + return true; + } }; /** diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 90542333..ded6eeaa 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -730,7 +730,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept callbackBreakdown.panning += voice->getLastPanningDuration(); if (voice->toBeCleanedUp()) - voice->reset(); + voice->reset(); } } diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp index a90e4950..b4eec3aa 100644 --- a/src/sfizz/VoiceStealing.cpp +++ b/src/sfizz/VoiceStealing.cpp @@ -13,16 +13,24 @@ sfz::Voice* sfz::VoiceStealing::steal(absl::Span voices) noexcept const auto sumEnvelope = absl::c_accumulate(voices, 0.0f, [](float sum, const Voice* v) { return sum + v->getAverageEnvelope(); }); + // We are checking the envelope to try and kill voices with relative low contribution + // to the output compared to the rest. const auto envThreshold = sumEnvelope / static_cast(voices.size()) * config::stealingEnvelopeCoeff; + // We are checking the age so that voices have the time to build up attack + // This is not perfect because pad-type voices will take a long time to output + // their sound, but it's reasonable for sounds with a quick attack and longer + // release. const auto ageThreshold = voices.front()->getAge() * config::stealingAgeCoeff; + // This needs to be positive + ASSERT(ageThreshold >= 0); Voice* returnedVoice = voices.front(); unsigned idx = 0; while (idx < voices.size()) { const auto ref = voices[idx]; - if (ref->getAge() < ageThreshold) { + if (ref->getAge() <= ageThreshold) { // Went too far, we'll kill the oldest note. break; } From 17a64cc665086f672e2a43f0742f1e5f987c441b Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 4 Jul 2020 09:41:53 +0200 Subject: [PATCH 22/27] lv2: Add time:Position to supported atoms --- lv2/sfizz.ttl.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index ef56b35b..217e56d9 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -12,6 +12,7 @@ @prefix rdf: . @prefix rdfs: . @prefix state: . +@prefix time: . @prefix units: . @prefix urid: . @prefix work: . @@ -82,7 +83,7 @@ midnam:update a lv2:Feature . lv2:port [ a lv2:InputPort, atom:AtomPort ; atom:bufferType atom:Sequence ; - atom:supports patch:Message, midi:MidiEvent ; + atom:supports patch:Message, midi:MidiEvent, time:Position ; lv2:designation lv2:control ; lv2:index 0 ; lv2:symbol "control" ; From d019bb1a93e42c691c456d8522e88349e52c05da Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 4 Jul 2020 09:18:49 +0200 Subject: [PATCH 23/27] lv2: add state path mapping --- lv2/sfizz.c | 81 ++++++++++++++++++++++++++++++++++++++------- lv2/sfizz.ttl.in | 2 +- src/sfizz/Synth.cpp | 6 +++- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 2271a466..9432bb84 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -911,9 +911,15 @@ restore(LV2_Handle instance, const LV2_Feature *const *features) { UNUSED(flags); - UNUSED(features); sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + LV2_State_Map_Path *map_path = NULL; + for (const LV2_Feature *const *f = features; *f; ++f) + { + if (!strcmp((*f)->URI, LV2_STATE__mapPath)) + map_path = (LV2_State_Map_Path *)(**f).data; + } + // Fetch back the saved file path, if any size_t size; uint32_t type; @@ -922,24 +928,46 @@ restore(LV2_Handle instance, value = retrieve(handle, self->sfizz_sfz_file_uri, &size, &type, &val_flags); if (value) { - lv2_log_note(&self->logger, "[sfizz] Restoring the file %s\n", (const char *)value); - sfizz_lv2_load_file(instance, (const char *)value); + const char *path = (const char *)value; + if (map_path) + { + path = map_path->absolute_path(map_path->handle, path); + if (!path) + return LV2_STATE_ERR_UNKNOWN; + } + + lv2_log_note(&self->logger, "[sfizz] Restoring the file %s\n", path); + sfizz_lv2_load_file(instance, path); + + if (map_path) + free((char *)path); } value = retrieve(handle, self->sfizz_scala_file_uri, &size, &type, &val_flags); if (value) { - if (sfizz_load_scala_file(self->synth, (const char *)value)) + const char *path = (const char *)value; + if (map_path) + { + path = map_path->absolute_path(map_path->handle, path); + if (!path) + return LV2_STATE_ERR_UNKNOWN; + } + + if (sfizz_load_scala_file(self->synth, path)) { lv2_log_note(&self->logger, - "[sfizz] Restoring the scale %s\n", (const char *)value); - strcpy(self->scala_file_path, (const char *)value); + "[sfizz] Restoring the scale %s\n", path); + strcpy(self->scala_file_path, path); } else { lv2_log_error(&self->logger, - "[sfizz] Error while restoring the scale %s\n", (const char *)value); + "[sfizz] Error while restoring the scale %s\n", path); } + + if (map_path) + free((char *)path); } value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags); @@ -988,23 +1016,52 @@ save(LV2_Handle instance, const LV2_Feature *const *features) { UNUSED(flags); - UNUSED(features); sfizz_plugin_t *self = (sfizz_plugin_t *)instance; + + LV2_State_Map_Path *map_path = NULL; + for (const LV2_Feature *const *f = features; *f; ++f) + { + if (!strcmp((*f)->URI, LV2_STATE__mapPath)) + map_path = (LV2_State_Map_Path *)(**f).data; + } + + const char *path; + // Save the file path + path = self->sfz_file_path; + if (map_path) + { + path = map_path->abstract_path(map_path->handle, path); + if (!path) + return LV2_STATE_ERR_UNKNOWN; + } store(handle, self->sfizz_sfz_file_uri, - self->sfz_file_path, - strlen(self->sfz_file_path) + 1, + path, + strlen(path) + 1, self->atom_path_uri, LV2_STATE_IS_POD); + if (map_path) + free((char *)path); // Save the scala file path + path = self->scala_file_path; + if (map_path) + { + path = map_path->abstract_path(map_path->handle, path); + if (!path) + return LV2_STATE_ERR_UNKNOWN; + } + if (!path) + return LV2_STATE_ERR_UNKNOWN; store(handle, self->sfizz_scala_file_uri, - self->scala_file_path, - strlen(self->scala_file_path) + 1, + path, + strlen(path) + 1, self->atom_path_uri, LV2_STATE_IS_POD); + if (map_path) + free((char *)path); // Save the number of voices store(handle, diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index ef56b35b..7e4acab1 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -67,7 +67,7 @@ midnam:update a lv2:Feature . lv2:microVersion @LV2PLUGIN_VERSION_MICRO@ ; lv2:requiredFeature urid:map, bufsize:boundedBlockLength, work:schedule ; - lv2:optionalFeature lv2:hardRTCapable, opts:options ; + lv2:optionalFeature lv2:hardRTCapable, opts:options, state:mapPath ; lv2:extensionData opts:interface, state:interface, work:interface ; lv2:optionalFeature midnam:update ; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 766c1786..49342ca6 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -331,7 +331,11 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) clear(); const std::lock_guard disableCallback { callbackGuard }; - parser.parseFile(file); + + std::error_code ec; + fs::path realFile = fs::canonical(file, ec); + + parser.parseFile(ec ? file : realFile); if (parser.getErrorCount() > 0) return false; From a293ccdc09b3e16ebf3bda3005dbbb8b1fd54bc4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 4 Jul 2020 10:44:39 +0200 Subject: [PATCH 24/27] lv2: implement use of the freePath feature --- lv2/sfizz.c | 28 ++++++++++++++++++++++++---- lv2/sfizz.ttl.in | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 9432bb84..e779a72b 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -179,6 +179,20 @@ enum SFIZZ_STRETCH_TUNING = 11, }; +static void +sfizz_lv2_state_free_path(LV2_State_Free_Path_Handle handle, + char *path) +{ + (void)handle; + free(path); +} + +static LV2_State_Free_Path sfizz_State_Free_Path = +{ + .handle = NULL, + .free_path = &sfizz_lv2_state_free_path, +}; + static void sfizz_lv2_map_required_uris(sfizz_plugin_t *self) { @@ -914,10 +928,13 @@ restore(LV2_Handle instance, sfizz_plugin_t *self = (sfizz_plugin_t *)instance; LV2_State_Map_Path *map_path = NULL; + LV2_State_Free_Path *free_path = &sfizz_State_Free_Path; for (const LV2_Feature *const *f = features; *f; ++f) { if (!strcmp((*f)->URI, LV2_STATE__mapPath)) map_path = (LV2_State_Map_Path *)(**f).data; + else if (!strcmp((*f)->URI, LV2_STATE__freePath)) + free_path = (LV2_State_Free_Path *)(**f).data; } // Fetch back the saved file path, if any @@ -940,7 +957,7 @@ restore(LV2_Handle instance, sfizz_lv2_load_file(instance, path); if (map_path) - free((char *)path); + free_path->free_path(free_path->handle, (char *)path); } value = retrieve(handle, self->sfizz_scala_file_uri, &size, &type, &val_flags); @@ -967,7 +984,7 @@ restore(LV2_Handle instance, } if (map_path) - free((char *)path); + free_path->free_path(free_path->handle, (char *)path); } value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags); @@ -1019,10 +1036,13 @@ save(LV2_Handle instance, sfizz_plugin_t *self = (sfizz_plugin_t *)instance; LV2_State_Map_Path *map_path = NULL; + LV2_State_Free_Path *free_path = &sfizz_State_Free_Path; for (const LV2_Feature *const *f = features; *f; ++f) { if (!strcmp((*f)->URI, LV2_STATE__mapPath)) map_path = (LV2_State_Map_Path *)(**f).data; + else if (!strcmp((*f)->URI, LV2_STATE__freePath)) + free_path = (LV2_State_Free_Path *)(**f).data; } const char *path; @@ -1042,7 +1062,7 @@ save(LV2_Handle instance, self->atom_path_uri, LV2_STATE_IS_POD); if (map_path) - free((char *)path); + free_path->free_path(free_path->handle, (char *)path); // Save the scala file path path = self->scala_file_path; @@ -1061,7 +1081,7 @@ save(LV2_Handle instance, self->atom_path_uri, LV2_STATE_IS_POD); if (map_path) - free((char *)path); + free_path->free_path(free_path->handle, (char *)path); // Save the number of voices store(handle, diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index 7e4acab1..837df85d 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -67,7 +67,7 @@ midnam:update a lv2:Feature . lv2:microVersion @LV2PLUGIN_VERSION_MICRO@ ; lv2:requiredFeature urid:map, bufsize:boundedBlockLength, work:schedule ; - lv2:optionalFeature lv2:hardRTCapable, opts:options, state:mapPath ; + lv2:optionalFeature lv2:hardRTCapable, opts:options, state:mapPath, state:freePath ; lv2:extensionData opts:interface, state:interface, work:interface ; lv2:optionalFeature midnam:update ; From c2dbd8929a16d3dab24b7e80e32f53a247e43ee9 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 4 Jul 2020 13:59:44 +0200 Subject: [PATCH 25/27] Differentiate between gnoise and noise in generators --- src/sfizz/Config.h | 1 + src/sfizz/Voice.cpp | 16 ++++++++++++++-- src/sfizz/Voice.h | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 918264ca..903eca64 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -78,6 +78,7 @@ namespace config { constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; constexpr int oscillatorsPerVoice { 9 }; + constexpr float uniformNoiseBounds { 0.25f }; constexpr float noiseVariance { 0.25f }; /** Minimum interval in frames between recomputations of coefficients of the diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index df3a46b6..4f23fd5d 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -609,8 +609,20 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept const auto rightSpan = buffer.getSpan(1); if (region->sampleId.filename() == "*noise") { - absl::c_generate(leftSpan, noiseDist); - absl::c_generate(rightSpan, noiseDist); + auto gen = [&]() { + return uniformNoiseDist(Random::randomGenerator); + }; + absl::c_generate(leftSpan, gen); + absl::c_generate(rightSpan, gen); + } else if (region->sampleId.filename() == "*gnoise") { + // You need to wrap in a lambda, otherwise generate will + // make a copy of the gaussian distribution *along with its state* + // leading to periodic behavior.... + auto gen = [&]() { + return gaussianNoiseDist(); + }; + absl::c_generate(leftSpan, gen); + absl::c_generate(rightSpan, gen); } else { const auto numFrames = buffer.getNumFrames(); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 54b03318..40accaf1 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -467,7 +467,8 @@ private: Voice* nextSisterVoice { this }; Voice* previousSisterVoice { this }; - fast_gaussian_generator noiseDist { 0.0f, config::noiseVariance }; + fast_real_distribution uniformNoiseDist { -config::uniformNoiseBounds, config::uniformNoiseBounds }; + fast_gaussian_generator gaussianNoiseDist { 0.0f, config::noiseVariance }; ModifierArray> modifierSmoothers; Smoother gainSmoother; From a4e55185420961d359de7db502924af237354a42 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 4 Jul 2020 14:44:10 +0200 Subject: [PATCH 26/27] Correct MSVC errors and warnings --- src/sfizz/MathHelpers.h | 1 + src/sfizz/Region.cpp | 3 ++- src/sfizz/Synth.cpp | 6 ++++-- tests/InterpolatorsT.cpp | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sfizz/MathHelpers.h b/src/sfizz/MathHelpers.h index b446d873..f0dbebc4 100644 --- a/src/sfizz/MathHelpers.h +++ b/src/sfizz/MathHelpers.h @@ -15,6 +15,7 @@ #include "SIMDConfig.h" #include "absl/types/span.h" #include +#include #include #include #include diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 424269f7..bbfd5e8e 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -409,8 +409,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) if (opcode.parameters.back() > 127) return false; + const auto inputVelocity = static_cast(opcode.parameters.back()); if (value) - velocityPoints.emplace_back(opcode.parameters.back(), *value); + velocityPoints.emplace_back(inputVelocity, *value); } break; case hash("xfin_lokey"): diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 0016f300..02793692 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -301,8 +301,10 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) ccLabels.emplace_back(member.parameters.back(), std::string(member.value)); break; case hash("label_key&"): - if (Default::keyRange.containsWithEnd(member.parameters.back())) - keyLabels.emplace_back(member.parameters.back(), std::string(member.value)); + if (member.parameters.back() <= Default::keyRange.getEnd()) { + const auto noteNumber = static_cast(member.parameters.back()); + keyLabels.emplace_back(noteNumber, std::string(member.value)); + } break; case hash("default_path"): defaultPath = absl::StrReplaceAll(trim(member.value), { { "\\", "/" } }); diff --git a/tests/InterpolatorsT.cpp b/tests/InterpolatorsT.cpp index 04742547..93193a12 100644 --- a/tests/InterpolatorsT.cpp +++ b/tests/InterpolatorsT.cpp @@ -6,7 +6,8 @@ #include "sfizz/Interpolators.h" #include "catch2/catch.hpp" -#include +#include +#include using namespace Catch::literals; TEST_CASE("[Interpolators] Sample at points") From 007fcbb3a3cbfca21766595ba2048dec7dad21c8 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 4 Jul 2020 18:55:39 +0200 Subject: [PATCH 27/27] Deactivate the gain smoothing --- 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 9268cb89..65cc2286 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -41,7 +41,7 @@ namespace config { constexpr int numVoices { 64 }; constexpr unsigned maxVoices { 256 }; constexpr unsigned smoothingSteps { 512 }; - constexpr uint8_t gainSmoothing { 5 }; + constexpr uint8_t gainSmoothing { 0 }; constexpr unsigned powerTableSizeExponent { 11 }; constexpr int maxFilePromises { maxVoices }; constexpr int sustainCC { 64 };