/* 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` which produces a simple effect class from an STFX effect template. */ #ifndef STFX_LIBRARY_H #define STFX_LIBRARY_H #include #include 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 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=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 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 can return a sub-block (with `.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).split(std::forward(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 void forEach(Callback callback) const { callback(0, length); } }; // Parameters can be assigned using `=`, and store their own history for transitions template 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 PInfoPlaceholder & range(V1, V2) { return *this; } template PInfoPlaceholder & range(V1, V2, V3) { return *this; } template PInfoPlaceholder & unit(Args...) { return *this; } PInfoPlaceholder & exact(double, const char *) { return *this; } template 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; using ParamSteps = LibraryParam; template 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` template class EffectTemplate, class ...ExtraConfig> class LibraryEffect : public EffectTemplate { using EffectClass = EffectTemplate; // 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 PInfoPlaceholder param(const char *, LibraryParam ¶m, const char *codeExpr=nullptr) { (void)codeExpr; rangeParams.push_back(¶m); return {}; } PInfoPlaceholder param(const char *, LibraryParam ¶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 void operator ()(OtherObject &obj) { obj.state(*this); } // Drop all names/labels we're given template void operator ()(const char *, V &v) { (*this)(v); } template void group(const char *, Fn fn) { fn(*this); } // Drop any name/description we're given template void info(Args...) {} } params; bool justHadReset = true; // Keep track of the A/B fade state double fadeRatio = 0; public: template LibraryEffect(Args &&...args) : EffectClass(std::forward(args)...) { 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) { 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 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