diff --git a/README.md b/README.md index 23e2cb9..b484117 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ A collection of basic effects, available as plugins and re-usable open-source (M ## How to use -Each effect can be used just by including the corresponding header (e.g. `limiter.h`). This defines two classes `LimiterFloat` and `LimiterDouble`. Some of these have initialisation arguments (e.g. maximum lookahead) but these are always optional. +Each effect can be used just by including the corresponding header (e.g. `limiter.h`). This defines two classes `...Float` and `...Double`. Some of these have initialisation arguments (e.g. maximum lookahead) but these are always optional. ```cpp // Limiter with maximum attack/lookahead of 100ms -signalsmith::basics::Limiter effect(100); +signalsmith::basics::LimiterFloat effect(100); ``` Before use, these classes must be configured. The `.configure()` method returns `true` if the config was accepted. They can be configured multiple times, but (obviously) not while actively processing audio. @@ -42,3 +42,5 @@ Integer parameters are declared as `ParamStepped`s. This is a similarly opaque ### Implementation The `.state()` method of these templates contain a lot of detail, almost all of which is ignored (and optimised away) when using the `...Float`/`...Double` classes. + +The `.configureSTFX()` and `.processSTFX()` methods are wrapped into more typical diff --git a/limiter.h b/limiter.h index d0173d2..6dba9c3 100644 --- a/limiter.h +++ b/limiter.h @@ -170,7 +170,7 @@ struct LimiterSTFX : public BaseEffect { Sample gain = channelGains[c]; // blend between individual/minimum gain gain += (minChannelGain - gain)*linkChannels; - if (smoothingStages > 1) { + if (stages == 2) { gain *= gain; sqrtWet += (1 - sqrtWet)*sqrtWetSlew; } else { @@ -179,7 +179,10 @@ struct LimiterSTFX : public BaseEffect { // smooth envelope gain auto &envelope = channelEnvelopes[c]; gain = envelope(gain); - if (sqrtWet > Sample(1e-30)) gain += (std::sqrt(gain) - gain)*sqrtWet; + if (sqrtWet > Sample(1e-30)) { + gain += (std::sqrt(gain) - gain)*sqrtWet; + gain = std::min(gain, channelGains[c]); + } Sample delayed = block.fade(i, multiBuffer[c][i - delaySamplesFrom], diff --git a/stfx/clap/param-info.h b/stfx/clap/param-info.h index 3099d37..983d7d1 100644 --- a/stfx/clap/param-info.h +++ b/stfx/clap/param-info.h @@ -9,6 +9,25 @@ #include namespace stfx { + class UnitRangeMap { + double a, b, d; + public: + UnitRangeMap() : a(0), b(1), d(0) {} // identity + UnitRangeMap(double min, double mid, double max) { + double k = (mid - min)/(max - mid); + a = min; + b = max*k - min; + d = k - 1; + } + + double toUnit(double value) const { + return (value - a)/(b - value*d); + } + double fromUnit(double unit) const { + return (a + unit*b)/(1 + unit*d); + } + }; + struct RangeParamInfo { double defaultValue; double low, mid, high; @@ -38,12 +57,12 @@ namespace stfx { } typedef double((*DoubleMap)(double)); - RangeParamInfo & unit(std::string suffix, int precision=2, DoubleMap fromUnit=identityMap, DoubleMap toUnit=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) { - unitOptions.emplace_back(suffix, fromUnit, toUnit, validLow, validHigh, precision); + RangeParamInfo & unit(std::string suffix, int precision=2, DoubleMap fromDisplay=identityMap, DoubleMap toDisplay=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) { + unitOptions.emplace_back(suffix, fromDisplay, toDisplay, validLow, validHigh, precision); return *this; } - RangeParamInfo & unit(std::string suffix, DoubleMap fromUnit, DoubleMap toUnit, double validLow=doubleLowest, double validHigh=doubleMax) { - return unit(suffix, 2, fromUnit, toUnit, validLow, validHigh); + RangeParamInfo & unit(std::string suffix, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow=doubleLowest, double validHigh=doubleMax) { + return unit(suffix, 2, fromDisplay, toDisplay, validLow, validHigh); } RangeParamInfo & unit(std::string suffix, int precision, double validLow, double validHigh) { return unit(suffix, precision, identityMap, identityMap, validLow, validHigh); @@ -130,7 +149,7 @@ namespace stfx { for (const auto &u : unitOptions) { if (u.unit == unit) { - return u.fromUnit(result); + return u.fromDisplay(result); } } return result; @@ -152,10 +171,10 @@ namespace stfx { } double toUnit(double value) const { - return rangeMap.toUnitRange(value); + return rangeMap.toUnit(value); } double fromUnit(double unit) const { - return rangeMap.fromUnitRange(unit); + return rangeMap.fromUnit(unit); } private: @@ -166,25 +185,7 @@ namespace stfx { } // A map from [0, 1] to another range with specified midpoint, based on a 1/x curve - class UnitRangeMapReciprocal { - double vMin, vTopFactor, vBottomFactor; - public: - UnitRangeMapReciprocal() : vMin(0), vTopFactor(1), vBottomFactor(0) {} // identity - UnitRangeMapReciprocal(double min, double mid, double max) { - vMin = min; - double k = (mid - min)/(max - mid); - vTopFactor = max*k - min; - vBottomFactor = k - 1; - } - - double toUnitRange(double value) const { - return (value - vMin)/(vTopFactor - value*vBottomFactor); - } - double fromUnitRange(double unit) const { - return (vMin + unit*vTopFactor)/(1 + unit*vBottomFactor); - } - }; - UnitRangeMapReciprocal rangeMap; + UnitRangeMap rangeMap; protected: struct ExactEntry { @@ -196,12 +197,12 @@ namespace stfx { std::string fixedDisplay = ""; std::string unit; bool addSpace = false, useFixed = false, keepZeros = false; - DoubleMap fromUnit, toUnit; + DoubleMap fromDisplay, toDisplay; double validLow, validHigh; int precision; double precisionOffset = 0; - UnitEntry(std::string unit, DoubleMap fromUnit, DoubleMap toUnit, double validLow, double validHigh, int precision=2) : unit(unit), fromUnit(fromUnit), toUnit(toUnit), validLow(validLow), validHigh(validHigh), precision(precision) { + UnitEntry(std::string unit, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow, double validHigh, int precision=2) : unit(unit), fromDisplay(fromDisplay), toDisplay(toDisplay), validLow(validLow), validHigh(validHigh), precision(precision) { if (validLow > validHigh) { std::swap(validLow, validHigh); keepZeros = true; @@ -223,9 +224,9 @@ namespace stfx { oss << std::fixed; double offset = 0; if (precision > 0) { - oss << toUnit(value) + offset; + oss << toDisplay(value) + offset; } else { - oss << (int)(toUnit(value) + offset); + oss << (int)(toDisplay(value) + offset); } valueString = oss.str(); // Strip trailing zeroes @@ -324,13 +325,6 @@ namespace stfx { } } - double toUnit(int value) const { - return (value - low)/double(high - low); - } - int fromUnit(double unit) const { - double value = low + unit*(high - low); - return int(std::round(value)); - } private: int nameIndex; protected: diff --git a/stfx/clap/stfx-clap.h b/stfx/clap/stfx-clap.h index caaa134..85dd0c9 100644 --- a/stfx/clap/stfx-clap.h +++ b/stfx/clap/stfx-clap.h @@ -353,6 +353,12 @@ struct Plugin : public clap_plugin { entry.steppedParam = ¶m; return entry.steppedInfo.emplace(param); } + template + bool changed(const char *key, V &v) { + (*this)(key, v); + return false; + } + void invalidate(const char *) {} }; void scanParams() { params.resize(0); @@ -514,7 +520,6 @@ struct Plugin : public clap_plugin { void info(const char *, const char *) {} int version(int v) { - LOG_EXPR(v); (*this)("version", v); return v; } @@ -530,6 +535,12 @@ struct Plugin : public clap_plugin { (*this)(key, v); return {}; } + template + bool changed(const char *key, V &v) { + (*this)(key, v); + return false; + } + void invalidate(const char *) {} }; struct StateReader : public StorageCborReader { using StorageCborReader::StorageCborReader; @@ -553,6 +564,13 @@ struct Plugin : public clap_plugin { param = v; return {}; } + template + bool changed(const char *key, V &v) { + V prev = v; + (*this)(key, v); + return v == prev; + } + void invalidate(const char *) {} }; std::vector stateBuffer; diff --git a/stfx/stfx-library.h b/stfx/stfx-library.h index affacbb..d6b75f2 100644 --- a/stfx/stfx-library.h +++ b/stfx/stfx-library.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace stfx { // Convenient units for range parameters - not really part of the main STFX API @@ -285,17 +286,18 @@ namespace stfx { // Parameters can be assigned using `=`, and store their own history for transitions template class LibraryParam { - Value current, prevBlock; + 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; + return current.load(std::memory_order_relaxed); } LibraryParam & operator =(const Value &v) { - current = v; + current.store(v, std::memory_order_relaxed); return *this; } // Return the current fade @@ -308,21 +310,25 @@ namespace stfx { template void state(Storage &storage) { - storage("value", current); + 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; + _to = current.load(std::memory_order_relaxed); return (_to != _from); } // Store previous value for block-level automation void _libEndBlock() { - prevBlock = current; + prevBlock = current.load(std::memory_order_relaxed); } Value _libCurrent() { - return current; + return current.load(std::memory_order_relaxed); } Value _libPrevBlock() { return prevBlock; @@ -378,12 +384,18 @@ namespace stfx { 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 - static int version(int v) {return v;} + int version(int v) {return v;} // Ignore the UI/synchronisation stuff - static bool extra() {return false;} - static bool extra(const char *, const char *) {return false;} - static void invalidate(const char *) {} + 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) {} @@ -393,23 +405,18 @@ namespace stfx { 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); } - template - void group(const char *, Fn fn) { - fn(*this); - } - // Drop any name/description we're given - template - void info(Args...) {} } params; bool justHadReset = true; diff --git a/stfx/storage/storage.h b/stfx/storage/storage.h index c01a8cc..67b75cb 100644 --- a/stfx/storage/storage.h +++ b/stfx/storage/storage.h @@ -163,7 +163,8 @@ struct StorageCborReader { } readValue(v); } - + +private: template void readValue(Obj &obj) { if (compact) { @@ -224,7 +225,6 @@ struct StorageCborReader { }); array.resize(length); } -private: protected: Cbor cbor;