1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
Geraint
4dd9d55c62 Split processing up according to events 2025-06-22 12:53:14 +01:00
Geraint
89aebf5d9e CLAP front-end processes audio (no parameters though) 2025-06-21 18:55:47 +01:00
14 changed files with 1351 additions and 613 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
**/.DS_Store **/.DS_Store
**/env.sh

View File

@ -40,7 +40,7 @@ include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
clap-wrapper clap-wrapper
GIT_REPOSITORY https://github.com/free-audio/clap-wrapper GIT_REPOSITORY https://github.com/free-audio/clap-wrapper
GIT_TAG 0.12.1 # first version with WCLAP stuff GIT_TAG v0.12.1 # first version with WCLAP stuff
GIT_SHALLOW ON GIT_SHALLOW ON
) )
FetchContent_MakeAvailable(clap-wrapper) FetchContent_MakeAvailable(clap-wrapper)
@ -62,8 +62,12 @@ target_link_libraries(${NAME}_static PUBLIC
clap clap
clap-helpers clap-helpers
) )
target_sources(${NAME}_static PUBLIC target_include_directories(${NAME}_static PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/source/${name}.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../include
${CMAKE_CURRENT_SOURCE_DIR}/../modules
)
target_sources(${NAME}_static PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/source/${NAME}.cpp
) )
make_clapfirst_plugins( make_clapfirst_plugins(

View File

@ -1,6 +1,6 @@
.PHONY: build build-emscripten emsdk .PHONY: build build-emscripten emsdk
PROJECT := plugins PROJECT := plugins
PLUGIN := example PLUGIN := basics
CMAKE_PARAMS := -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=.. -G Xcode # -DCMAKE_BUILD_TYPE=Release CMAKE_PARAMS := -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=.. -G Xcode # -DCMAKE_BUILD_TYPE=Release

View File

@ -3,11 +3,11 @@
# define LOG_EXPR(expr) std::cout << #expr " = " << (expr) << std::endl; # define LOG_EXPR(expr) std::cout << #expr " = " << (expr) << std::endl;
#endif #endif
#include "../crunch.h" #include "signalsmith-basics/crunch.h"
#include "../limiter.h" #include "signalsmith-basics/limiter.h"
#include "../reverb.h" #include "signalsmith-basics/reverb.h"
#include "./clap-stfx.h" #include "../../stfx/clap/stfx-clap.h"
static stfx::clap::Plugins plugins; static stfx::clap::Plugins plugins;
bool clap_init(const char *path) { bool clap_init(const char *path) {
@ -55,7 +55,7 @@ bool clap_init(const char *path) {
return plugins.clap_init(path); return plugins.clap_init(path);
} }
void clap_deinit() { void clap_deinit() {
plugins.clap_deinit(path); plugins.clap_deinit();
} }
const void * clap_get_factory(const char *id) { const void * clap_get_factory(const char *id) {
return plugins.clap_get_factory(id); return plugins.clap_get_factory(id);

View File

@ -1,41 +0,0 @@
#include "clap/clap.h"
#include <vector>
#include <functional>
#include <initializer_list>
namespace stfx { namespace clap {
template<template<class> class EffectSTFX>
struct Plugin : public clap_plugin {
template<class ...Args>
Plugin(const clap_plugin_descriptor *desc, Args ...args) : effect(args...) {
this->desc = desc;
this->plugin_data = nullptr;
}
private:
stfx::LibraryEffect<float, EffectSTFX> effect;
};
struct Plugins {
template<template<class> class EffectSTFX, class ...Args>
void add(clap_plugin_descriptor desc, std::initializer_list<const char *> features, Args ...args) {
size_t index = featureLists.size();
featureLists.emplace_back(features);
featureLists[index].push_back(nullptr);
desc.features = featureLists[index].data();
descriptors.push_back(desc);
creates.push_back([=](){
return new Plugin<EffectSTFX>(&descriptors[index], args...);
});
}
private:
std::vector<std::vector<const char *>> featureLists;
std::vector<clap_plugin_descriptor> descriptors;
std::vector<std::function<const clap_plugin_t *()>> creates;
};
}} // namespace

View File

@ -4,9 +4,10 @@ See LICENSE.txt and SUPPORT.txt */
#define SIGNALSMITH_BASICS_CRUNCH_H #define SIGNALSMITH_BASICS_CRUNCH_H
#include "dsp/rates.h" #include "dsp/rates.h"
#include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 4, 1) SIGNALSMITH_DSP_VERSION_CHECK(1, 4, 1)
#include "./stfx-library.h" #include "stfx/stfx-library.h"
#include <cmath> #include <cmath>
@ -19,14 +20,14 @@ using CrunchFloat = stfx::LibraryEffect<float, CrunchSTFX>;
using CrunchDouble = stfx::LibraryEffect<double, CrunchSTFX>; using CrunchDouble = stfx::LibraryEffect<double, CrunchSTFX>;
template<class BaseEffect> template<class BaseEffect>
class CrunchSTFX : public BaseEffect { struct CrunchSTFX : public BaseEffect {
static constexpr int oversampleHalfLatency = 16;
static constexpr Sample autoGainLevel = 0.1;
using typename BaseEffect::Sample; using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange; using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped; using typename BaseEffect::ParamStepped;
static constexpr int oversampleHalfLatency = 16;
static constexpr Sample autoGainLevel = 0.1;
ParamRange drive{4}; ParamRange drive{4};
ParamRange fuzz{0}; ParamRange fuzz{0};
ParamRange toneHz{2000}; ParamRange toneHz{2000};
@ -41,27 +42,20 @@ class CrunchSTFX : public BaseEffect {
int version = storage.version(0); int version = storage.version(0);
if (version != 0) return; if (version != 0) return;
using namespace signalsmith::units; using stfx::units::dbToGain;
storage.range("drive", drive) stfx::units::rangeGain(storage.range("drive", drive)
.info("drive", "pre-distortion input gain") .info("drive", "pre-distortion input gain")
.range(dbToGain(-12), 4, dbToGain(40)) .range(dbToGain(-12), 4, dbToGain(40)));
.unit("dB", 1, dbToGain, gainToDb) stfx::units::rangePercent(storage.range("fuzz", fuzz)
.unit("");
storage.range("fuzz", fuzz)
.info("fuzz", "amplitude-independent distortion") .info("fuzz", "amplitude-independent distortion")
.range(0, 0.5, 1) .range(0, 0.5, 1));
.unit("%", 0, pcToRatio, ratioToPc); stfx::units::rangeHz(storage.range("toneHz", toneHz)
storage.range("toneHz", toneHz)
.info("tone", "limits the brightness of the distortion") .info("tone", "limits the brightness of the distortion")
.range(100, 4000, 20000) .range(100, 4000, 20000));
.unit("Hz", 0, 0, 1000)
.unit("kHz", 1, kHzToHz, hzToKHz, 1000, 1e100);
storage.range("outGain", outGain) stfx::units::rangeGain(storage.range("outGain", outGain)
.info("out", "output gain") .info("out", "output gain")
.range(dbToGain(-12), 1, dbToGain(24)) .range(dbToGain(-12), 1, dbToGain(24)));
.unit("dB", 1, dbToGain, gainToDb)
.unit("");
} }
template<class Config> template<class Config>
@ -120,6 +114,7 @@ class CrunchSTFX : public BaseEffect {
oversampler.downChannel(c, io.output[c], block.length); oversampler.downChannel(c, io.output[c], block.length);
} }
} }
private: private:
int channels = 0; int channels = 0;
signalsmith::rates::Oversampler2xFIR<Sample> oversampler; signalsmith::rates::Oversampler2xFIR<Sample> oversampler;

View File

@ -0,0 +1 @@
#include "../../crunch.h"

View File

@ -0,0 +1 @@
#include "../../limiter.h"

View File

@ -0,0 +1 @@
#include "../../reverb.h"

View File

@ -7,26 +7,26 @@ Released under the Boost Software License (see LICENSE.txt) */
#include "dsp/envelopes.h" #include "dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0) SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0)
#include "./units.h" #include "stfx/stfx-library.h"
#include "./stfx-library.h"
#include <cmath> #include <cmath>
namespace signalsmith { namespace basics { namespace signalsmith { namespace basics {
template<class BaseEffect> template<class BaseEffect>
class LimiterSTFX : public BaseEffect { class LimiterSTFX;
using LimiterFloat = stfx::LibraryEffect<float, LimiterSTFX>;
using LimiterDouble = stfx::LibraryEffect<double, LimiterSTFX>;
template<class BaseEffect>
struct LimiterSTFX : public BaseEffect {
using typename BaseEffect::Sample; using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange; using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped; using typename BaseEffect::ParamStepped;
int channels = 0;
double maxDelayMs = 0;
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
public:
ParamRange inputGain{1}; ParamRange inputGain{1};
ParamRange outputLimit{signalsmith::units::dbToGain(-3)}; ParamRange outputLimit{stfx::units::dbToGain(-3)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0}; ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
ParamStepped smoothingStages{1}; ParamStepped smoothingStages{1};
@ -36,43 +36,33 @@ namespace signalsmith { namespace basics {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
using namespace signalsmith::units;
storage.info("[Basics] Limiter", "A simple lookahead limiter"); storage.info("[Basics] Limiter", "A simple lookahead limiter");
int version = storage.version(4); int version = storage.version(4);
if (version != 4) return; if (version != 4) return;
storage.range("inputGain", inputGain) using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting") .info("pre-gain", "amplifies the input before limiting")
.range(dbToGain(-12), 1, dbToGain(24)) .range(dbToGain(-12), 1, dbToGain(24)));
.unit("dB", 1, dbToGain, gainToDb) stfx::units::rangeGain(storage.range("outputLimit", outputLimit)
.unit("");
storage.range("outputLimit", outputLimit)
.info("limit", "maximum output amplitude") .info("limit", "maximum output amplitude")
.range(dbToGain(-24), dbToGain(-12), 1) .range(dbToGain(-24), dbToGain(-12), 1));
.unit("dB", 1, dbToGain, gainToDb) stfx::units::rangeMs(storage.range("attackMs", attackMs)
// Extra resolution between -1dB and 0dB
.unit("dB", 2, dbToGain, gainToDb, dbToGain(-1), 1)
.unit("");
storage.range("attackMs", attackMs)
.info("attack", "envelope smoothing time") .info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2) .range(1, 10, maxDelayMs/2));
.unit("ms", 0); stfx::units::rangeMs(storage.range("holdMs", holdMs)
storage.range("holdMs", holdMs)
.info("hold", "hold constant after peaks") .info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2) .range(0, 10, maxDelayMs/2));
.unit("ms", 0); stfx::units::rangeMs(storage.range("releaseMs", releaseMs)
storage.range("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)") .info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250) .range(0, 10, 250));
.unit("ms", 0);
storage.stepped("smoothingStages", smoothingStages) storage.stepped("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing") .info("smoothing", "smoothing filter(s) used for attack-smoothing")
.label(1, "rect", "double"); .label(1, "rect", "double");
storage.range("linkChannels", linkChannels) stfx::units::rangePercent(storage.range("linkChannels", linkChannels)
.info("link", "link channel gains together") .info("link", "link channel gains together")
.range(0, 0.5, 1) .range(0, 0.5, 1));
.unit("%", 0, pcToRatio, ratioToPc);
} }
// Gain envelopes are calculated per-channel // Gain envelopes are calculated per-channel
@ -110,7 +100,7 @@ namespace signalsmith { namespace basics {
Sample sampleRate; Sample sampleRate;
std::vector<Sample> channelGains; std::vector<Sample> channelGains;
template<class Config> template<class Config>
void configure(Config &config) { void configureSTFX(Config &config) {
channels = config.outputChannels = config.inputChannels; channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0); config.auxInputs.resize(0);
config.auxOutputs.resize(0); config.auxOutputs.resize(0);
@ -191,9 +181,12 @@ namespace signalsmith { namespace basics {
} }
multiBuffer += block.length; multiBuffer += block.length;
} }
};
using Limiter = stfx::LibraryEffect<float, LimiterSTFX>; private:
int channels = 0;
double maxDelayMs = 0;
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
};
}} // namespace }} // namespace

View File

@ -8,16 +8,21 @@ Released under the Boost Software License (see LICENSE.txt) */
#include "dsp/filters.h" #include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3) SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
#include "./stfx-library.h" #include "stfx/stfx-library.h"
#include "./units.h"
#include <cmath> #include <cmath>
#include <random> #include <random>
namespace signalsmith { namespace basics { namespace signalsmith { namespace basics {
template<class BaseEffect> template<class BaseEffect>
struct ReverbSTFX : public BaseEffect { struct ReverbSTFX;
using ReverbFloat = stfx::LibraryEffect<float, ReverbSTFX>;
using ReverbDouble = stfx::LibraryEffect<double, ReverbSTFX>;
template<class BaseEffect>
struct ReverbSTFX : public BaseEffect {
using typename BaseEffect::Sample; using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange; using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped; using typename BaseEffect::ParamStepped;
@ -37,63 +42,48 @@ namespace signalsmith { namespace basics {
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
using namespace signalsmith::units;
storage.info("[Basics] Reverb", "An FDN reverb"); storage.info("[Basics] Reverb", "An FDN reverb");
int version = storage.version(5); int version = storage.version(5);
if (version != 5) return; if (version != 5) return;
storage.range("dry", dry) using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("dry", dry)
.info("dry", "dry signal gain") .info("dry", "dry signal gain")
.range(0, 1, 4) .range(0, 1, 4));
.unit("dB", 1, dbToGain, gainToDb)
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("");
storage.range("wet", wet) stfx::units::rangeGain(storage.range("wet", wet)
.info("wet", "reverb tail gain") .info("wet", "reverb tail gain")
.range(0, 1, 4) .range(0, 1, 4));
.unit("dB", 1, dbToGain, gainToDb)
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("");
storage.range("roomMs", roomMs) stfx::units::rangeMs(storage.range("roomMs", roomMs)
.info("room", "room size (1ms ~ 1 foot)") .info("room", "room size (1ms ~ 1 foot)")
.range(10, 100, maxRoomMs) .range(10, 100, maxRoomMs));
.unit("ms", 0);
storage.range("rt20", rt20) stfx::units::rangeSec(storage.range("rt20", rt20)
.info("decay", "RT20: decay time to -20dB") .info("decay", "RT20: decay time to -20dB")
.range(0.01, 2, 30) .range(0.01, 2, 30));
.unit("seconds", 2, 0, 1)
.unit("seconds", 1, 1, 1e100);
storage.range("early", early) stfx::units::rangePercent(storage.range("early", early)
.info("early", "Early reflections") .info("early", "Early reflections")
.range(0, 1, 2.5) .range(0, 1, 2.5));
.unit("%", 0, pcToRatio, ratioToPc);
storage.range("detune", detune) storage.range("detune", detune)
.info("detune", "Detuning rate (inside feedback loop)") .info("detune", "Detuning rate (inside feedback loop)")
.range(0, 5, 50) .range(0, 5, 50)
.unit("", 1); .unit("", 1);
storage.range("lowCutHz", lowCutHz) stfx::units::rangeHz(storage.range("lowCutHz", lowCutHz)
.info("low cut", "Removes low frequencies") .info("low cut", "Removes low frequencies")
.range(10, 80, 500) .range(10, 80, 500));
.unit("Hz", 0);
storage.range("lowDampRate", lowDampRate) storage.range("lowDampRate", lowDampRate)
.info("low damp", "Reduce low frequencies over time") .info("low damp", "Reduce low frequencies over time")
.range(1, 2, 10) .range(1, 2, 10)
.unit("", 1); .unit("", 1);
storage.range("highCutHz", highCutHz) stfx::units::rangeHz(storage.range("highCutHz", highCutHz)
.info("high cut", "Removes high frequencies") .info("high cut", "Removes high frequencies")
.range(1000, 5000, 20000) .range(1000, 5000, 20000));
.unit("kHz", 1, kHzToHz, hzToKHz, 1000, 1e100)
.unit("Hz", 0);
storage.range("highDampRate", highDampRate) storage.range("highDampRate", highDampRate)
.info("high damp", "Reduce high frequencies over time") .info("high damp", "Reduce high frequencies over time")
.range(1, 2, 10) .range(1, 2, 10)
@ -116,7 +106,7 @@ namespace signalsmith { namespace basics {
} }
template<class Config> template<class Config>
void configure(Config &config) { void configureSTFX(Config &config) {
sampleRate = config.sampleRate; sampleRate = config.sampleRate;
config.outputChannels = config.inputChannels = 2; // stereo effect only config.outputChannels = config.inputChannels = 2; // stereo effect only
config.auxInputs.resize(0); config.auxInputs.resize(0);
@ -175,7 +165,7 @@ namespace signalsmith { namespace basics {
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
auto smoothedWetGain = block.smooth(wet.from(), wet.to()); auto smoothedWetGain = block.smooth(wet.from(), wet.to());
using signalsmith::units::dbToGain; using stfx::units::dbToGain;
double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from())); double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from()));
double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to())); double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to()));
auto smoothedDecayGain = block.smooth(decayGainFrom, decayGainTo); auto smoothedDecayGain = block.smooth(decayGainFrom, decayGainTo);
@ -281,7 +271,7 @@ namespace signalsmith { namespace basics {
detuneLfoPhase -= std::floor(detuneLfoPhase); detuneLfoPhase -= std::floor(detuneLfoPhase);
} }
private: private:
int channels = 0; int channels = 0;
double sampleRate = 1; double sampleRate = 1;
double maxRoomMs, detuneDepthMs; double maxRoomMs, detuneDepthMs;
@ -415,10 +405,7 @@ namespace signalsmith { namespace basics {
delayFeedback.updateLengthsExponential(roomSamples); delayFeedback.updateLengthsExponential(roomSamples);
delayEarly.updateLengths(0x0BDDE171, roomSamples); delayEarly.updateLengths(0x0BDDE171, roomSamples);
} }
}; };
using Reverb = stfx::LibraryEffect<float, ReverbSTFX>;
}} // namespace }} // namespace
#endif // include guard #endif // include guard

342
stfx/clap/param-info.h Normal file
View File

@ -0,0 +1,342 @@
#ifndef SIGNALSMITH_STFX_STFX2_PARAM_INFO_H
#define SIGNALSMITH_STFX_STFX2_PARAM_INFO_H
#include <vector>
#include <cmath>
#include <string>
#include <sstream>
#include <iomanip>
#include <map>
namespace stfx {
struct RangeParamInfo {
double defaultValue;
double low, mid, high;
std::string name, description;
RangeParamInfo(double defaultValue) : defaultValue(defaultValue), low(defaultValue), mid(defaultValue), high(defaultValue) {}
RangeParamInfo(const RangeParamInfo &other) = delete;
RangeParamInfo(RangeParamInfo &&other) = default;
RangeParamInfo & info(std::string pName, std::string pDescription) {
name = pName;
description = pDescription;
return *this;
}
RangeParamInfo & range(double pLow, double pHigh) {
return range(pLow, pHigh, (pLow + pHigh)*0.5);
}
RangeParamInfo & range(double pLow, double pMid, double pHigh) {
low = pLow;
mid = pMid;
high = pHigh;
// sanity check in case we mix the argument order up
if ((mid < high) != (low < high)) std::swap(mid, high); // middle crosses the high
if ((low < mid) != (low < high)) std::swap(low, mid); // middle crosses the low
rangeMap = {low, mid, high};
return *this;
}
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);
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, int precision, double validLow, double validHigh) {
return unit(suffix, precision, identityMap, identityMap, validLow, validHigh);
}
RangeParamInfo & unit(std::string suffix, double validLow, double validHigh) {
return unit(suffix, identityMap, identityMap, validLow, validHigh);
}
RangeParamInfo & exact(double v, std::string valueName) {
exactOptions.push_back({v, valueName});
return *this;
}
std::string toString(double value) const {
for (auto &option : exactOptions) {
if (value == option.value) return option.name;
}
for (const auto &unit : unitOptions) {
if (unit.valid(value)) return unit.toString(value);
}
if (unitOptions.empty()) return std::to_string(value);
return unitOptions[0].toString(value);
}
void toString(double value, std::string &valueString, std::string &unitString) const {
for (auto &option : exactOptions) {
if (value == option.value) {
valueString = option.name;
unitString = "";
return;
}
}
for (const auto &unit : unitOptions) {
if (unit.valid(value)) return unit.toString(value, valueString, unitString);
}
if (unitOptions.empty()) {
valueString = std::to_string(value);
unitString = "";
}
unitOptions[0].toString(value, valueString, unitString);
}
double fromString(const std::string &str) const {
bool hasDecimal = false, hasDigit = false;;
{ // check for a parseable number
size_t pos = 0;
while (str[pos] == '\t' || str[pos] == ' ') ++pos; // strip leading whitespace
if (str[pos] == '-') ++pos;
while (pos < str.length()) {
if (!hasDecimal && (str[pos] == '.' || str[pos] == ',')) {
hasDecimal = true;
} else if (str[pos] >= '0' && str[pos] <= '9') {
hasDigit = true;
} else {
break;
}
++pos;
}
}
// It's not a number - look for an exact match
if (!hasDigit) {
int longestMatch = -1;
double result = defaultValue;
for (auto &option : exactOptions) {
for (size_t i = 0; i < option.name.size() && i < str.size(); ++i) {
if (option.name[i] == str[i]) {
if (int(i) > longestMatch) {
longestMatch = i;
result = option.value;
}
} else {
break;
}
}
}
return result;
}
size_t pos = 0;
double result = std::stod(str, 0);
// Skip whitespace after the number
while (pos < str.length() && (str[pos] == ' ' || str[pos] == '\t')) ++pos;
size_t end = str.length(); // and at the end
while (end > pos && end > 0 && (str[end - 1] == ' ' || str[end - 1] == '\t')) --end;
std::string unit = str.substr(pos, end - pos);
for (const auto &u : unitOptions) {
if (u.unit == unit) {
return u.fromUnit(result);
}
}
return result;
}
std::vector<std::string> getUnits() const {
std::vector<std::string> result;
for (size_t i = 0; i < unitOptions.size(); ++i) {
bool duplicate = false;
for (size_t j = 0; j < i; ++j) {
if (unitOptions[i].unit == unitOptions[j].unit) {
duplicate = true;
break;
}
}
if (!duplicate) result.push_back(unitOptions[i].unit);
}
return result;
}
double toUnit(double value) const {
return rangeMap.toUnitRange(value);
}
double fromUnit(double unit) const {
return rangeMap.fromUnitRange(unit);
}
private:
static constexpr double doubleLowest = std::numeric_limits<double>::lowest();
static constexpr double doubleMax = std::numeric_limits<double>::max();
static double identityMap(double v) {
return v;
}
// 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;
protected:
struct ExactEntry {
double value;
std::string name;
};
std::vector<ExactEntry> exactOptions;
struct UnitEntry {
std::string fixedDisplay = "";
std::string unit;
bool addSpace = false, useFixed = false, keepZeros = false;
DoubleMap fromUnit, toUnit;
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) {
if (validLow > validHigh) {
std::swap(validLow, validHigh);
keepZeros = true;
}
if (unit[0] == ' ') {
addSpace = true;
this->unit = unit.substr(1, unit.size() - 1);
}
precisionOffset = 0.4999*std::pow(10, -precision);
}
bool valid(double value) const {
return value >= validLow && value <= validHigh;
}
void toString(double value, std::string &valueString, std::string &unitString) const {
std::ostringstream oss;
oss.precision(precision);
oss << std::fixed;
double offset = 0;
if (precision > 0) {
oss << toUnit(value) + offset;
} else {
oss << (int)(toUnit(value) + offset);
}
valueString = oss.str();
// Strip trailing zeroes
if (!keepZeros) for (int i = 0; i < (int)valueString.size(); ++i) {
if (valueString[i] == '.') {
int zeros = valueString.size();
while (zeros > i && valueString[zeros - 1] == '0') --zeros;
if (zeros == i + 1) --zeros;
valueString = valueString.substr(0, zeros);
break;
}
}
unitString = unit;
}
std::string toString(double value) const {
std::string valueString, unitString;
toString(value, valueString, unitString);
return valueString + (addSpace ? " " : "") + unitString;
}
};
std::vector<UnitEntry> unitOptions;
};
struct SteppedParamInfo {
int defaultValue;
int low, high;
std::string name, description;
SteppedParamInfo(int defaultValue) : defaultValue(defaultValue), low(defaultValue), high(defaultValue), nameIndex(defaultValue) {}
SteppedParamInfo(const SteppedParamInfo &other) = delete;
SteppedParamInfo(SteppedParamInfo &&other) = default;
SteppedParamInfo & info(std::string pName, std::string pDescription) {
name = pName;
description = pDescription;
return *this;
}
SteppedParamInfo & range(int pLow, int pHigh) {
low = pLow;
high = pHigh;
return *this;
}
SteppedParamInfo & label(const char *n) {
if (nameIndex > high && high >= low) high = nameIndex;
if (nameIndex > low && low > high) low = nameIndex;
nameMap[nameIndex] = n;
valueMap[n] = nameIndex;
return *this;
}
bool fullyLabelled() const {
return int(nameMap.size()) == (high - low + 1);
}
template<class ...Args>
SteppedParamInfo & label(const char *first, Args... others) {
label(first);
++nameIndex;
return label(others...);
}
template<class ...Args>
SteppedParamInfo & label(int start, const char *first, Args... others) {
nameIndex = start;
return label(first, others...);
}
const std::string * getLabel(int value) const {
auto pair = nameMap.find(value);
if (pair != nameMap.end()) {
return &pair->second;
}
return nullptr;
}
std::string toString(int value) const {
const std::string *maybeLabel = getLabel(value);
if (maybeLabel) return *maybeLabel;
return std::to_string(value);
}
int fromString(const std::string &str) const {
auto pair = valueMap.find(str);
if (pair != valueMap.end()) return pair->second;
size_t pos = 0;
if (str[0] == '-') ++pos;
while (pos < str.length() && str[pos] >= '0' && str[pos] <= '9') ++pos;
if (pos >= str.length() || pos == 0 || (pos == 1 && str[0] == '-')) {
return defaultValue;
}
int result = std::stoi(str, &pos);
if (high >= low) {
return std::max<int>(std::min<int>(high, result), low);
} else {
return std::max<int>(std::min<int>(low, result), high);
}
}
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:
std::map<int, std::string> nameMap;
std::map<std::string, int> valueMap;
};
} // namespace
#endif // include guard

