Merge pull request #424 from paulfd/eg-lfo-filters-eq

EGs and LFOs sources for the filters and EQs
This commit is contained in:
Paul Ferrand 2020-09-21 13:14:12 +02:00 committed by GitHub
commit 8012829000
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1618 additions and 1480 deletions

View file

@ -156,11 +156,11 @@ namespace Default
constexpr uint8_t filterKeycenter { 60 };
constexpr int filterRandom { 0 };
constexpr int filterVeltrack { 0 };
constexpr int filterCutoffCC { 0 };
constexpr float filterCutoffCC { 0 };
constexpr float filterResonanceCC { 0 };
constexpr float filterGainCC { 0 };
constexpr Range<float> filterCutoffRange { 0.0f, 20000.0f };
constexpr Range<int> filterCutoffModRange { -9600, 9600 };
constexpr Range<float> filterCutoffModRange { -9600, 9600 };
constexpr Range<float> filterGainRange { -96.0f, 96.0f };
constexpr Range<float> filterGainModRange { -96.0f, 96.0f };
constexpr Range<int> filterKeytrackRange { 0, 1200 };

View file

@ -20,8 +20,5 @@ struct EQDescription
float vel2frequency { Default::eqVel2frequency };
float vel2gain { Default::eqVel2gain };
EqType type { EqType::kEqPeak };
CCMap<float> bandwidthCC { Default::eqBandwidthCC };
CCMap<float> frequencyCC { Default::eqFrequencyCC };
CCMap<float> gainCC { Default::eqGainCC };
};
}

View file

@ -4,147 +4,85 @@
#include "SIMDHelpers.h"
#include "SwapAndPop.h"
sfz::EQHolder::EQHolder(const MidiState& state)
:midiState(state)
sfz::EQHolder::EQHolder(Resources& resources)
: resources(resources)
{
eq = absl::make_unique<FilterEq>();
eq->init(config::defaultSampleRate);
}
void sfz::EQHolder::reset()
{
eq.clear();
eq->clear();
prepared = false;
}
void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels, float velocity)
void sfz::EQHolder::setup(const Region& region, unsigned eqId, float velocity)
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
eq.setType(description.type);
eq.setChannels(numChannels);
this->description = &description;
ASSERT(eqId < region.equalizers.size());
this->description = &region.equalizers[eqId];
eq->setType(description->type);
eq->setChannels(region.isStereo() ? 2 : 1);
// Setup the base values
baseFrequency = description.frequency + velocity * description.vel2frequency;
baseBandwidth = description.bandwidth;
baseGain = description.gain + velocity * description.vel2gain;
baseFrequency = description->frequency + velocity * description->vel2frequency;
baseBandwidth = description->bandwidth;
baseGain = description->gain + velocity * description->vel2gain;
// Setup the modulated values
lastFrequency = baseFrequency;
for (const auto& mod : description.frequencyCC)
lastFrequency += midiState.getCCValue(mod.cc) * mod.data;
lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency);
gainTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqGain, region.id, eqId));
bandwidthTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqBandwidth, region.id, eqId));
frequencyTarget = resources.modMatrix.findTarget(ModKey::createNXYZ(ModId::EqFrequency, region.id, eqId));
lastBandwidth = baseBandwidth;
for (const auto& mod : description.bandwidthCC)
lastBandwidth += midiState.getCCValue(mod.cc) * mod.data;
lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth);
lastGain = baseGain;
for (const auto& mod : description.gainCC)
lastGain += midiState.getCCValue(mod.cc) * mod.data;
lastGain = Default::filterGainRange.clamp(lastGain);
// Initialize the EQ
eq.prepare(lastFrequency, lastBandwidth, lastGain);
// Disables smoothing of the parameters on the first call
prepared = false;
}
void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numFrames)
{
auto justCopy = [&]() {
for (unsigned channelIdx = 0; channelIdx < eq.channels(); channelIdx++)
copy<float>({ inputs[channelIdx], numFrames }, { outputs[channelIdx], numFrames });
};
if (description == nullptr) {
justCopy();
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 (const auto& mod : description->frequencyCC)
lastFrequency += midiState.getCCValue(mod.cc) * mod.data;
lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency);
ModMatrix& mm = resources.modMatrix;
auto frequencySpan = resources.bufferPool.getBuffer(numFrames);
auto bandwidthSpan = resources.bufferPool.getBuffer(numFrames);
auto gainSpan = resources.bufferPool.getBuffer(numFrames);
lastBandwidth = baseBandwidth;
for (const auto& mod : description->bandwidthCC)
lastBandwidth += midiState.getCCValue(mod.cc) * mod.data;
lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth);
lastGain = baseGain;
for (const auto& mod : description->gainCC)
lastGain += midiState.getCCValue(mod.cc) * mod.data;
lastGain = Default::filterGainRange.clamp(lastGain);
if (lastGain == 0.0f) {
justCopy();
if (!frequencySpan || !bandwidthSpan || !gainSpan)
return;
fill<float>(*frequencySpan, baseFrequency);
if (float* mod = mm.getModulation(frequencyTarget))
add<float>(absl::Span<float>(mod, numFrames), *frequencySpan);
fill<float>(*bandwidthSpan, baseBandwidth);
if (float* mod = mm.getModulation(bandwidthTarget))
add<float>(absl::Span<float>(mod, numFrames), *bandwidthSpan);
fill<float>(*gainSpan, baseGain);
if (float* mod = mm.getModulation(gainTarget))
add<float>(absl::Span<float>(mod, numFrames), *gainSpan);
if (!prepared) {
eq->prepare(frequencySpan->front(), bandwidthSpan->front(), gainSpan->front());
prepared = true;
}
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;
eq->processModulated(
inputs,
outputs,
frequencySpan->data(),
bandwidthSpan->data(),
gainSpan->data(),
numFrames
);
}
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, float velocity)
{
const std::unique_lock<SpinMutex> lock { eqGuard, std::try_to_lock };
if (!lock.owns_lock())
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)
{
const std::lock_guard<SpinMutex> eqLock { eqGuard };
swapAndPopAll(eqs, [](sfz::EQHolderPtr& eq) { return eq.use_count() == 1; });
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);
eq->init(static_cast<double>(sampleRate));
}

