1
0

CLAP front-end processes audio (no parameters though)

This commit is contained in:
Geraint 2025-06-21 18:55:47 +01:00
parent 417dbc1944
commit 89aebf5d9e
14 changed files with 1223 additions and 613 deletions

1
.gitignore vendored
View File

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

View File

@ -40,7 +40,7 @@ include(FetchContent)
FetchContent_Declare(
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
)
FetchContent_MakeAvailable(clap-wrapper)
@ -62,8 +62,12 @@ target_link_libraries(${NAME}_static PUBLIC
clap
clap-helpers
)
target_sources(${NAME}_static PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/source/${name}.cpp
target_include_directories(${NAME}_static PRIVATE
${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(

View File

@ -1,6 +1,6 @@
.PHONY: build build-emscripten emsdk
PROJECT := plugins
PLUGIN := example
PLUGIN := basics
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;
#endif
#include "../crunch.h"
#include "../limiter.h"
#include "../reverb.h"
#include "signalsmith-basics/crunch.h"
#include "signalsmith-basics/limiter.h"
#include "signalsmith-basics/reverb.h"
#include "./clap-stfx.h"
#include "../../stfx/clap/stfx-clap.h"
static stfx::clap::Plugins plugins;
bool clap_init(const char *path) {
@ -55,7 +55,7 @@ bool clap_init(const char *path) {
return plugins.clap_init(path);
}
void clap_deinit() {
plugins.clap_deinit(path);
plugins.clap_deinit();
}
const void * clap_get_factory(const char *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
#include "dsp/rates.h"
#include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 4, 1)
#include "./stfx-library.h"
#include "stfx/stfx-library.h"
#include <cmath>
@ -19,14 +20,14 @@ using CrunchFloat = stfx::LibraryEffect<float, CrunchSTFX>;
using CrunchDouble = stfx::LibraryEffect<double, CrunchSTFX>;
template<class BaseEffect>
class CrunchSTFX : public BaseEffect {
static constexpr int oversampleHalfLatency = 16;
static constexpr Sample autoGainLevel = 0.1;
struct CrunchSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
static constexpr int oversampleHalfLatency = 16;
static constexpr Sample autoGainLevel = 0.1;
ParamRange drive{4};
ParamRange fuzz{0};
ParamRange toneHz{2000};
@ -41,27 +42,20 @@ class CrunchSTFX : public BaseEffect {
int version = storage.version(0);
if (version != 0) return;
using namespace signalsmith::units;
storage.range("drive", drive)
using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("drive", drive)
.info("drive", "pre-distortion input gain")
.range(dbToGain(-12), 4, dbToGain(40))
.unit("dB", 1, dbToGain, gainToDb)
.unit("");
storage.range("fuzz", fuzz)
.range(dbToGain(-12), 4, dbToGain(40)));
stfx::units::rangePercent(storage.range("fuzz", fuzz)
.info("fuzz", "amplitude-independent distortion")
.range(0, 0.5, 1)
.unit("%", 0, pcToRatio, ratioToPc);
storage.range("toneHz", toneHz)
.range(0, 0.5, 1));
stfx::units::rangeHz(storage.range("toneHz", toneHz)
.info("tone", "limits the brightness of the distortion")
.range(100, 4000, 20000)
.unit("Hz", 0, 0, 1000)
.unit("kHz", 1, kHzToHz, hzToKHz, 1000, 1e100);
.range(100, 4000, 20000));
storage.range("outGain", outGain)
stfx::units::rangeGain(storage.range("outGain", outGain)
.info("out", "output gain")
.range(dbToGain(-12), 1, dbToGain(24))
.unit("dB", 1, dbToGain, gainToDb)
.unit("");
.range(dbToGain(-12), 1, dbToGain(24)));
}
template<class Config>
@ -120,6 +114,7 @@ class CrunchSTFX : public BaseEffect {
oversampler.downChannel(c, io.output[c], block.length);
}
}
private:
int channels = 0;
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"

335
limiter.h
View File

@ -7,193 +7,186 @@ Released under the Boost Software License (see LICENSE.txt) */
#include "dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0)
#include "./units.h"
#include "./stfx-library.h"
#include "stfx/stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<class BaseEffect>
class LimiterSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
template<class BaseEffect>
class LimiterSTFX;
int channels = 0;
double maxDelayMs = 0;
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
using LimiterFloat = stfx::LibraryEffect<float, LimiterSTFX>;
using LimiterDouble = stfx::LibraryEffect<double, LimiterSTFX>;
public:
ParamRange inputGain{1};
ParamRange outputLimit{signalsmith::units::dbToGain(-3)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
template<class BaseEffect>
struct LimiterSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
ParamStepped smoothingStages{1};
ParamRange linkChannels{0.5};
ParamRange inputGain{1};
ParamRange outputLimit{stfx::units::dbToGain(-3)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
ParamStepped smoothingStages{1};
ParamRange linkChannels{0.5};
LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {}
template<class Storage>
void state(Storage &storage) {
storage.info("[Basics] Limiter", "A simple lookahead limiter");
int version = storage.version(4);
if (version != 4) return;
using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting")
.range(dbToGain(-12), 1, dbToGain(24)));
stfx::units::rangeGain(storage.range("outputLimit", outputLimit)
.info("limit", "maximum output amplitude")
.range(dbToGain(-24), dbToGain(-12), 1));
stfx::units::rangeMs(storage.range("attackMs", attackMs)
.info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2));
stfx::units::rangeMs(storage.range("holdMs", holdMs)
.info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2));
stfx::units::rangeMs(storage.range("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250));
storage.stepped("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
.label(1, "rect", "double");
stfx::units::rangePercent(storage.range("linkChannels", linkChannels)
.info("link", "link channel gains together")
.range(0, 0.5, 1));
}
// Gain envelopes are calculated per-channel
struct ChannelEnvelope {
signalsmith::envelopes::PeakHold<Sample> peakHold{0};
signalsmith::envelopes::BoxFilter<Sample> smoother1{0}, smoother2{0};
Sample released = 1;
Sample releaseSlew = 1;
LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {}
template<class Storage>
void state(Storage &storage) {
using namespace signalsmith::units;
storage.info("[Basics] Limiter", "A simple lookahead limiter");
int version = storage.version(4);
if (version != 4) return;
storage.range("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting")
.range(dbToGain(-12), 1, dbToGain(24))
.unit("dB", 1, dbToGain, gainToDb)
.unit("");
storage.range("outputLimit", outputLimit)
.info("limit", "maximum output amplitude")
.range(dbToGain(-24), dbToGain(-12), 1)
.unit("dB", 1, dbToGain, gainToDb)
// Extra resolution between -1dB and 0dB
.unit("dB", 2, dbToGain, gainToDb, dbToGain(-1), 1)
.unit("");
storage.range("attackMs", attackMs)
.info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2)
.unit("ms", 0);
storage.range("holdMs", holdMs)
.info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2)
.unit("ms", 0);
storage.range("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250)
.unit("ms", 0);
storage.stepped("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
.label(1, "rect", "double");
storage.range("linkChannels", linkChannels)
.info("link", "link channel gains together")
.range(0, 0.5, 1)
.unit("%", 0, pcToRatio, ratioToPc);
void reset(Sample value=1) {
peakHold.reset(-value);
smoother1.reset(value);
smoother2.reset(value);
released = value;
}
// Gain envelopes are calculated per-channel
struct ChannelEnvelope {
signalsmith::envelopes::PeakHold<Sample> peakHold{0};
signalsmith::envelopes::BoxFilter<Sample> smoother1{0}, smoother2{0};
Sample released = 1;
Sample releaseSlew = 1;
void reset(Sample value=1) {
peakHold.reset(-value);
smoother1.reset(value);
smoother2.reset(value);
released = value;
}
void configure(int maxDelaySamples) {
peakHold.resize(maxDelaySamples);
smoother1.resize(maxDelaySamples);
smoother2.resize(maxDelaySamples/2 + 1);
}
Sample operator ()(Sample maxGain) {
// Moving minimum
Sample gain = -peakHold(-maxGain);
// Exponential release curve
released += (gain - released)*releaseSlew;
released = std::min(gain, released);
// Smoothing (attack)
return smoother1(smoother2(released));
}
};
std::vector<ChannelEnvelope> channelEnvelopes;
Sample sampleRate;
std::vector<Sample> channelGains;
template<class Config>
void configure(Config &config) {
channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
sampleRate = config.sampleRate;
int maxDelaySamples = std::ceil(maxDelayMs*0.001*sampleRate);
multiBuffer.resize(channels, maxDelaySamples + 1);
channelEnvelopes.resize(channels);
for (auto &e : channelEnvelopes) e.configure(maxDelaySamples);
channelGains.resize(channels);
void configure(int maxDelaySamples) {
peakHold.resize(maxDelaySamples);
smoother1.resize(maxDelaySamples);
smoother2.resize(maxDelaySamples/2 + 1);
}
void reset() {
multiBuffer.reset();
for (auto &e : channelEnvelopes) e.reset();
}
int latencySamples() const {
int attackSamples = std::ceil(attackMs*0.001*sampleRate);
return attackSamples;
}
template <class Io, class Config, class Block>
void processSTFX(Io &io, Config &, Block &block) {
Sample thresholdAmp = outputLimit;
auto smoothedPreGain = block.smooth(inputGain);
// If we change the attack, we want to fade between the two delay times
int delaySamplesFrom = std::ceil(attackMs.from()*0.001*sampleRate);
int delaySamplesTo = std::ceil(attackMs.to()*0.001*sampleRate);
int attackSamples = delaySamplesTo;
int holdSamples = std::ceil(holdMs*0.001*sampleRate);
Sample releaseSamples = releaseMs*0.001*sampleRate;
int stages = smoothingStages;
for (auto &envelope : channelEnvelopes) {
envelope.peakHold.set(attackSamples + holdSamples);
if (stages == 2) {
// Split into two (non-equal) box filters
int split = std::round(attackSamples*0.5822419);
envelope.smoother1.set(split + 1);
envelope.smoother2.set(attackSamples - split + 1);
} else {
envelope.smoother1.set(attackSamples + 1);
envelope.smoother2.set(1);
}
// Reasonable approximation for release curve: https://www.desmos.com/calculator/wbxakdgw1o
constexpr Sample ln2{0.69314718056};
envelope.releaseSlew = ln2/(releaseSamples + ln2);
}
for (int i = 0; i < block.length; ++i) {
Sample minChannelGain = 1;
for (int c = 0; c < channels; ++c) {
Sample value = io.input[c][i]*smoothedPreGain.at(i);
multiBuffer[c][i] = value;
// maximum gain (clips output to threshold)
Sample gain = thresholdAmp/std::max(thresholdAmp, std::abs(value));
channelGains[c] = gain;
minChannelGain = std::min(minChannelGain, gain);
}
for (int c = 0; c < channels; ++c) {
Sample gain = channelGains[c];
// blend between individual/minimum gain
gain += (minChannelGain - gain)*linkChannels;
// smooth envelope gain
auto &envelope = channelEnvelopes[c];
gain = envelope(gain);
Sample delayed = block.fade(i,
multiBuffer[c][i - delaySamplesFrom],
multiBuffer[c][i - delaySamplesTo]
);
io.output[c][i] = delayed*gain;
}
}
multiBuffer += block.length;
Sample operator ()(Sample maxGain) {
// Moving minimum
Sample gain = -peakHold(-maxGain);
// Exponential release curve
released += (gain - released)*releaseSlew;
released = std::min(gain, released);
// Smoothing (attack)
return smoother1(smoother2(released));
}
};
std::vector<ChannelEnvelope> channelEnvelopes;
Sample sampleRate;
std::vector<Sample> channelGains;
template<class Config>
void configureSTFX(Config &config) {
channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
sampleRate = config.sampleRate;
int maxDelaySamples = std::ceil(maxDelayMs*0.001*sampleRate);
multiBuffer.resize(channels, maxDelaySamples + 1);
channelEnvelopes.resize(channels);
for (auto &e : channelEnvelopes) e.configure(maxDelaySamples);
channelGains.resize(channels);
}
void reset() {
multiBuffer.reset();
for (auto &e : channelEnvelopes) e.reset();
}
int latencySamples() const {
int attackSamples = std::ceil(attackMs*0.001*sampleRate);
return attackSamples;
}
template <class Io, class Config, class Block>
void processSTFX(Io &io, Config &, Block &block) {
Sample thresholdAmp = outputLimit;
auto smoothedPreGain = block.smooth(inputGain);
// If we change the attack, we want to fade between the two delay times
int delaySamplesFrom = std::ceil(attackMs.from()*0.001*sampleRate);
int delaySamplesTo = std::ceil(attackMs.to()*0.001*sampleRate);
using Limiter = stfx::LibraryEffect<float, LimiterSTFX>;
int attackSamples = delaySamplesTo;
int holdSamples = std::ceil(holdMs*0.001*sampleRate);
Sample releaseSamples = releaseMs*0.001*sampleRate;
int stages = smoothingStages;
for (auto &envelope : channelEnvelopes) {
envelope.peakHold.set(attackSamples + holdSamples);
if (stages == 2) {
// Split into two (non-equal) box filters
int split = std::round(attackSamples*0.5822419);
envelope.smoother1.set(split + 1);
envelope.smoother2.set(attackSamples - split + 1);
} else {
envelope.smoother1.set(attackSamples + 1);
envelope.smoother2.set(1);
}
// Reasonable approximation for release curve: https://www.desmos.com/calculator/wbxakdgw1o
constexpr Sample ln2{0.69314718056};
envelope.releaseSlew = ln2/(releaseSamples + ln2);
}
for (int i = 0; i < block.length; ++i) {
Sample minChannelGain = 1;
for (int c = 0; c < channels; ++c) {
Sample value = io.input[c][i]*smoothedPreGain.at(i);
multiBuffer[c][i] = value;
// maximum gain (clips output to threshold)
Sample gain = thresholdAmp/std::max(thresholdAmp, std::abs(value));
channelGains[c] = gain;
minChannelGain = std::min(minChannelGain, gain);
}
for (int c = 0; c < channels; ++c) {
Sample gain = channelGains[c];
// blend between individual/minimum gain
gain += (minChannelGain - gain)*linkChannels;
// smooth envelope gain
auto &envelope = channelEnvelopes[c];
gain = envelope(gain);
Sample delayed = block.fade(i,
multiBuffer[c][i - delaySamplesFrom],
multiBuffer[c][i - delaySamplesTo]
);
io.output[c][i] = delayed*gain;
}
}
multiBuffer += block.length;
}
private:
int channels = 0;
double maxDelayMs = 0;
signalsmith::delay::MultiBuffer<Sample> multiBuffer;
};
}} // namespace

727
reverb.h
View File

@ -8,417 +8,404 @@ Released under the Boost Software License (see LICENSE.txt) */
#include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
#include "./stfx-library.h"
#include "./units.h"
#include "stfx/stfx-library.h"
#include <cmath>
#include <random>
namespace signalsmith { namespace basics {
template<class BaseEffect>
struct ReverbSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
using Array = std::array<Sample, 8>;
using Array3 = std::array<Sample, 3>;
template<class BaseEffect>
struct ReverbSTFX;
ParamRange dry{1}, wet{0.5};
ParamRange roomMs{80};
ParamRange rt20{1};
ParamRange early{1.5};
ParamRange detune{2};
using ReverbFloat = stfx::LibraryEffect<float, ReverbSTFX>;
using ReverbDouble = stfx::LibraryEffect<double, ReverbSTFX>;
ParamRange lowCutHz{80}, highCutHz{12000};
ParamRange lowDampRate{1.5}, highDampRate{2.5};
template<class BaseEffect>
struct ReverbSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
using Array = std::array<Sample, 8>;
using Array3 = std::array<Sample, 3>;
ReverbSTFX(double maxRoomMs=200, double detuneDepthMs=2) : maxRoomMs(maxRoomMs), detuneDepthMs(detuneDepthMs) {}
ParamRange dry{1}, wet{0.5};
ParamRange roomMs{80};
ParamRange rt20{1};
ParamRange early{1.5};
ParamRange detune{2};
template<class Storage>
void state(Storage &storage) {
using namespace signalsmith::units;
ParamRange lowCutHz{80}, highCutHz{12000};
ParamRange lowDampRate{1.5}, highDampRate{2.5};
storage.info("[Basics] Reverb", "An FDN reverb");
int version = storage.version(5);
if (version != 5) return;
ReverbSTFX(double maxRoomMs=200, double detuneDepthMs=2) : maxRoomMs(maxRoomMs), detuneDepthMs(detuneDepthMs) {}
storage.range("dry", dry)
.info("dry", "dry signal gain")
.range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb)
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("");
template<class Storage>
void state(Storage &storage) {
storage.range("wet", wet)
.info("wet", "reverb tail gain")
.range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb)
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("");
storage.info("[Basics] Reverb", "An FDN reverb");
int version = storage.version(5);
if (version != 5) return;
storage.range("roomMs", roomMs)
.info("room", "room size (1ms ~ 1 foot)")
.range(10, 100, maxRoomMs)
.unit("ms", 0);
using stfx::units::dbToGain;
stfx::units::rangeGain(storage.range("dry", dry)
.info("dry", "dry signal gain")
.range(0, 1, 4));
storage.range("rt20", rt20)
.info("decay", "RT20: decay time to -20dB")
.range(0.01, 2, 30)
.unit("seconds", 2, 0, 1)
.unit("seconds", 1, 1, 1e100);
stfx::units::rangeGain(storage.range("wet", wet)
.info("wet", "reverb tail gain")
.range(0, 1, 4));
storage.range("early", early)
.info("early", "Early reflections")
.range(0, 1, 2.5)
.unit("%", 0, pcToRatio, ratioToPc);
stfx::units::rangeMs(storage.range("roomMs", roomMs)
.info("room", "room size (1ms ~ 1 foot)")
.range(10, 100, maxRoomMs));
stfx::units::rangeSec(storage.range("rt20", rt20)
.info("decay", "RT20: decay time to -20dB")
.range(0.01, 2, 30));
stfx::units::rangePercent(storage.range("early", early)
.info("early", "Early reflections")
.range(0, 1, 2.5));
storage.range("detune", detune)
.info("detune", "Detuning rate (inside feedback loop)")
.range(0, 5, 50)
.unit("", 1);
stfx::units::rangeHz(storage.range("lowCutHz", lowCutHz)
.info("low cut", "Removes low frequencies")
.range(10, 80, 500));
storage.range("lowDampRate", lowDampRate)
.info("low damp", "Reduce low frequencies over time")
.range(1, 2, 10)
.unit("", 1);
stfx::units::rangeHz(storage.range("highCutHz", highCutHz)
.info("high cut", "Removes high frequencies")
.range(1000, 5000, 20000));
storage.range("highDampRate", highDampRate)
.info("high damp", "Reduce high frequencies over time")
.range(1, 2, 10)
.unit("", 1);
}
template<class Preset>
void presets(Preset &preset) {
if (preset("ambient")) {
wet = 0.85;
roomMs = 80;
rt20 = 8.5;
early = 0.55;
detune = 8.5;
lowCutHz = 50;
lowDampRate = 1.5;
highCutHz = 7200;
highDampRate = 2;
}
}
template<class Config>
void configureSTFX(Config &config) {
sampleRate = config.sampleRate;
config.outputChannels = config.inputChannels = 2; // stereo effect only
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
detuneDepthSamples = detuneDepthMs*0.001*config.sampleRate;
double maxRoomSamples = maxRoomMs*0.001*config.sampleRate;
delay1.configure(maxRoomSamples, 0.125);
delay2.configure(maxRoomSamples, 1);
delay3.configure(maxRoomSamples, 0.5);
delay4.configure(maxRoomSamples, 0.25);
delayFeedback.configure(maxRoomSamples*1.6 + detuneDepthSamples, 1);
delayEarly.configure(maxRoomSamples, 0.25);
}
void reset() {
delay1.reset();
delay2.reset();
delay3.reset();
delayFeedback.reset();
delayEarly.reset();
for (auto &f : lowCutFilters) f.reset();
for (auto &f : highCutFilters) f.reset();
for (auto &f : lowDampFilters) f.reset();
for (auto &f : highDampFilters) f.reset();
detuneLfoPhase = 0;
}
int latencySamples() const {
return 0;
}
int tailSamples() {
return std::round(sampleRate*rt20*3); // decay to -60dB
}
template<class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
using Hadamard = signalsmith::mix::Hadamard<Sample, 8>;
using Householder = signalsmith::mix::Householder<Sample, 8>;
auto &&inputLeft = io.input[0];
auto &&inputRight = io.input[1];
auto &&outputLeft = io.output[0];
auto &&outputRight = io.output[1];
block.setupFade([&](){
updateDelays(roomMs.to()*0.001*config.sampleRate);
});
bool fading = block.fading();
auto smoothedDryGain = block.smooth(dry);
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
auto smoothedWetGain = block.smooth(wet.from(), wet.to());
using stfx::units::dbToGain;
double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from()));
double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to()));
auto smoothedDecayGain = block.smooth(decayGainFrom, decayGainTo);
auto smoothedInputGain = block.smooth( // scale according to the number of expected echoes in the first 100ms
2*scalingFactor*std::sqrt((1 - decayGainFrom)/(1 - std::pow(decayGainFrom, 100/roomMs.from()))),
2*scalingFactor*std::sqrt((1 - decayGainTo)/(1 - std::pow(decayGainTo, 100/roomMs.to())))
);
auto smoothedEarlyGain = block.smooth(early, [&](double g) {
return g*0.35; // tuned by ear
});
updateFilters(decayGainTo);
// Detuning LFO rate
double detuneCentsPerLoop = detune*std::sqrt(roomMs*0.001);
double detuneLfoRate = (detuneCentsPerLoop*0.0004)/detuneDepthSamples; // tuned by ear, assuming 3/8 channels are detuned
for (int i = 0; i < block.length; ++i) {
Sample inputGain = smoothedInputGain.at(i);
Sample decayGain = smoothedDecayGain.at(i);
Sample earlyGain = smoothedEarlyGain.at(i);
std::array<Sample, 2> stereoIn = {Sample(inputLeft[i]), Sample(inputRight[i])};
Array samples;
std::array<Sample, 2> stereoInScaled = {stereoIn[0]*inputGain, stereoIn[1]*inputGain};
stereoMixer.stereoToMulti(stereoInScaled, samples);
storage.range("detune", detune)
.info("detune", "Detuning rate (inside feedback loop)")
.range(0, 5, 50)
.unit("", 1);
double lfoCos = std::cos(detuneLfoPhase*2*M_PI), lfoSin = std::sin(detuneLfoPhase*2*M_PI);
Array3 lfoArray = {
Sample((0.5 + lfoCos*0.5)*detuneDepthSamples),
Sample((0.5 + lfoCos*-0.25 + lfoSin*0.43301270189)*detuneDepthSamples),
Sample((0.5 + lfoCos*-0.25 + lfoSin*-0.43301270189)*detuneDepthSamples)
};
detuneLfoPhase += detuneLfoRate;
if (fading) {
Sample fade = block.fade(i);
samples = delay1.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
samples = delay2.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
storage.range("lowCutHz", lowCutHz)
.info("low cut", "Removes low frequencies")
.range(10, 80, 500)
.unit("Hz", 0);
storage.range("lowDampRate", lowDampRate)
.info("low damp", "Reduce low frequencies over time")
.range(1, 2, 10)
.unit("", 1);
Array feedback = delayFeedback.readDetuned(lfoArray, fade);
Householder::inPlace(feedback);
for (int c = 0; c < 8; ++c) {
feedback[c] = highDampFilters[c](lowDampFilters[c](feedback[c]));
}
Array feedbackInput;
for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput);
storage.range("highCutHz", highCutHz)
.info("high cut", "Removes high frequencies")
.range(1000, 5000, 20000)
.unit("kHz", 1, kHzToHz, hzToKHz, 1000, 1e100)
.unit("Hz", 0);
storage.range("highDampRate", highDampRate)
.info("high damp", "Reduce high frequencies over time")
.range(1, 2, 10)
.unit("", 1);
}
Array earlyReflections = delayEarly.write(samples).read(fade);
Hadamard::unscaledInPlace(earlyReflections);
for (int c = 0; c < 8; ++c) samples[c] = feedback[c] + earlyReflections[c]*earlyGain;
template<class Preset>
void presets(Preset &preset) {
if (preset("ambient")) {
wet = 0.85;
roomMs = 80;
rt20 = 8.5;
early = 0.55;
detune = 8.5;
lowCutHz = 50;
lowDampRate = 1.5;
highCutHz = 7200;
highDampRate = 2;
samples = delay3.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
samples = delay4.write(samples).read(fade);
} else {
samples = delay1.write(samples).read();
Hadamard::unscaledInPlace(samples);
samples = delay2.write(samples).read();
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.readDetuned(lfoArray);
Householder::inPlace(feedback);
for (int c = 0; c < 8; ++c) {
feedback[c] = highDampFilters[c](lowDampFilters[c](feedback[c]));
}
Array feedbackInput;
for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read();
Hadamard::unscaledInPlace(earlyReflections);
for (int c = 0; c < 8; ++c) samples[c] = feedback[c] + earlyReflections[c]*earlyGain;
samples = delay3.write(samples).read();
Hadamard::unscaledInPlace(samples);
samples = delay4.write(samples).read();
}
std::array<Sample, 2> stereoOut;
stereoMixer.multiToStereo(samples, stereoOut);
for (int c = 0; c < 2; ++c) {
stereoOut[c] = highCutFilters[c](lowCutFilters[c](stereoOut[c]));
}
Sample dryGain = smoothedDryGain.at(i);
Sample wetGain = smoothedWetGain.at(i);
outputLeft[i] = stereoIn[0]*dryGain + stereoOut[0]*wetGain;
outputRight[i] = stereoIn[1]*dryGain + stereoOut[1]*wetGain;
}
detuneLfoPhase -= std::floor(detuneLfoPhase);
}
template<class Config>
void configure(Config &config) {
sampleRate = config.sampleRate;
config.outputChannels = config.inputChannels = 2; // stereo effect only
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
private:
int channels = 0;
double sampleRate = 1;
double maxRoomMs, detuneDepthMs;
double detuneLfoPhase = 0;
double detuneDepthSamples = 0;
using Filter = signalsmith::filters::BiquadStatic<Sample>;
std::array<Filter, 2> lowCutFilters, highCutFilters;
std::array<Filter, 8> lowDampFilters, highDampFilters;
void updateFilters(double feedbackGain) {
for (auto &f : lowCutFilters) f.highpassQ(lowCutHz/sampleRate, 0.5);
for (auto &f : highCutFilters) f.lowpassQ(highCutHz/sampleRate, 0.5);
detuneDepthSamples = detuneDepthMs*0.001*config.sampleRate;
double maxRoomSamples = maxRoomMs*0.001*config.sampleRate;
delay1.configure(maxRoomSamples, 0.125);
delay2.configure(maxRoomSamples, 1);
delay3.configure(maxRoomSamples, 0.5);
delay4.configure(maxRoomSamples, 0.25);
delayFeedback.configure(maxRoomSamples*1.6 + detuneDepthSamples, 1);
delayEarly.configure(maxRoomSamples, 0.25);
Sample lowDampHz = lowCutHz + 100;
Sample highDampHz = highCutHz*0.5;
Sample lowDampGain = std::max(std::pow(feedbackGain, lowDampRate), 1e-3);
Sample highDampGain = std::max(std::pow(feedbackGain, highDampRate), 1e-3);
for (auto &f : lowDampFilters) f.lowShelfQ(lowDampHz/sampleRate, lowDampGain, 0.5);
for (auto &f : highDampFilters) f.highShelf(highDampHz/sampleRate, highDampGain);
}
static Sample getDecayDb(Sample rt20, Sample loopMs) {
Sample dbPerSecond = -20/rt20;
Sample secondsPerLoop = loopMs*Sample(0.001);
return dbPerSecond*secondsPerLoop;
}
signalsmith::mix::StereoMultiMixer<Sample, 8> stereoMixer;
struct MultiDelay {
signalsmith::delay::MultiBuffer<Sample> buffer;
double delayScale = 1;
std::array<int, 8> delayOffsets, delayOffsetsPrev;
void configure(double maxRangeSamples, double scale) {
delayScale = scale;
buffer.resize(8, std::ceil(maxRangeSamples*delayScale) + 1);
}
void reset() {
delay1.reset();
delay2.reset();
delay3.reset();
delayFeedback.reset();
delayEarly.reset();
for (auto &f : lowCutFilters) f.reset();
for (auto &f : highCutFilters) f.reset();
for (auto &f : lowDampFilters) f.reset();
for (auto &f : highDampFilters) f.reset();
detuneLfoPhase = 0;
buffer.reset();
}
int latencySamples() const {
return 0;
}
int tailSamples() {
return std::round(sampleRate*rt20*3); // decay to -60dB
}
template<class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
using Hadamard = signalsmith::mix::Hadamard<Sample, 8>;
using Householder = signalsmith::mix::Householder<Sample, 8>;
auto &&inputLeft = io.input[0];
auto &&inputRight = io.input[1];
auto &&outputLeft = io.output[0];
auto &&outputRight = io.output[1];
block.setupFade([&](){
updateDelays(roomMs.to()*0.001*config.sampleRate);
});
bool fading = block.fading();
auto smoothedDryGain = block.smooth(dry);
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
auto smoothedWetGain = block.smooth(wet.from(), wet.to());
using signalsmith::units::dbToGain;
double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from()));
double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to()));
auto smoothedDecayGain = block.smooth(decayGainFrom, decayGainTo);
auto smoothedInputGain = block.smooth( // scale according to the number of expected echoes in the first 100ms
2*scalingFactor*std::sqrt((1 - decayGainFrom)/(1 - std::pow(decayGainFrom, 100/roomMs.from()))),
2*scalingFactor*std::sqrt((1 - decayGainTo)/(1 - std::pow(decayGainTo, 100/roomMs.to())))
);
auto smoothedEarlyGain = block.smooth(early, [&](double g) {
return g*0.35; // tuned by ear
});
updateFilters(decayGainTo);
// Detuning LFO rate
double detuneCentsPerLoop = detune*std::sqrt(roomMs*0.001);
double detuneLfoRate = (detuneCentsPerLoop*0.0004)/detuneDepthSamples; // tuned by ear, assuming 3/8 channels are detuned
for (int i = 0; i < block.length; ++i) {
Sample inputGain = smoothedInputGain.at(i);
Sample decayGain = smoothedDecayGain.at(i);
Sample earlyGain = smoothedEarlyGain.at(i);
std::array<Sample, 2> stereoIn = {Sample(inputLeft[i]), Sample(inputRight[i])};
Array samples;
std::array<Sample, 2> stereoInScaled = {stereoIn[0]*inputGain, stereoIn[1]*inputGain};
stereoMixer.stereoToMulti(stereoInScaled, samples);
double lfoCos = std::cos(detuneLfoPhase*2*M_PI), lfoSin = std::sin(detuneLfoPhase*2*M_PI);
Array3 lfoArray = {
Sample((0.5 + lfoCos*0.5)*detuneDepthSamples),
Sample((0.5 + lfoCos*-0.25 + lfoSin*0.43301270189)*detuneDepthSamples),
Sample((0.5 + lfoCos*-0.25 + lfoSin*-0.43301270189)*detuneDepthSamples)
};
detuneLfoPhase += detuneLfoRate;
if (fading) {
Sample fade = block.fade(i);
samples = delay1.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
samples = delay2.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.readDetuned(lfoArray, fade);
Householder::inPlace(feedback);
for (int c = 0; c < 8; ++c) {
feedback[c] = highDampFilters[c](lowDampFilters[c](feedback[c]));
}
Array feedbackInput;
for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read(fade);
Hadamard::unscaledInPlace(earlyReflections);
for (int c = 0; c < 8; ++c) samples[c] = feedback[c] + earlyReflections[c]*earlyGain;
samples = delay3.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
samples = delay4.write(samples).read(fade);
} else {
samples = delay1.write(samples).read();
Hadamard::unscaledInPlace(samples);
samples = delay2.write(samples).read();
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.readDetuned(lfoArray);
Householder::inPlace(feedback);
for (int c = 0; c < 8; ++c) {
feedback[c] = highDampFilters[c](lowDampFilters[c](feedback[c]));
}
Array feedbackInput;
for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read();
Hadamard::unscaledInPlace(earlyReflections);
for (int c = 0; c < 8; ++c) samples[c] = feedback[c] + earlyReflections[c]*earlyGain;
samples = delay3.write(samples).read();
Hadamard::unscaledInPlace(samples);
samples = delay4.write(samples).read();
}
std::array<Sample, 2> stereoOut;
stereoMixer.multiToStereo(samples, stereoOut);
for (int c = 0; c < 2; ++c) {
stereoOut[c] = highCutFilters[c](lowCutFilters[c](stereoOut[c]));
}
Sample dryGain = smoothedDryGain.at(i);
Sample wetGain = smoothedWetGain.at(i);
outputLeft[i] = stereoIn[0]*dryGain + stereoOut[0]*wetGain;
outputRight[i] = stereoIn[1]*dryGain + stereoOut[1]*wetGain;
void updateLengths(int seed, double rangeSamples, bool minimise=true) {
rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets;
std::mt19937 engine(seed);
std::uniform_real_distribution<float> unitDist(0, 1);
for (int i = 0; i < 8; ++i) {
float unit = unitDist(engine);
delayOffsets[i] = int(-std::floor(rangeSamples*(unit + i)/8));
std::uniform_int_distribution<int> indexDist(0, i);
int swapIndex = indexDist(engine);
std::swap(delayOffsets[i], delayOffsets[swapIndex]);
}
if (minimise) { // Moves things along so the shortest delay is always 0
int maximumDelay = delayOffsets[0];
for (auto &d : delayOffsets) maximumDelay = std::max(d, maximumDelay);
for (auto &d : delayOffsets) d -= maximumDelay;
}
detuneLfoPhase -= std::floor(detuneLfoPhase);
}
private:
int channels = 0;
double sampleRate = 1;
double maxRoomMs, detuneDepthMs;
double detuneLfoPhase = 0;
double detuneDepthSamples = 0;
using Filter = signalsmith::filters::BiquadStatic<Sample>;
std::array<Filter, 2> lowCutFilters, highCutFilters;
std::array<Filter, 8> lowDampFilters, highDampFilters;
void updateFilters(double feedbackGain) {
for (auto &f : lowCutFilters) f.highpassQ(lowCutHz/sampleRate, 0.5);
for (auto &f : highCutFilters) f.lowpassQ(highCutHz/sampleRate, 0.5);
Sample lowDampHz = lowCutHz + 100;
Sample highDampHz = highCutHz*0.5;
Sample lowDampGain = std::max(std::pow(feedbackGain, lowDampRate), 1e-3);
Sample highDampGain = std::max(std::pow(feedbackGain, highDampRate), 1e-3);
for (auto &f : lowDampFilters) f.lowShelfQ(lowDampHz/sampleRate, lowDampGain, 0.5);
for (auto &f : highDampFilters) f.highShelf(highDampHz/sampleRate, highDampGain);
}
static Sample getDecayDb(Sample rt20, Sample loopMs) {
Sample dbPerSecond = -20/rt20;
Sample secondsPerLoop = loopMs*Sample(0.001);
return dbPerSecond*secondsPerLoop;
void updateLengthsExponential(double rangeSamples) {
rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets;
constexpr double ratios[8] = {0.0625, -0.0625, 0.1875, -0.1875, 0.3125, -0.3125, 0.4375, -0.4375};
for (int i = 0; i < 8; ++i) {
delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i])));
}
}
signalsmith::mix::StereoMultiMixer<Sample, 8> stereoMixer;
struct MultiDelay {
signalsmith::delay::MultiBuffer<Sample> buffer;
double delayScale = 1;
std::array<int, 8> delayOffsets, delayOffsetsPrev;
void configure(double maxRangeSamples, double scale) {
delayScale = scale;
buffer.resize(8, std::ceil(maxRangeSamples*delayScale) + 1);
}
void reset() {
buffer.reset();
}
void updateLengths(int seed, double rangeSamples, bool minimise=true) {
rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets;
std::mt19937 engine(seed);
std::uniform_real_distribution<float> unitDist(0, 1);
for (int i = 0; i < 8; ++i) {
float unit = unitDist(engine);
delayOffsets[i] = int(-std::floor(rangeSamples*(unit + i)/8));
std::uniform_int_distribution<int> indexDist(0, i);
int swapIndex = indexDist(engine);
std::swap(delayOffsets[i], delayOffsets[swapIndex]);
}
if (minimise) { // Moves things along so the shortest delay is always 0
int maximumDelay = delayOffsets[0];
for (auto &d : delayOffsets) maximumDelay = std::max(d, maximumDelay);
for (auto &d : delayOffsets) d -= maximumDelay;
}
}
void updateLengthsExponential(double rangeSamples) {
rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets;
constexpr double ratios[8] = {0.0625, -0.0625, 0.1875, -0.1875, 0.3125, -0.3125, 0.4375, -0.4375};
for (int i = 0; i < 8; ++i) {
delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i])));
}
}
MultiDelay & write(const Array &arr) {
++buffer;
for (int i = 0; i < 8; ++i) {
buffer[i][0] = arr[i];
}
return *this;
MultiDelay & write(const Array &arr) {
++buffer;
for (int i = 0; i < 8; ++i) {
buffer[i][0] = arr[i];
}
return *this;
}
Array read() {
Array result;
for (int i = 0; i < 8; ++i) {
result[i] = buffer[i][delayOffsets[i]];
}
return result;
Array read() {
Array result;
for (int i = 0; i < 8; ++i) {
result[i] = buffer[i][delayOffsets[i]];
}
Array read(Sample fade) {
Array result;
for (int i = 0; i < 8; ++i) {
Sample to = buffer[i][delayOffsets[i]];
Sample from = buffer[i][delayOffsetsPrev[i]];
result[i] = from + (to - from)*fade;
}
return result;
return result;
}
Array read(Sample fade) {
Array result;
for (int i = 0; i < 8; ++i) {
Sample to = buffer[i][delayOffsets[i]];
Sample from = buffer[i][delayOffsetsPrev[i]];
result[i] = from + (to - from)*fade;
}
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorLagrange7> fractionalReader;
Array readDetuned(Array3 lfoDepths) {
Array result;
for (int i = 0; i < 3; ++i) {
result[i] = fractionalReader.read(buffer[i], lfoDepths[i] - delayOffsets[i]);
}
for (int i = 3; i < 8; ++i) {
result[i] = buffer[i][delayOffsets[i]];
}
return result;
}
Array readDetuned(Array3 lfoDepths, Sample fade) {
Array result;
for (int i = 0; i < 3; ++i) {
Sample to = fractionalReader.read(buffer[i], lfoDepths[i] - delayOffsets[i]);
Sample from = fractionalReader.read(buffer[i], lfoDepths[i] - delayOffsetsPrev[i]);
result[i] = from + (to - from)*fade;
}
for (int i = 3; i < 8; ++i) {
Sample to = buffer[i][delayOffsets[i]];
Sample from = buffer[i][delayOffsetsPrev[i]];
result[i] = from + (to - from)*fade;
}
return result;
}
};
return result;
}
MultiDelay delay1, delay2, delay3, delay4, delayFeedback, delayEarly;
void updateDelays(double roomSamples) {
delay1.updateLengths(0x6DD09EE5, roomSamples, false);
delay2.updateLengths(0x876753A5, roomSamples);
delay3.updateLengths(0x5974DF44, roomSamples);
delay4.updateLengths(0x8CDBF7E6, roomSamples);
delayFeedback.updateLengthsExponential(roomSamples);
delayEarly.updateLengths(0x0BDDE171, roomSamples);
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorLagrange7> fractionalReader;
Array readDetuned(Array3 lfoDepths) {
Array result;
for (int i = 0; i < 3; ++i) {
result[i] = fractionalReader.read(buffer[i], lfoDepths[i] - delayOffsets[i]);
}
for (int i = 3; i < 8; ++i) {
result[i] = buffer[i][delayOffsets[i]];
}
return result;
}
Array readDetuned(Array3 lfoDepths, Sample fade) {
Array result;
for (int i = 0; i < 3; ++i) {
Sample to = fractionalReader.read(buffer[i], lfoDepths[i] - delayOffsets[i]);
Sample from = fractionalReader.read(buffer[i], lfoDepths[i] - delayOffsetsPrev[i]);
result[i] = from + (to - from)*fade;
}
for (int i = 3; i < 8; ++i) {
Sample to = buffer[i][delayOffsets[i]];
Sample from = buffer[i][delayOffsetsPrev[i]];
result[i] = from + (to - from)*fade;
}
return result;
}
};
using Reverb = stfx::LibraryEffect<float, ReverbSTFX>;
MultiDelay delay1, delay2, delay3, delay4, delayFeedback, delayEarly;
void updateDelays(double roomSamples) {
delay1.updateLengths(0x6DD09EE5, roomSamples, false);
delay2.updateLengths(0x876753A5, roomSamples);
delay3.updateLengths(0x5974DF44, roomSamples);
delay4.updateLengths(0x8CDBF7E6, roomSamples);
delayFeedback.updateLengthsExponential(roomSamples);
delayEarly.updateLengths(0x0BDDE171, roomSamples);
}
};
}} // namespace
#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

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