454
stfx/clap/stfx-clap.h Normal file
View File

@ -0,0 +1,454 @@
#include "clap/clap.h"
#include <vector>
#include <string>
#include <functional>
#include <initializer_list>
#include <cstdio>
#include <optional>
#include "./param-info.h"
namespace stfx { namespace clap {
// A CLAP plugin made from an STFX template
template<template<class> class EffectSTFX>
struct Plugin;
// A helper to make a CLAP plugin factory from STFX templates
struct Plugins {
template<template<class> class EffectSTFX, class ...Args>
void add(clap_plugin_descriptor desc, std::initializer_list<const char *> features, Args ...args) {
size_t index = featureLists.size();
featureLists.emplace_back(features);
featureLists[index].push_back(nullptr);
desc.features = featureLists[index].data();
descriptors.push_back(desc);
creates.push_back([=](const clap_host *host){
return new Plugin<EffectSTFX>(*this, &descriptors[index], host, args...);
});
}
bool clap_init(const char *path) {
modulePath = path;
return true;
}
void clap_deinit() {}
const void * clap_get_factory(const char *id) {
if (!std::strcmp(id, CLAP_PLUGIN_FACTORY_ID)) {
// static variables like this are thread-safe (since C++11)
// https://en.cppreference.com/w/cpp/language/storage_duration.html#Static_block_variables
static PluginFactory factory{*this};
return &factory;
}
return nullptr;
}
std::string modulePath;
private:
std::vector<std::vector<const char *>> featureLists;
std::vector<clap_plugin_descriptor> descriptors;
std::vector<std::function<const clap_plugin_t *(const clap_host *)>> creates;
struct PluginFactory : public clap_plugin_factory {
Plugins &plugins;
PluginFactory(Plugins &plugins) : plugins(plugins) {
get_plugin_count = static_get_plugin_count;
get_plugin_descriptor = static_get_plugin_descriptor;
create_plugin = static_create_plugin;
}
static uint32_t static_get_plugin_count(const clap_plugin_factory *factory) {
const auto &plugins = ((PluginFactory *)factory)->plugins;
return uint32_t(plugins.creates.size());
}
static const clap_plugin_descriptor * static_get_plugin_descriptor(const clap_plugin_factory *factory, uint32_t index) {
const auto &plugins = ((PluginFactory *)factory)->plugins;
if (index >= plugins.descriptors.size()) return nullptr;
return &plugins.descriptors[index];
}
static const clap_plugin * static_create_plugin(const clap_plugin_factory *factory, const clap_host *host, const char *pluginId) {
const auto &plugins = ((PluginFactory *)factory)->plugins;
for (size_t index = 0; index < plugins.descriptors.size(); ++index) {
auto &desc = plugins.descriptors[index];
if (!std::strcmp(pluginId, desc.id)) {
return plugins.creates[index](host);
}
}
return nullptr;
}
};
};
struct Crc32 {
void add(uint8_t byte) {
uint32_t val = (crc^byte)&0xFF;
for (int i = 0; i < 8; ++i) {
val = (val&1) ? (val>>1)^0xEDB88320 : (val>>1);
}
crc = val^(crc>>8);
}
Crc32 & addString(const char *str, bool includeNull=true) {
while (*str) {
add(uint8_t(*str));
++str;
}
if (includeNull) add(0);
return *this;
}
Crc32 copy() {
return {*this};
}
uint32_t done() const {
return crc^0xFFFFFFFFu;
}
private:
uint32_t crc = 0xFFFFFFFFu;
};
template<template<class> class EffectSTFX>
struct Plugin : public clap_plugin {
const Plugins &plugins;
const clap_host *host;
using Effect = stfx::LibraryEffect<float, EffectSTFX>;
Effect effect;
template<class ...Args>
Plugin(const Plugins &plugins, const clap_plugin_descriptor *desc, const clap_host *host, Args ...args) : plugins(plugins), host(host), effect(args...) {
this->desc = desc;
this->plugin_data = nullptr;
this->init = plugin_init;
this->destroy = plugin_destroy;
this->activate = plugin_activate;
this->deactivate = plugin_deactivate;
this->start_processing = plugin_start_processing;
this->stop_processing = plugin_stop_processing;
this->reset = plugin_reset;
this->process = plugin_process;
this->get_extension = plugin_get_extension;
this->on_main_thread = plugin_on_main_thread;
scanParams();
}
// plugin state
bool isConfigured = false;
// for library STFX, all inputs/outputs (main and aux) get concatenated together
std::vector<const float *> inputBuffers;
std::vector<float *> outputBuffers;
void processEvent(const clap_event_header *header) {
LOG_EXPR(header->size);
LOG_EXPR(header->time);
LOG_EXPR(header->space_id);
LOG_EXPR(header->type);
LOG_EXPR(header->flags);
}
// CLAP plugin methods
static bool plugin_init(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
LOG_EXPR(plugin.plugins.modulePath);
return true;
}
static void plugin_destroy(const clap_plugin *obj) {
delete (Plugin *)obj;
}
static bool plugin_activate(const clap_plugin *obj, double sampleRate, uint32_t minFrames, uint32_t maxFrames) {
auto &plugin = *(Plugin *)obj;
auto &config = plugin.effect.config;
if (!plugin.isConfigured || sampleRate != plugin.effect.config.sampleRate) {
auto prevConfig = config;
plugin.isConfigured = plugin.effect.configure();
if (!plugin.isConfigured) {
// Can't change config (e.g. sample-rate/ports/etc.) here, return to previous
config = prevConfig;
}
}
size_t inputChannels = config.inputChannels;
for (auto &a : config.auxInputs) inputChannels += a;
plugin.inputBuffers.reserve(inputChannels);
size_t outputChannels = config.outputChannels;
for (auto &a : config.auxOutputs) a += outputChannels;
plugin.outputBuffers.reserve(outputChannels);
return plugin.isConfigured;
}
static void plugin_deactivate(const clap_plugin *obj) {}
static bool plugin_start_processing(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
return plugin.isConfigured;
}
static void plugin_stop_processing(const clap_plugin *obj) {}
static void plugin_reset(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
if (plugin.isConfigured) plugin.effect.reset();
}
static const void * plugin_get_extension(const clap_plugin *obj, const char *extId) {
if (!std::strcmp(extId, CLAP_EXT_PARAMS)) {
static struct clap_plugin_params ext{
params_count,
params_get_info,
params_get_value,
params_value_to_text,
params_text_to_value,
params_flush
};
return &ext;
} else if (!std::strcmp(extId, CLAP_EXT_AUDIO_PORTS)) {
static struct clap_plugin_audio_ports ext{
audio_ports_count,
audio_ports_get
};
return &ext;
}
return nullptr;
}
static clap_process_status plugin_process(const clap_plugin *obj, const clap_process *process) {
auto &plugin = *(Plugin *)obj;
auto &inputBuffers = plugin.inputBuffers;
for (uint32_t i = 0; i < process->audio_inputs_count; ++i) {
auto &buffer = process->audio_inputs[i];
for (uint32_t c = 0; c < buffer.channel_count; ++c) {
inputBuffers.push_back(buffer.data32[c]);
}
}
auto &outputBuffers = plugin.outputBuffers;
for (uint32_t i = 0; i < process->audio_outputs_count; ++i) {
auto &buffer = process->audio_outputs[i];
for (uint32_t c = 0; c < buffer.channel_count; ++c) {
outputBuffers.push_back(buffer.data32[c]);
}
}
size_t length = process->frames_count;
auto inputEventCount = process->in_events->size(process->in_events);
size_t offset = 0, nextEventIndex = 0;
while (offset < length) {
size_t nextEventOffset = length;
const clap_event_header *event = nullptr;
if (nextEventIndex < inputEventCount) {
event = process->in_events->get(process->in_events, nextEventIndex);
nextEventOffset = std::max<size_t>(offset, event->time);
}
// process up until the next event (or end)
if (nextEventOffset > offset) {
auto delta = nextEventOffset - offset;
plugin.effect.process(inputBuffers.data(), outputBuffers.data(), delta);
offset = nextEventOffset;
for (auto &p : inputBuffers) p += delta;
for (auto &p : outputBuffers) p += delta;
}
if (event) {
plugin.processEvent(event);
++nextEventIndex;
}
}
inputBuffers.resize(0);
outputBuffers.resize(0);
return CLAP_PROCESS_CONTINUE;
}
static void plugin_on_main_thread(const clap_plugin *obj) {}
// parameters
struct Param : public clap_param_info {
typename Effect::ParamRange *rangeParam = nullptr;
std::optional<RangeParamInfo> rangeInfo;
typename Effect::ParamStepped *steppedParam = nullptr;
std::optional<SteppedParamInfo> steppedInfo;
void setClapInfo() {
flags |= CLAP_PARAM_IS_AUTOMATABLE;
cookie = this;
if (rangeParam) {
std::strncpy(name, rangeInfo->name.c_str(), CLAP_NAME_SIZE);
// STFX range params are mapped to [0, 1] so we can give them a nonlinear shape
min_value = 0;
max_value = 1;
default_value = rangeInfo->toUnit(rangeInfo->defaultValue);
} else {
std::strncpy(name, steppedInfo->name.c_str(), CLAP_NAME_SIZE);
flags |= CLAP_PARAM_IS_STEPPED;
min_value = steppedInfo->low;
max_value = steppedInfo->high;
default_value = steppedInfo->defaultValue;
}
}
};
std::vector<Param> params;
struct ParamScanner {
std::vector<Param> &params;
Crc32 crc;
// Do nothing for most types
template<class V>
void operator()(const char *key, V &v) {
LOG_EXPR(key);
if constexpr (std::is_void_v<decltype(v.state(*this))>) {
LOG_EXPR("recursing");
ParamScanner recurse{*this};
recurse.crc.addString(key, true);
v.state(recurse);
}
}
// Extra methods we add for STFX storage
void info(const char *, const char *) {}
int version(int v) {
return v;
}
template<class PR>
RangeParamInfo & range(const char *key, PR &param) {
params.emplace_back();
auto &entry = params.back();
entry.id = crc.copy().addString(key, true).done();
entry.rangeParam = &param;
return entry.rangeInfo.emplace(param);
}
template<class PS>
SteppedParamInfo & stepped(const char *key, PS &param) {
params.emplace_back();
auto &entry = params.back();
entry.id = crc.copy().addString(key, true).done();
entry.steppedParam = &param;
return entry.steppedInfo.emplace(param);
}
};
void scanParams() {
params.resize(0);
ParamScanner scanner{params};
effect.state(scanner);
for (auto &entry : params) entry.setClapInfo();
}
// CLAP parameter methods
static uint32_t params_count(const clap_plugin *obj) {
auto &plugin = *(Plugin *)obj;
return plugin.params.size();
}
static bool params_get_info(const clap_plugin *obj, uint32_t index, clap_param_info *info) {
auto &plugin = *(Plugin *)obj;
if (index >= plugin.params.size()) return false;
*info = plugin.params[index];
return true;
}
static bool params_get_value(const clap_plugin *obj, clap_id paramId, double *value) {
auto &plugin = *(Plugin *)obj;
for (auto &param : plugin.params) {
if (param.id == paramId) {
if (param.rangeParam) {
*value = param.rangeInfo->toUnit((double)*param.rangeParam);
} else {
*value = (int)*param.steppedParam;
}
return true;
}
}
return false;
}
static bool params_value_to_text(const clap_plugin *obj, clap_id paramId, double value, char *text, uint32_t textCapacity) {
auto &plugin = *(Plugin *)obj;
for (auto &param : plugin.params) {
if (param.id == paramId) {
if (param.rangeParam) {
auto str = param.rangeInfo->toString(value);
std::strncpy(text, str.c_str(), textCapacity);
} else {
auto str = param.steppedInfo->toString(int(std::round(value)));
std::strncpy(text, str.c_str(), textCapacity);
}
return true;
}
}
return false;
}
static bool params_text_to_value(const clap_plugin *obj, clap_id paramId, const char *text, double *value) {
auto &plugin = *(Plugin *)obj;
for (auto &param : plugin.params) {
if (param.id == paramId) {
std::string str = text;
if (param.rangeParam) {
*value = param.rangeInfo->toUnit(param.rangeInfo->fromString(str));
} else {
*value = param.steppedInfo->fromString(str);
}
return true;
}
}
return false;
}
static void params_flush(const clap_plugin *obj, const clap_input_events *inEvents, const clap_output_events *outEvents) {
auto &plugin = *(Plugin *)obj;
auto count = inEvents->size(inEvents);
for (uint32_t i = 0; i < count; ++i) {
auto *header = inEvents->get(inEvents, i);
plugin.processEvent(header);
}
}
// CLAP audio port methods
static uint32_t audio_ports_count(const clap_plugin *obj, bool inputPorts) {
auto &plugin = *(Plugin *)obj;
if (!plugin.isConfigured) {
plugin.isConfigured = plugin.effect.configurePersistent();
if (!plugin.isConfigured) return 0;
}
auto &config = plugin.effect.config;
if (inputPorts) {
return config.auxInputs.size() + (config.inputChannels > 0);
} else {
return config.auxOutputs.size() + (config.outputChannels > 0);
}
}
static bool audio_ports_get(const clap_plugin *obj, uint32_t index, bool inputPorts, clap_audio_port_info *info) {
auto &plugin = *(Plugin *)obj;
if (!plugin.isConfigured) return false;
auto &config = plugin.effect.config;
clap_id portIdBase = (inputPorts ? 0x1000000 : 0x2000000);
auto main = uint32_t(inputPorts ? config.inputChannels : config.outputChannels);
auto &aux = (inputPorts ? config.auxInputs : config.auxOutputs);
auto auxIndex = index;
if (main) {
if (index == 0) {
*info = {
.id=portIdBase,
.name={'m', 'a', 'i', 'n'},
.flags=CLAP_AUDIO_PORT_IS_MAIN,
.channel_count=main,
.port_type=nullptr,
.in_place_pair=CLAP_INVALID_ID
};
return true;
}
--auxIndex;
}
if (auxIndex < aux.size()) {
*info = {
.id=portIdBase + index,
.name={'a', 'u', 'x'},
.flags=CLAP_AUDIO_PORT_IS_MAIN,
.channel_count=main,
.port_type=nullptr,
.in_place_pair=CLAP_INVALID_ID
};
if (aux.size() > 1) {
info->name[3] = '1' + auxIndex;
}
return true;
}
return false;
}
};
}} // namespace

View File

@ -430,7 +430,7 @@ namespace stfx {
/// 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. /// 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() { bool configure() {
Config prevConfig = config; Config prevConfig = config;
EffectClass::configure(config); EffectClass::configureSTFX(config);
if (config == prevConfig) { if (config == prevConfig) {
reset(); reset();
return true; return true;