View file

@ -1,7 +1,7 @@
#pragma once
#include "SfzFilter.h"
#include "EQDescription.h"
#include "MidiState.h"
#include "Region.h"
#include "Resources.h"
#include "utility/SpinMutex.h"
#include <vector>
#include <memory>
@ -14,15 +14,15 @@ class EQHolder
{
public:
EQHolder() = delete;
EQHolder(const MidiState& state);
EQHolder(Resources& resources);
/**
* @brief Setup a new EQ based on an EQ description.
* @brief Setup a new EQ from a region and an index
*
* @param description the EQ description
* @param numChannels the number of channels for the EQ
* @param description the triggering velocity/value
* @param description the region from which we take the EQ
* @param eqId the EQ index in the region
* @param description the triggering velocity/value
*/
void setup(const EQDescription& description, unsigned numChannels, float velocity);
void setup(const Region& region, unsigned eqId, float velocity);
/**
* @brief Process a block of stereo inputs
*
@ -31,94 +31,27 @@ public:
* @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().
* Reset the filter.
*/
void reset();
const MidiState& midiState;
private:
Resources& resources;
const EQDescription* description;
FilterEq eq;
std::unique_ptr<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 };
bool prepared { false };
ModMatrix::TargetId gainTarget;
ModMatrix::TargetId frequencyTarget;
ModMatrix::TargetId bandwidthTarget;
};
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, float 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:
SpinMutex eqGuard;
float sampleRate { config::defaultSampleRate };
const MidiState& midiState;
std::vector<EQHolderPtr> eqs;
};
}

View file

@ -22,8 +22,5 @@ struct FilterDescription
int veltrack { Default::filterVeltrack };
int random { Default::filterRandom };
FilterType type { FilterType::kFilterLpf2p };
CCMap<int> cutoffCC { Default::filterCutoffCC };
CCMap<float> resonanceCC { Default::filterResonanceCC };
CCMap<float> gainCC { Default::filterGainCC };
};
}

View file

@ -5,153 +5,102 @@
#include <thread>
#include <chrono>
sfz::FilterHolder::FilterHolder(const MidiState& midiState)
: midiState(midiState)
sfz::FilterHolder::FilterHolder(Resources& resources)
: resources(resources)
{
filter = absl::make_unique<Filter>();
filter->init(config::defaultSampleRate);
}
void sfz::FilterHolder::reset()
{
filter.clear();
filter->clear();
prepared = false;
}
void sfz::FilterHolder::setup(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity)
void sfz::FilterHolder::setup(const Region& region, unsigned filterId, int noteNumber, float velocity)
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
ASSERT(filterId < region.filters.size());
this->description = &description;
filter.setType(description.type);
filter.setChannels(numChannels);
this->description = &region.filters[filterId];
filter->setType(description->type);
filter->setChannels(region.isStereo() ? 2 : 1);
// Setup the base values
baseCutoff = description.cutoff;
if (description.random != 0) {
dist.param(filterRandomDist::param_type(0, description.random));
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);
const auto keytrack = description->keytrack * (noteNumber - description->keycenter);
baseCutoff *= centsFactor(keytrack);
const auto veltrack = static_cast<float>(description.veltrack) * velocity;
const auto veltrack = static_cast<float>(description->veltrack) * velocity;
baseCutoff *= centsFactor(veltrack);
baseCutoff = Default::filterCutoffRange.clamp(baseCutoff);
baseGain = description.gain;
baseResonance = description.resonance;
baseGain = description->gain;
baseResonance = description->resonance;
// Setup the modulated values
lastCutoff = baseCutoff;
for (const auto& mod : description.cutoffCC)
lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data);
lastCutoff = Default::filterCutoffRange.clamp(lastCutoff);
ModMatrix& mm = resources.modMatrix;
gainTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilGain, region.id, filterId));
cutoffTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilCutoff, region.id, filterId));
resonanceTarget = mm.findTarget(ModKey::createNXYZ(ModId::FilResonance, region.id, filterId));
lastResonance = baseResonance;
for (const auto& mod : description.resonanceCC)
lastResonance += midiState.getCCValue(mod.cc) * mod.data;
lastResonance = Default::filterResonanceRange.clamp(lastResonance);
lastGain = baseGain;
for (const auto& mod : description.gainCC)
lastGain += midiState.getCCValue(mod.cc) * mod.data;
lastGain = Default::filterGainRange.clamp(lastGain);
// Initialize the filter
filter.prepare(lastCutoff, lastResonance, lastGain);
// Disable smoothing of the parameters on the first call
prepared = false;
}
void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned numFrames)
{
if (numFrames == 0)
return;
if (description == nullptr) {
for (unsigned channelIdx = 0; channelIdx < filter.channels(); channelIdx++)
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
// TODO: the template deduction could be automatic here?
lastCutoff = baseCutoff;
for (const auto& mod : description->cutoffCC)
lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.data);
lastCutoff = Default::filterCutoffRange.clamp(lastCutoff);
ModMatrix& mm = resources.modMatrix;
auto cutoffSpan = resources.bufferPool.getBuffer(numFrames);
auto resonanceSpan = resources.bufferPool.getBuffer(numFrames);
auto gainSpan = resources.bufferPool.getBuffer(numFrames);
lastResonance = baseResonance;
for (const auto& mod : description->resonanceCC)
lastResonance += midiState.getCCValue(mod.cc) * mod.data;
lastResonance = Default::filterResonanceRange.clamp(lastResonance);
if (!cutoffSpan || !resonanceSpan || !gainSpan)
return;
lastGain = baseGain;
for (const auto& mod : description->gainCC)
lastGain += midiState.getCCValue(mod.cc) * mod.data;
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, float velocity)
{
const std::unique_lock<SpinMutex> lock { filterGuard, std::try_to_lock };
if (!lock.owns_lock())
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)
{
const std::lock_guard<SpinMutex> filterLock { filterGuard };
swapAndPopAll(filters, [](sfz::FilterHolderPtr& filter) { return filter.use_count() == 1; });
for (size_t i = filters.size(); i < numFilters; ++i) {
filters.emplace_back(std::make_shared<FilterHolder>(midiState));
filters.back()->setSampleRate(sampleRate);
fill<float>(*cutoffSpan, baseCutoff);
if (float* mod = mm.getModulation(cutoffTarget)) {
for (size_t i = 0; i < numFrames; ++i)
(*cutoffSpan)[i] *= centsFactor(mod[i]);
}
return filters.size();
fill<float>(*resonanceSpan, baseResonance);
if (float* mod = mm.getModulation(resonanceTarget))
add<float>(absl::Span<float>(mod, numFrames), *resonanceSpan);
fill<float>(*gainSpan, baseGain);
if (float* mod = mm.getModulation(gainTarget))
add<float>(absl::Span<float>(mod, numFrames), *gainSpan);
if (!prepared) {
filter->prepare(cutoffSpan->front(), resonanceSpan->front(), gainSpan->front());
prepared = true;
}
filter->processModulated(
inputs,
outputs,
cutoffSpan->data(),
resonanceSpan->data(),
gainSpan->data(),
numFrames
);
}
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));
filter->init(static_cast<double>(sampleRate));
}

