1
0

Compare commits

...

10 Commits

Author SHA1 Message Date
Geraint
b79063c97b Update STFX library 2024-02-29 14:08:17 +00:00
Geraint
ba22c38a61 Remove license - it was never publicly released anyway, and I need to think about what license I'd prefer. 2023-12-05 09:58:34 +00:00
Geraint
d22cd189fe Update to new STFX 2023-12-03 20:49:49 +00:00
Geraint
7896c05a5e Add Crunch 2023-01-05 13:24:37 +00:00
Geraint
e3fc8462b5 Scale shelfing frequencies correctly 2022-12-06 20:30:54 +00:00
Geraint
3771df3f1f Spread feedback delays out better 2022-12-06 20:12:16 +00:00
Geraint
9df6e69313 Correct dsp submodule 2022-12-06 18:09:01 +00:00
Geraint
e38dada1d7 Increase float compatibility, update DSP library 2022-12-06 18:02:21 +00:00
Geraint
a5c32ecaf8 Damping and high/low cuts 2022-12-06 00:00:28 +00:00
Geraint
713f3a309a Add detuning to reverb 2022-12-05 22:37:28 +00:00
9 changed files with 552 additions and 139 deletions

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "dsp"] [submodule "dsp"]
path = dsp path = dsp
url = https://signalsmith-audio.co.uk/code/dsp.git/ url = https://signalsmith-audio.co.uk/code/dsp.git

View File

@ -1,23 +0,0 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# Signalsmith Basics
A collection of basic effects, available as plugins and re-usable open-source C++ classes.
## How to use
The [main project page](https://signalsmith-audio.co.uk/code/basics/) has details about the specific effects (and audio examples), but they are all quite similar to use:
```cpp
// Limiter with maximum attack/lookahead of 100ms
signalsmith::basics::Limiter effect(100);
effect.configure(sampleRate, maxBlockSize, channels);
effect.configure(sampleRate, maxBlockSize, inputChannels, outputChannels);
```
Then when processing (all on the audio thread):
```cpp
// clear buffers
effect.reset()
// Change parameters with assignment
effect.attackMs = 20;
// process a block
float **inputBuffers, **outputBuffers;
int blockSize;
effect.process(inputBuffers, outputBuffers, blockSize);
```
You can also inspect latency (`effect.latencySamples()`) and tail length (`effect.tailSamples()`).

171
crunch.h Normal file
View File

@ -0,0 +1,171 @@
/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */
#ifndef SIGNALSMITH_BASICS_CRUNCH_H
#define SIGNALSMITH_BASICS_CRUNCH_H
#include "dsp/rates.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 4, 1)
#include "./stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<class BaseEffect>
class CrunchSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamStepped;
int channels = 0;
signalsmith::rates::Oversampler2xFIR<Sample> oversampler;
struct GainshapeADAA {
Sample prevX = 0, prevIntegral = 0;
Sample fuzzPositive = 1, fuzzNegative = 1;
void setFuzzFactor(Sample k) {
fuzzPositive = 1 + k - k*k;
fuzzNegative = 1 - k - k*k;
prevIntegral = integralGain(prevX);
}
Sample gain(Sample x) const {
Sample fuzzGain = (x >= 0 ? fuzzPositive : fuzzNegative);
return fuzzGain/std::sqrt(1 + x*x);
}
Sample integralGain(Sample x) const {
if (x >= 0) {
return fuzzPositive*std::log(std::sqrt(1 + x*x) + x);
} else { // more accurate if we flip it
return -fuzzNegative*std::log(std::sqrt(1 + x*x) - x);
}
}
Sample averageGain(Sample range) const {
// Average gain from 0-range, ignoring fuzz
return std::log(std::sqrt(1 + range*range) + range)/range;
}
static constexpr Sample minDiffX = 1e-4;
void reset() {
prevX = 0;
prevIntegral = integralGain(prevX);
}
Sample operator()(Sample x) {
Sample diffX = x - prevX;
Sample integral = integralGain(x);
Sample diffIntegral = integral - prevIntegral;
prevX = x;
prevIntegral = integral;
if (std::abs(diffX) < minDiffX) return gain(x);
return diffIntegral/diffX;
}
};
std::vector<GainshapeADAA> gainshapers;
using Filter = signalsmith::filters::BiquadStatic<Sample>;
std::vector<Filter> toneFilters, outputFilters;
static constexpr int oversampleHalfLatency = 16;
static constexpr Sample autoGainLevel = 0.1;
public:
ParamRange drive{4};
ParamRange fuzz{0};
ParamRange toneHz{2000};
ParamRange outGain{1};
bool autoGain;
CrunchSTFX(bool autoGain=true) : autoGain(autoGain) {}
template<class Storage>
void state(Storage &storage) {
storage.info("[Basics] Crunch", "A simple distortion/saturation");
int version = storage.version(0);
if (version != 0) return;
using namespace signalsmith::units;
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)
.info("fuzz", "amplitude-independent distortion")
.range(0, 0.5, 1)
.unit("%", 0, pcToRatio, ratioToPc);
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);
storage.range("outGain", outGain)
.info("out", "output gain")
.range(dbToGain(-12), 1, dbToGain(24))
.unit("dB", 1, dbToGain, gainToDb)
.unit("");
}
template<class Config>
void configure(Config &config) {
channels = config.outputChannels = config.inputChannels;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
oversampler.resize(channels, config.maxBlockSize, oversampleHalfLatency, std::min(0.45, 21000/config.sampleRate));
gainshapers.resize(channels);
toneFilters.resize(channels);
outputFilters.resize(channels);
}
void reset() {
oversampler.reset();
for (auto &g : gainshapers) g.reset();
for (auto &f : toneFilters) f.reset();
for (auto &f : outputFilters) f.reset();
}
int latencySamples() const {
return oversampleHalfLatency*2;
}
template <class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
auto inputGain = block.smooth(drive);
double outputGainFrom = outGain.from();
double outputGainTo = outGain.to();
if (autoGain) {
Sample averageGain = gainshapers[0].averageGain(autoGainLevel*drive.from());
outputGainFrom /= drive.from()*averageGain;
outputGainTo /= drive.to()*gainshapers[0].averageGain(autoGainLevel*drive.to());
}
auto outputGain = block.smooth(outputGainFrom, outputGainTo);
for (int c = 0; c < channels; ++c) {
auto &gainshaper = gainshapers[c];
gainshaper.setFuzzFactor(fuzz);
auto &toneFilter = toneFilters[c];
toneFilter.lowpass(toneHz/(config.sampleRate*2));
auto &outputFilter = outputFilters[c];
outputFilter.highpass((10 + 40*fuzz)/(config.sampleRate*2)); // more aggressive when fuzz is enabled, since it's very asymmetrical
oversampler.upChannel(c, io.input[c], block.length);
Sample *samples = oversampler[c];
for (int i = 0; i < block.length*2; ++i) {
double hi = i*0.5;
Sample x = samples[i]*inputGain.at(hi);
Sample gain = gainshaper(x)*outputGain.at(hi);
Sample y = x*toneFilter(gain);
samples[i] = outputFilter(y);
}
oversampler.downChannel(c, io.output[c], block.length);
}
}
};
using Crunch = stfx::LibraryEffect<float, CrunchSTFX>;
}} // namespace
#endif // include guard

