diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cd938d2b..117c3a4b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,8 @@ include(CheckLibraryExists) 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/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..cbbfa41a 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -53,6 +53,10 @@ namespace config { constexpr uint8_t numCCs { 143 }; constexpr int chunkSize { 1024 }; constexpr float defaultAmpEGRelease { 0.02f }; + constexpr int filtersInPool { maxVoices * 2 }; + constexpr int filtersPerVoice { 2 }; + constexpr int eqsPerVoice { 3 }; + 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. 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/EQPool.cpp b/src/sfizz/EQPool.cpp new file mode 100644 index 00000000..1004328a --- /dev/null +++ b/src/sfizz/EQPool.cpp @@ -0,0 +1,138 @@ +#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) +{ + +} + +void sfz::EQHolder::reset() +{ + eq.clear(); +} + +void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels, uint8_t velocity) +{ + reset(); + eq.setChannels(numChannels); + 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, unsigned numChannels, 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, numChannels, 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..879a2cdd --- /dev/null +++ b/src/sfizz/EQPool.h @@ -0,0 +1,123 @@ +#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 + * @param numChannels the number of channels for the EQ + * @param description the triggering velocity/value + */ + void setup(const EQDescription& description, unsigned numChannels, 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 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, unsigned numChannels, 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; +}; +} diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp new file mode 100644 index 00000000..5eb6cb3f --- /dev/null +++ b/src/sfizz/FilterPool.cpp @@ -0,0 +1,152 @@ +#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) +{ + +} + +void sfz::FilterHolder::reset() +{ + filter.clear(); +} + +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) { + 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 * normalizeVelocity(velocity); + baseCutoff *= centsFactor(veltrack); + baseCutoff = Default::filterCutoffRange.clamp(baseCutoff); + + 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 + lastCutoff = baseCutoff; + 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); +} + +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) +{ + setNumFilters(numFilters); +} + +sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, unsigned numChannels, 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; + }); + + if (filter == filters.end()) + return {}; + + (**filter).setup(description, numChannels, noteNumber, velocity); + return *filter; +} + +size_t sfz::FilterPool::getActiveFilters() const +{ + return absl::c_count_if(filters, [](const FilterHolderPtr& holder) { + return holder.use_count() > 1; + }); +} + +size_t sfz::FilterPool::setNumFilters(size_t numFilters) +{ + 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)); + filters.back()->setSampleRate(sampleRate); + } + + 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 new file mode 100644 index 00000000..792777c6 --- /dev/null +++ b/src/sfizz/FilterPool.h @@ -0,0 +1,127 @@ +#pragma once +#include "SfzFilter.h" +#include "FilterDescription.h" +#include "MidiState.h" +#include +#include + +namespace sfz +{ + +class FilterHolder +{ +public: + FilterHolder() = delete; + FilterHolder(const MidiState& state); + /** + * @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/value + */ + void setup(const FilterDescription& description, unsigned numChannels, 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; + /** + * @brief Set the sample rate for a filter + * + * @param sampleRate + */ + void setSampleRate(float sampleRate); +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 }; +}; + +using FilterHolderPtr = std::shared_ptr; + +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::filtersInPool); + /** + * @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, unsigned numChannels, int noteNumber = static_cast(Default::filterKeycenter), uint8_t velocity = 0); + /** + * @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); + /** + * @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/Resources.h b/src/sfizz/Resources.h index 0d11e088..616e2cac 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -6,13 +6,18 @@ #pragma once #include "FilePool.h" +#include "FilterPool.h" +#include "EQPool.h" #include "Logger.h" namespace sfz { struct Resources { + MidiState midiState; Logger logger; FilePool filePool { logger }; + FilterPool filterPool { midiState }; + EQPool eqPool { midiState }; }; } diff --git a/src/sfizz/SfzFilter.cpp b/src/sfizz/SfzFilter.cpp index 477ffb82..3d9dedc1 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(); @@ -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(); diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 9d9fdc53..3efdb6cf 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"): @@ -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(); @@ -287,7 +290,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) { @@ -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; } @@ -391,6 +401,9 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept this->sampleRate = sampleRate; for (auto& voice : voices) voice->setSampleRate(sampleRate); + + resources.filterPool.setSampleRate(sampleRate); + resources.eqPool.setSampleRate(sampleRate); } void sfz::Synth::renderBlock(AudioSpan buffer) noexcept @@ -430,7 +443,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 +457,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 +466,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 +521,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 +541,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 +624,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)); for (auto& voice: voices) { voice->setSampleRate(this->sampleRate); @@ -678,7 +691,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 { "" }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index d4a3e6f0..2a36a27c 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -14,9 +14,11 @@ #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) { + filters.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 @@ -45,7 +47,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 +56,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 +85,24 @@ 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())); + + // 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()); + + const unsigned numChannels = region->isStereo ? 2 : 1; + for (auto& filter: region->filters) { + 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, numChannels, value); + if (newEQ) + equalizers.push_back(newEQ); + } sourcePosition = region->getOffset(); triggerDelay = delay; @@ -98,7 +117,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 +160,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 +211,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)); } } @@ -290,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); @@ -361,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 @@ -458,35 +500,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 @@ -528,6 +576,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 +594,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 81cb1faf..ca4dbc09 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace sfz { /** @@ -34,7 +35,7 @@ public: * * @param midiState */ - Voice(const MidiState& midiState, Resources& resources); + Voice(Resources& resources); enum class TriggerType { NoteOn, NoteOff, @@ -196,6 +197,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 @@ -281,9 +294,11 @@ private: int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 }; float sampleRate { config::defaultSampleRate }; - const MidiState& midiState; Resources& resources; + std::vector filters; + std::vector equalizers; + ADSREnvelope egEnvelope; LinearEnvelope amplitudeEnvelope; // linear events LinearEnvelope crossfadeEnvelope; @@ -294,6 +309,8 @@ private: MultiplicativeEnvelope volumeEnvelope; float bendStepFactor { centsFactor(1) }; + std::normal_distribution noiseDist { 0, config::noiseVariance }; + HistoricalBuffer powerHistory { config::powerHistoryLength }; LEAK_DETECTOR(Voice); };