424 lines
13 KiB
C++
424 lines
13 KiB
C++
/* Copyright 2021 Geraint Luff / Signalsmith Audio Ltd
|
|
|
|
This file is released under the MIT License - if you need anything else, let us know. 🙂
|
|
|
|
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>
|
|
|
|
namespace stfx {
|
|
|
|
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(int 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 = false;
|
|
|
|
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=false) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), 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 can return a sub-block (with `.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).split(std::forward<Others>(others)...);
|
|
}
|
|
/// Base-case for the templated recursion
|
|
const Block & split() const {
|
|
return *this;
|
|
}
|
|
/// Execute the callback once per sub-block, with sample-index arguments: `callback(int start, int end)`
|
|
template<class Callback>
|
|
void forEach(Callback callback) const {
|
|
callback(0, length);
|
|
}
|
|
};
|
|
|
|
// Parameters can be assigned using `=`, and store their own history for transitions
|
|
template<typename Value>
|
|
class LibraryParam {
|
|
Value current, prevBlock;
|
|
// Used for the 20ms parameter fade
|
|
Value _from, _to;
|
|
public:
|
|
LibraryParam(const Value &initial) : current(initial), prevBlock(initial), _from(initial), _to(initial) {}
|
|
operator Value () const {
|
|
return current;
|
|
}
|
|
LibraryParam & operator =(const Value &v) {
|
|
current = v;
|
|
return *this;
|
|
}
|
|
// Return the
|
|
Value from() const {
|
|
return _from;
|
|
}
|
|
Value to() const {
|
|
return _to;
|
|
}
|
|
|
|
// Shuffle the internal values along to start a new fade, return whether it's actually changing
|
|
bool _libStartFade() {
|
|
_from = _to;
|
|
_to = current;
|
|
return (_to != _from);
|
|
}
|
|
// Store previous value for block-level automation
|
|
void _libEndBlock() {
|
|
prevBlock = current;
|
|
}
|
|
Value _libCurrent() {
|
|
return current;
|
|
}
|
|
Value _libPrevBlock() {
|
|
return prevBlock;
|
|
}
|
|
};
|
|
|
|
// When we want to ignore parameter info
|
|
struct PInfoPlaceholder {
|
|
PInfoPlaceholder & info(const char*, const char*) {
|
|
return *this;
|
|
}
|
|
PInfoPlaceholder & info(std::string, std::string) {
|
|
return *this;
|
|
}
|
|
template<typename V1, typename V2>
|
|
PInfoPlaceholder & range(V1, V2) {
|
|
return *this;
|
|
}
|
|
template<typename V1, typename V2, typename V3>
|
|
PInfoPlaceholder & range(V1, V2, V3) {
|
|
return *this;
|
|
}
|
|
template<class ...Args>
|
|
PInfoPlaceholder & unit(Args...) {
|
|
return *this;
|
|
}
|
|
PInfoPlaceholder & exact(double, const char *) {
|
|
return *this;
|
|
}
|
|
template<typename ...Args>
|
|
PInfoPlaceholder & names(Args ...) {
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
/// Base class for our effect to inherit from. Provides parameter classes and some default config.
|
|
class LibraryEffectBase {
|
|
protected:
|
|
using ParamRange = LibraryParam<double>;
|
|
using ParamSteps = LibraryParam<int>;
|
|
|
|
template<typename Enum, int size>
|
|
class ParamEnum : public ParamSteps {
|
|
static Enum castToEnum(int v) {return (Enum)v;};
|
|
public:
|
|
using Value = Enum;
|
|
ParamEnum(Enum value) : ParamSteps((int)value) {}
|
|
operator Enum() {
|
|
return (Enum)this->latest;
|
|
}
|
|
};
|
|
public:
|
|
double paramFadeMs() {
|
|
return 20;
|
|
}
|
|
int tailSamples() {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
/// Creates an effect class from an effect template, with optional extra config.
|
|
/// The effect template takes `EffectTemplate<Sample, BaseClass, ...ExtraConfig>`
|
|
template<typename Sample, template <class, class, class...> class EffectTemplate, class ...ExtraConfig>
|
|
class LibraryEffect : public EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...> {
|
|
using EffectClass = EffectTemplate<Sample, stfx::LibraryEffectBase, 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
|
|
PInfoPlaceholder param(const char *, LibraryParam<double> ¶m, const char *codeExpr=nullptr) {
|
|
(void)codeExpr;
|
|
rangeParams.push_back(¶m);
|
|
return {};
|
|
}
|
|
PInfoPlaceholder param(const char *, LibraryParam<int> ¶m, const char *codeExpr=nullptr) {
|
|
(void)codeExpr;
|
|
stepParams.push_back(¶m);
|
|
return {};
|
|
}
|
|
|
|
// The effect might ask us to store/fetch the serialisation version, we just echo it back
|
|
static int version(int v) {return v;}
|
|
|
|
// We ignore any basic type
|
|
void operator ()(bool) {}
|
|
void operator ()(int) {}
|
|
void operator ()(long) {}
|
|
void operator ()(double) {}
|
|
void operator ()(float) {}
|
|
// Assume all other arguments have a `.state()`, and recurse into it
|
|
template<class OtherObject>
|
|
void operator ()(OtherObject &obj) {
|
|
obj.state(*this);
|
|
}
|
|
// Drop all names/labels we're given
|
|
template<class V>
|
|
void operator ()(const char *, V &v) {
|
|
(*this)(v);
|
|
}
|
|
template<class Fn>
|
|
void group(const char *, Fn fn) {
|
|
fn(*this);
|
|
}
|
|
// Drop any name/description we're given
|
|
template<class ...Args>
|
|
void info(Args...) {}
|
|
} 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)...) {
|
|
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) {
|
|
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::configure(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;
|
|
}
|
|
|
|
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(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};
|
|
Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset);
|
|
|
|
((EffectClass *)this)->process(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();
|
|
}
|
|
};
|
|
|
|
} // stfx::
|
|
|
|
#endif // include guard
|