Merge pull request #53 from paulfd/initial-filter-processing
Initial filter processing
This commit is contained in:
commit
d26acd023f
14 changed files with 699 additions and 53 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ public:
|
|||
*/
|
||||
bool contains(int index) const noexcept { return container.find(index) != container.end(); }
|
||||
typename std::map<int, ValueType>::iterator begin() { return container.begin(); }
|
||||
typename std::map<int, ValueType>::const_iterator begin() const { return container.cbegin(); }
|
||||
typename std::map<int, ValueType>::iterator end() { return container.end(); }
|
||||
typename std::map<int, ValueType>::const_iterator end() const { return container.cend(); }
|
||||
private:
|
||||
const ValueType defaultValue;
|
||||
std::map<int, ValueType> container;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
138
src/sfizz/EQPool.cpp
Normal file
138
src/sfizz/EQPool.cpp
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#include "EQPool.h"
|
||||
#include "AtomicGuard.h"
|
||||
#include <thread>
|
||||
#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<float>({ 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<double>(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<EQHolder>(midiState));
|
||||
eqs.back()->setSampleRate(sampleRate);
|
||||
}
|
||||
|
||||
return eqs.size();
|
||||
}
|
||||
void sfz::EQPool::setSampleRate(float sampleRate)
|
||||
{
|
||||
for (auto& eq: eqs)
|
||||
eq->setSampleRate(sampleRate);
|
||||
}
|
||||
123
src/sfizz/EQPool.h
Normal file
123
src/sfizz/EQPool.h
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#pragma once
|
||||
#include "SfzFilter.h"
|
||||
#include "EQDescription.h"
|
||||
#include "MidiState.h"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
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<EQHolder>;
|
||||
|
||||
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<bool> givingOutEQs { false };
|
||||
std::atomic<bool> canGiveOutEQs { true };
|
||||
float sampleRate { config::defaultSampleRate };
|
||||
const MidiState& midiState;
|
||||
std::vector<EQHolderPtr> eqs;
|
||||
};
|
||||
}
|
||||
152
src/sfizz/FilterPool.cpp
Normal file
152
src/sfizz/FilterPool.cpp
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#include "FilterPool.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include "absl/algorithm/container.h"
|
||||
#include "AtomicGuard.h"
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
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<float>({ 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<FilterHolder>(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<double>(sampleRate));
|
||||
}
|
||||
127
src/sfizz/FilterPool.h
Normal file
127
src/sfizz/FilterPool.h
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#pragma once
|
||||
#include "SfzFilter.h"
|
||||
#include "FilterDescription.h"
|
||||
#include "MidiState.h"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
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<int>(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<int>;
|
||||
filterRandomDist dist { 0, sfz::Default::filterRandom };
|
||||
};
|
||||
|
||||
using FilterHolderPtr = std::shared_ptr<FilterHolder>;
|
||||
|
||||
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<int>(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<bool> givingOutFilters { false };
|
||||
std::atomic<bool> canGiveOutFilters { true };
|
||||
float sampleRate { config::defaultSampleRate };
|
||||
const MidiState& midiState;
|
||||
std::vector<FilterHolderPtr> filters;
|
||||
};
|
||||
}
|
||||
|
|
@ -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 };
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ void sfz::Synth::callback(absl::string_view header, const std::vector<Opcode>& m
|
|||
|
||||
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||
{
|
||||
auto lastRegion = std::make_unique<Region>(midiState, defaultPath);
|
||||
auto lastRegion = std::make_unique<Region>(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<Opcode>& 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<float> 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<Voice>(midiState, resources));
|
||||
voices.push_back(std::make_unique<Voice>(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)
|
||||
|
|
|
|||
|
|
@ -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 { "" };
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@
|
|||
#include "absl/algorithm/container.h"
|
||||
#include <memory>
|
||||
|
||||
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<float>(midiState.getPitchBend()));
|
||||
pitchBendEnvelope.reset(static_cast<float>(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<int>(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<float> buffer) noexcept
|
|||
egEnvelope.getBlock(span1);
|
||||
applyGain<float>(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<float>(leftBuffer, rightBuffer);
|
||||
|
||||
|
|
@ -361,6 +391,18 @@ void sfz::Voice::processStereo(AudioSpan<float> buffer) noexcept
|
|||
multiplyAdd<float>(span2, span3, rightBuffer);
|
||||
applyGain<float>(sqrtTwoInv<float>, leftBuffer);
|
||||
applyGain<float>(sqrtTwoInv<float>, 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<float> buffer) noexcept
|
||||
|
|
@ -458,35 +500,41 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
|||
|
||||
void sfz::Voice::fillWithGenerator(AudioSpan<float> 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<float> / sampleRate;
|
||||
fill<float>(jumps, step);
|
||||
|
||||
const float step = baseFrequency * twoPi<float> / sampleRate;
|
||||
fill<float>(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<float>(bends, jumps);
|
||||
jumps[0] += phase;
|
||||
cumsum<float>(jumps, phases);
|
||||
phase = phases.back();
|
||||
|
||||
applyGain<float>(bends, jumps);
|
||||
jumps[0] += phase;
|
||||
cumsum<float>(jumps, phases);
|
||||
phase = phases.back();
|
||||
sin<float>(phases, leftSpan);
|
||||
copy<float>(leftSpan, rightSpan);
|
||||
|
||||
sin<float>(phases, buffer.getSpan(0));
|
||||
copy<float>(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<int>(phase / twoPi<float>);
|
||||
phase -= twoPi<float> * static_cast<float>(numTwoPiWraps);
|
||||
// Wrap the phase so we don't loose too much precision on longer notes
|
||||
const auto numTwoPiWraps = static_cast<int>(phase / twoPi<float>);
|
||||
phase -= twoPi<float> * static_cast<float>(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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <absl/types/span.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
|
||||
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<FilterHolderPtr> filters;
|
||||
std::vector<EQHolderPtr> equalizers;
|
||||
|
||||
ADSREnvelope<float> egEnvelope;
|
||||
LinearEnvelope<float> amplitudeEnvelope; // linear events
|
||||
LinearEnvelope<float> crossfadeEnvelope;
|
||||
|
|
@ -294,6 +309,8 @@ private:
|
|||
MultiplicativeEnvelope<float> volumeEnvelope;
|
||||
float bendStepFactor { centsFactor(1) };
|
||||
|
||||
std::normal_distribution<float> noiseDist { 0, config::noiseVariance };
|
||||
|
||||
HistoricalBuffer<float> powerHistory { config::powerHistoryLength };
|
||||
LEAK_DETECTOR(Voice);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue