CLAP front-end processes audio (no parameters though)
This commit is contained in:
parent
417dbc1944
commit
89aebf5d9e
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
**/.DS_Store
|
||||
**/env.sh
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
37
crunch.h
37
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 <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;
|
||||
|
||||
1
include/signalsmith-basics/crunch.h
Normal file
1
include/signalsmith-basics/crunch.h
Normal file
@ -0,0 +1 @@
|
||||
#include "../../crunch.h"
|
||||
1
include/signalsmith-basics/limiter.h
Normal file
1
include/signalsmith-basics/limiter.h
Normal file
@ -0,0 +1 @@
|
||||
#include "../../limiter.h"
|
||||
1
include/signalsmith-basics/reverb.h
Normal file
1
include/signalsmith-basics/reverb.h
Normal file
@ -0,0 +1 @@
|
||||
#include "../../reverb.h"
|
||||
335
limiter.h
335
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 <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
727
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 <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
342
stfx/clap/param-info.h
Normal 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
326
stfx/clap/stfx-clap.h
Normal 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 ¶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
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user