diff --git a/.gitignore b/.gitignore index 79b5594..e559c5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ **/.DS_Store +**/env.sh diff --git a/clap/CMakeLists.txt b/clap/CMakeLists.txt index 33d918e..5d180f5 100644 --- a/clap/CMakeLists.txt +++ b/clap/CMakeLists.txt @@ -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( diff --git a/clap/Makefile b/clap/Makefile index 375301d..2df44f6 100644 --- a/clap/Makefile +++ b/clap/Makefile @@ -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 diff --git a/clap/source/basics.cpp b/clap/source/basics.cpp index cd9ccfe..941740b 100644 --- a/clap/source/basics.cpp +++ b/clap/source/basics.cpp @@ -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); diff --git a/clap/source/clap-stfx.h b/clap/source/clap-stfx.h deleted file mode 100644 index 8ef2da4..0000000 --- a/clap/source/clap-stfx.h +++ /dev/null @@ -1,41 +0,0 @@ -#include "clap/clap.h" - -#include -#include -#include - -namespace stfx { namespace clap { - -template class EffectSTFX> -struct Plugin : public clap_plugin { - template - Plugin(const clap_plugin_descriptor *desc, Args ...args) : effect(args...) { - this->desc = desc; - this->plugin_data = nullptr; - } -private: - stfx::LibraryEffect effect; -}; - -struct Plugins { - - template class EffectSTFX, class ...Args> - void add(clap_plugin_descriptor desc, std::initializer_list 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(&descriptors[index], args...); - }); - } -private: - std::vector> featureLists; - std::vector descriptors; - std::vector> creates; -}; - -}} // namespace diff --git a/crunch.h b/crunch.h index 660767a..72cc3d1 100644 --- a/crunch.h +++ b/crunch.h @@ -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 @@ -19,14 +20,14 @@ using CrunchFloat = stfx::LibraryEffect; using CrunchDouble = stfx::LibraryEffect; template -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 @@ -120,6 +114,7 @@ class CrunchSTFX : public BaseEffect { oversampler.downChannel(c, io.output[c], block.length); } } + private: int channels = 0; signalsmith::rates::Oversampler2xFIR oversampler; diff --git a/include/signalsmith-basics/crunch.h b/include/signalsmith-basics/crunch.h new file mode 100644 index 0000000..89d661b --- /dev/null +++ b/include/signalsmith-basics/crunch.h @@ -0,0 +1 @@ +#include "../../crunch.h" diff --git a/include/signalsmith-basics/limiter.h b/include/signalsmith-basics/limiter.h new file mode 100644 index 0000000..32d4c5f --- /dev/null +++ b/include/signalsmith-basics/limiter.h @@ -0,0 +1 @@ +#include "../../limiter.h" diff --git a/include/signalsmith-basics/reverb.h b/include/signalsmith-basics/reverb.h new file mode 100644 index 0000000..76218a6 --- /dev/null +++ b/include/signalsmith-basics/reverb.h @@ -0,0 +1 @@ +#include "../../reverb.h" diff --git a/limiter.h b/limiter.h index 5a3d00d..0188078 100644 --- a/limiter.h +++ b/limiter.h @@ -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 namespace signalsmith { namespace basics { - template - class LimiterSTFX : public BaseEffect { - using typename BaseEffect::Sample; - using typename BaseEffect::ParamRange; - using typename BaseEffect::ParamStepped; +template +class LimiterSTFX; - int channels = 0; - double maxDelayMs = 0; - signalsmith::delay::MultiBuffer multiBuffer; +using LimiterFloat = stfx::LibraryEffect; +using LimiterDouble = stfx::LibraryEffect; - public: - ParamRange inputGain{1}; - ParamRange outputLimit{signalsmith::units::dbToGain(-3)}; - ParamRange attackMs{20}, holdMs{0}, releaseMs{0}; +template +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 + 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 peakHold{0}; + signalsmith::envelopes::BoxFilter smoother1{0}, smoother2{0}; + Sample released = 1; + Sample releaseSlew = 1; - LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {} - - template - 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 peakHold{0}; - signalsmith::envelopes::BoxFilter 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 channelEnvelopes; - - Sample sampleRate; - std::vector channelGains; - template - 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 - 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 channelEnvelopes; + + Sample sampleRate; + std::vector channelGains; + template + 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 + 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; + 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 multiBuffer; +}; }} // namespace diff --git a/reverb.h b/reverb.h index 482363f..1d2eb54 100644 --- a/reverb.h +++ b/reverb.h @@ -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 #include namespace signalsmith { namespace basics { - template - struct ReverbSTFX : public BaseEffect { - using typename BaseEffect::Sample; - using typename BaseEffect::ParamRange; - using typename BaseEffect::ParamStepped; - using Array = std::array; - using Array3 = std::array; +template +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; +using ReverbDouble = stfx::LibraryEffect; - ParamRange lowCutHz{80}, highCutHz{12000}; - ParamRange lowDampRate{1.5}, highDampRate{2.5}; +template +struct ReverbSTFX : public BaseEffect { + using typename BaseEffect::Sample; + using typename BaseEffect::ParamRange; + using typename BaseEffect::ParamStepped; + using Array = std::array; + using Array3 = std::array; - 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 - 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 + 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 + 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 + 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 + void processSTFX(Io &io, Config &config, Block &block) { + using Hadamard = signalsmith::mix::Hadamard; + using Householder = signalsmith::mix::Householder; + + 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 stereoIn = {Sample(inputLeft[i]), Sample(inputRight[i])}; + + Array samples; + std::array 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 - 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 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 - 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; + std::array lowCutFilters, highCutFilters; + std::array 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 stereoMixer; + + struct MultiDelay { + signalsmith::delay::MultiBuffer buffer; + double delayScale = 1; + std::array 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 - void processSTFX(Io &io, Config &config, Block &block) { - using Hadamard = signalsmith::mix::Hadamard; - using Householder = signalsmith::mix::Householder; - - 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 stereoIn = {Sample(inputLeft[i]), Sample(inputRight[i])}; - - Array samples; - std::array 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 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 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 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; - std::array lowCutFilters, highCutFilters; - std::array 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 stereoMixer; - - struct MultiDelay { - signalsmith::delay::MultiBuffer buffer; - double delayScale = 1; - std::array 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 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 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 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 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; + + 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 diff --git a/stfx/clap/param-info.h b/stfx/clap/param-info.h new file mode 100644 index 0000000..3099d37 --- /dev/null +++ b/stfx/clap/param-info.h @@ -0,0 +1,342 @@ +#ifndef SIGNALSMITH_STFX_STFX2_PARAM_INFO_H +#define SIGNALSMITH_STFX_STFX2_PARAM_INFO_H + +#include +#include +#include +#include +#include +#include + +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 getUnits() const { + std::vector 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::lowest(); + static constexpr double doubleMax = std::numeric_limits::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 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 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 + SteppedParamInfo & label(const char *first, Args... others) { + label(first); + ++nameIndex; + return label(others...); + } + + template + 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(std::min(high, result), low); + } else { + return std::max(std::min(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 nameMap; + std::map valueMap; + }; + +} // namespace +#endif // include guard diff --git a/stfx/clap/stfx-clap.h b/stfx/clap/stfx-clap.h new file mode 100644 index 0000000..a1f1e28 --- /dev/null +++ b/stfx/clap/stfx-clap.h @@ -0,0 +1,326 @@ +#include "clap/clap.h" + +#include +#include +#include +#include +#include + +#include "./param-info.h" + +namespace stfx { namespace clap { + +// A CLAP plugin made from an STFX template +template class EffectSTFX> +struct Plugin; + +// A helper to make a CLAP plugin factory from STFX templates +struct Plugins { + template class EffectSTFX, class ...Args> + void add(clap_plugin_descriptor desc, std::initializer_list 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(*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> featureLists; + std::vector descriptors; + std::vector> 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 class EffectSTFX> +struct Plugin : public clap_plugin { + const Plugins &plugins; + const clap_host *host; + using Effect = stfx::LibraryEffect; + Effect effect; + + template + 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 inputBuffers; + std::vector 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 rangeInfo; + typename Effect::ParamStepped *steppedParam = nullptr; + std::unique_ptr steppedInfo; + }; + std::vector 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 ¶m : 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 diff --git a/stfx/stfx-library.h b/stfx/stfx-library.h index cd1adca..9caa938 100644 --- a/stfx/stfx-library.h +++ b/stfx/stfx-library.h @@ -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;