@ -0,0 +1,326 @@
#include "clap/clap.h"
#include <vector>
#include <string>
#include <functional>
#include <initializer_list>
#include <cstdio>
#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;
}
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;
}
// Configuration state, used by multiple extensions
bool isConfigured = false;
// Main 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;
if (!plugin.isConfigured || sampleRate != plugin.effect.config.sampleRate) {
auto prevConfig = plugin.effect.config;
plugin.isConfigured = plugin.effect.configure();
if (!plugin.isConfigured) {
// Can't change config (e.g. sample-rate/ports/etc.) here
plugin.effect.config = prevConfig;
}
}
auto &inputBuffers = plugin.inputBuffers;
inputBuffers.resize(plugin.effect.config.inputChannels);
for (auto &a : plugin.effect.config.auxInputs) {
inputBuffers.resize(inputBuffers.size() + a);
}
auto &outputBuffers = plugin.outputBuffers;
outputBuffers.resize(plugin.effect.config.outputChannels);
for (auto &a : plugin.effect.config.auxOutputs) {
outputBuffers.resize(outputBuffers.size() + a);
}
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;
}
std::vector<const float *> inputBuffers;
std::vector<float *> outputBuffers;
static clap_process_status plugin_process(const clap_plugin *obj, const clap_process *process) {
auto &plugin = *(Plugin *)obj;
auto &inputBuffers = plugin.inputBuffers;
inputBuffers.resize(0);
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;
outputBuffers.resize(0);
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]);
}
}
plugin.effect.process(inputBuffers.data(), outputBuffers.data(), process->frames_count);
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::unique_ptr<RangeParamInfo> rangeInfo;
typename Effect::ParamStepped *steppedParam = nullptr;
std::unique_ptr<SteppedParamInfo> steppedInfo;
};
std::vector<Param> params;
void scanParams() {
params.resize(0);
}
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 = (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 *outBuffer, uint32_t outCapacity) {
return false;
}
static bool params_text_to_value(const clap_plugin *obj, clap_id paramId, const char *text, double *value) {
return false;
}
static void params_flush(const clap_plugin *obj, const clap_input_events *inEvents, const clap_output_events *outEvents) {
// not implemented yet
}
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.
bool configure() {
Config prevConfig = config;
EffectClass::configure(config);
EffectClass::configureSTFX(config);
if (config == prevConfig) {
reset();
return true;