From 290860502fec83df2e1b928c2ae79af2d60829dc Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sat, 8 Feb 2020 12:09:19 +0100 Subject: [PATCH 01/20] FilterPool (untested yet) --- src/CMakeLists.txt | 1 + src/sfizz/CCMap.h | 2 + src/sfizz/Config.h | 1 + src/sfizz/Defaults.h | 2 +- src/sfizz/FilterPool.cpp | 97 ++++++++++++++++++++++++++++++++++++++++ src/sfizz/FilterPool.h | 44 ++++++++++++++++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/sfizz/FilterPool.cpp create mode 100644 src/sfizz/FilterPool.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 94d9eff8..b456bf15 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,7 @@ include(CheckLibraryExists) set (SFIZZ_SOURCES sfizz/Synth.cpp sfizz/FilePool.cpp + sfizz/FilterPool.cpp sfizz/Region.cpp sfizz/Voice.cpp sfizz/ScopedFTZ.cpp diff --git a/src/sfizz/CCMap.h b/src/sfizz/CCMap.h index c89df41a..34bf5b7c 100644 --- a/src/sfizz/CCMap.h +++ b/src/sfizz/CCMap.h @@ -86,7 +86,9 @@ public: */ bool contains(int index) const noexcept { return container.find(index) != container.end(); } typename std::map::iterator begin() { return container.begin(); } + typename std::map::const_iterator begin() const { return container.cbegin(); } typename std::map::iterator end() { return container.end(); } + typename std::map::const_iterator end() const { return container.cend(); } private: const ValueType defaultValue; std::map container; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 92765c35..e9818e39 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -53,6 +53,7 @@ namespace config { constexpr uint8_t numCCs { 143 }; constexpr int chunkSize { 1024 }; constexpr float defaultAmpEGRelease { 0.02f }; + constexpr int defaultNumFilters { maxVoices * 2 }; /** Minimum interval in frames between recomputations of coefficients of the modulated filter. The lower, the more CPU resources are consumed. diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 9ed5cf72..2b734d91 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -131,7 +131,7 @@ namespace Default constexpr float filterGain { 0 }; constexpr int filterKeytrack { 0 }; constexpr uint8_t filterKeycenter { 60 }; - constexpr int filterRandom { 60 }; + constexpr int filterRandom { 0 }; constexpr int filterVeltrack { 0 }; constexpr int filterCutoffCC { 0 }; constexpr float filterResonanceCC { 0 }; diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp new file mode 100644 index 00000000..d43ec3e0 --- /dev/null +++ b/src/sfizz/FilterPool.cpp @@ -0,0 +1,97 @@ +#include "FilterPool.h" +#include "SIMDHelpers.h" +#include "absl/algorithm/container.h" + +sfz::FilterHolder::FilterHolder(const MidiState& midiState) +: midiState(midiState) +{ + filter.setChannels(2); +} + +void sfz::FilterHolder::reset() +{ + filter.clear(); +} + +void sfz::FilterHolder::setup(const FilterDescription& description, int noteNumber, uint8_t velocity) +{ + reset(); + this->description = &description; + filter.setType(description.type); + + baseCutoff = description.cutoff; + if (description.random != 0) { + dist.param(filterRandomDist::param_type(0, description.random)); + baseCutoff *= centsFactor(dist(Random::randomGenerator)); + } + const auto keytrack = description.keytrack * (noteNumber - description.keycenter); + baseCutoff *= centsFactor(keytrack); + const auto veltrack = description.veltrack * velocity; + baseCutoff *= centsFactor(veltrack); + + baseGain = description.gain; + baseResonance = description.resonance; +} + +void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned numFrames) +{ + if (description == nullptr) { + for (unsigned channelIdx = 0; channelIdx < filter.channels(); channelIdx++) + copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); + return; + } + + // TODO: Once the midistate envelopes are done, add modulation in there! + // For now we take the last value + auto cutoff = baseCutoff; + for (auto& mod: description->cutoffCC) { + cutoff *= centsFactor(midiState.getCCValue(mod.first) * mod.second); + } + + auto resonance = baseResonance; + for (auto& mod: description->resonanceCC) { + resonance += midiState.getCCValue(mod.first) * mod.second; + } + + auto gain = baseGain; + for (auto& mod: description->gainCC) { + gain += midiState.getCCValue(mod.first) * mod.second; + } + + filter.process(inputs, outputs, cutoff, resonance, gain,numFrames); +} + + +sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) +: midiState(state) +{ + setNumFilters(numFilters); +} + +sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, int noteNumber, uint8_t velocity) +{ + auto filter = absl::c_find_if(filters, [](const FilterHolderPtr& holder) { + return holder.use_count() == 1; + }); + + if (filter == filters.end()) + return {}; + + (**filter).setup(description, noteNumber, velocity); + return *filter; +} + +int sfz::FilterPool::getActiveFilters() const +{ + return absl::c_count_if(filters, [](const FilterHolderPtr& holder) { + return holder.use_count() > 1; + }); +} + +void sfz::FilterPool::setNumFilters(int numFilters) +{ + filters.clear(); + filters.reserve(numFilters); + for (int i = 0; i < numFilters; ++i) + filters.emplace_back(std::make_shared(midiState)); +} diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h new file mode 100644 index 00000000..8bd3f70e --- /dev/null +++ b/src/sfizz/FilterPool.h @@ -0,0 +1,44 @@ +#pragma once +#include "SfzFilter.h" +#include "FilterDescription.h" +#include "MidiState.h" +#include +#include + +namespace sfz +{ + +class FilterHolder +{ +public: + FilterHolder() = delete; + FilterHolder(const MidiState& state); + void reset(); + void setup(const FilterDescription& description, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); + void process(const float** inputs, float** outputs, unsigned numFrames); +private: + const MidiState& midiState; + const FilterDescription* description; + Filter filter; + float baseCutoff { Default::filterCutoff }; + float baseResonance { Default::filterResonance }; + float baseGain { Default::filterGain }; + using filterRandomDist = std::uniform_int_distribution; + filterRandomDist dist { 0, sfz::Default::filterRandom }; +}; + +using FilterHolderPtr = std::shared_ptr; + +class FilterPool +{ +public: + FilterPool() = delete; + FilterPool(const MidiState& state, int numFilters = config::defaultNumFilters); + FilterHolderPtr getFilter(const FilterDescription& description, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); + int getActiveFilters() const; + void setNumFilters(int numFilters); +private: + const MidiState& midiState; + std::vector filters; +}; +} From 43ace5defbbce53ecdfc76a59a3018c9ec833cc1 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 13:29:42 +0100 Subject: [PATCH 02/20] Added comments and working on the filter pool --- src/sfizz/FilterPool.cpp | 61 +++++++++++++++++++++++++------- src/sfizz/FilterPool.h | 75 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index d43ec3e0..6922d6ef 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -1,6 +1,10 @@ #include "FilterPool.h" #include "SIMDHelpers.h" #include "absl/algorithm/container.h" +#include "AtomicGuard.h" +#include +#include +using namespace std::chrono_literals; sfz::FilterHolder::FilterHolder(const MidiState& midiState) : midiState(midiState) @@ -43,24 +47,36 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned // TODO: Once the midistate envelopes are done, add modulation in there! // For now we take the last value - auto cutoff = baseCutoff; + lastCutoff = baseCutoff; for (auto& mod: description->cutoffCC) { - cutoff *= centsFactor(midiState.getCCValue(mod.first) * mod.second); + lastCutoff *= centsFactor(midiState.getCCValue(mod.first) * mod.second); } - auto resonance = baseResonance; + lastResonance = baseResonance; for (auto& mod: description->resonanceCC) { - resonance += midiState.getCCValue(mod.first) * mod.second; + lastResonance += midiState.getCCValue(mod.first) * mod.second; } - auto gain = baseGain; + lastGain = baseGain; for (auto& mod: description->gainCC) { - gain += midiState.getCCValue(mod.first) * mod.second; + lastResonance += midiState.getCCValue(mod.first) * mod.second; } - filter.process(inputs, outputs, cutoff, resonance, gain,numFrames); + filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); } +float sfz::FilterHolder::getLastCutoff() const +{ + return lastCutoff; +} +float sfz::FilterHolder::getLastResonance() const +{ + return lastResonance; +} +float sfz::FilterHolder::getLastGain() const +{ + return lastGain; +} sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) : midiState(state) @@ -70,6 +86,10 @@ sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, int noteNumber, uint8_t velocity) { + AtomicGuard guard { givingOutFilters }; + if (!canGiveOutFilters) + return {}; + auto filter = absl::c_find_if(filters, [](const FilterHolderPtr& holder) { return holder.use_count() == 1; }); @@ -81,17 +101,34 @@ sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& descrip return *filter; } -int sfz::FilterPool::getActiveFilters() const +size_t sfz::FilterPool::getActiveFilters() const { return absl::c_count_if(filters, [](const FilterHolderPtr& holder) { return holder.use_count() > 1; }); } -void sfz::FilterPool::setNumFilters(int numFilters) +size_t sfz::FilterPool::setNumFilters(size_t numFilters) { - filters.clear(); - filters.reserve(numFilters); - for (int i = 0; i < numFilters; ++i) + AtomicDisabler disabler { canGiveOutFilters }; + + while(givingOutFilters) + std::this_thread::sleep_for(1ms); + + auto filterIterator = filters.begin(); + auto filterSentinel = filters.rbegin(); + while (filterIterator < filterSentinel.base()) { + if (filterIterator->use_count() == 1) { + std::iter_swap(filterIterator, filterSentinel); + ++filterSentinel; + } else { + ++filterIterator; + } + } + + filters.resize(std::distance(filters.begin(), filterSentinel.base())); + for (size_t i = filters.size(); i < numFilters; ++i) filters.emplace_back(std::make_shared(midiState)); + + return filters.size(); } diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index 8bd3f70e..f7d910f7 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -13,16 +13,54 @@ class FilterHolder public: FilterHolder() = delete; FilterHolder(const MidiState& state); - void reset(); + /** + * @brief Setup a new filter based on a filter description, and a triggering note parameters. + * + * @param description the filter description + * @param noteNumber the triggering note number + * @param velocity the triggering note velocity + */ void setup(const FilterDescription& description, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); + /** + * @brief Process a block of stereo inputs + * + * @param inputs + * @param outputs + * @param numFrames + */ void process(const float** inputs, float** outputs, unsigned numFrames); + /** + * @brief Returns the last value of the cutoff for the filter + * + * @return float + */ + float getLastCutoff() const; + /** + * @brief Returns the last value of the resonance for the filter + * + * @return float + */ + float getLastResonance() const; + /** + * @brief Returns the last value of the gain for the filter + * + * @return float + */ + float getLastGain() const; private: + /** + * Reset the filter. Is called internally when using setup(). + */ + void reset(); const MidiState& midiState; const FilterDescription* description; Filter filter; float baseCutoff { Default::filterCutoff }; float baseResonance { Default::filterResonance }; float baseGain { Default::filterGain }; + float lastCutoff { Default::filterCutoff }; + float lastResonance { Default::filterResonance }; + float lastGain { Default::filterGain }; using filterRandomDist = std::uniform_int_distribution; filterRandomDist dist { 0, sfz::Default::filterRandom }; }; @@ -33,11 +71,42 @@ class FilterPool { public: FilterPool() = delete; + /** + * @brief Construct a new Filter Pool object + * + * @param state the associated midi state + * @param numFilters the number of inactive filters to hold in the pool + */ FilterPool(const MidiState& state, int numFilters = config::defaultNumFilters); + /** + * @brief Get a filter object to use in Voices + * + * @param description the filter description to bind to the filter + * @param noteNumber the triggering note number + * @param velocity the triggering note velocity + * @return FilterHolderPtr release this when done with the filter; no deallocation will be done + */ FilterHolderPtr getFilter(const FilterDescription& description, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); - int getActiveFilters() const; - void setNumFilters(int numFilters); + /** + * @brief Get the number of active filters + * + * @return size_t + */ + size_t getActiveFilters() const; + /** + * @brief Set the number of filters in the pool. This function may sleep and should be called from a background thread. + * No filters will be distributed during the reallocation of filters. Existing running filters are kept. If the target + * number of filters is less that the number of active filters, the function will not remove them and you may need + * to call it again after existing filters have run out. + * + * @param numFilters + * @return size_t the actual number of filters in the pool + */ + size_t setNumFilters(size_t numFilters); private: + std::atomic givingOutFilters { false }; + std::atomic canGiveOutFilters { true }; + const MidiState& midiState; std::vector filters; }; From bbad781bf9d7d1e01982fb867a888d7d6f621e33 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 14:05:08 +0100 Subject: [PATCH 03/20] Put the midi state in the resources --- src/sfizz/Resources.h | 2 ++ src/sfizz/Synth.cpp | 22 +++++++++++----------- src/sfizz/Synth.h | 3 +-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 0d11e088..03a19ca3 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -6,12 +6,14 @@ #pragma once #include "FilePool.h" +#include "FilterPool.h" #include "Logger.h" namespace sfz { struct Resources { + MidiState midiState; Logger logger; FilePool filePool { logger }; }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 9d9fdc53..c7915663 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -78,7 +78,7 @@ void sfz::Synth::callback(absl::string_view header, const std::vector& m void sfz::Synth::buildRegion(const std::vector& regionOpcodes) { - auto lastRegion = std::make_unique(midiState, defaultPath); + auto lastRegion = std::make_unique(resources.midiState, defaultPath); auto parseOpcodes = [&](const auto& opcodes) { for (auto& opcode : opcodes) { @@ -125,7 +125,7 @@ void sfz::Synth::clear() fileTicket = -1; defaultSwitch = absl::nullopt; defaultPath = ""; - midiState.reset(); + resources.midiState.reset(); ccNames.clear(); globalOpcodes.clear(); masterOpcodes.clear(); @@ -159,7 +159,7 @@ void sfz::Synth::handleControlOpcodes(const std::vector& members) case hash("set_cc"): if (backParameter && Default::ccNumberRange.containsWithEnd(*backParameter)) { const auto ccValue = readOpcode(member.value, Default::ccValueRange).value_or(0); - midiState.ccEvent(*backParameter, ccValue); + resources.midiState.ccEvent(*backParameter, ccValue); } break; case hash("Label_cc"): @@ -287,7 +287,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) // Defaults for (int ccIndex = 0; ccIndex < config::numCCs; ccIndex++) { - region->registerCC(ccIndex, midiState.getCCValue(ccIndex)); + region->registerCC(ccIndex, resources.midiState.getCCValue(ccIndex)); } if (defaultSwitch) { @@ -430,7 +430,7 @@ void sfz::Synth::noteOn(int delay, int noteNumber, uint8_t velocity) noexcept ASSERT(noteNumber < 128); ASSERT(noteNumber >= 0); - midiState.noteOnEvent(noteNumber, velocity); + resources.midiState.noteOnEvent(noteNumber, velocity); AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) @@ -444,7 +444,7 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity [[maybe_unu ASSERT(noteNumber < 128); ASSERT(noteNumber >= 0); - midiState.noteOffEvent(noteNumber, velocity); + resources.midiState.noteOffEvent(noteNumber, velocity); AtomicGuard callbackGuard { inCallback }; if (!canEnterCallback) @@ -453,7 +453,7 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity [[maybe_unu // FIXME: Some keyboards (e.g. Casio PX5S) can send a real note-off velocity. In this case, do we have a // way in sfz to specify that a release trigger should NOT use the note-on velocity? // auto replacedVelocity = (velocity == 0 ? sfz::getNoteVelocity(noteNumber) : velocity); - const auto replacedVelocity = midiState.getNoteVelocity(noteNumber); + const auto replacedVelocity = resources.midiState.getNoteVelocity(noteNumber); for (auto& voice : voices) voice->registerNoteOff(delay, noteNumber, replacedVelocity); @@ -508,7 +508,7 @@ void sfz::Synth::cc(int delay, int ccNumber, uint8_t ccValue) noexcept return; } - midiState.ccEvent(ccNumber, ccValue); + resources.midiState.ccEvent(ccNumber, ccValue); for (auto& voice : voices) voice->registerCC(delay, ccNumber, ccValue); @@ -528,7 +528,7 @@ void sfz::Synth::pitchWheel(int delay, int pitch) noexcept ASSERT(pitch <= 8192); ASSERT(pitch >= -8192); - midiState.pitchBendEvent(pitch); + resources.midiState.pitchBendEvent(pitch); for (auto& region: regions) { region->registerPitchWheel(pitch); @@ -611,7 +611,7 @@ void sfz::Synth::resetVoices(int numVoices) voices.clear(); for (int i = 0; i < numVoices; ++i) - voices.push_back(std::make_unique(midiState, resources)); + voices.push_back(std::make_unique(resources.midiState, resources)); for (auto& voice: voices) { voice->setSampleRate(this->sampleRate); @@ -678,7 +678,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept if (!canEnterCallback) return; - midiState.resetAllControllers(); + resources.midiState.resetAllControllers(); for (auto& voice: voices) { voice->registerPitchWheel(delay, 0); for (int cc = 0; cc < config::numCCs; ++cc) diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 6b978ba6..6605951c 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -324,7 +324,7 @@ public: */ void disableFreeWheeling() noexcept; - const MidiState& getMidiState() const noexcept { return midiState; } + const MidiState& getMidiState() const noexcept { return resources.midiState; } /** * @brief Check if the SFZ should be reloaded. @@ -446,7 +446,6 @@ private: // Singletons passed as references to the voices Resources resources; - MidiState midiState; // Control opcodes std::string defaultPath { "" }; From 137607b2c6f058793af7d6812bd8ead833ff9194 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 14:16:52 +0100 Subject: [PATCH 04/20] Voices only need the resource set --- src/sfizz/Synth.cpp | 2 +- src/sfizz/Voice.cpp | 24 ++++++++++++------------ src/sfizz/Voice.h | 3 +-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c7915663..d6cd7f7d 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -611,7 +611,7 @@ void sfz::Synth::resetVoices(int numVoices) voices.clear(); for (int i = 0; i < numVoices; ++i) - voices.push_back(std::make_unique(resources.midiState, resources)); + voices.push_back(std::make_unique(resources)); for (auto& voice: voices) { voice->setSampleRate(this->sampleRate); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d4a3e6f0..14aac783 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -14,8 +14,8 @@ #include "absl/algorithm/container.h" #include -sfz::Voice::Voice(const sfz::MidiState& midiState, sfz::Resources& resources) - : midiState(midiState), resources(resources) +sfz::Voice::Voice(sfz::Resources& resources) +: resources(resources) { } @@ -45,7 +45,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value baseVolumedB = region->getBaseVolumedB(number); auto volumedB { baseVolumedB }; if (region->volumeCC) - volumedB += normalizeCC(midiState.getCCValue(region->volumeCC->first)) * region->volumeCC->second; + volumedB += normalizeCC(resources.midiState.getCCValue(region->volumeCC->first)) * region->volumeCC->second; volumeEnvelope.reset(db2mag(Default::volumeRange.clamp(volumedB))); baseGain = region->getBaseGain(); @@ -54,28 +54,28 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value float gain { baseGain }; if (region->amplitudeCC) - gain *= normalizeCC(midiState.getCCValue(region->amplitudeCC->first)) * normalizePercents(region->amplitudeCC->second); + gain *= normalizeCC(resources.midiState.getCCValue(region->amplitudeCC->first)) * normalizePercents(region->amplitudeCC->second); amplitudeEnvelope.reset(Default::normalizedRange.clamp(gain)); - float crossfadeGain { region->getCrossfadeGain(midiState.getCCArray()) }; + float crossfadeGain { region->getCrossfadeGain(resources.midiState.getCCArray()) }; crossfadeEnvelope.reset(Default::normalizedRange.clamp(crossfadeGain)); basePan = normalizeNegativePercents(region->pan); auto pan { basePan }; if (region->panCC) - pan += normalizeCC(midiState.getCCValue(region->panCC->first)) * normalizeNegativePercents(region->panCC->second); + pan += normalizeCC(resources.midiState.getCCValue(region->panCC->first)) * normalizeNegativePercents(region->panCC->second); panEnvelope.reset(Default::symmetricNormalizedRange.clamp(pan)); basePosition = normalizeNegativePercents(region->position); auto position { basePosition }; if (region->positionCC) - position += normalizeCC(midiState.getCCValue(region->positionCC->first)) * normalizeNegativePercents(region->positionCC->second); + position += normalizeCC(resources.midiState.getCCValue(region->positionCC->first)) * normalizeNegativePercents(region->positionCC->second); positionEnvelope.reset(Default::symmetricNormalizedRange.clamp(position)); baseWidth = normalizeNegativePercents(region->width); auto width { baseWidth }; if (region->widthCC) - width += normalizeCC(midiState.getCCValue(region->widthCC->first)) * normalizeNegativePercents(region->widthCC->second); + width += normalizeCC(resources.midiState.getCCValue(region->widthCC->first)) * normalizeNegativePercents(region->widthCC->second); widthEnvelope.reset(Default::symmetricNormalizedRange.clamp(width)); pitchBendEnvelope.setFunction([region](float pitchValue){ @@ -83,7 +83,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value const auto bendInCents = normalizedBend > 0.0f ? normalizedBend * region->bendUp : -normalizedBend * region->bendDown; return centsFactor(bendInCents); }); - pitchBendEnvelope.reset(static_cast(midiState.getPitchBend())); + pitchBendEnvelope.reset(static_cast(resources.midiState.getPitchBend())); sourcePosition = region->getOffset(); triggerDelay = delay; @@ -98,7 +98,7 @@ void sfz::Voice::prepareEGEnvelope(int delay, uint8_t velocity) noexcept auto secondsToSamples = [this](auto timeInSeconds) { return static_cast(timeInSeconds * sampleRate); }; - const auto& ccArray = midiState.getCCArray(); + const auto& ccArray = resources.midiState.getCCArray(); egEnvelope.reset( secondsToSamples(region->amplitudeEG.getAttack(ccArray, velocity)), secondsToSamples(region->amplitudeEG.getRelease(ccArray, velocity)), @@ -141,7 +141,7 @@ void sfz::Voice::registerNoteOff(int delay, int noteNumber, uint8_t velocity [[m if (region->loopMode == SfzLoopMode::one_shot) return; - if (!region->checkSustain || midiState.getCCValue(config::sustainCC) < config::halfCCThreshold) + if (!region->checkSustain || resources.midiState.getCCValue(config::sustainCC) < config::halfCCThreshold) release(delay); } } @@ -192,7 +192,7 @@ void sfz::Voice::registerCC(int delay, int ccNumber, uint8_t ccValue) noexcept } if (region->crossfadeCCInRange.contains(ccNumber) || region->crossfadeCCOutRange.contains(ccNumber)) { - const float crossfadeGain = region->getCrossfadeGain(midiState.getCCArray()); + const float crossfadeGain = region->getCrossfadeGain(resources.midiState.getCCArray()); crossfadeEnvelope.registerEvent(delay, Default::normalizedRange.clamp(crossfadeGain)); } } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 81cb1faf..2ac5fe88 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -34,7 +34,7 @@ public: * * @param midiState */ - Voice(const MidiState& midiState, Resources& resources); + Voice(Resources& resources); enum class TriggerType { NoteOn, NoteOff, @@ -281,7 +281,6 @@ private: int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 }; float sampleRate { config::defaultSampleRate }; - const MidiState& midiState; Resources& resources; ADSREnvelope egEnvelope; From 8c93c87f36184c0c7560371ed5b9ac974a977bcc Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 14:17:05 +0100 Subject: [PATCH 05/20] Added the filter pool to the resources --- src/sfizz/Resources.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 03a19ca3..8cf7efc1 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -16,5 +16,6 @@ struct Resources MidiState midiState; Logger logger; FilePool filePool { logger }; + FilterPool filterPool { midiState }; }; } From 3aaf0005ab3a8574905609845013c55504d20f36 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 14:43:31 +0100 Subject: [PATCH 06/20] Assertion too strict --- src/sfizz/SfzFilter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/SfzFilter.cpp b/src/sfizz/SfzFilter.cpp index 477ffb82..7c3ecac7 100644 --- a/src/sfizz/SfzFilter.cpp +++ b/src/sfizz/SfzFilter.cpp @@ -160,7 +160,7 @@ unsigned Filter::channels() const void Filter::setChannels(unsigned channels) { - ASSERT(channels < Impl::maxChannels); + ASSERT(channels <= Impl::maxChannels); if (P->fChannels != channels) { P->fChannels = channels; clear(); From d455ee5232e3545979ad918255b289ebb50f0722 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 14:45:11 +0100 Subject: [PATCH 07/20] Initial grafting of the filters in the voice process (untested) --- src/sfizz/Config.h | 4 +++- src/sfizz/FilterPool.h | 2 +- src/sfizz/Synth.cpp | 10 ++++++++++ src/sfizz/Voice.cpp | 36 ++++++++++++++++++++++++++++++++++++ src/sfizz/Voice.h | 15 +++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index e9818e39..22a18327 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -53,7 +53,9 @@ namespace config { constexpr uint8_t numCCs { 143 }; constexpr int chunkSize { 1024 }; constexpr float defaultAmpEGRelease { 0.02f }; - constexpr int defaultNumFilters { maxVoices * 2 }; + constexpr int filtersInPool { maxVoices * 2 }; + constexpr int filtersPerVoice { 2 }; + constexpr int eqsPerVoice { 3 }; /** Minimum interval in frames between recomputations of coefficients of the modulated filter. The lower, the more CPU resources are consumed. diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index f7d910f7..cb1da2c9 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -77,7 +77,7 @@ public: * @param state the associated midi state * @param numFilters the number of inactive filters to hold in the pool */ - FilterPool(const MidiState& state, int numFilters = config::defaultNumFilters); + FilterPool(const MidiState& state, int numFilters = config::filtersInPool); /** * @brief Get a filter object to use in Voices * diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index d6cd7f7d..53761633 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -234,6 +234,9 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) ++lastRegion; }; + size_t maxFilters { 0 }; + size_t maxEQs { 0 }; + while (currentRegion < lastRegion.base()) { auto region = currentRegion->get(); @@ -316,6 +319,8 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) region->registerPitchWheel(0); region->registerAftertouch(0); region->registerTempo(2.0f); + maxFilters = max(maxFilters, region->filters.size()); + maxEQs = max(maxEQs, region->equalizers.size()); ++currentRegion; } @@ -324,6 +329,11 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) regions.resize(remainingRegions); modificationTime = checkModificationTime(); + for (auto& voice: voices) { + voice->setMaxFiltersPerVoice(maxFilters); + voice->setMaxEQsPerVoice(maxEQs); + } + return parserReturned; } diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 14aac783..e39faf7a 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -17,6 +17,8 @@ sfz::Voice::Voice(sfz::Resources& resources) : resources(resources) { + filters.reserve(config::filtersPerVoice); + equalizers.reserve(config::filtersPerVoice); } void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value, sfz::Voice::TriggerType triggerType) noexcept @@ -85,6 +87,18 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value }); pitchBendEnvelope.reset(static_cast(resources.midiState.getPitchBend())); + // Check that we can handle the number of filters; filters should be cleared here + ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); + ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size()); + + for(auto& filter: region->filters) { + auto newFilter = resources.filterPool.getFilter(filter, number, value); + if (newFilter) + filters.push_back(newFilter); + } + + // TODO: Equalizers + sourcePosition = region->getOffset(); triggerDelay = delay; initialDelay = delay + static_cast(region->getDelay() * sampleRate); @@ -258,6 +272,12 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept else processMono(buffer); + const float* inputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; + float* outputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; + for (auto filter: filters) { + filter->process(inputChannels, outputChannels, buffer.getNumFrames()); + } + if (!egEnvelope.isSmoothing()) reset(); @@ -528,6 +548,8 @@ void sfz::Voice::reset() noexcept sourcePosition = 0; floatPositionOffset = 0.0f; noteIsOff = false; + filters.clear(); + equalizers.clear(); } float sfz::Voice::getMeanSquaredAverage() const noexcept @@ -544,3 +566,17 @@ uint32_t sfz::Voice::getSourcePosition() const noexcept { return sourcePosition; } + +void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) +{ + // There are filters in there, this call is unexpected + ASSERT(filters.size() == 0); + filters.reserve(numFilters); +} + +void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) +{ + // There are filters in there, this call is unexpected + ASSERT(filters.size() == 0); + filters.reserve(numFilters); +} diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 2ac5fe88..caaaee72 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -196,6 +196,18 @@ public: * @return */ const Region* getRegion() const noexcept { return region; } + /** + * @brief Set the max number of filters per voice + * + * @param numFilters + */ + void setMaxFiltersPerVoice(size_t numFilters); + /** + * @brief Set the max number of EQs per voice + * + * @param numFilters + */ + void setMaxEQsPerVoice(size_t numEQs); private: /** * @brief Fill a span with data from a file source. This is the first step @@ -283,6 +295,9 @@ private: Resources& resources; + std::vector filters; + std::vector equalizers; + ADSREnvelope egEnvelope; LinearEnvelope amplitudeEnvelope; // linear events LinearEnvelope crossfadeEnvelope; From 6461b4f4b4a1bcb32c596be2056197b1db4dfa01 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 14:55:46 +0100 Subject: [PATCH 08/20] Added a white noise generator --- src/sfizz/Config.h | 1 + src/sfizz/Voice.cpp | 52 +++++++++++++++++++++++++-------------------- src/sfizz/Voice.h | 3 +++ 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 22a18327..32778125 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -56,6 +56,7 @@ namespace config { constexpr int filtersInPool { maxVoices * 2 }; constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; + constexpr float noiseVariance { 0.1f }; /** Minimum interval in frames between recomputations of coefficients of the modulated filter. The lower, the more CPU resources are consumed. diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index e39faf7a..df7430c8 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -478,35 +478,41 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept { - if (region->sample != "*sine") - return; + const auto leftSpan = buffer.getSpan(0); + const auto rightSpan = buffer.getSpan(1); + if (region->sample == "*noise") { + absl::c_generate(leftSpan, [&](){ return noiseDist(Random::randomGenerator); }); + absl::c_generate(rightSpan, [&](){ return noiseDist(Random::randomGenerator); }); + } + else if (region->sample == "*sine") { + // TODO: wavetables for sine and other generators + if (buffer.getNumFrames() == 0) + return; - if (buffer.getNumFrames() == 0) - return; + auto jumps = tempSpan1.first(buffer.getNumFrames()); + auto bends = tempSpan2.first(buffer.getNumFrames()); + auto phases = tempSpan2.first(buffer.getNumFrames()); - auto jumps = tempSpan1.first(buffer.getNumFrames()); - auto bends = tempSpan2.first(buffer.getNumFrames()); - auto phases = tempSpan2.first(buffer.getNumFrames()); + const float step = baseFrequency * twoPi / sampleRate; + fill(jumps, step); - const float step = baseFrequency * twoPi / sampleRate; - fill(jumps, step); + if (region->bendStep > 1) + pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor); + else + pitchBendEnvelope.getBlock(bends); - if (region->bendStep > 1) - pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor); - else - pitchBendEnvelope.getBlock(bends); + applyGain(bends, jumps); + jumps[0] += phase; + cumsum(jumps, phases); + phase = phases.back(); - applyGain(bends, jumps); - jumps[0] += phase; - cumsum(jumps, phases); - phase = phases.back(); + sin(phases, leftSpan); + copy(leftSpan, rightSpan); - sin(phases, buffer.getSpan(0)); - copy(buffer.getSpan(0), buffer.getSpan(1)); - - // Wrap the phase so we don't loose too much precision on longer notes - const auto numTwoPiWraps = static_cast(phase / twoPi); - phase -= twoPi * static_cast(numTwoPiWraps); + // Wrap the phase so we don't loose too much precision on longer notes + const auto numTwoPiWraps = static_cast(phase / twoPi); + phase -= twoPi * static_cast(numTwoPiWraps); + } } bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index caaaee72..adf18930 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace sfz { /** @@ -308,6 +309,8 @@ private: MultiplicativeEnvelope volumeEnvelope; float bendStepFactor { centsFactor(1) }; + std::normal_distribution noiseDist { 0, config::noiseVariance }; + HistoricalBuffer powerHistory { config::powerHistoryLength }; LEAK_DETECTOR(Voice); }; From f30675d0d84e00dfc6a22ace2751a9bdaafc9b41 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 16:48:35 +0100 Subject: [PATCH 09/20] Loop by reference --- src/sfizz/Voice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index df7430c8..398f6407 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -274,7 +274,7 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept const float* inputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; float* outputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; - for (auto filter: filters) { + for (auto& filter: filters) { filter->process(inputChannels, outputChannels, buffer.getNumFrames()); } From 2acd8f45a3ebef115d447745baa82afecdecda7e Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 16:49:19 +0100 Subject: [PATCH 10/20] Increase base noise generator variance --- 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 32778125..cbbfa41a 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -56,7 +56,7 @@ namespace config { constexpr int filtersInPool { maxVoices * 2 }; constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; - constexpr float noiseVariance { 0.1f }; + constexpr float noiseVariance { 0.25f }; /** Minimum interval in frames between recomputations of coefficients of the modulated filter. The lower, the more CPU resources are consumed. From d3592214b1c5d0bb5950ef4908cfcd6594574663 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 16:49:54 +0100 Subject: [PATCH 11/20] The filters have to be initialized with samplerate --- src/sfizz/FilterPool.cpp | 13 ++++++++++++- src/sfizz/FilterPool.h | 14 +++++++++++++- src/sfizz/Synth.cpp | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 6922d6ef..d037abc4 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -59,7 +59,7 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned lastGain = baseGain; for (auto& mod: description->gainCC) { - lastResonance += midiState.getCCValue(mod.first) * mod.second; + lastGain += midiState.getCCValue(mod.first) * mod.second; } filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); @@ -132,3 +132,14 @@ size_t sfz::FilterPool::setNumFilters(size_t numFilters) return filters.size(); } + +void sfz::FilterPool::setSampleRate(float sampleRate) +{ + for (auto& filter: filters) + filter->setSampleRate(sampleRate); +} + +void sfz::FilterHolder::setSampleRate(float sampleRate) +{ + filter.init(static_cast(sampleRate)); +} diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index cb1da2c9..b76d48da 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -47,6 +47,12 @@ public: * @return float */ float getLastGain() const; + /** + * @brief Set the sample rate for a filter + * + * @param sampleRate + */ + void setSampleRate(float sampleRate); private: /** * Reset the filter. Is called internally when using setup(). @@ -103,10 +109,16 @@ public: * @return size_t the actual number of filters in the pool */ size_t setNumFilters(size_t numFilters); + /** + * @brief Set the sample rate for all filters + * + * @param sampleRate + */ + void setSampleRate(float sampleRate); private: std::atomic givingOutFilters { false }; std::atomic canGiveOutFilters { true }; - + float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector filters; }; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 53761633..c61bb0c9 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -401,6 +401,8 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept this->sampleRate = sampleRate; for (auto& voice : voices) voice->setSampleRate(sampleRate); + + resources.filterPool.setSampleRate(sampleRate); } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept From 1acefb19d9a21d04b02efa4907312742ac59a654 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 17:03:12 +0100 Subject: [PATCH 12/20] Reinit filters if their number is changed --- src/sfizz/FilterPool.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index d037abc4..bdd92f47 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -127,8 +127,10 @@ size_t sfz::FilterPool::setNumFilters(size_t numFilters) } filters.resize(std::distance(filters.begin(), filterSentinel.base())); - for (size_t i = filters.size(); i < numFilters; ++i) - filters.emplace_back(std::make_shared(midiState)); + for (size_t i = filters.size(); i < numFilters; ++i) { + auto filter = filters.emplace_back(std::make_shared(midiState)); + filter->setSampleRate(sampleRate); + } return filters.size(); } From fe62e9a307373993273399ab82d0332e9638d7eb Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:03:18 +0100 Subject: [PATCH 13/20] Normalize the velocity before tracking --- src/sfizz/FilterPool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index bdd92f47..0904d62e 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -30,7 +30,7 @@ void sfz::FilterHolder::setup(const FilterDescription& description, int noteNumb } const auto keytrack = description.keytrack * (noteNumber - description.keycenter); baseCutoff *= centsFactor(keytrack); - const auto veltrack = description.veltrack * velocity; + const auto veltrack = description.veltrack * normalizeVelocity(velocity); baseCutoff *= centsFactor(veltrack); baseGain = description.gain; From 9e6f77ac8640cd36bcacc377385223967c8c2ff2 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:07:56 +0100 Subject: [PATCH 14/20] Clamp filter coefficients --- src/sfizz/FilterPool.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 0904d62e..4af9f7b6 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -32,6 +32,7 @@ void sfz::FilterHolder::setup(const FilterDescription& description, int noteNumb baseCutoff *= centsFactor(keytrack); const auto veltrack = description.veltrack * normalizeVelocity(velocity); baseCutoff *= centsFactor(veltrack); + baseCutoff = Default::filterCutoffRange.clamp(baseCutoff); baseGain = description.gain; baseResonance = description.resonance; @@ -51,16 +52,19 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned for (auto& mod: description->cutoffCC) { lastCutoff *= centsFactor(midiState.getCCValue(mod.first) * mod.second); } + lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); lastResonance = baseResonance; for (auto& mod: description->resonanceCC) { lastResonance += midiState.getCCValue(mod.first) * mod.second; } + lastResonance = Default::filterResonanceRange.clamp(lastResonance); lastGain = baseGain; for (auto& mod: description->gainCC) { lastGain += midiState.getCCValue(mod.first) * mod.second; } + lastGain = Default::filterGainRange.clamp(lastGain); filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); } From 3d60de459fe70297f9fd173dc90d141597589bac Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:16:18 +0100 Subject: [PATCH 15/20] Emplace back does not give back a reference before c++17 --- src/sfizz/FilterPool.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 4af9f7b6..e64b9bde 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -132,8 +132,8 @@ size_t sfz::FilterPool::setNumFilters(size_t numFilters) filters.resize(std::distance(filters.begin(), filterSentinel.base())); for (size_t i = filters.size(); i < numFilters; ++i) { - auto filter = filters.emplace_back(std::make_shared(midiState)); - filter->setSampleRate(sampleRate); + filters.emplace_back(std::make_shared(midiState)); + filters.back()->setSampleRate(sampleRate); } return filters.size(); From ad798c38df2f63ec0bed7269ad9e40ca0a5395f1 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:17:15 +0100 Subject: [PATCH 16/20] Added EQPool --- src/CMakeLists.txt | 1 + src/sfizz/EQPool.cpp | 137 +++++++++++++++++++++++++++++++++++++++++++ src/sfizz/EQPool.h | 121 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 src/sfizz/EQPool.cpp create mode 100644 src/sfizz/EQPool.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b456bf15..77fa31a4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ set (SFIZZ_SOURCES sfizz/Synth.cpp sfizz/FilePool.cpp sfizz/FilterPool.cpp + sfizz/EQPool.cpp sfizz/Region.cpp sfizz/Voice.cpp sfizz/ScopedFTZ.cpp diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp new file mode 100644 index 00000000..b0b76289 --- /dev/null +++ b/src/sfizz/EQPool.cpp @@ -0,0 +1,137 @@ +#include "EQPool.h" +#include "AtomicGuard.h" +#include +#include "absl/algorithm/container.h" +#include "SIMDHelpers.h" +using namespace std::chrono_literals; + +sfz::EQHolder::EQHolder(const MidiState& state) +:midiState(state) +{ + eq.setChannels(2); +} + +void sfz::EQHolder::reset() +{ + eq.clear(); +} + +void sfz::EQHolder::setup(const EQDescription& description, uint8_t velocity) +{ + reset(); + this->description = &description; + const auto normalizedVelocity = normalizeVelocity(velocity); + + baseBandwidth = description.bandwidth; + baseGain = Default::eqGainRange.clamp(description.gain + normalizedVelocity * description.vel2gain); + baseFrequency = Default::eqFrequencyRange.clamp(description.frequency + normalizedVelocity * description.vel2frequency); +} + +void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numFrames) +{ + if (description == nullptr) { + for (unsigned channelIdx = 0; channelIdx < eq.channels(); channelIdx++) + copy({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames }); + return; + } + + // TODO: Once the midistate envelopes are done, add modulation in there! + // For now we take the last value + lastFrequency = baseFrequency; + for (auto& mod: description->frequencyCC) { + lastFrequency += midiState.getCCValue(mod.first) * mod.second; + } + lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); + + lastBandwidth = baseBandwidth; + for (auto& mod: description->bandwidthCC) { + lastBandwidth += midiState.getCCValue(mod.first) * mod.second; + } + lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); + + lastGain = baseGain; + for (auto& mod: description->gainCC) { + lastGain += midiState.getCCValue(mod.first) * mod.second; + } + lastGain = Default::eqGainRange.clamp(lastGain); + + eq.process(inputs, outputs, lastFrequency, lastBandwidth, lastGain, numFrames); +} +float sfz::EQHolder::getLastFrequency() const +{ + return lastFrequency; +} +float sfz::EQHolder::getLastBandwidth() const +{ + return lastBandwidth; +} +float sfz::EQHolder::getLastGain() const +{ + return lastGain; +} +void sfz::EQHolder::setSampleRate(float sampleRate) +{ + eq.init(static_cast(sampleRate)); +} + +sfz::EQPool::EQPool(const MidiState& state, int numEQs) +: midiState(state) +{ + setnumEQs(numEQs); +} + +sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, uint8_t velocity) +{ + AtomicGuard guard { givingOutEQs }; + if (!canGiveOutEQs) + return {}; + + auto eq = absl::c_find_if(eqs, [](const EQHolderPtr& holder) { + return holder.use_count() == 1; + }); + + if (eq == eqs.end()) + return {}; + + (**eq).setup(description, velocity); + return *eq; +} + +size_t sfz::EQPool::getActiveEQs() const +{ + return absl::c_count_if(eqs, [](const EQHolderPtr& holder) { + return holder.use_count() > 1; + }); +} + +size_t sfz::EQPool::setnumEQs(size_t numEQs) +{ + AtomicDisabler disabler { canGiveOutEQs }; + + while(givingOutEQs) + std::this_thread::sleep_for(1ms); + + auto eqIterator = eqs.begin(); + auto eqSentinel = eqs.rbegin(); + while (eqIterator < eqSentinel.base()) { + if (eqIterator->use_count() == 1) { + std::iter_swap(eqIterator, eqSentinel); + ++eqSentinel; + } else { + ++eqIterator; + } + } + + eqs.resize(std::distance(eqs.begin(), eqSentinel.base())); + for (size_t i = eqs.size(); i < numEQs; ++i) { + eqs.emplace_back(std::make_shared(midiState)); + eqs.back()->setSampleRate(sampleRate); + } + + return eqs.size(); +} +void sfz::EQPool::setSampleRate(float sampleRate) +{ + for (auto& eq: eqs) + eq->setSampleRate(sampleRate); +} diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h new file mode 100644 index 00000000..f3e2ddbf --- /dev/null +++ b/src/sfizz/EQPool.h @@ -0,0 +1,121 @@ +#pragma once +#include "SfzFilter.h" +#include "EQDescription.h" +#include "MidiState.h" +#include +#include + +namespace sfz +{ + +class EQHolder +{ +public: + EQHolder() = delete; + EQHolder(const MidiState& state); + /** + * @brief Setup a new EQ based on an EQ description. + * + * @param description the EQ description + */ + void setup(const EQDescription& description, uint8_t velocity); + /** + * @brief Process a block of stereo inputs + * + * @param inputs + * @param outputs + * @param numFrames + */ + void process(const float** inputs, float** outputs, unsigned numFrames); + /** + * @brief Returns the last value of the frequency for the EQ + * + * @return float + */ + float getLastFrequency() const; + /** + * @brief Returns the last value of the bandwitdh for the EQ + * + * @return float + */ + float getLastBandwidth() const; + /** + * @brief Returns the last value of the gain for the EQ + * + * @return float + */ + float getLastGain() const; + /** + * @brief Set the sample rate for the EQ + * + * @param sampleRate + */ + void setSampleRate(float sampleRate); +private: + /** + * Reset the filter. Is called internally when using setup(). + */ + void reset(); + const MidiState& midiState; + const EQDescription* description; + FilterEq eq; + float baseBandwidth { Default::eqBandwidth }; + float baseFrequency { Default::eqFrequency1 }; + float baseGain { Default::eqGain }; + float lastBandwidth { Default::eqBandwidth }; + float lastFrequency { Default::eqFrequency1 }; + float lastGain { Default::eqGain }; +}; + +using EQHolderPtr = std::shared_ptr; + +class EQPool +{ +public: + EQPool() = delete; + /** + * @brief Construct a new EQPool object + * + * @param state the associated midi state + * @param numEQs the number of inactive EQs to hold in the pool + */ + EQPool(const MidiState& state, int numEQs = config::filtersInPool); + /** + * @brief Get an EQ object to use in Voices + * + * @param description the filter description to bind to the EQ + * @param noteNumber the triggering note number + * @param velocity the triggering note velocity + * @return EQHolderPtr release this when done with the filter; no deallocation will be done + */ + EQHolderPtr getEQ(const EQDescription& description, uint8_t velocity); + /** + * @brief Get the number of active EQs + * + * @return size_t + */ + size_t getActiveEQs() const; + /** + * @brief Set the number of EQs in the pool. This function may sleep and should be called from a background thread. + * No EQs will be distributed during the reallocation of EQs. Existing running EQs are kept. If the target + * number of EQs is less that the number of active EQs, the function will not remove them and you may need + * to call it again after existing EQs have run out. + * + * @param numEQs + * @return size_t the actual number of EQs in the pool + */ + size_t setnumEQs(size_t numEQs); + /** + * @brief Set the sample rate for all EQs + * + * @param sampleRate + */ + void setSampleRate(float sampleRate); +private: + std::atomic givingOutEQs { false }; + std::atomic canGiveOutEQs { true }; + float sampleRate { config::defaultSampleRate }; + const MidiState& midiState; + std::vector eqs; +}; +} From 6d6872bb3f107a7ef71ac1c98d36a10229538fa6 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:19:03 +0100 Subject: [PATCH 17/20] Add the EQPool in resources --- src/sfizz/Resources.h | 2 ++ src/sfizz/Synth.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 8cf7efc1..616e2cac 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -7,6 +7,7 @@ #pragma once #include "FilePool.h" #include "FilterPool.h" +#include "EQPool.h" #include "Logger.h" namespace sfz @@ -17,5 +18,6 @@ struct Resources Logger logger; FilePool filePool { logger }; FilterPool filterPool { midiState }; + EQPool eqPool { midiState }; }; } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index c61bb0c9..3efdb6cf 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -403,6 +403,7 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept voice->setSampleRate(sampleRate); resources.filterPool.setSampleRate(sampleRate); + resources.eqPool.setSampleRate(sampleRate); } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept From f365e4dfbe04de4200b194158f7fb0c1e5aae86f Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:21:09 +0100 Subject: [PATCH 18/20] Add equalizer processing --- src/sfizz/Voice.cpp | 15 ++++++++++++--- src/sfizz/Voice.h | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 398f6407..939d1250 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -18,7 +18,7 @@ sfz::Voice::Voice(sfz::Resources& resources) : resources(resources) { filters.reserve(config::filtersPerVoice); - equalizers.reserve(config::filtersPerVoice); + equalizers.reserve(config::eqsPerVoice); } void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value, sfz::Voice::TriggerType triggerType) noexcept @@ -91,13 +91,17 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size()); - for(auto& filter: region->filters) { + for (auto& filter: region->filters) { auto newFilter = resources.filterPool.getFilter(filter, number, value); if (newFilter) filters.push_back(newFilter); } - // TODO: Equalizers + for (auto& eq: region->equalizers) { + auto newEQ = resources.eqPool.getEQ(eq, value); + if (newEQ) + equalizers.push_back(newEQ); + } sourcePosition = region->getOffset(); triggerDelay = delay; @@ -274,10 +278,15 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept const float* inputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; float* outputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; + for (auto& filter: filters) { filter->process(inputChannels, outputChannels, buffer.getNumFrames()); } + for (auto& eq: equalizers) { + eq->process(inputChannels, outputChannels, buffer.getNumFrames()); + } + if (!egEnvelope.isSmoothing()) reset(); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index adf18930..ca4dbc09 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -297,7 +297,7 @@ private: Resources& resources; std::vector filters; - std::vector equalizers; + std::vector equalizers; ADSREnvelope egEnvelope; LinearEnvelope amplitudeEnvelope; // linear events From d11576771df535b9e121e1e09cb616529ea9a354 Mon Sep 17 00:00:00 2001 From: Paul Fd Date: Sun, 9 Feb 2020 18:26:39 +0100 Subject: [PATCH 19/20] Assertion too strict --- src/sfizz/SfzFilter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/SfzFilter.cpp b/src/sfizz/SfzFilter.cpp index 7c3ecac7..3d9dedc1 100644 --- a/src/sfizz/SfzFilter.cpp +++ b/src/sfizz/SfzFilter.cpp @@ -330,7 +330,7 @@ unsigned FilterEq::channels() const void FilterEq::setChannels(unsigned channels) { - ASSERT(channels < Impl::maxChannels); + ASSERT(channels <= Impl::maxChannels); if (P->fChannels != channels) { P->fChannels = channels; clear(); From 56e0212cba3748b54743577e7449fa6516734dac Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 10 Feb 2020 11:12:46 +0100 Subject: [PATCH 20/20] Adapt the processing to mono/stereo --- src/sfizz/EQPool.cpp | 9 +++++---- src/sfizz/EQPool.h | 10 ++++++---- src/sfizz/FilterPool.cpp | 9 +++++---- src/sfizz/FilterPool.h | 8 +++++--- src/sfizz/Voice.cpp | 39 ++++++++++++++++++++++++++------------- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index b0b76289..1004328a 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -8,7 +8,7 @@ using namespace std::chrono_literals; sfz::EQHolder::EQHolder(const MidiState& state) :midiState(state) { - eq.setChannels(2); + } void sfz::EQHolder::reset() @@ -16,9 +16,10 @@ void sfz::EQHolder::reset() eq.clear(); } -void sfz::EQHolder::setup(const EQDescription& description, uint8_t velocity) +void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels, uint8_t velocity) { reset(); + eq.setChannels(numChannels); this->description = &description; const auto normalizedVelocity = normalizeVelocity(velocity); @@ -80,7 +81,7 @@ sfz::EQPool::EQPool(const MidiState& state, int numEQs) setnumEQs(numEQs); } -sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, uint8_t velocity) +sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, unsigned numChannels, uint8_t velocity) { AtomicGuard guard { givingOutEQs }; if (!canGiveOutEQs) @@ -93,7 +94,7 @@ sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, uint8_t ve if (eq == eqs.end()) return {}; - (**eq).setup(description, velocity); + (**eq).setup(description, numChannels, velocity); return *eq; } diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index f3e2ddbf..879a2cdd 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -17,8 +17,10 @@ public: * @brief Setup a new EQ based on an EQ description. * * @param description the EQ description + * @param numChannels the number of channels for the EQ + * @param description the triggering velocity/value */ - void setup(const EQDescription& description, uint8_t velocity); + void setup(const EQDescription& description, unsigned numChannels, uint8_t velocity); /** * @brief Process a block of stereo inputs * @@ -84,11 +86,11 @@ public: * @brief Get an EQ object to use in Voices * * @param description the filter description to bind to the EQ - * @param noteNumber the triggering note number - * @param velocity the triggering note velocity + * @param numChannels the number of channels for the EQ + * @param velocity the triggering note velocity/value * @return EQHolderPtr release this when done with the filter; no deallocation will be done */ - EQHolderPtr getEQ(const EQDescription& description, uint8_t velocity); + EQHolderPtr getEQ(const EQDescription& description, unsigned numChannels, uint8_t velocity); /** * @brief Get the number of active EQs * diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index e64b9bde..5eb6cb3f 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -9,7 +9,7 @@ using namespace std::chrono_literals; sfz::FilterHolder::FilterHolder(const MidiState& midiState) : midiState(midiState) { - filter.setChannels(2); + } void sfz::FilterHolder::reset() @@ -17,11 +17,12 @@ void sfz::FilterHolder::reset() filter.clear(); } -void sfz::FilterHolder::setup(const FilterDescription& description, int noteNumber, uint8_t velocity) +void sfz::FilterHolder::setup(const FilterDescription& description, unsigned numChannels, int noteNumber, uint8_t velocity) { reset(); this->description = &description; filter.setType(description.type); + filter.setChannels(numChannels); baseCutoff = description.cutoff; if (description.random != 0) { @@ -88,7 +89,7 @@ sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) setNumFilters(numFilters); } -sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, int noteNumber, uint8_t velocity) +sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber, uint8_t velocity) { AtomicGuard guard { givingOutFilters }; if (!canGiveOutFilters) @@ -101,7 +102,7 @@ sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& descrip if (filter == filters.end()) return {}; - (**filter).setup(description, noteNumber, velocity); + (**filter).setup(description, numChannels, noteNumber, velocity); return *filter; } diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index b76d48da..792777c6 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -17,10 +17,11 @@ public: * @brief Setup a new filter based on a filter description, and a triggering note parameters. * * @param description the filter description + * @param numChannels the number of channels * @param noteNumber the triggering note number - * @param velocity the triggering note velocity + * @param velocity the triggering note velocity/value */ - void setup(const FilterDescription& description, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); + void setup(const FilterDescription& description, unsigned numChannels, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); /** * @brief Process a block of stereo inputs * @@ -88,11 +89,12 @@ public: * @brief Get a filter object to use in Voices * * @param description the filter description to bind to the filter + * @param numChannels the number of channels in the underlying filter * @param noteNumber the triggering note number * @param velocity the triggering note velocity * @return FilterHolderPtr release this when done with the filter; no deallocation will be done */ - FilterHolderPtr getFilter(const FilterDescription& description, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); + FilterHolderPtr getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); /** * @brief Get the number of active filters * diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 939d1250..2a36a27c 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -91,14 +91,15 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, uint8_t value ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size()); + const unsigned numChannels = region->isStereo ? 2 : 1; for (auto& filter: region->filters) { - auto newFilter = resources.filterPool.getFilter(filter, number, value); + auto newFilter = resources.filterPool.getFilter(filter, numChannels, number, value); if (newFilter) filters.push_back(newFilter); } for (auto& eq: region->equalizers) { - auto newEQ = resources.eqPool.getEQ(eq, value); + auto newEQ = resources.eqPool.getEQ(eq, numChannels, value); if (newEQ) equalizers.push_back(newEQ); } @@ -276,17 +277,6 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept else processMono(buffer); - const float* inputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; - float* outputChannels[2] { buffer.getChannel(0), buffer.getChannel(1) }; - - for (auto& filter: filters) { - filter->process(inputChannels, outputChannels, buffer.getNumFrames()); - } - - for (auto& eq: equalizers) { - eq->process(inputChannels, outputChannels, buffer.getNumFrames()); - } - if (!egEnvelope.isSmoothing()) reset(); @@ -319,6 +309,17 @@ void sfz::Voice::processMono(AudioSpan buffer) noexcept egEnvelope.getBlock(span1); applyGain(span1, leftBuffer); + // Filtering and EQ + const float* inputChannel[1] { leftBuffer.data() }; + float* outputChannel[1] { leftBuffer.data() }; + for (auto& filter: filters) { + filter->process(inputChannel, outputChannel, numSamples); + } + + for (auto& eq: equalizers) { + eq->process(inputChannel, outputChannel, numSamples); + } + // Prepare for stereo output copy(leftBuffer, rightBuffer); @@ -390,6 +391,18 @@ void sfz::Voice::processStereo(AudioSpan buffer) noexcept multiplyAdd(span2, span3, rightBuffer); applyGain(sqrtTwoInv, leftBuffer); applyGain(sqrtTwoInv, rightBuffer); + + // Filtering and EQ + const float* inputChannels[2] { leftBuffer.data(), rightBuffer.data() }; + float* outputChannels[2] { leftBuffer.data(), rightBuffer.data() }; + + for (auto& filter: filters) { + filter->process(inputChannels, outputChannels, numSamples); + } + + for (auto& eq: equalizers) { + eq->process(inputChannels, outputChannels, numSamples); + } } void sfz::Voice::fillWithData(AudioSpan buffer) noexcept