View file

@ -1,7 +1,7 @@
#pragma once
#include "SfzFilter.h"
#include "FilterDescription.h"
#include "MidiState.h"
#include "Region.h"
#include "Resources.h"
#include "Defaults.h"
#include "utility/SpinMutex.h"
#include <vector>
@ -15,16 +15,16 @@ class FilterHolder
{
public:
FilterHolder() = delete;
FilterHolder(const MidiState& state);
FilterHolder(Resources& resources);
/**
* @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
* @param description the region from which we take the filter
* @param filterId the filter index in the region
* @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), float velocity = 0);
void setup(const Region& region, unsigned filterId, int noteNumber = static_cast<int>(Default::filterKeycenter), float velocity = 0);
/**
* @brief Process a block of stereo inputs
*
@ -33,97 +33,29 @@ public:
* @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().
* Reset the filter.
*/
void reset();
const MidiState& midiState;
private:
Resources& resources;
const FilterDescription* description;
Filter filter;
std::unique_ptr<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 };
ModMatrix::TargetId gainTarget;
ModMatrix::TargetId cutoffTarget;
ModMatrix::TargetId resonanceTarget;
bool prepared { false };
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), float 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:
SpinMutex filterGuard;
float sampleRate { config::defaultSampleRate };
const MidiState& midiState;
std::vector<FilterHolderPtr> filters;
};
}

File diff suppressed because it is too large Load diff

View file