2
dsp

@ -1 +1 @@
Subproject commit 763fd4751da69ce412878a31ffd383811e562f5f Subproject commit 618097bed9e7fb1b87a99592f78c9a8a964eda08

View File

@ -3,34 +3,22 @@ Released under the Boost Software License (see LICENSE.txt) */
#ifndef SIGNALSMITH_BASICS_LIMITER_H #ifndef SIGNALSMITH_BASICS_LIMITER_H
#define SIGNALSMITH_BASICS_LIMITER_H #define SIGNALSMITH_BASICS_LIMITER_H
#include "./dsp/delay.h" #include "dsp/delay.h"
#include "./dsp/envelopes.h" #include "dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0) SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0)
#include "./units.h"
#include "./stfx-library.h" #include "./stfx-library.h"
#include <cmath> #include <cmath>
namespace signalsmith { namespace basics { namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect> template<class BaseEffect>
class LimiterSTFX : public BaseEffect { class LimiterSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange; using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamSteps; using typename BaseEffect::ParamStepped;
// Unit conversions (for display only)
static double db_gain(double db) {
return std::pow(10, db*0.05);
}
static double gain_db(double gain) {
return std::log10(std::max<double>(gain, 1e-10))*20;
}
static double pc_linear(double percent) {
return percent*0.01;
}
static double linear_pc(double linear) {
return linear*100;
}
int channels = 0; int channels = 0;
double maxDelayMs = 0; double maxDelayMs = 0;
@ -38,52 +26,53 @@ namespace signalsmith { namespace basics {
public: public:
ParamRange inputGain{1}; ParamRange inputGain{1};
ParamRange outputLimit{db_gain(-3)}; ParamRange outputLimit{signalsmith::units::dbToGain(-3)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0}; ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
ParamSteps smoothingStages{1}; ParamStepped smoothingStages{1};
ParamRange linkChannels{0.5}; ParamRange linkChannels{0.5};
LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {} LimiterSTFX(double maxDelayMs=100) : maxDelayMs(maxDelayMs) {}
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
using namespace signalsmith::units;
storage.info("[Basics] Limiter", "A simple lookahead limiter"); storage.info("[Basics] Limiter", "A simple lookahead limiter");
int version = storage.version(4); int version = storage.version(4);
if (version != 4) return; if (version != 4) return;
storage.param("inputGain", inputGain) storage.range("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting") .info("pre-gain", "amplifies the input before limiting")
.range(db_gain(-12), 1, db_gain(24)) .range(dbToGain(-12), 1, dbToGain(24))
.unit("dB", 1, db_gain, gain_db) .unit("dB", 1, dbToGain, gainToDb)
.unit(""); .unit("");
storage.param("outputLimit", outputLimit) storage.range("outputLimit", outputLimit)
.info("limit", "maximum output amplitude") .info("limit", "maximum output amplitude")
.range(db_gain(-24), db_gain(-12), 1) .range(dbToGain(-24), dbToGain(-12), 1)
.unit("dB", 1, db_gain, gain_db) .unit("dB", 1, dbToGain, gainToDb)
// Extra resolution between -1dB and 0dB // Extra resolution between -1dB and 0dB
.unit("dB", 2, db_gain, gain_db, db_gain(-1), 1) .unit("dB", 2, dbToGain, gainToDb, dbToGain(-1), 1)
.unit(""); .unit("");
storage.param("attackMs", attackMs) storage.range("attackMs", attackMs)
.info("attack", "envelope smoothing time") .info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2) .range(1, 10, maxDelayMs/2)
.unit("ms", 0); .unit("ms", 0);
storage.param("holdMs", holdMs) storage.range("holdMs", holdMs)
.info("hold", "hold constant after peaks") .info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2) .range(0, 10, maxDelayMs/2)
.unit("ms", 0); .unit("ms", 0);
storage.param("releaseMs", releaseMs) storage.range("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)") .info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250) .range(0, 10, 250)
.unit("ms", 0); .unit("ms", 0);
storage.param("smoothingStages", smoothingStages) storage.stepped("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing") .info("smoothing", "smoothing filter(s) used for attack-smoothing")
.names(1, "rect", "double"); .label(1, "rect", "double");
storage.param("linkChannels", linkChannels) storage.range("linkChannels", linkChannels)
.info("link", "link channel gains together") .info("link", "link channel gains together")
.range(0, 0.5, 1) .range(0, 0.5, 1)
.unit("%", 0, pc_linear, linear_pc); .unit("%", 0, pcToRatio, ratioToPc);
} }
// Gain envelopes are calculated per-channel // Gain envelopes are calculated per-channel
@ -156,7 +145,7 @@ namespace signalsmith { namespace basics {
int attackSamples = delaySamplesTo; int attackSamples = delaySamplesTo;
int holdSamples = std::ceil(holdMs*0.001*sampleRate); int holdSamples = std::ceil(holdMs*0.001*sampleRate);
Sample releaseSamples = releaseMs*0.001*sampleRate; Sample releaseSamples = releaseMs*0.001*sampleRate;
int stages = smoothingStages.to(); int stages = smoothingStages;
for (auto &envelope : channelEnvelopes) { for (auto &envelope : channelEnvelopes) {
envelope.peakHold.set(attackSamples + holdSamples); envelope.peakHold.set(attackSamples + holdSamples);

225
reverb.h
View File

@ -5,6 +5,7 @@ Released under the Boost Software License (see LICENSE.txt) */
#include "dsp/delay.h" #include "dsp/delay.h"
#include "dsp/mix.h" #include "dsp/mix.h"
#include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3) SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
#include "./stfx-library.h" #include "./stfx-library.h"
@ -15,28 +16,34 @@ SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
namespace signalsmith { namespace basics { namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect> template<class BaseEffect>
struct ReverbSTFX : public BaseEffect { struct ReverbSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange; using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamSteps; using typename BaseEffect::ParamStepped;
using Array = std::array<Sample, 8>; using Array = std::array<Sample, 8>;
using Array3 = std::array<Sample, 3>;
ParamRange dry{1}, wet{0.25}; ParamRange dry{1}, wet{0.5};
ParamRange roomMs{100}; ParamRange roomMs{80};
ParamRange rt20{3}; ParamRange rt20{1};
ParamRange early{1}; ParamRange early{1.5};
ParamRange detune{2};
ReverbSTFX(double maxRoomMs=200) : maxRoomMs(maxRoomMs) {} ParamRange lowCutHz{80}, highCutHz{12000};
ParamRange lowDampRate{1.5}, highDampRate{2.5};
ReverbSTFX(double maxRoomMs=200, double detuneDepthMs=2) : maxRoomMs(maxRoomMs), detuneDepthMs(detuneDepthMs) {}
template<class Storage> template<class Storage>
void state(Storage &storage) { void state(Storage &storage) {
using namespace signalsmith::units; using namespace signalsmith::units;
storage.info("[Basics] Reverb", "An FDN reverb"); storage.info("[Basics] Reverb", "An FDN reverb");
int version = storage.version(3); int version = storage.version(5);
if (version != 3) return; if (version != 5) return;
storage.param("dry", dry) storage.range("dry", dry)
.info("dry", "dry signal gain") .info("dry", "dry signal gain")
.range(0, 1, 4) .range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb) .unit("dB", 1, dbToGain, gainToDb)
@ -44,7 +51,7 @@ namespace signalsmith { namespace basics {
.exact(0, "off") .exact(0, "off")
.unit(""); .unit("");
storage.param("wet", wet) storage.range("wet", wet)
.info("wet", "reverb tail gain") .info("wet", "reverb tail gain")
.range(0, 1, 4) .range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb) .unit("dB", 1, dbToGain, gainToDb)
@ -52,35 +59,77 @@ namespace signalsmith { namespace basics {
.exact(0, "off") .exact(0, "off")
.unit(""); .unit("");
storage.param("roomMs", roomMs) storage.range("roomMs", roomMs)
.info("room", "room size (1ms ~ 1 foot)") .info("room", "room size (1ms ~ 1 foot)")
.range(10, 100, maxRoomMs) .range(10, 100, maxRoomMs)
.unit("ms", 0); .unit("ms", 0);
storage.param("rt20", rt20) storage.range("rt20", rt20)
.info("decay", "RT20: decay time to -20dB") .info("decay", "RT20: decay time to -20dB")
.range(0.1, 3, 30) .range(0.01, 2, 30)
.unit("seconds", 2, 0, 1) .unit("seconds", 2, 0, 1)
.unit("seconds", 1, 1, 1e100); .unit("seconds", 1, 1, 1e100);
storage.param("early", early) storage.range("early", early)
.info("early", "Early reflections") .info("early", "Early reflections")
.range(0, 1, 2) .range(0, 1, 2.5)
.unit("%", 0, pcToRatio, ratioToPc); .unit("%", 0, pcToRatio, ratioToPc);
storage.range("detune", detune)
.info("detune", "Detuning rate (inside feedback loop)")
.range(0, 5, 50)
.unit("", 1);
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);
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);
}
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> template<class Config>
void configure(Config &config) { void configure(Config &config) {
config.outputChannels = config.inputChannels = 2; sampleRate = config.sampleRate;
config.outputChannels = config.inputChannels = 2; // stereo effect only
config.auxInputs.resize(0); config.auxInputs.resize(0);
config.auxOutputs.resize(0); config.auxOutputs.resize(0);
detuneDepthSamples = detuneDepthMs*0.001*config.sampleRate;
double maxRoomSamples = maxRoomMs*0.001*config.sampleRate; double maxRoomSamples = maxRoomMs*0.001*config.sampleRate;
delay1.configure(maxRoomSamples, 0.125); delay1.configure(maxRoomSamples, 0.125);
delay2.configure(maxRoomSamples, 1); delay2.configure(maxRoomSamples, 1);
delay3.configure(maxRoomSamples, 0.5); delay3.configure(maxRoomSamples, 0.5);
delay4.configure(maxRoomSamples, 0.25); delay4.configure(maxRoomSamples, 0.25);
delayFeedback.configure(maxRoomSamples*2, 1); delayFeedback.configure(maxRoomSamples*1.6 + detuneDepthSamples, 1);
delayEarly.configure(maxRoomSamples, 0.25); delayEarly.configure(maxRoomSamples, 0.25);
} }
@ -90,12 +139,23 @@ namespace signalsmith { namespace basics {
delay3.reset(); delay3.reset();
delayFeedback.reset(); delayFeedback.reset();
delayEarly.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 { int latencySamples() const {
return 0; return 0;
} }
int tailSamples() {
return std::round(sampleRate*rt20*3); // decay to -60dB
}
template<class Io, class Config, class Block> template<class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) { void processSTFX(Io &io, Config &config, Block &block) {
using Hadamard = signalsmith::mix::Hadamard<Sample, 8>; using Hadamard = signalsmith::mix::Hadamard<Sample, 8>;
@ -106,35 +166,51 @@ namespace signalsmith { namespace basics {
auto &&outputLeft = io.output[0]; auto &&outputLeft = io.output[0];
auto &&outputRight = io.output[1]; auto &&outputRight = io.output[1];
block.setupFade([&](){
updateDelays(roomMs.to()*0.001*config.sampleRate);
});
bool fading = block.fading();
auto smoothedDryGain = block.smooth(dry); auto smoothedDryGain = block.smooth(dry);
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
auto smoothedWetGain = block.smooth(wet.from(), wet.to()); auto smoothedWetGain = block.smooth(wet.from(), wet.to());
auto smoothedInputGain = block.smooth( // tuned by ear: smaller feedback loops with longer decays sound louder
scalingFactor*std::sqrt(roomMs.from()/(rt20.from()*50 + roomMs.from())),
scalingFactor*std::sqrt(roomMs.to()/(rt20.to()*50 + roomMs.to()))
);
using signalsmith::units::dbToGain; using signalsmith::units::dbToGain;
auto smoothedDecayGain = block.smooth( double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from()));
dbToGain(getDecayDb(rt20.from(), roomMs.from())), double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to()));
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) { auto smoothedEarlyGain = block.smooth(early, [&](double g) {
return g*0.35; // tuned by ear return g*0.35; // tuned by ear
}); });
block.setupFade([&](){ updateFilters(decayGainTo);
updateDelays(roomMs.to()*0.001*config.sampleRate);
}); // Detuning LFO rate
bool fading = block.fading(); 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) { for (int i = 0; i < block.length; ++i) {
Sample inputGain = smoothedInputGain.at(i); Sample inputGain = smoothedInputGain.at(i);
Sample decayGain = smoothedDecayGain.at(i); Sample decayGain = smoothedDecayGain.at(i);
Sample earlyGain = smoothedEarlyGain.at(i); Sample earlyGain = smoothedEarlyGain.at(i);
std::array<Sample, 2> stereoIn = {Sample(inputLeft[i]), Sample(inputRight[i])};
Array samples; Array samples;
std::array<Sample, 2> stereoIn = {inputLeft[i]*inputGain, inputRight[i]*inputGain}; std::array<Sample, 2> stereoInScaled = {stereoIn[0]*inputGain, stereoIn[1]*inputGain};
stereoMixer.stereoToMulti(stereoIn, samples); 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) { if (fading) {
Sample fade = block.fade(i); Sample fade = block.fade(i);
@ -143,10 +219,16 @@ namespace signalsmith { namespace basics {
samples = delay2.write(samples).read(fade); samples = delay2.write(samples).read(fade);
Hadamard::unscaledInPlace(samples); Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.read(fade); Array feedback = delayFeedback.readDetuned(lfoArray, fade);
Householder::inPlace(feedback); Householder::inPlace(feedback);
for (int c = 0; c < 8; ++c) {
feedback[c] = highDampFilters[c](lowDampFilters[c](feedback[c]));
}
Array feedbackInput; Array feedbackInput;
for (int c = 0; c < 8; ++c) feedbackInput[c] = samples[c] + feedback[c]*decayGain; for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput); delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read(fade); Array earlyReflections = delayEarly.write(samples).read(fade);
@ -162,10 +244,16 @@ namespace signalsmith { namespace basics {
samples = delay2.write(samples).read(); samples = delay2.write(samples).read();
Hadamard::unscaledInPlace(samples); Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.read(); Array feedback = delayFeedback.readDetuned(lfoArray);
Householder::inPlace(feedback); Householder::inPlace(feedback);
for (int c = 0; c < 8; ++c) {
feedback[c] = highDampFilters[c](lowDampFilters[c](feedback[c]));
}
Array feedbackInput; Array feedbackInput;
for (int c = 0; c < 8; ++c) feedbackInput[c] = samples[c] + feedback[c]*decayGain; for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput); delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read(); Array earlyReflections = delayEarly.write(samples).read();
@ -180,16 +268,41 @@ namespace signalsmith { namespace basics {
std::array<Sample, 2> stereoOut; std::array<Sample, 2> stereoOut;
stereoMixer.multiToStereo(samples, 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 dryGain = smoothedDryGain.at(i);
Sample wetGain = smoothedWetGain.at(i); Sample wetGain = smoothedWetGain.at(i);
outputLeft[i] = stereoIn[0]*dryGain + stereoOut[0]*wetGain; outputLeft[i] = stereoIn[0]*dryGain + stereoOut[0]*wetGain;
outputRight[i] = stereoIn[1]*dryGain + stereoOut[1]*wetGain; outputRight[i] = stereoIn[1]*dryGain + stereoOut[1]*wetGain;
} }
detuneLfoPhase -= std::floor(detuneLfoPhase);
} }
private: private:
int channels = 0; int channels = 0;
double maxRoomMs; 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) { static Sample getDecayDb(Sample rt20, Sample loopMs) {
Sample dbPerSecond = -20/rt20; Sample dbPerSecond = -20/rt20;
@ -232,16 +345,12 @@ namespace signalsmith { namespace basics {
} }
} }
void updateLengthsExponential(int seed, double rangeSamples) { void updateLengthsExponential(double rangeSamples) {
rangeSamples *= delayScale; rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets; delayOffsetsPrev = delayOffsets;
std::mt19937 engine(seed); constexpr double ratios[8] = {0.0625, -0.0625, 0.1875, -0.1875, 0.3125, -0.3125, 0.4375, -0.4375};
constexpr double ratios[8] = {0.125, -0.125, 0.375, -0.375, 0.625, -0.625, 0.875, -0.875};
for (int i = 0; i < 8; ++i) { for (int i = 0; i < 8; ++i) {
delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i]/2))); delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i])));
std::uniform_int_distribution<int> indexDist(0, i);
int swapIndex = indexDist(engine);
std::swap(delayOffsets[i], delayOffsets[swapIndex]);
} }
} }
@ -269,15 +378,41 @@ namespace signalsmith { namespace basics {
} }
return result; return result;
} }
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;
}
}; };
MultiDelay delay1, delay2, delay3, delay4, delayFeedback, delayEarly; MultiDelay delay1, delay2, delay3, delay4, delayFeedback, delayEarly;
void updateDelays(double roomSamples) { void updateDelays(double roomSamples) {
delay1.updateLengths(0x876753A5, roomSamples, false); delay1.updateLengths(0x6DD09EE5, roomSamples, false);
delay2.updateLengths(0x876753A5, roomSamples); delay2.updateLengths(0x876753A5, roomSamples);
delay3.updateLengths(0x5974DF44, roomSamples); delay3.updateLengths(0x5974DF44, roomSamples);
delay4.updateLengths(0x8CDBF7E6, roomSamples); delay4.updateLengths(0x8CDBF7E6, roomSamples);
delayFeedback.updateLengthsExponential(0xC6BF7158, roomSamples); delayFeedback.updateLengthsExponential(roomSamples);
delayEarly.updateLengths(0x0BDDE171, roomSamples); delayEarly.updateLengths(0x0BDDE171, roomSamples);
} }
}; };

