diff --git a/lv2/sfizz.c b/lv2/sfizz.c index 2271a466..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) { @@ -911,9 +925,18 @@ 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; + 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 size_t size; uint32_t type; @@ -922,24 +945,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_path->free_path(free_path->handle, (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_path->free_path(free_path->handle, (char *)path); } value = retrieve(handle, self->sfizz_num_voices_uri, &size, &type, &val_flags); @@ -988,23 +1033,55 @@ 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; + 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; + // 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_path->free_path(free_path->handle, (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_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 ef56b35b..6c661987 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: . @@ -67,7 +68,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, state:freePath ; lv2:extensionData opts:interface, state:interface, work:interface ; lv2:optionalFeature midnam:update ; @@ -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" ; 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/Config.h b/src/sfizz/Config.h index c6e2e843..020d2e20 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 allSoundOffCC { 120 }; @@ -77,6 +77,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 @@ -98,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/Defaults.h b/src/sfizz/Defaults.h index 1bcc7771..940b6872 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 }; 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/FilePool.cpp b/src/sfizz/FilePool.cpp index 95519ac6..9bec98ef 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) { @@ -125,11 +131,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,28 +371,36 @@ void sfz::FilePool::tryToClearPromises() void sfz::FilePool::clearingThread() { - while (!quitThread) { + raiseCurrentThreadPriority(); + + 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 { + raiseCurrentThreadPriority(); + FilePromisePtr promise; - while (!quitThread) { + do { + 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 (quitThread) + return; if (!promiseQueue.try_pop(promise)) { continue; @@ -406,13 +425,11 @@ 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); } void sfz::FilePool::clear() @@ -438,12 +455,15 @@ 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); }; - swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear); + if (swapAndPopAll(temporaryFilePromises, promiseUsedOnce, moveToClear) > 0) + semClearingRequest.post(); } void sfz::FilePool::setOversamplingFactor(sfz::Oversampling factor) noexcept @@ -474,12 +494,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 @@ -496,3 +512,35 @@ void sfz::FilePool::waitForBackgroundLoading() noexcept std::this_thread::sleep_for(std::chrono::microseconds(100)); } } + +void sfz::FilePool::raiseCurrentThreadPriority() noexcept +{ +#if defined(_WIN32) + 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 25eaf068..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; @@ -268,13 +273,16 @@ private: atomic_queue::AtomicQueue2 promiseQueue; atomic_queue::AtomicQueue2 filledPromiseQueue; + RTSemaphore semFilledPromiseQueueAvailable { config::maxVoices }; uint32_t preloadSize { config::preloadSize }; Oversampling oversamplingFactor { config::defaultOversamplingFactor }; // Signals volatile bool quitThread { false }; volatile bool emptyQueue { false }; + RTSemaphore semEmptyQueueFinished; std::atomic threadsLoading { 0 }; RTSemaphore workerBarrier; + RTSemaphore semClearingRequest; // File promises data structures along with their guards. std::vector emptyPromises; 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/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/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 new file mode 100644 index 00000000..d3e1413b --- /dev/null +++ b/src/sfizz/PolyphonyGroup.h @@ -0,0 +1,60 @@ +// 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" +#include "absl/algorithm/container.h" + +namespace sfz +{ +class PolyphonyGroup { +public: + /** + * @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/Region.cpp b/src/sfizz/Region.cpp index 12f2ae33..fae61cc5 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; @@ -408,8 +412,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"): @@ -819,25 +824,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/src/sfizz/Region.h b/src/sfizz/Region.h index 09626703..03936bbd 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 @@ -367,6 +371,8 @@ struct Region { ModifierArray> modifiers; bool triggerOnCC { false }; // whether the region triggers on CC events or note events + // Parent + RegionSet* parent { nullptr }; private: const MidiState& midiState; bool keySwitched { true }; 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 new file mode 100644 index 00000000..24120173 --- /dev/null +++ b/src/sfizz/RegionSet.h @@ -0,0 +1,114 @@ +// 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" +#include + +namespace sfz +{ + +class RegionSet { +public: + /** + * @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; + std::vector subsets; + std::vector voices; + unsigned polyphonyLimit { config::maxVoices }; +}; + +} diff --git a/src/sfizz/SisterVoiceRing.h b/src/sfizz/SisterVoiceRing.h index 2f8fac16..78935335 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" @@ -11,9 +12,17 @@ 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) + static void applyToRing(T* voice, F&& lambda) noexcept { auto v = voice->getNextSisterVoice(); while (v != voice) { @@ -24,7 +33,13 @@ struct SisterVoiceRing { lambda(voice); } - static unsigned countSisterVoices(const Voice* start) + /** + * @brief Count the number of sister voices in a ring + * + * @param start + * @return unsigned + */ + static unsigned countSisterVoices(const Voice* start) noexcept { if (!start) return 0; @@ -40,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; + } }; /** @@ -49,7 +112,7 @@ struct SisterVoiceRing { */ class SisterVoiceRingBuilder { public: - ~SisterVoiceRingBuilder() { + ~SisterVoiceRingBuilder() noexcept { if (lastStartedVoice != nullptr) { ASSERT(firstStartedVoice); lastStartedVoice->setNextSisterVoice(firstStartedVoice); @@ -62,7 +125,7 @@ public: * * @param voice */ - void addVoiceToRing(Voice* voice) { + void addVoiceToRing(Voice* voice) noexcept { if (firstStartedVoice == nullptr) firstStartedVoice = voice; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 65f61473..c2c7779c 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -51,14 +51,30 @@ 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); + 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 = OpcodeScope::kOpcodeScopeGlobal; groupOpcodes.clear(); masterOpcodes.clear(); handleGlobalOpcodes(members); @@ -69,11 +85,19 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorgetParent()); + else + newRegionSet(currentSet); + lastHeader = OpcodeScope::kOpcodeScopeGroup; handleGroupOpcodes(members, masterOpcodes); numGroups++; break; @@ -105,6 +129,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 +154,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 +175,10 @@ void sfz::Synth::clear() for (auto& list : ccActivationLists) list.clear(); + lastHeader = OpcodeScope::kOpcodeScopeGlobal; + 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) @@ -237,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), { { "\\", "/" } }); @@ -331,7 +397,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; @@ -437,8 +507,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))) @@ -546,52 +618,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 @@ -652,7 +683,6 @@ void sfz::Synth::renderVoiceToOutputs(Voice& voice, AudioSpan& tempSpan) bus->addToInputs(tempSpan, addGain, tempSpan.getNumFrames()); } } - } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept @@ -708,7 +738,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept callbackBreakdown.panning += voice->getLastPanningDuration(); if (voice->toBeCleanedUp()) - voice->reset(); + voice->reset(); } } @@ -811,6 +841,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); } } } @@ -822,21 +854,25 @@ 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 }; Voice* selfMaskCandidate { nullptr }; + Voice* selectedVoice { nullptr }; + regionPolyphonyArray.clear(); for (auto& voice : voices) { - const auto voiceRegion = voice->getRegion(); - if (voiceRegion == nullptr) + if (voice->isFree()) { + if (selectedVoice == nullptr) + selectedVoice = voice.get(); continue; + } - if (voiceRegion->group == region->group) - activeNotesInGroup += 1; + if (voice->getRegion() == region) { + regionPolyphonyArray.push_back(voice.get()); + } 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) { @@ -856,22 +892,62 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } - if (activeNotesInGroup >= groupMaxPolyphony[region->group]) - continue; - - if (region->notePolyphony && activeNotes >= *region->notePolyphony) { + // 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 voice = findFreeVoice(); - if (voice == nullptr) - continue; + auto parent = region->parent; - voice->startVoice(region, delay, noteNumber, velocity, Voice::TriggerType::NoteOn); - ring.addVoiceToRing(voice); + // 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()) { + const auto activeVoices = absl::MakeSpan(parent->getActiveVoices()); + selectedVoice = stealer.steal(activeVoices); + goto render; + } + parent = parent->getParent(); + } + + // 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); + polyphonyGroups[region->group].registerVoice(selectedVoice); } } } @@ -926,6 +1002,8 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept } ring.addVoiceToRing(voice); + RegionSet::registerVoiceInHierarchy(region, voice); + polyphonyGroups[region->group].registerVoice(voice); } } } @@ -1088,6 +1166,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(); @@ -1127,6 +1215,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; @@ -1207,6 +1300,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); @@ -1235,8 +1331,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); @@ -1348,8 +1446,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..bcd79542 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -9,11 +9,14 @@ #include "Parser.h" #include "Voice.h" #include "Region.h" +#include "RegionSet.h" +#include "PolyphonyGroup.h" #include "Effects.h" #include "LeakDetector.h" #include "MidiState.h" #include "AudioSpan.h" #include "parser/Parser.h" +#include "VoiceStealing.h" #include "absl/types/span.h" #include #include @@ -220,17 +223,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 +597,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 +622,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 +679,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 +700,27 @@ 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 + RegionSet* currentSet; + OpcodeScope lastHeader { OpcodeScope::kOpcodeScopeGlobal }; + 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 regionPolyphonyArray; + VoiceStealing stealer; + + VoiceViewVector voiceViewArray; + std::array noteActivationLists; + std::array ccActivationLists; // Effect factory and buses EffectFactory effectFactory; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 1b10fb3b..11e1941a 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); @@ -606,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; diff --git a/src/sfizz/VoiceStealing.cpp b/src/sfizz/VoiceStealing.cpp new file mode 100644 index 00000000..b4eec3aa --- /dev/null +++ b/src/sfizz/VoiceStealing.cpp @@ -0,0 +1,53 @@ +#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(); + }); + // 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) { + // 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 new file mode 100644 index 00000000..7251f37c --- /dev/null +++ b/src/sfizz/VoiceStealing.h @@ -0,0 +1,54 @@ +// 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 "Config.h" +#include "Voice.h" +#include "SisterVoiceRing.h" +#include +#include "absl/types/span.h" + +namespace sfz +{ +class VoiceStealing +{ +public: + 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 + { + 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; +}; +} 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/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/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") diff --git a/tests/PolyphonyT.cpp b/tests/PolyphonyT.cpp new file mode 100644 index 00000000..23716cab --- /dev/null +++ b/tests/PolyphonyT.cpp @@ -0,0 +1,226 @@ +// 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=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); +} + +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()); +} diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 5d508ca7..bce1012a 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") @@ -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")