@ -85,6 +85,7 @@ end_region_oncc:
egV1 = "ampeg"|"fileg"|"pitcheg";
eqV1 = "eq" number;
lfoV2 = "lfo" number;
egV2 = "eg" number;
(lfoV1) "_" ("depth"|"freq"|"fade") "cc" (number) END {
opcode = absl::StrCat(group(1), "_", group(2), "_oncc", group(3));
@ -102,7 +103,14 @@ end_region_oncc:
opcode = absl::StrCat(group(1), "_", group(2), "1");
goto end_region;
}
(lfoV2) "_" ("cutoff"|"resonance") ("_" any)? END {
opcode = absl::StrCat(group(1), "_", group(2), "1", group(3));
goto end_region;
}
(egV2) "_" ("cutoff"|"resonance") ("_" any)? END {
opcode = absl::StrCat(group(1), "_", group(2), "1", group(3));
goto end_region;
}
"loop" ("mode"|"start"|"end") END {
opcode = absl::StrCat("loop_", group(1));
goto end_region;

View file

@ -45,6 +45,23 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
case hash(x "_stepcc&"): \
case hash(x "_smoothcc&")
#define LFO_EG_filter_EQ_target(sourceKey, targetKey, range) \
{ \
const auto number = opcode.parameters.front(); \
if (number == 0) \
return false; \
\
const auto index = opcode.parameters.size() == 2 ? opcode.parameters.back() - 1 : 0; \
if (!extendIfNecessary(filters, index + 1, Default::numFilters)) \
return false; \
\
if (auto value = readOpcode(opcode.value, range)) { \
const ModKey source = ModKey::createNXYZ(sourceKey, id, number - 1); \
const ModKey target = ModKey::createNXYZ(targetKey, id, index); \
getOrCreateConnection(source, target).sourceDepth = *value; \
} \
}
// Sound source: sample playback
case hash("sample"):
{
@ -579,30 +596,22 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
setValueFromOpcode(opcode, filters[filterIndex].resonance, Default::filterResonanceRange);
}
break;
case hash("cutoff&_oncc&"): // also cutoff_oncc&, cutoff_cc&, cutoff&_cc&
case_any_ccN("cutoff&"): // also cutoff_oncc&, cutoff_cc&, cutoff&_cc&
{
const auto filterIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters))
return false;
setValueFromOpcode(
opcode,
filters[filterIndex].cutoffCC[opcode.parameters.back()],
Default::filterCutoffModRange
);
processGenericCc(opcode, Default::filterCutoffModRange, ModKey::createNXYZ(ModId::FilCutoff, id, filterIndex));
}
break;
case hash("resonance&_oncc&"): // also resonance_oncc&, resonance_cc&, resonance&_cc&
case_any_ccN("resonance&"): // also resonance_oncc&, resonance_cc&, resonance&_cc&
{
const auto filterIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters))
return false;
setValueFromOpcode(
opcode,
filters[filterIndex].resonanceCC[opcode.parameters.back()],
Default::filterResonanceModRange
);
processGenericCc(opcode, Default::filterResonanceModRange, ModKey::createNXYZ(ModId::FilResonance, id, filterIndex));
}
break;
case hash("fil&_keytrack"): // also fil_keytrack
@ -650,17 +659,13 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
setValueFromOpcode(opcode, filters[filterIndex].gain, Default::filterGainRange);
}
break;
case hash("fil&_gain_oncc&"): // also fil_gain_oncc&
case_any_ccN("fil&_gain"): // also fil_gain_oncc&
{
const auto filterIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(filters, filterIndex + 1, Default::numFilters))
return false;
setValueFromOpcode(
opcode,
filters[filterIndex].gainCC[opcode.parameters.back()],
Default::filterGainModRange
);
processGenericCc(opcode, Default::filterGainModRange, ModKey::createNXYZ(ModId::FilGain, id, filterIndex));
}
break;
case hash("fil&_type"): // also fil_type, filtype
@ -683,107 +688,90 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
// Performance parameters: EQ
case hash("eq&_bw"):
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidth, Default::eqBandwidthRange);
}
break;
case hash("eq&_bw_oncc&"): // also eq&_bwcc&
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].bandwidthCC[opcode.parameters.back()], Default::eqBandwidthModRange);
setValueFromOpcode(opcode, equalizers[eqIndex].bandwidth, Default::eqBandwidthRange);
}
break;
case_any_ccN("eq&_bw"): // also eq&_bwcc&
{
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
processGenericCc(opcode, Default::eqBandwidthModRange, ModKey::createNXYZ(ModId::EqBandwidth, id, eqIndex));
}
break;
case hash("eq&_freq"):
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].frequency, Default::eqFrequencyRange);
setValueFromOpcode(opcode, equalizers[eqIndex].frequency, Default::eqFrequencyRange);
}
break;
case hash("eq&_freq_oncc&"): // also eq&_freqcc&
case_any_ccN("eq&_freq"): // also eq&_freqcc&
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].frequencyCC[opcode.parameters.back()], Default::eqFrequencyModRange);
processGenericCc(opcode, Default::eqFrequencyModRange, ModKey::createNXYZ(ModId::EqFrequency, id, eqIndex));
}
break;
case hash("eq&_vel&freq"):
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
const auto eqIndex = opcode.parameters.front() - 1;
if (opcode.parameters[1] != 2)
return false; // was eqN_vel3freq or something else than eqN_vel2freq
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].vel2frequency, Default::eqFrequencyModRange);
setValueFromOpcode(opcode, equalizers[eqIndex].vel2frequency, Default::eqFrequencyModRange);
}
break;
case hash("eq&_gain"):
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].gain, Default::eqGainRange);
setValueFromOpcode(opcode, equalizers[eqIndex].gain, Default::eqGainRange);
}
break;
case hash("eq&_gain_oncc&"): // also eq&_gaincc&
case_any_ccN("eq&_gain"): // also eq&_gaincc&
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].gainCC[opcode.parameters.back()], Default::eqGainModRange);
processGenericCc(opcode, Default::eqGainModRange, ModKey::createNXYZ(ModId::EqGain, id, eqIndex));
}
break;
case hash("eq&_vel&gain"):
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
const auto eqIndex = opcode.parameters.front() - 1;
if (opcode.parameters[1] != 2)
return false; // was eqN_vel3gain or something else than eqN_vel2gain
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
setValueFromOpcode(opcode, equalizers[eqNumber - 1].vel2gain, Default::eqGainModRange);
setValueFromOpcode(opcode, equalizers[eqIndex].vel2gain, Default::eqGainModRange);
}
break;
case hash("eq&_type"):
{
const auto eqNumber = opcode.parameters.front();
if (eqNumber == 0)
return false;
if (!extendIfNecessary(equalizers, eqNumber, Default::numEQs))
const auto eqIndex = opcode.parameters.front() - 1;
if (!extendIfNecessary(equalizers, eqIndex + 1, Default::numEQs))
return false;
absl::optional<EqType> ftype = FilterEq::typeFromName(opcode.value);
if (ftype)
equalizers[eqNumber - 1].type = *ftype;
equalizers[eqIndex].type = *ftype;
else {
equalizers[eqNumber - 1].type = EqType::kEqNone;
equalizers[eqIndex].type = EqType::kEqNone;
DBG("Unknown EQ type: " << opcode.value);
}
}
@ -1045,6 +1033,24 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
}
}
break;
case hash("lfo&_cutoff&"):
LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilCutoff, Default::filterCutoffModRange);
break;
case hash("lfo&_resonance&"):
LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilResonance, Default::filterResonanceModRange);
break;
case hash("lfo&_fil&_gain"):
LFO_EG_filter_EQ_target(ModId::LFO, ModId::FilGain, Default::filterGainModRange);
break;
case hash("lfo&_eq&gain"):
LFO_EG_filter_EQ_target(ModId::LFO, ModId::EqGain, Default::eqGainModRange);
break;
case hash("lfo&_eq&freq"):
LFO_EG_filter_EQ_target(ModId::LFO, ModId::EqFrequency, Default::eqFrequencyModRange);
break;
case hash("lfo&_eq&bw"):
LFO_EG_filter_EQ_target(ModId::LFO, ModId::EqBandwidth, Default::eqBandwidthModRange);
break;
// Modulation: Flex EG (targets)
case hash("eg&_amplitude"):
@ -1119,6 +1125,24 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
}
}
break;
case hash("eg&_cutoff&"):
LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilCutoff, Default::filterCutoffModRange);
break;
case hash("eg&_resonance&"):
LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilResonance, Default::filterResonanceModRange);
break;
case hash("eg&_fil&_gain"):
LFO_EG_filter_EQ_target(ModId::Envelope, ModId::FilGain, Default::filterGainModRange);
break;
case hash("eg&_eq&gain"):
LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqGain, Default::eqGainModRange);
break;
case hash("eg&_eq&freq"):
LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqFrequency, Default::eqFrequencyModRange);
break;
case hash("eg&_eq&bw"):
LFO_EG_filter_EQ_target(ModId::Envelope, ModId::EqBandwidth, Default::eqBandwidthModRange);
break;
// Amplitude Envelope
case hash("ampeg_attack"):
@ -1321,6 +1345,7 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode)
return false;
#undef case_any_ccN
#undef LFO_EG_filter_EQ_target
}
return true;

