Merge pull request #53 from paulfd/initial-filter-processing

Initial filter processing
This commit is contained in:
Paul Ferrand 2020-02-10 12:53:26 +01:00 committed by GitHub
commit d26acd023f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 699 additions and 53 deletions

View file

@ -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

View file

@ -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;

View file

@ -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.

View file

@ -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
View 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
View 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
View 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
View 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;
};
}

View file

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

View file

@ -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();

View file

@ -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)

View file

@ -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 { "" };

View file

@ -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);
}

View file

@ -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);
};