View File

@ -10,8 +10,86 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <cmath>
namespace stfx { namespace stfx {
// Convenient units for range parameters - not really part of the main STFX API
namespace units {
static inline double dbToGain(double db) {
return std::pow(10, db*0.05);
}
static inline double gainToDb(double gain) {
return std::log10(std::max<double>(gain, 1e-10))*20;
}
static inline double dbToEnergy(double db) {
return std::pow(10, db*0.1);
}
static inline double energyToDb(double gain) {
return std::log10(std::max<double>(gain, 1e-10))*10;
}
static inline double pcToRatio(double percent) {
return percent*0.01;
}
static inline double ratioToPc(double linear) {
return linear*100;
}
static inline double kHzToHz(double kHz) {
return kHz*1000;
}
static inline double hzToKHz(double hz) {
return hz*0.001;
}
static inline double sToMs(double sec) {
return sec*1000;
}
static inline double msToS(double ms) {
return ms*0.001;
}
template<class RangeParam>
RangeParam & rangeGain(RangeParam &param) {
return param
.unit("dB", 1, dbToGain, gainToDb) // default display is dB
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("x"); // Allow things like "2x" (~6dB) for text-input
}
template<class RangeParam>
RangeParam & rangeHz(RangeParam &param) {
return param
.unit("Hz", 0, 10, 1000)
.unit("Hz", 1, 1, 10)
.unit("Hz", 2, 0, 1)
.unit("kHz", 1, kHzToHz, hzToKHz, 10000, 1e100)
.unit("kHz", 2, kHzToHz, hzToKHz, 1000, 10000);
}
template<class RangeParam>
RangeParam & rangePercent(RangeParam &param) {
return param
.unit("%", 0, pcToRatio, ratioToPc)
.unit("x");
}
template<class RangeParam>
RangeParam & rangeMs(RangeParam &param) {
return param
.unit("ms", 2, 0, 1)
.unit("ms", 1, 1, 10)
.unit("ms", 0, 10, 1000)
.unit("seconds", 2, sToMs, msToS, 0, 1)
.unit("seconds", 1, sToMs, msToS, 1, 10)
.unit("seconds", 0, sToMs, msToS, 10, 1e100);
}
template<class RangeParam>
RangeParam & rangeSec(RangeParam &param) {
return param
.unit("seconds", 2, 0, 1)
.unit("seconds", 1, 1, 10)
.unit("seconds", 0, 10, 1e100)
.unit("ms", 2, msToS, sToMs, 0, 0.001)
.unit("ms", 1, msToS, sToMs, 0.001, 0.01)
.unit("ms", 0, msToS, sToMs, 0.01, 1);
}
}
namespace { namespace {
// A value which changes linearly within a given index range (e.g. a block) // A value which changes linearly within a given index range (e.g. a block)
@ -20,7 +98,7 @@ namespace stfx {
public: public:
LinearSegment(double offset, double gradient) : offset(offset), gradient(gradient) {} LinearSegment(double offset, double gradient) : offset(offset), gradient(gradient) {}
double at(int i) { double at(double i) {
return offset + i*gradient; return offset + i*gradient;
} }
bool changing() { bool changing() {
@ -142,7 +220,7 @@ namespace stfx {
} }
template<class EventList, class ...Others> template<class EventList, class ...Others>
const Block & split(EventList &&list, Others &&...others) const { const Block & split(EventList &&list, Others &&...others) const {
return split(list).split(std::forward<Others>(others)...); return split(list, list.size()).split(std::forward<Others>(others)...);
} }
/// Base-case for templated recursion /// Base-case for templated recursion
const Block & split() const { const Block & split() const {
@ -170,7 +248,7 @@ namespace stfx {
current = v; current = v;
return *this; return *this;
} }
// Return the // Return the current fade
Value from() const { Value from() const {
return _from; return _from;
} }
@ -178,6 +256,11 @@ namespace stfx {
return _to; return _to;
} }
template<class Storage>
void state(Storage &storage) {
storage("value", current);
}
// Shuffle the internal values along to start a new fade, return whether it's actually changing // Shuffle the internal values along to start a new fade, return whether it's actually changing
bool _libStartFade() { bool _libStartFade() {
_from = _to; _from = _to;
@ -220,7 +303,7 @@ namespace stfx {
return *this; return *this;
} }
template<typename ...Args> template<typename ...Args>
PInfoPlaceholder & names(Args ...) { PInfoPlaceholder & label(Args ...) {
return *this; return *this;
} }
}; };
@ -228,26 +311,35 @@ namespace stfx {
} }
/// Base class for our effect to inherit from. Provides parameter classes and some default config. /// Base class for our effect to inherit from. Provides parameter classes and some default config.
template<typename SampleType>
class LibraryEffectBase { class LibraryEffectBase {
protected: protected:
using ParamRange = LibraryParam<double>; using ParamRange = LibraryParam<double>;
using ParamSteps = LibraryParam<int>; using ParamStepped = LibraryParam<int>;
public: public:
using Sample = SampleType;
ParamRange bpm{120}; ParamRange bpm{120};
double paramFadeMs() { double paramFadeMs() {
return 20; return 20;
} }
int latencySamples() {
return 0;
}
int tailSamples() { int tailSamples() {
return 0; return 0;
} }
template<class Presets>
void presets(Presets &) {}
}; };
/// Creates an effect class from an effect template, with optional extra config. /// Creates an effect class from an effect template, with optional extra config.
/// The effect template takes `EffectTemplate<Sample, BaseClass, ...ExtraConfig>` /// The effect template takes `EffectTemplate<BaseClass, ...ExtraConfig>`
template<typename Sample, template <class, class, class...> class EffectTemplate, class ...ExtraConfig> template<typename Sample, template <class, class...> class EffectTemplate, class ...ExtraConfig>
class LibraryEffect : public EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...> { class LibraryEffect : public EffectTemplate<stfx::LibraryEffectBase<Sample>, ExtraConfig...> {
using EffectClass = EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...>; using EffectClass = EffectTemplate<stfx::LibraryEffectBase<Sample>, ExtraConfig...>;
// This is passed to the effect's `.state()` method during initialisation, and collects pointers to the effect's parameters // This is passed to the effect's `.state()` method during initialisation, and collects pointers to the effect's parameters
class CollectParams { class CollectParams {
@ -256,12 +348,12 @@ namespace stfx {
std::vector<LibraryParam<int> *> stepParams; std::vector<LibraryParam<int> *> stepParams;
// Add registered parameters to the list // Add registered parameters to the list
PInfoPlaceholder param(const char *, LibraryParam<double> &param, const char *codeExpr=nullptr) { PInfoPlaceholder range(const char *, LibraryParam<double> &param, const char *codeExpr=nullptr) {
(void)codeExpr; (void)codeExpr;
rangeParams.push_back(&param); rangeParams.push_back(&param);
return {}; return {};
} }
PInfoPlaceholder param(const char *, LibraryParam<int> &param, const char *codeExpr=nullptr) { PInfoPlaceholder stepped(const char *, LibraryParam<int> &param, const char *codeExpr=nullptr) {
(void)codeExpr; (void)codeExpr;
stepParams.push_back(&param); stepParams.push_back(&param);
return {}; return {};
@ -269,23 +361,28 @@ namespace stfx {
// The effect might ask us to store/fetch the serialisation version, we just echo it back // The effect might ask us to store/fetch the serialisation version, we just echo it back
static int version(int v) {return v;} static int version(int v) {return v;}
// Ignore the UI/synchronisation stuff
static bool extra() {return false;}
static void invalidate(const char *) {}
// We ignore any basic type // We ignore any basic type
void operator ()(bool) {} void operator()(const char *, bool) {}
void operator ()(int) {} void operator()(const char *, int) {}
void operator ()(long) {} void operator()(const char *, long) {}
void operator ()(double) {} void operator()(const char *, double) {}
void operator ()(float) {} void operator()(const char *, float) {}
// And strings
void operator()(const char *, std::string &) {}
// Iterate through vectors
template<class Item>
void operator()(const char *label, std::vector<Item> &vector) {
for (auto &item : vector) (*this)(label, item);
}
// Assume all other arguments have a `.state()`, and recurse into it // Assume all other arguments have a `.state()`, and recurse into it
template<class OtherObject> template<class OtherObject>
void operator ()(OtherObject &obj) { void operator()(const char *, OtherObject &obj) {
obj.state(*this); obj.state(*this);
} }
// Drop all names/labels we're given
template<class V>
void operator ()(const char *, V &v) {
(*this)(v);
}
template<class Fn> template<class Fn>
void group(const char *, Fn fn) { void group(const char *, Fn fn) {
fn(*this); fn(*this);
@ -311,7 +408,7 @@ namespace stfx {
std::vector<int> auxInputs, auxOutputs; std::vector<int> auxInputs, auxOutputs;
int maxBlockSize = 256; int maxBlockSize = 256;
bool operator ==(const Config &other) { bool operator ==(const Config &other) const {
return sampleRate == other.sampleRate return sampleRate == other.sampleRate
&& inputChannels == other.inputChannels && inputChannels == other.inputChannels
&& outputChannels == other.outputChannels && outputChannels == other.outputChannels
@ -367,7 +464,7 @@ namespace stfx {
/// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(io, config, block)`. /// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(io, config, block)`.
/// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc. /// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc.
template<class Inputs, class Outputs> template<class Inputs, class Outputs>
void process(Inputs inputs, Outputs outputs, int blockLength) { void process(Inputs &&inputs, Outputs &&outputs, int blockLength) {
// How long should the parameter fade take? // How long should the parameter fade take?
double fadeSamples = EffectClass::paramFadeMs()*0.001*config.sampleRate; double fadeSamples = EffectClass::paramFadeMs()*0.001*config.sampleRate;
// Fade position at the end of the block // Fade position at the end of the block

12
units.h
View File

@ -13,12 +13,24 @@ namespace signalsmith { namespace units {
static double gainToDb(double gain) { static double gainToDb(double gain) {
return std::log10(std::max<double>(gain, 1e-10))*20; return std::log10(std::max<double>(gain, 1e-10))*20;
} }
static double dbToEnergy(double db) {
return std::pow(10, db*0.1);
}
static double energyToDb(double gain) {
return std::log10(std::max<double>(gain, 1e-10))*10;
}
static double pcToRatio(double percent) { static double pcToRatio(double percent) {
return percent*0.01; return percent*0.01;
} }
static double ratioToPc(double linear) { static double ratioToPc(double linear) {
return linear*100; return linear*100;
} }
static double kHzToHz(double kHz) {
return kHz*1000;
}
static double hzToKHz(double hz) {
return hz*0.001;
}
}} // namespace }} // namespace
#endif // include guard #endif // include guard