View file

@ -6,10 +6,9 @@
#pragma once
#include "SynthConfig.h"
#include "MidiState.h"
#include "FilePool.h"
#include "BufferPool.h"
#include "FilterPool.h"
#include "EQPool.h"
#include "Logger.h"
#include "Wavetables.h"
#include "Curve.h"
@ -29,8 +28,6 @@ struct Resources
Logger logger;
CurveSet curves;
FilePool filePool { logger };
FilterPool filterPool { midiState };
EQPool eqPool { midiState };
WavetablePool wavePool;
Tuning tuning;
absl::optional<StretchTuning> stretch;
@ -39,8 +36,6 @@ struct Resources
void setSampleRate(float samplerate)
{
midiState.setSampleRate(samplerate);
filterPool.setSampleRate(samplerate);
eqPool.setSampleRate(samplerate);
modMatrix.setSampleRate(samplerate);
}

View file

@ -23,8 +23,11 @@
sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources)
: id{voiceNumber}, stateListener(nullptr), resources(resources)
{
filters.reserve(config::filtersPerVoice);
equalizers.reserve(config::eqsPerVoice);
for (unsigned i = 0; i < config::filtersPerVoice; ++i)
filters.emplace_back(resources);
for (unsigned i = 0; i < config::eqsPerVoice; ++i)
equalizers.emplace_back(resources);
for (WavetableOscillator& osc : waveOscillators)
osc.init(sampleRate);
@ -118,21 +121,12 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event
gainSmoother.reset();
resetCrossfades();
// 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, triggerEvent.number, triggerEvent.value);
if (newFilter)
filters.push_back(newFilter);
for (unsigned i = 0; i < region->filters.size(); ++i) {
filters[i].setup(*region, i, triggerEvent.number, triggerEvent.value);
}
for (auto& eq: region->equalizers) {
auto newEQ = resources.eqPool.getEQ(eq, numChannels, triggerEvent.value);
if (newEQ)
equalizers.push_back(newEQ);
for (unsigned i = 0; i < region->equalizers.size(); ++i) {
equalizers[i].setup(*region, i, triggerEvent.value);
}
sourcePosition = region->getOffset();
@ -253,6 +247,12 @@ void sfz::Voice::setSampleRate(float sampleRate) noexcept
for (auto& lfo : lfos)
lfo->setSampleRate(sampleRate);
for (auto& filter : filters)
filter.setSampleRate(sampleRate);
for (auto& eq : equalizers)
eq.setSampleRate(sampleRate);
powerFollower.setSampleRate(sampleRate);
}
@ -498,12 +498,12 @@ void sfz::Voice::filterStageMono(AudioSpan<float> buffer) noexcept
const auto leftBuffer = buffer.getSpan(0);
const float* inputChannel[1] { leftBuffer.data() };
float* outputChannel[1] { leftBuffer.data() };
for (auto& filter : filters) {
filter->process(inputChannel, outputChannel, numSamples);
for (unsigned i = 0; i < region->filters.size(); ++i) {
filters[i].process(inputChannel, outputChannel, numSamples);
}
for (auto& eq : equalizers) {
eq->process(inputChannel, outputChannel, numSamples);
for (unsigned i = 0; i < region->equalizers.size(); ++i) {
equalizers[i].process(inputChannel, outputChannel, numSamples);
}
}
@ -517,12 +517,12 @@ void sfz::Voice::filterStageStereo(AudioSpan<float> buffer) noexcept
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 (unsigned i = 0; i < region->filters.size(); ++i) {
filters[i].process(inputChannels, outputChannels, numSamples);
}
for (auto& eq : equalizers) {
eq->process(inputChannels, outputChannels, numSamples);
for (unsigned i = 0; i < region->equalizers.size(); ++i) {
equalizers[i].process(inputChannels, outputChannels, numSamples);
}
}
@ -611,7 +611,7 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
sourcePosition = indices->back();
floatPositionOffset = coeffs->back();
#if 0
#if 1
ASSERT(!hasNanInf(buffer.getConstSpan(0)));
ASSERT(!hasNanInf(buffer.getConstSpan(1)));
SFIZZ_CHECK(isReasonableAudio(buffer.getConstSpan(0)));
@ -731,8 +731,11 @@ void sfz::Voice::reset() noexcept
powerFollower.clear();
filters.clear();
equalizers.clear();
for (auto& filter : filters)
filter.reset();
for (auto& eq : equalizers)
eq.reset();
removeVoiceFromRing();
}
@ -776,16 +779,22 @@ uint32_t sfz::Voice::getSourcePosition() const noexcept
void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters)
{
// There are filters in there, this call is unexpected
ASSERT(filters.size() == 0);
filters.reserve(numFilters);
if (numFilters == filters.size())
return;
filters.clear();
for (unsigned i = 0; i < numFilters; ++i)
filters.emplace_back(resources);
}
void sfz::Voice::setMaxEQsPerVoice(size_t numFilters)
{
// There are filters in there, this call is unexpected
ASSERT(equalizers.size() == 0);
equalizers.reserve(numFilters);
if (numFilters == equalizers.size())
return;
equalizers.clear();
for (unsigned i = 0; i < numFilters; ++i)
equalizers.emplace_back(resources);
}
void sfz::Voice::setMaxLFOsPerVoice(size_t numLFOs)

