570 lines
18 KiB
C++
570 lines
18 KiB
C++
/* Signalsmith's Templated FX
|
|
|
|
Copyright 2021-2022 Geraint Luff / Signalsmith Audio Ltd
|
|
Released under the Boost Software License (see LICENSE.txt)
|
|
|
|
The main thing you need is `stfx::LibraryEffect<Sample, EffectTemplate>`
|
|
which produces a simple effect class from an STFX effect template.
|
|
*/
|
|
|
|
#ifndef STFX_LIBRARY_H
|
|
#define STFX_LIBRARY_H
|
|
|
|
#include <vector>
|
|
#include <string>
|
|
#include <cmath>
|
|
#include <atomic>
|
|
|
|
namespace stfx {
|
|
// Convenient units for range parameters - not really part of the main STFX API
|
|
namespace units {
|
|
static inline double dbToGain(double db) {
|
|
return std::pow(10, db*0.05);
|
|
}
|
|
static inline double gainToDb(double gain) {
|
|
return std::log10(std::max<double>(gain, 1e-10))*20;
|
|
}
|
|
static inline double dbToEnergy(double db) {
|
|
return std::pow(10, db*0.1);
|
|
}
|
|
static inline double energyToDb(double gain) {
|
|
return std::log10(std::max<double>(gain, 1e-10))*10;
|
|
}
|
|
static inline double pcToRatio(double percent) {
|
|
return percent*0.01;
|
|
}
|
|
static inline double ratioToPc(double linear) {
|
|
return linear*100;
|
|
}
|
|
static inline double kHzToHz(double kHz) {
|
|
return kHz*1000;
|
|
}
|
|
static inline double hzToKHz(double hz) {
|
|
return hz*0.001;
|
|
}
|
|
static inline double sToMs(double sec) {
|
|
return sec*1000;
|
|
}
|
|
static inline double msToS(double ms) {
|
|
return ms*0.001;
|
|
}
|
|
|
|
// Templates to add commonly-used units to range parameters
|
|
template<class RangeParam>
|
|
RangeParam & rangeGain(RangeParam ¶m) {
|
|
return param
|
|
.unit("dB", 1, dbToGain, gainToDb) // default display is dB
|
|
.unit("%", 0, pcToRatio, ratioToPc)
|
|
.exact(0, "off")
|
|
.unit("x"); // Allow things like "2x" (~6dB) for text-input
|
|
}
|
|
template<class RangeParam>
|
|
RangeParam & rangeHz(RangeParam ¶m) {
|
|
return param
|
|
.unit("Hz", 2, -1, 1)
|
|
.unit("Hz", 1, -10, 10)
|
|
.unit("Hz", 0, -1000, 1000)
|
|
.unit("kHz", 2, kHzToHz, hzToKHz, -1e4, 1e4)
|
|
.unit("kHz", 1, kHzToHz, hzToKHz);
|
|
}
|
|
template<class RangeParam>
|
|
RangeParam & rangePercent(RangeParam ¶m) {
|
|
return param
|
|
.unit("%", 0, pcToRatio, ratioToPc)
|
|
.unit("x");
|
|
}
|
|
template<class RangeParam>
|
|
RangeParam & rangeMs(RangeParam ¶m) {
|
|
return param
|
|
.unit("ms", 2, -1, 1)
|
|
.unit("ms", 1, -10, 10)
|
|
.unit("ms", 0, -1000, 1000)
|
|
.unit(" sec", 2, sToMs, msToS, -1e3, 1e3)
|
|
.unit(" sec", 1, sToMs, msToS, -1e4, 1e4)
|
|
.unit(" sec", 0, sToMs, msToS);
|
|
}
|
|
template<class RangeParam>
|
|
RangeParam & rangeSec(RangeParam ¶m) {
|
|
return param
|
|
.unit("ms", 2, msToS, sToMs, -1e-3, 1e-3)
|
|
.unit("ms", 1, msToS, sToMs, -1e-2, 1e-2)
|
|
.unit("ms", 0, msToS, sToMs, -1e-1, 1e-1)
|
|
.unit(" sec", 2, -1, 1)
|
|
.unit(" sec", 1, -10, 10)
|
|
.unit(" sec", 0);
|
|
}
|
|
}
|
|
|
|
// When we want to ignore parameter info (covers both range and stepped)
|
|
struct RangeParamIgnore {
|
|
RangeParamIgnore & info(const char*, const char*) {
|
|
return *this;
|
|
}
|
|
RangeParamIgnore & range(double low, double high) {
|
|
return *this;
|
|
}
|
|
RangeParamIgnore & range(double low, double mid, double high) {
|
|
return *this;
|
|
}
|
|
/* Always takes a suffix. Units registered first are preferred when formatting values
|
|
Optional arguments (any can be omitted if the order is correct):
|
|
precision:
|
|
int: decimalDigits
|
|
display mapping (e.g. for value in Hz, display in kHz):
|
|
double displayToValue(double)
|
|
double valueToDisplay(double)
|
|
valid range:
|
|
double low
|
|
double high
|
|
*/
|
|
template<class ...Args>
|
|
RangeParamIgnore & unit(const char *suffix, Args...) {
|
|
return *this;
|
|
}
|
|
RangeParamIgnore & exact(double, const char *) {
|
|
return *this;
|
|
}
|
|
};
|
|
struct SteppedParamIgnore {
|
|
SteppedParamIgnore & info(const char*, const char*) {
|
|
return *this;
|
|
}
|
|
SteppedParamIgnore & range(int low, int high) {
|
|
return *this;
|
|
}
|
|
template<typename ...Args>
|
|
SteppedParamIgnore & label(Args ...) {
|
|
return *this;
|
|
}
|
|
};
|
|
namespace {
|
|
// A value which changes linearly within a given index range (e.g. a block)
|
|
class LinearSegment {
|
|
double offset, gradient;
|
|
public:
|
|
LinearSegment(double offset, double gradient) : offset(offset), gradient(gradient) {}
|
|
|
|
double at(double i) {
|
|
return offset + i*gradient;
|
|
}
|
|
bool changing() {
|
|
return gradient != 0;
|
|
}
|
|
};
|
|
|
|
/** A processing block (not including IO).
|
|
|
|
There are two fades happening within each block:
|
|
* automation: changes are smoothed linearly across the block
|
|
* A/B fade: a slower transition for parameters which need to change slowly or do cross-fades
|
|
*/
|
|
class Block {
|
|
// slow fade start/rate
|
|
double fadeStart, fadeStep;
|
|
// per-block automation gradient rate
|
|
double blockFade;
|
|
// we might need some additional setup on the fiand/or after a directly
|
|
bool firstBlockAfterReset;
|
|
bool metersRequested;
|
|
bool &metersChecked;
|
|
|
|
template<class Param>
|
|
bool paramsChanging(Param ¶m) const {
|
|
return param.from() != param.to();
|
|
}
|
|
template<class Param, class ...Others>
|
|
bool paramsChanging(Param ¶m, Others &...others) const {
|
|
return paramsChanging(param) || paramsChanging(others...);
|
|
}
|
|
public:
|
|
// Block length in samples
|
|
int length;
|
|
|
|
Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset, bool wantsMeters, bool &metersChecked) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), metersRequested(wantsMeters), metersChecked(metersChecked), length(length) {}
|
|
// Not copyable, because that's probably a mistake
|
|
Block(const Block &) = delete;
|
|
Block & operator =(const Block&) = delete;
|
|
|
|
/// Fade ratio at a given sample index
|
|
double fade(int i) const {
|
|
return fadeStart + i*fadeStep;
|
|
}
|
|
/// Mix two values according to the fade ratio
|
|
template<class Value=double>
|
|
Value fade(int i, Value from, Value to) const {
|
|
return from + (to - from)*fade(i);
|
|
}
|
|
|
|
/// Is there a fade currently active?
|
|
bool fading() const {
|
|
return fadeStep != 0;
|
|
}
|
|
/// Is a fade happening, and are any of the parameters included in it?
|
|
template<class Param, class ...Others>
|
|
bool fading(Param ¶m, Others &...others) const {
|
|
return fading() && paramsChanging(param, others...);
|
|
}
|
|
|
|
/// Set up an A/B fade. Executes once at the beginning of a fade, and also once directly after a `.reset()`.
|
|
template<class Fn>
|
|
void setupFade(Fn fn) const {
|
|
setupFade(fading(), fn);
|
|
}
|
|
/// Same as above, but only run on fade-start if `fading` is true. Mostly useful when used with `.fading(params...)`
|
|
template<class Fn>
|
|
void setupFade(bool fading, Fn fn) const {
|
|
if (firstBlockAfterReset) fn();
|
|
if (fading && fadeStart == 0) fn();
|
|
}
|
|
|
|
/// Produce a linear segment corresponding to A/B fading a parameter
|
|
template<class Param>
|
|
LinearSegment smooth(Param ¶m) const {
|
|
return smooth(param.from(), param.to());
|
|
}
|
|
/// Same as above, but the parameter's values can be mapped
|
|
template<class Param, class Map>
|
|
LinearSegment smooth(Param ¶m, Map map) const {
|
|
return smooth(map(param.from()), map(param.to()));
|
|
}
|
|
/// Produce a linear segment corresponding to A/B fading two values.
|
|
/// These values should _not_ generally change every block, but only when a new fade starts. They should probably be derived from `param.from()` and `param.to()`, and this method is mostly useful for combining multiple parameters.
|
|
LinearSegment smooth(double from, double to) const {
|
|
double diff = to - from;
|
|
return {from + diff*fadeStart, diff*fadeStep};
|
|
}
|
|
|
|
/// Automation provides a curve for a parameter, and is also an event list where the events update the curve.
|
|
/// This pattern allows sample-accurate automation in environments where that's supported.
|
|
struct BlockAutomation : public LinearSegment {
|
|
BlockAutomation(const LinearSegment &smoothed) : LinearSegment(smoothed) {}
|
|
|
|
// For this implementation, we just provide a linear segment and no update events.
|
|
static constexpr int size() {
|
|
return 0;
|
|
}
|
|
struct DoNothingEvent {
|
|
int offset = 0;
|
|
void operator()() {}
|
|
};
|
|
DoNothingEvent operator [](int) {
|
|
return DoNothingEvent();
|
|
}
|
|
};
|
|
/// Get an automation curve for a parameter
|
|
template<class Param>
|
|
BlockAutomation automation(Param ¶m) const {
|
|
double start = param._libPrevBlock();
|
|
auto diff = param._libCurrent() - start;
|
|
return LinearSegment{start, diff*blockFade};
|
|
}
|
|
|
|
/// Blocks can be processed in sub-blocks, which are split up by events.
|
|
/// This method may return a different sub-block type (which will also have `.split()` and `.forEach()` methods).
|
|
template<class EventList>
|
|
const Block & split(EventList &&list, int count) const {
|
|
for (int i = 0; i < count; ++i) list[i]();
|
|
return *this;
|
|
}
|
|
template<class EventList, class ...Others>
|
|
const Block & split(EventList &&list, Others &&...others) const {
|
|
return split(list, list.size()).split(std::forward<Others>(others)...);
|
|
}
|
|
/// Base-case for templated recursion
|
|
const Block & split() const {
|
|
return *this;
|
|
}
|
|
/// Execute the callback once per sub-block, with sample-index arguments: `callback(int start, int end)`, calling events in between
|
|
template<class Callback>
|
|
void forEach(Callback callback) const {
|
|
callback(0, length);
|
|
}
|
|
|
|
bool wantsMeters() const {
|
|
metersChecked = true;
|
|
return metersRequested;
|
|
}
|
|
};
|
|
|
|
// Parameters can be assigned using `=`, and store their own history for transitions
|
|
template<typename Value>
|
|
class LibraryParam {
|
|
std::atomic<Value> current;
|
|
Value prevBlock; // inter-block fade
|
|
// Used for the 20ms parameter fade
|
|
Value _from, _to;
|
|
public:
|
|
LibraryParam(const Value &initial, const Value &) : LibraryParam(initial) {}
|
|
LibraryParam(const Value &initial) : current(initial), prevBlock(initial), _from(initial), _to(initial) {}
|
|
operator Value () const {
|
|
return current.load(std::memory_order_relaxed);
|
|
}
|
|
LibraryParam & operator =(const Value &v) {
|
|
current.store(v, std::memory_order_relaxed);
|
|
return *this;
|
|
}
|
|
// Return the current fade
|
|
Value from() const {
|
|
return _from;
|
|
}
|
|
Value to() const {
|
|
return _to;
|
|
}
|
|
|
|
template<class Storage>
|
|
void state(Storage &storage) {
|
|
auto v = current.load(std::memory_order_relaxed);
|
|
auto vPrev = v;
|
|
storage("value", v);
|
|
if (v != vPrev) current.store(v, std::memory_order_relaxed);
|
|
}
|
|
|
|
// The following are only called from `.process()`
|
|
// Shuffle the internal values along to start a new fade, return whether it's actually changing
|
|
bool _libStartFade() {
|
|
_from = _to;
|
|
_to = current.load(std::memory_order_relaxed);
|
|
return (_to != _from);
|
|
}
|
|
// Store previous value for block-level automation
|
|
void _libEndBlock() {
|
|
prevBlock = current.load(std::memory_order_relaxed);
|
|
}
|
|
Value _libCurrent() {
|
|
return current.load(std::memory_order_relaxed);
|
|
}
|
|
Value _libPrevBlock() {
|
|
return prevBlock;
|
|
}
|
|
};
|
|
}
|
|
|
|
/// Base class for our effect to inherit from. Provides parameter classes and some default config.
|
|
template<typename SampleType>
|
|
class LibraryEffectBase {
|
|
public:
|
|
using ParamRange = LibraryParam<double>;
|
|
using ParamStepped = LibraryParam<int>;
|
|
using Sample = SampleType;
|
|
|
|
ParamRange bpm{120};
|
|
|
|
double paramFadeMs() {
|
|
return 20;
|
|
}
|
|
int latencySamples() {
|
|
return 0;
|
|
}
|
|
int tailSamples() {
|
|
return 0;
|
|
}
|
|
|
|
template<class Presets>
|
|
void presets(Presets &) {}
|
|
|
|
// passes ownership of any meter values back to the audio thread
|
|
void wantsMeters(bool meters=true) {
|
|
metersReady.clear();
|
|
if (meters) {
|
|
metersRequested.test_and_set();
|
|
} else {
|
|
metersRequested.clear();
|
|
}
|
|
}
|
|
// whether the meter values can be read
|
|
bool hasMeters() const {
|
|
return metersReady.test();
|
|
}
|
|
protected:
|
|
std::atomic_flag metersRequested = ATOMIC_FLAG_INIT, metersReady = ATOMIC_FLAG_INIT;
|
|
};
|
|
|
|
/// Creates an effect class from an effect template, with optional extra config.
|
|
/// The effect template takes `EffectTemplate<BaseClass, ...ExtraConfig>`
|
|
template<typename Sample, template <class, class...> class EffectTemplate, class ...ExtraConfig>
|
|
class LibraryEffect : public EffectTemplate<stfx::LibraryEffectBase<Sample>, ExtraConfig...> {
|
|
using EffectClass = EffectTemplate<stfx::LibraryEffectBase<Sample>, ExtraConfig...>;
|
|
|
|
// This is passed to the effect's `.state()` method during initialisation, and collects pointers to the effect's parameters
|
|
class CollectParams {
|
|
public:
|
|
std::vector<LibraryParam<double> *> rangeParams;
|
|
std::vector<LibraryParam<int> *> stepParams;
|
|
|
|
// Add registered parameters to the list
|
|
RangeParamIgnore range(const char *, LibraryParam<double> ¶m, const char *codeExpr=nullptr) {
|
|
(void)codeExpr;
|
|
rangeParams.push_back(¶m);
|
|
return {};
|
|
}
|
|
SteppedParamIgnore stepped(const char *, LibraryParam<int> ¶m, const char *codeExpr=nullptr) {
|
|
(void)codeExpr;
|
|
stepParams.push_back(¶m);
|
|
return {};
|
|
}
|
|
|
|
// Drop any name/description we're given
|
|
template<class ...Args>
|
|
void info(Args...) {}
|
|
// The effect might ask us to store/fetch the serialisation version, we just echo it back
|
|
int version(int v) {return v;}
|
|
// Ignore the UI/synchronisation stuff
|
|
bool extra() {return false;}
|
|
bool extra(const char *, const char *) {return false;}
|
|
void invalidate(const char *) {}
|
|
// This storage only reads values, never changes them
|
|
template<class T>
|
|
bool changed(const char *, T &v) {return false;}
|
|
|
|
// We ignore any basic type
|
|
void operator()(const char *, bool) {}
|
|
void operator()(const char *, int) {}
|
|
void operator()(const char *, long) {}
|
|
void operator()(const char *, double) {}
|
|
void operator()(const char *, float) {}
|
|
// And strings
|
|
void operator()(const char *, std::string &) {}
|
|
|
|
// Iterate through vectors
|
|
template<class Item>
|
|
void operator()(const char *label, std::vector<Item> &vector) {
|
|
for (auto &item : vector) (*this)(label, item);
|
|
}
|
|
|
|
// Assume all other arguments have a `.state()`, and recurse into it
|
|
template<class OtherObject>
|
|
void operator()(const char *, OtherObject &obj) {
|
|
obj.state(*this);
|
|
}
|
|
} params;
|
|
|
|
bool justHadReset = true;
|
|
// Keep track of the A/B fade state
|
|
double fadeRatio = 0;
|
|
public:
|
|
template<class ...Args>
|
|
LibraryEffect(Args &&...args) : EffectClass(std::forward<Args>(args)...) {
|
|
params.rangeParams.push_back(&this->bpm);
|
|
EffectClass::state(params);
|
|
}
|
|
|
|
struct Config {
|
|
double sampleRate = 48000;
|
|
int inputChannels = 2, outputChannels = 2;
|
|
std::vector<int> auxInputs, auxOutputs;
|
|
int maxBlockSize = 256;
|
|
|
|
bool operator ==(const Config &other) const {
|
|
return sampleRate == other.sampleRate
|
|
&& inputChannels == other.inputChannels
|
|
&& outputChannels == other.outputChannels
|
|
&& auxInputs == other.auxInputs
|
|
&& auxOutputs == other.auxOutputs
|
|
&& maxBlockSize == other.maxBlockSize;
|
|
}
|
|
};
|
|
/// The current (proposed) effect configuration
|
|
Config config;
|
|
/// Returns `true` if the current `.config` was accepted. Otherwise, you can check how `.config` was modified, make your own adjustments (if needed) and try again.
|
|
bool configure() {
|
|
Config prevConfig = config;
|
|
EffectClass::configureSTFX(config);
|
|
if (config == prevConfig) {
|
|
reset();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/// Attempts to find a valid configuration by iteration
|
|
bool configurePersistent(int attempts=10) {
|
|
for (int i = 0; i < attempts; ++i) {
|
|
if (configure()) return true;
|
|
}
|
|
return false;
|
|
}
|
|
/// Returns true if the effect was successfully configured with _exactly_ these parameters
|
|
bool configure(double sampleRate, int maxBlockSize, int channels=2, int outputChannels=-1) {
|
|
if (outputChannels < 0) outputChannels = channels;
|
|
config.sampleRate = sampleRate;
|
|
config.inputChannels = channels;
|
|
config.outputChannels = outputChannels;
|
|
config.maxBlockSize = maxBlockSize;
|
|
|
|
return configure();
|
|
}
|
|
|
|
/// Clears effect buffers and resets parameters
|
|
void reset() {
|
|
for (auto param : params.rangeParams) {
|
|
param->_libStartFade();
|
|
param->_libEndBlock();
|
|
}
|
|
for (auto param : params.stepParams) {
|
|
param->_libStartFade();
|
|
param->_libEndBlock();
|
|
}
|
|
EffectClass::reset();
|
|
justHadReset = true;
|
|
}
|
|
|
|
template<class Buffers>
|
|
void process(Buffers &&buffers, int blockLength) {
|
|
process(buffers, buffers, blockLength);
|
|
}
|
|
|
|
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.processSTFX(io, config, block)`.
|
|
/// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc.
|
|
template<class Inputs, class Outputs>
|
|
void process(Inputs &&inputs, Outputs &&outputs, int blockLength) {
|
|
// How long should the parameter fade take?
|
|
double fadeSamples = EffectClass::paramFadeMs()*0.001*config.sampleRate;
|
|
// Fade position at the end of the block
|
|
double fadeRatioEnd = fadeRatio + blockLength/fadeSamples;
|
|
// If the fade will finish this block, get there exactly
|
|
double fadeRatioStep = (fadeRatioEnd >= 1) ? (1 - fadeRatio)/blockLength : 1/fadeSamples;
|
|
|
|
// If we're just starting a new fade, move all the parameter values along
|
|
if (fadeRatio == 0) {
|
|
bool needsFade = false;
|
|
for (auto param : params.rangeParams) {
|
|
if (param->_libStartFade()) needsFade = true;
|
|
}
|
|
for (auto param : params.stepParams) {
|
|
if (param->_libStartFade()) needsFade = true;
|
|
}
|
|
// None of our parameters are actually fading, just skip it
|
|
if (!needsFade) fadeRatioStep = fadeRatioEnd = 0;
|
|
}
|
|
|
|
struct Io {
|
|
Inputs input;
|
|
Outputs output;
|
|
};
|
|
Io io{inputs, outputs};
|
|
bool metersChecked = false;
|
|
Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset, this->metersRequested.test(), metersChecked);
|
|
|
|
((EffectClass *)this)->processSTFX(io, (const Config &)config, (const Block &)block);
|
|
|
|
if (fadeRatioEnd >= 1) {
|
|
// Fade just finished, so we reset
|
|
fadeRatio = 0;
|
|
} else {
|
|
fadeRatio = fadeRatioEnd;
|
|
}
|
|
justHadReset = false;
|
|
for (auto param : params.rangeParams) param->_libEndBlock();
|
|
for (auto param : params.stepParams) param->_libEndBlock();
|
|
|
|
// Meters are filled - pass ownership of meter values to the main thread
|
|
if (this->metersRequested.test() && metersChecked) {
|
|
this->metersRequested.clear();
|
|
this->metersReady.test_and_set();
|
|
}
|
|
}
|
|
};
|
|
|
|
} // stfx::
|
|
|
|
#endif // include guard
|