1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
Geraint
7e02cfd77b Add dummy .changed()/.invalidate() methods to Storage impls. 2025-06-23 20:08:46 +01:00
Geraint
767906a31e Library parameters are atomic 2025-06-23 19:51:42 +01:00
5 changed files with 81 additions and 60 deletions

View File

@ -8,11 +8,11 @@ A collection of basic effects, available as plugins and re-usable open-source (M
## How to use ## 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 ```cpp
// Limiter with maximum attack/lookahead of 100ms // 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. 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 ### 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 `.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

View File

@ -9,6 +9,25 @@
#include <map> #include <map>
namespace stfx { 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 { struct RangeParamInfo {
double defaultValue; double defaultValue;
double low, mid, high; double low, mid, high;
@ -38,12 +57,12 @@ namespace stfx {
} }
typedef double((*DoubleMap)(double)); typedef double((*DoubleMap)(double));
RangeParamInfo & unit(std::string suffix, int precision=2, DoubleMap fromUnit=identityMap, DoubleMap toUnit=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) { RangeParamInfo & unit(std::string suffix, int precision=2, DoubleMap fromDisplay=identityMap, DoubleMap toDisplay=identityMap, double validLow=doubleLowest, double validHigh=doubleMax) {
unitOptions.emplace_back(suffix, fromUnit, toUnit, validLow, validHigh, precision); unitOptions.emplace_back(suffix, fromDisplay, toDisplay, validLow, validHigh, precision);
return *this; return *this;
} }
RangeParamInfo & unit(std::string suffix, DoubleMap fromUnit, DoubleMap toUnit, double validLow=doubleLowest, double validHigh=doubleMax) { RangeParamInfo & unit(std::string suffix, DoubleMap fromDisplay, DoubleMap toDisplay, double validLow=doubleLowest, double validHigh=doubleMax) {
return unit(suffix, 2, fromUnit, toUnit, validLow, validHigh); return unit(suffix, 2, fromDisplay, toDisplay, validLow, validHigh);
} }
RangeParamInfo & unit(std::string suffix, int precision, double validLow, double validHigh) { RangeParamInfo & unit(std::string suffix, int precision, double validLow, double validHigh) {
return unit(suffix, precision, identityMap, identityMap, validLow, validHigh); return unit(suffix, precision, identityMap, identityMap, validLow, validHigh);
@ -130,7 +149,7 @@ namespace stfx {
for (const auto &u : unitOptions) { for (const auto &u : unitOptions) {
if (u.unit == unit) { if (u.unit == unit) {
return u.fromUnit(result); return u.fromDisplay(result);
} }
} }
return result; return result;
@ -152,10 +171,10 @@ namespace stfx {
} }
double toUnit(double value) const { double toUnit(double value) const {
return rangeMap.toUnitRange(value); return rangeMap.toUnit(value);
} }
double fromUnit(double unit) const { double fromUnit(double unit) const {
return rangeMap.fromUnitRange(unit); return rangeMap.fromUnit(unit);
} }
private: private:
@ -166,25 +185,7 @@ namespace stfx {
} }
// A map from [0, 1] to another range with specified midpoint, based on a 1/x curve // A map from [0, 1] to another range with specified midpoint, based on a 1/x curve
class UnitRangeMapReciprocal { UnitRangeMap rangeMap;
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;
protected: protected:
struct ExactEntry { struct ExactEntry {
@ -196,12 +197,12 @@ namespace stfx {
std::string fixedDisplay = ""; std::string fixedDisplay = "";
std::string unit; std::string unit;
bool addSpace = false, useFixed = false, keepZeros = false; bool addSpace = false, useFixed = false, keepZeros = false;
DoubleMap fromUnit, toUnit; DoubleMap fromDisplay, toDisplay;
double validLow, validHigh; double validLow, validHigh;
int precision; int precision;
double precisionOffset = 0; 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) { if (validLow > validHigh) {
std::swap(validLow, validHigh); std::swap(validLow, validHigh);
keepZeros = true; keepZeros = true;
@ -223,9 +224,9 @@ namespace stfx {
oss << std::fixed; oss << std::fixed;
double offset = 0; double offset = 0;
if (precision > 0) { if (precision > 0) {
oss << toUnit(value) + offset; oss << toDisplay(value) + offset;
} else { } else {
oss << (int)(toUnit(value) + offset); oss << (int)(toDisplay(value) + offset);
} }
valueString = oss.str(); valueString = oss.str();
// Strip trailing zeroes // 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: private:
int nameIndex; int nameIndex;
protected: protected:

View File

@ -353,6 +353,12 @@ struct Plugin : public clap_plugin {
entry.steppedParam = &param; entry.steppedParam = &param;
return entry.steppedInfo.emplace(param); return entry.steppedInfo.emplace(param);
} }
template<class V>
bool changed(const char *key, V &v) {
(*this)(key, v);
return false;
}
void invalidate(const char *) {}
}; };
void scanParams() { void scanParams() {
params.resize(0); params.resize(0);
@ -514,7 +520,6 @@ struct Plugin : public clap_plugin {
void info(const char *, const char *) {} void info(const char *, const char *) {}
int version(int v) { int version(int v) {
LOG_EXPR(v);
(*this)("version", v); (*this)("version", v);
return v; return v;
} }
@ -530,6 +535,12 @@ struct Plugin : public clap_plugin {
(*this)(key, v); (*this)(key, v);
return {}; return {};
} }
template<class V>
bool changed(const char *key, V &v) {
(*this)(key, v);
return false;
}
void invalidate(const char *) {}
}; };
struct StateReader : public StorageCborReader<true, StateReader> { struct StateReader : public StorageCborReader<true, StateReader> {
using StorageCborReader<true, StateReader>::StorageCborReader; using StorageCborReader<true, StateReader>::StorageCborReader;
@ -553,6 +564,13 @@ struct Plugin : public clap_plugin {
param = v; param = v;
return {}; return {};
} }
template<class V>
bool changed(const char *key, V &v) {
V prev = v;
(*this)(key, v);
return v == prev;
}
void invalidate(const char *) {}
}; };
std::vector<unsigned char> stateBuffer; std::vector<unsigned char> stateBuffer;

View File

@ -13,6 +13,7 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <cmath> #include <cmath>
#include <atomic>
namespace stfx { namespace stfx {
// Convenient units for range parameters - not really part of the main STFX API // 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 // Parameters can be assigned using `=`, and store their own history for transitions
template<typename Value> template<typename Value>
class LibraryParam { class LibraryParam {
Value current, prevBlock; std::atomic<Value> current;
Value prevBlock; // inter-block fade
// Used for the 20ms parameter fade // Used for the 20ms parameter fade
Value _from, _to; Value _from, _to;
public: public:
LibraryParam(const Value &initial, const Value &) : LibraryParam(initial) {} LibraryParam(const Value &initial, const Value &) : LibraryParam(initial) {}
LibraryParam(const Value &initial) : current(initial), prevBlock(initial), _from(initial), _to(initial) {} LibraryParam(const Value &initial) : current(initial), prevBlock(initial), _from(initial), _to(initial) {}
operator Value () const { operator Value () const {
return current; return current.load(std::memory_order_relaxed);
} }
LibraryParam & operator =(const Value &v) { LibraryParam & operator =(const Value &v) {
current = v; current.store(v, std::memory_order_relaxed);
return *this; return *this;
} }
// Return the current fade // Return the current fade
@ -308,21 +310,25 @@ namespace stfx {
template<class Storage> template<class Storage>
void state(Storage &storage) { 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 // Shuffle the internal values along to start a new fade, return whether it's actually changing
bool _libStartFade() { bool _libStartFade() {
_from = _to; _from = _to;
_to = current; _to = current.load(std::memory_order_relaxed);
return (_to != _from); return (_to != _from);
} }
// Store previous value for block-level automation // Store previous value for block-level automation
void _libEndBlock() { void _libEndBlock() {
prevBlock = current; prevBlock = current.load(std::memory_order_relaxed);
} }
Value _libCurrent() { Value _libCurrent() {
return current; return current.load(std::memory_order_relaxed);
} }
Value _libPrevBlock() { Value _libPrevBlock() {
return prevBlock; return prevBlock;
@ -378,12 +384,18 @@ namespace stfx {
return {}; 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 // 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 // Ignore the UI/synchronisation stuff
static bool extra() {return false;} bool extra() {return false;}
static bool extra(const char *, const char *) {return false;} bool extra(const char *, const char *) {return false;}
static void invalidate(const char *) {} 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 // We ignore any basic type
void operator()(const char *, bool) {} void operator()(const char *, bool) {}
@ -393,23 +405,18 @@ namespace stfx {
void operator()(const char *, float) {} void operator()(const char *, float) {}
// And strings // And strings
void operator()(const char *, std::string &) {} void operator()(const char *, std::string &) {}
// Iterate through vectors // Iterate through vectors
template<class Item> template<class Item>
void operator()(const char *label, std::vector<Item> &vector) { void operator()(const char *label, std::vector<Item> &vector) {
for (auto &item : vector) (*this)(label, item); for (auto &item : vector) (*this)(label, item);
} }
// Assume all other arguments have a `.state()`, and recurse into it // Assume all other arguments have a `.state()`, and recurse into it
template<class OtherObject> template<class OtherObject>
void operator()(const char *, OtherObject &obj) { void operator()(const char *, OtherObject &obj) {
obj.state(*this); obj.state(*this);
} }
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; } params;
bool justHadReset = true; bool justHadReset = true;

View File

@ -163,7 +163,8 @@ struct StorageCborReader {
} }
readValue(v); readValue(v);
} }
private:
template<class Obj> template<class Obj>
void readValue(Obj &obj) { void readValue(Obj &obj) {
if (compact) { if (compact) {
@ -224,7 +225,6 @@ struct StorageCborReader {
}); });
array.resize(length); array.resize(length);
} }
private:
protected: protected:
Cbor cbor; Cbor cbor;