View file

@ -12,6 +12,8 @@
#include "Region.h"
#include "AudioBuffer.h"
#include "Resources.h"
#include "FilterPool.h"
#include "EQPool.h"
#include "Smoothers.h"
#include "AudioSpan.h"
#include "LeakDetector.h"
@ -454,8 +456,8 @@ private:
Resources& resources;
std::vector<FilterHolderPtr> filters;
std::vector<EQHolderPtr> equalizers;
std::vector<FilterHolder> filters;
std::vector<EQHolder> equalizers;
std::vector<std::unique_ptr<LFO>> lfos;
std::vector<std::unique_ptr<FlexEnvelope>> flexEGs;

View file

@ -44,6 +44,18 @@ int ModIds::flags(ModId id) noexcept
return kModIsPerVoice|kModIsAdditive;
case ModId::Volume:
return kModIsPerVoice|kModIsAdditive;
case ModId::FilGain:
return kModIsPerVoice|kModIsAdditive;
case ModId::FilCutoff:
return kModIsPerVoice|kModIsAdditive;
case ModId::FilResonance:
return kModIsPerVoice|kModIsAdditive;
case ModId::EqGain:
return kModIsPerVoice|kModIsAdditive;
case ModId::EqFrequency:
return kModIsPerVoice|kModIsAdditive;
case ModId::EqBandwidth:
return kModIsPerVoice|kModIsAdditive;
// unknown
default:

View file

