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; +}; +}