/* 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` which produces a simple effect class from an STFX effect template. */ #ifndef STFX_LIBRARY_H #define STFX_LIBRARY_H #include #include #include #include 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(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(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 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 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 RangeParam & rangePercent(RangeParam ¶m) { return param .unit("%", 0, pcToRatio, ratioToPc) .unit("x"); } template 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 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 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 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 bool paramsChanging(Param ¶m) const { return param.from() != param.to(); } template 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 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 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 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 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 LinearSegment smooth(Param ¶m) const { return smooth(param.from(), param.to()); } /// Same as above, but the parameter's values can be mapped template 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 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 const Block & split(EventList &&list, int count) const { for (int i = 0; i < count; ++i) list[i](); return *this; } template const Block & split(EventList &&list, Others &&...others) const { return split(list, list.size()).split(std::forward(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 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 class LibraryParam { std::atomic 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 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 class LibraryEffectBase { public: using ParamRange = LibraryParam; using ParamStepped = LibraryParam; using Sample = SampleType; ParamRange bpm{120}; double paramFadeMs() { return 20; } int latencySamples() { return 0; } int tailSamples() { return 0; } template 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` template class EffectTemplate, class ...ExtraConfig> class LibraryEffect : public EffectTemplate, ExtraConfig...> { using EffectClass = EffectTemplate, 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 *> rangeParams; std::vector *> stepParams; // Add registered parameters to the list RangeParamIgnore range(const char *, LibraryParam ¶m, const char *codeExpr=nullptr) { (void)codeExpr; rangeParams.push_back(¶m); return {}; } SteppedParamIgnore stepped(const char *, LibraryParam ¶m, const char *codeExpr=nullptr) { (void)codeExpr; stepParams.push_back(¶m); return {}; } // Drop any name/description we're given template 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 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 void operator()(const char *label, std::vector &vector) { for (auto &item : vector) (*this)(label, item); } // Assume all other arguments have a `.state()`, and recurse into it template 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 LibraryEffect(Args &&...args) : EffectClass(std::forward(args)...) { params.rangeParams.push_back(&this->bpm); EffectClass::state(params); } struct Config { double sampleRate = 48000; int inputChannels = 2, outputChannels = 2; std::vector 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 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 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