@ -37,6 +37,12 @@ enum class ModId : int {
Position,
Pitch,
Volume,
FilGain,
FilCutoff,
FilResonance,
EqGain,
EqFrequency,
EqBandwidth,
_TargetsEnd,
// [/targets] --------------------------------------------------------------

View file

@ -87,6 +87,18 @@ std::string ModKey::toString() const
return absl::StrCat("Pitch {", region_.number(), "}");
case ModId::Volume:
return absl::StrCat("Volume {", region_.number(), "}");
case ModId::FilGain:
return absl::StrCat("FilterGain {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::FilCutoff:
return absl::StrCat("FilterCutoff {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::FilResonance:
return absl::StrCat("FilterResonance {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::EqGain:
return absl::StrCat("EqGain {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::EqFrequency:
return absl::StrCat("EqFrequency {", region_.number(), ", N=", 1 + params_.N, "}");
case ModId::EqBandwidth:
return absl::StrCat("EqBandwidth {", region_.number(), ", N=", 1 + params_.N, "}");
default:
return {};

View file

@ -168,7 +168,7 @@ ModMatrix::TargetId ModMatrix::registerTarget(const ModKey& key)
return id;
}
ModMatrix::SourceId ModMatrix::findSource(const ModKey& key)
ModMatrix::SourceId ModMatrix::findSource(const ModKey& key) const
{
Impl& impl = *impl_;
@ -179,7 +179,7 @@ ModMatrix::SourceId ModMatrix::findSource(const ModKey& key)
return SourceId(it->second);
}
ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key)
ModMatrix::TargetId ModMatrix::findTarget(const ModKey& key) const
{
Impl& impl = *impl_;

View file

@ -77,14 +77,14 @@ public:
*
* @param key source key
*/
SourceId findSource(const ModKey& key);
SourceId findSource(const ModKey& key) const;
/**
* @brief Look up a target by key.
*
* @param key target key
*/
TargetId findTarget(const ModKey& key);
TargetId findTarget(const ModKey& key) const;
/**
* @brief Connect a source and a destination inside the matrix.

View file

@ -99,3 +99,135 @@ width_oncc425=29
R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {0}")",
}));
}
TEST_CASE("[Modulations] Filter CC connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine
cutoff=100 fil1_gain_oncc3=5 fil1_gain_stepcc3=0.5
cutoff2=300 cutoff2_cc2=100 cutoff2_curvecc2=2
resonance3=-1 resonance3_oncc1=2 resonance3_smoothcc1=10
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createReferenceGraph({
R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "FilterResonance {0, N=3}")",
R"("Controller 2 {curve=2, smooth=0, value=100, step=0}" -> "FilterCutoff {0, N=2}")",
R"("Controller 3 {curve=0, smooth=0, value=5, step=0.5}" -> "FilterGain {0, N=1}")",
}));
}
TEST_CASE("[Modulations] EQ CC connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine
eq1_gain_oncc2=5 eq1_gain_stepcc2=0.5
eq2_freq_oncc3=300 eq2_freq_curvecc3=3
eq3_bw_oncc1=2 eq3_bw_smoothcc1=10
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createReferenceGraph({
R"("Controller 1 {curve=0, smooth=10, value=2, step=0}" -> "EqBandwidth {0, N=3}")",
R"("Controller 2 {curve=0, smooth=0, value=5, step=0.5}" -> "EqGain {0, N=1}")",
R"("Controller 3 {curve=3, smooth=0, value=300, step=0}" -> "EqFrequency {0, N=2}")",
}));
}
TEST_CASE("[Modulations] LFO Filter connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine
lfo1_freq=0.1 lfo1_cutoff1=1
lfo2_freq=1 lfo2_cutoff=2
lfo3_freq=2 lfo3_resonance=3
lfo4_freq=0.5 lfo4_resonance1=4
lfo5_freq=0.5 lfo5_resonance2=5
lfo6_freq=3 lfo6_fil1_gain=-1
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createReferenceGraph({
R"("LFO 1 {0}" -> "FilterCutoff {0, N=1}")",
R"("LFO 2 {0}" -> "FilterCutoff {0, N=1}")",
R"("LFO 3 {0}" -> "FilterResonance {0, N=1}")",
R"("LFO 4 {0}" -> "FilterResonance {0, N=1}")",
R"("LFO 5 {0}" -> "FilterResonance {0, N=2}")",
R"("LFO 6 {0}" -> "FilterGain {0, N=1}")",
}));
}
TEST_CASE("[Modulations] EG Filter connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine
eg1_time1=0.1 eg1_cutoff1=1
eg2_time1=1 eg2_cutoff=2
eg3_time1=2 eg3_resonance=3
eg4_time1=0.5 eg4_resonance1=4
eg5_time1=0.5 eg5_resonance2=5
eg6_time1=3 eg6_fil1_gain=-1
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createReferenceGraph({
R"("EG 1 {0}" -> "FilterCutoff {0, N=1}")",
R"("EG 2 {0}" -> "FilterCutoff {0, N=1}")",
R"("EG 3 {0}" -> "FilterResonance {0, N=1}")",
R"("EG 4 {0}" -> "FilterResonance {0, N=1}")",
R"("EG 5 {0}" -> "FilterResonance {0, N=2}")",
R"("EG 6 {0}" -> "FilterGain {0, N=1}")",
}));
}
TEST_CASE("[Modulations] LFO EQ connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine
lfo1_freq=0.1 lfo1_eq1bw=1
lfo2_freq=1 lfo2_eq2freq=2
lfo3_freq=2 lfo3_eq3gain=3
lfo4_freq=0.5 lfo4_eq3bw=4
lfo5_freq=0.5 lfo5_eq2gain=5
lfo6_freq=3 lfo6_eq1freq=-1
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createReferenceGraph({
R"("LFO 1 {0}" -> "EqBandwidth {0, N=1}")",
R"("LFO 2 {0}" -> "EqFrequency {0, N=2}")",
R"("LFO 3 {0}" -> "EqGain {0, N=3}")",
R"("LFO 4 {0}" -> "EqBandwidth {0, N=3}")",
R"("LFO 5 {0}" -> "EqGain {0, N=2}")",
R"("LFO 6 {0}" -> "EqFrequency {0, N=1}")",
}));
}
TEST_CASE("[Modulations] EG EQ connections")
{
sfz::Synth synth;
synth.loadSfzString("/modulation.sfz", R"(
<region> sample=*sine
eg1_freq=0.1 eg1_eq1bw=1
eg2_freq=1 eg2_eq2freq=2
eg3_freq=2 eg3_eq3gain=3
eg4_freq=0.5 eg4_eq3bw=4
eg5_freq=0.5 eg5_eq2gain=5
eg6_freq=3 eg6_eq1freq=-1
)");
const std::string graph = synth.getResources().modMatrix.toDotGraph();
REQUIRE(graph == createReferenceGraph({
R"("EG 1 {0}" -> "EqBandwidth {0, N=1}")",
R"("EG 2 {0}" -> "EqFrequency {0, N=2}")",
R"("EG 3 {0}" -> "EqGain {0, N=3}")",
R"("EG 4 {0}" -> "EqBandwidth {0, N=3}")",
R"("EG 5 {0}" -> "EqGain {0, N=2}")",
R"("EG 6 {0}" -> "EqFrequency {0, N=1}")",
}));
}

View file

@ -1313,9 +1313,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.filters[0].gain == 0);
REQUIRE(region.filters[0].veltrack == 0);
REQUIRE(region.filters[0].resonance == 0.0f);
REQUIRE(region.filters[0].cutoffCC.empty());
REQUIRE(region.filters[0].gainCC.empty());
REQUIRE(region.filters[0].resonanceCC.empty());
region.parseOpcode({ "cutoff2", "5000" });
REQUIRE(region.filters.size() == 2);
@ -1327,9 +1324,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.filters[1].gain == 0);
REQUIRE(region.filters[1].veltrack == 0);
REQUIRE(region.filters[1].resonance == 0.0f);
REQUIRE(region.filters[1].cutoffCC.empty());
REQUIRE(region.filters[1].gainCC.empty());
REQUIRE(region.filters[1].resonanceCC.empty());
region.parseOpcode({ "cutoff4", "50" });
REQUIRE(region.filters.size() == 4);
@ -1342,18 +1336,12 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.filters[2].gain == 0);
REQUIRE(region.filters[2].veltrack == 0);
REQUIRE(region.filters[2].resonance == 0.0f);
REQUIRE(region.filters[2].cutoffCC.empty());
REQUIRE(region.filters[2].gainCC.empty());
REQUIRE(region.filters[2].resonanceCC.empty());
REQUIRE(region.filters[3].keycenter == 60);
REQUIRE(region.filters[3].type == FilterType::kFilterLpf2p);
REQUIRE(region.filters[3].keytrack == 0);
REQUIRE(region.filters[3].gain == 0);
REQUIRE(region.filters[3].veltrack == 0);
REQUIRE(region.filters[3].resonance == 0.0f);
REQUIRE(region.filters[3].cutoffCC.empty());
REQUIRE(region.filters[3].gainCC.empty());
REQUIRE(region.filters[3].resonanceCC.empty());
}
SECTION("Filter parameter dispatch")
@ -1373,16 +1361,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.filters[1].veltrack == -100);
region.parseOpcode({ "fil3_keytrack", "100" });
REQUIRE(region.filters[2].keytrack == 100);
REQUIRE(region.filters[0].cutoffCC.empty());
region.parseOpcode({ "cutoff1_cc15", "210" });
REQUIRE(region.filters[0].cutoffCC.contains(15));
REQUIRE(region.filters[0].cutoffCC[15] == 210);
region.parseOpcode({ "resonance3_cc24", "10" });
REQUIRE(region.filters[2].resonanceCC.contains(24));
REQUIRE(region.filters[2].resonanceCC[24] == 10);
region.parseOpcode({ "fil2_gain_oncc12", "-50" });
REQUIRE(region.filters[1].gainCC.contains(12));
REQUIRE(region.filters[1].gainCC[12] == -50.0f);
}
@ -1430,16 +1408,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.filters[0].gain == 96.0f);
region.parseOpcode({ "fil_gain", "-200" });
REQUIRE(region.filters[0].gain == -96.0f);
region.parseOpcode({ "cutoff_cc43", "10000" });
REQUIRE(region.filters[0].cutoffCC[43] == 9600);
region.parseOpcode({ "cutoff_cc43", "-10000" });
REQUIRE(region.filters[0].cutoffCC[43] == -9600);
region.parseOpcode({ "resonance_cc43", "100" });
REQUIRE(region.filters[0].resonanceCC[43] == 96.0f);
region.parseOpcode({ "resonance_cc43", "-5" });
REQUIRE(region.filters[0].resonanceCC[43] == 0.0f);
}
SECTION("Filter types")
@ -1512,9 +1480,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.equalizers[0].frequency == 0.0f);
REQUIRE(region.equalizers[0].vel2frequency == 0);
REQUIRE(region.equalizers[0].vel2gain == 0);
REQUIRE(region.equalizers[0].frequencyCC.empty());
REQUIRE(region.equalizers[0].bandwidthCC.empty());
REQUIRE(region.equalizers[0].gainCC.empty());
region.parseOpcode({ "eq2_gain", "-400" });
REQUIRE(region.equalizers.size() == 2);
@ -1525,9 +1490,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.equalizers[1].frequency == 0.0f);
REQUIRE(region.equalizers[1].vel2frequency == 0);
REQUIRE(region.equalizers[1].vel2gain == 0);
REQUIRE(region.equalizers[1].frequencyCC.empty());
REQUIRE(region.equalizers[1].bandwidthCC.empty());
REQUIRE(region.equalizers[1].gainCC.empty());
region.parseOpcode({ "eq4_gain", "500" });
REQUIRE(region.equalizers.size() == 4);
@ -1539,16 +1501,10 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.equalizers[2].frequency == 0.0f);
REQUIRE(region.equalizers[2].vel2frequency == 0);
REQUIRE(region.equalizers[2].vel2gain == 0);
REQUIRE(region.equalizers[2].frequencyCC.empty());
REQUIRE(region.equalizers[2].bandwidthCC.empty());
REQUIRE(region.equalizers[2].gainCC.empty());
REQUIRE(region.equalizers[3].bandwidth == 1.0f);
REQUIRE(region.equalizers[3].frequency == 0.0f);
REQUIRE(region.equalizers[3].vel2frequency == 0);
REQUIRE(region.equalizers[3].vel2gain == 0);
REQUIRE(region.equalizers[3].frequencyCC.empty());
REQUIRE(region.equalizers[3].bandwidthCC.empty());
REQUIRE(region.equalizers[3].gainCC.empty());
}
SECTION("EQ types")
@ -1578,24 +1534,8 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.equalizers[2].vel2gain == 10.0f);
region.parseOpcode({ "eq1_vel2freq", "100" });
REQUIRE(region.equalizers[0].vel2frequency == 100.0f);
REQUIRE(region.equalizers[0].bandwidthCC.empty());
region.parseOpcode({ "eq1_bwcc24", "0.5" });
REQUIRE(region.equalizers[0].bandwidthCC.contains(24));
REQUIRE(region.equalizers[0].bandwidthCC[24] == 0.5f);
region.parseOpcode({ "eq1_bw_oncc24", "1.5" });
REQUIRE(region.equalizers[0].bandwidthCC[24] == 1.5f);
region.parseOpcode({ "eq3_freqcc15", "10" });
REQUIRE(region.equalizers[2].frequencyCC.contains(15));
REQUIRE(region.equalizers[2].frequencyCC[15] == 10.0f);
region.parseOpcode({ "eq3_freq_oncc15", "20" });
REQUIRE(region.equalizers[2].frequencyCC[15] == 20.0f);
region.parseOpcode({ "eq1_type", "hshelf" });
REQUIRE(region.equalizers[0].type == EqType::kEqHighShelf);
region.parseOpcode({ "eq2_gaincc123", "2" });
REQUIRE(region.equalizers[1].gainCC.contains(123));
REQUIRE(region.equalizers[1].gainCC[123] == 2.0f);
region.parseOpcode({ "eq2_gain_oncc123", "-2" });
REQUIRE(region.equalizers[1].gainCC[123] == -2.0f);
}
SECTION("EQ parameter values")
@ -1625,24 +1565,6 @@ TEST_CASE("[Region] Parsing opcodes")
REQUIRE(region.equalizers[0].vel2frequency == 30000.0f);
region.parseOpcode({ "eq1_vel2freq", "-35000" });
REQUIRE(region.equalizers[0].vel2frequency == -30000.0f);
region.parseOpcode({ "eq1_bwcc15", "2" });
REQUIRE(region.equalizers[0].bandwidthCC[15] == 2.0f);
region.parseOpcode({ "eq1_bwcc15", "-5" });
REQUIRE(region.equalizers[0].bandwidthCC[15] == -4.0f);
region.parseOpcode({ "eq1_bwcc15", "5" });
REQUIRE(region.equalizers[0].bandwidthCC[15] == 4.0f);
region.parseOpcode({ "eq1_gaincc15", "2" });
REQUIRE(region.equalizers[0].gainCC[15] == 2.0f);
region.parseOpcode({ "eq1_gaincc15", "-500" });
REQUIRE(region.equalizers[0].gainCC[15] == -96.0f);
region.parseOpcode({ "eq1_gaincc15", "500" });
REQUIRE(region.equalizers[0].gainCC[15] == 96.0f);
region.parseOpcode({ "eq1_freqcc15", "200" });
REQUIRE(region.equalizers[0].frequencyCC[15] == 200.0f);
region.parseOpcode({ "eq1_freqcc15", "-50000" });
REQUIRE(region.equalizers[0].frequencyCC[15] == -30000.0f);
region.parseOpcode({ "eq1_freqcc15", "50000" });
REQUIRE(region.equalizers[0].frequencyCC[15] == 30000.0f);
}
SECTION("Effects send")