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"]
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
#define SIGNALSMITH_BASICS_LIMITER_H
#include "./dsp/delay.h"
#include "./dsp/envelopes.h"
#include "dsp/delay.h"
#include "dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0)
#include "./units.h"
#include "./stfx-library.h"
#include <cmath>
namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect>
template<class BaseEffect>
class LimiterSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamSteps;
// 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;
}
using typename BaseEffect::ParamStepped;
int channels = 0;
double maxDelayMs = 0;
@ -38,52 +26,53 @@ namespace signalsmith { namespace basics {
public:
ParamRange inputGain{1};
ParamRange outputLimit{db_gain(-3)};
ParamRange outputLimit{signalsmith::units::dbToGain(-3)};
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
ParamSteps smoothingStages{1};
ParamStepped smoothingStages{1};
ParamRange linkChannels{0.5};
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.param("inputGain", inputGain)
storage.range("inputGain", inputGain)
.info("pre-gain", "amplifies the input before limiting")
.range(db_gain(-12), 1, db_gain(24))
.unit("dB", 1, db_gain, gain_db)
.range(dbToGain(-12), 1, dbToGain(24))
.unit("dB", 1, dbToGain, gainToDb)
.unit("");
storage.param("outputLimit", outputLimit)
storage.range("outputLimit", outputLimit)
.info("limit", "maximum output amplitude")
.range(db_gain(-24), db_gain(-12), 1)
.unit("dB", 1, db_gain, gain_db)
.range(dbToGain(-24), dbToGain(-12), 1)
.unit("dB", 1, dbToGain, gainToDb)
// 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("");
storage.param("attackMs", attackMs)
storage.range("attackMs", attackMs)
.info("attack", "envelope smoothing time")
.range(1, 10, maxDelayMs/2)
.unit("ms", 0);
storage.param("holdMs", holdMs)
storage.range("holdMs", holdMs)
.info("hold", "hold constant after peaks")
.range(0, 10, maxDelayMs/2)
.unit("ms", 0);
storage.param("releaseMs", releaseMs)
storage.range("releaseMs", releaseMs)
.info("release", "extra release time (in addition to attack + hold)")
.range(0, 10, 250)
.unit("ms", 0);
storage.param("smoothingStages", smoothingStages)
storage.stepped("smoothingStages", smoothingStages)
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
.names(1, "rect", "double");
storage.param("linkChannels", linkChannels)
.label(1, "rect", "double");
storage.range("linkChannels", linkChannels)
.info("link", "link channel gains together")
.range(0, 0.5, 1)
.unit("%", 0, pc_linear, linear_pc);
.unit("%", 0, pcToRatio, ratioToPc);
}
// Gain envelopes are calculated per-channel
@ -156,7 +145,7 @@ namespace signalsmith { namespace basics {
int attackSamples = delaySamplesTo;
int holdSamples = std::ceil(holdMs*0.001*sampleRate);
Sample releaseSamples = releaseMs*0.001*sampleRate;
int stages = smoothingStages.to();
int stages = smoothingStages;
for (auto &envelope : channelEnvelopes) {
envelope.peakHold.set(attackSamples + holdSamples);

243
reverb.h
View File

@ -5,6 +5,7 @@ Released under the Boost Software License (see LICENSE.txt) */
#include "dsp/delay.h"
#include "dsp/mix.h"
#include "dsp/filters.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
#include "./stfx-library.h"
@ -15,28 +16,34 @@ SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect>
template<class BaseEffect>
struct ReverbSTFX : public BaseEffect {
using typename BaseEffect::Sample;
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamSteps;
using typename BaseEffect::ParamStepped;
using Array = std::array<Sample, 8>;
using Array3 = std::array<Sample, 3>;
ParamRange dry{1}, wet{0.25};
ParamRange roomMs{100};
ParamRange rt20{3};
ParamRange early{1};
ReverbSTFX(double maxRoomMs=200) : maxRoomMs(maxRoomMs) {}
ParamRange dry{1}, wet{0.5};
ParamRange roomMs{80};
ParamRange rt20{1};
ParamRange early{1.5};
ParamRange detune{2};
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>
void state(Storage &storage) {
using namespace signalsmith::units;
storage.info("[Basics] Reverb", "An FDN reverb");
int version = storage.version(3);
if (version != 3) return;
int version = storage.version(5);
if (version != 5) return;
storage.param("dry", dry)
storage.range("dry", dry)
.info("dry", "dry signal gain")
.range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb)
@ -44,7 +51,7 @@ namespace signalsmith { namespace basics {
.exact(0, "off")
.unit("");
storage.param("wet", wet)
storage.range("wet", wet)
.info("wet", "reverb tail gain")
.range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb)
@ -52,35 +59,77 @@ namespace signalsmith { namespace basics {
.exact(0, "off")
.unit("");
storage.param("roomMs", roomMs)
storage.range("roomMs", roomMs)
.info("room", "room size (1ms ~ 1 foot)")
.range(10, 100, maxRoomMs)
.unit("ms", 0);
storage.param("rt20", rt20)
storage.range("rt20", rt20)
.info("decay", "RT20: decay time to -20dB")
.range(0.1, 3, 30)
.range(0.01, 2, 30)
.unit("seconds", 2, 0, 1)
.unit("seconds", 1, 1, 1e100);
storage.param("early", early)
storage.range("early", early)
.info("early", "Early reflections")
.range(0, 1, 2)
.range(0, 1, 2.5)
.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>
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.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*2, 1);
delayFeedback.configure(maxRoomSamples*1.6 + detuneDepthSamples, 1);
delayEarly.configure(maxRoomSamples, 0.25);
}
@ -90,12 +139,23 @@ namespace signalsmith { namespace basics {
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>;
@ -105,36 +165,52 @@ namespace signalsmith { namespace basics {
auto &&inputRight = io.input[1];
auto &&outputLeft = io.output[0];
auto &&outputRight = io.output[1];
auto smoothedDryGain = block.smooth(dry);
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
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;
auto smoothedDecayGain = block.smooth(
dbToGain(getDecayDb(rt20.from(), roomMs.from())),
dbToGain(getDecayDb(rt20.to(), roomMs.to()))
);
auto smoothedEarlyGain = block.smooth(early, [&](double g) {
return g*0.35; // tuned by ear
});
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> stereoIn = {inputLeft[i]*inputGain, inputRight[i]*inputGain};
stereoMixer.stereoToMulti(stereoIn, 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);
@ -143,10 +219,16 @@ namespace signalsmith { namespace basics {
samples = delay2.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.read(fade);
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) 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);
Array earlyReflections = delayEarly.write(samples).read(fade);
@ -162,10 +244,16 @@ namespace signalsmith { namespace basics {
samples = delay2.write(samples).read();
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.read();
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) 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);
Array earlyReflections = delayEarly.write(samples).read();
@ -179,17 +267,42 @@ namespace signalsmith { namespace basics {
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);
}
private:
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) {
Sample dbPerSecond = -20/rt20;
@ -232,16 +345,12 @@ namespace signalsmith { namespace basics {
}
}
void updateLengthsExponential(int seed, double rangeSamples) {
void updateLengthsExponential(double rangeSamples) {
rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets;
std::mt19937 engine(seed);
constexpr double ratios[8] = {0.125, -0.125, 0.375, -0.375, 0.625, -0.625, 0.875, -0.875};
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]/2)));
std::uniform_int_distribution<int> indexDist(0, i);
int swapIndex = indexDist(engine);
std::swap(delayOffsets[i], delayOffsets[swapIndex]);
delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i])));
}
}
@ -269,15 +378,41 @@ namespace signalsmith { namespace basics {
}
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;
void updateDelays(double roomSamples) {
delay1.updateLengths(0x876753A5, roomSamples, false);
delay1.updateLengths(0x6DD09EE5, roomSamples, false);
delay2.updateLengths(0x876753A5, roomSamples);
delay3.updateLengths(0x5974DF44, roomSamples);
delay4.updateLengths(0x8CDBF7E6, roomSamples);
delayFeedback.updateLengthsExponential(0xC6BF7158, roomSamples);
delayFeedback.updateLengthsExponential(roomSamples);
delayEarly.updateLengths(0x0BDDE171, roomSamples);
}
};

View File

@ -10,8 +10,86 @@
#include <vector>
#include <string>
#include <cmath>
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 {
// A value which changes linearly within a given index range (e.g. a block)
@ -20,7 +98,7 @@ namespace stfx {
public:
LinearSegment(double offset, double gradient) : offset(offset), gradient(gradient) {}
double at(int i) {
double at(double i) {
return offset + i*gradient;
}
bool changing() {
@ -119,7 +197,7 @@ namespace stfx {
}
struct DoNothingEvent {
int offset = 0;
void operator ()() {}
void operator()() {}
};
DoNothingEvent operator [](int) {
return DoNothingEvent();
@ -142,7 +220,7 @@ namespace stfx {
}
template<class EventList, class ...Others>
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
const Block & split() const {
@ -170,7 +248,7 @@ namespace stfx {
current = v;
return *this;
}
// Return the
// Return the current fade
Value from() const {
return _from;
}
@ -178,6 +256,11 @@ namespace stfx {
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
bool _libStartFade() {
_from = _to;
@ -220,7 +303,7 @@ namespace stfx {
return *this;
}
template<typename ...Args>
PInfoPlaceholder & names(Args ...) {
PInfoPlaceholder & label(Args ...) {
return *this;
}
};
@ -228,26 +311,35 @@ namespace stfx {
}
/// Base class for our effect to inherit from. Provides parameter classes and some default config.
template<typename SampleType>
class LibraryEffectBase {
protected:
using ParamRange = LibraryParam<double>;
using ParamSteps = LibraryParam<int>;
using ParamStepped = LibraryParam<int>;
public:
using Sample = SampleType;
ParamRange bpm{120};
double paramFadeMs() {
return 20;
}
int latencySamples() {
return 0;
}
int tailSamples() {
return 0;
}
template<class Presets>
void presets(Presets &) {}
};
/// Creates an effect class from an effect template, with optional extra config.
/// The effect template takes `EffectTemplate<Sample, BaseClass, ...ExtraConfig>`
template<typename Sample, template <class, class, class...> class EffectTemplate, class ...ExtraConfig>
class LibraryEffect : public EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...> {
using EffectClass = EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...>;
/// The effect template takes `EffectTemplate<BaseClass, ...ExtraConfig>`
template<typename Sample, template <class, class...> class EffectTemplate, class ...ExtraConfig>
class LibraryEffect : public EffectTemplate<stfx::LibraryEffectBase<Sample>, 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
class CollectParams {
@ -256,12 +348,12 @@ namespace stfx {
std::vector<LibraryParam<int> *> stepParams;
// 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;
rangeParams.push_back(&param);
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;
stepParams.push_back(&param);
return {};
@ -269,23 +361,28 @@ namespace stfx {
// The effect might ask us to store/fetch the serialisation version, we just echo it back
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
void operator ()(bool) {}
void operator ()(int) {}
void operator ()(long) {}
void operator ()(double) {}
void operator ()(float) {}
void operator()(const char *, bool) {}
void operator()(const char *, int) {}
void operator()(const char *, long) {}
void operator()(const char *, double) {}
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
template<class OtherObject>
void operator ()(OtherObject &obj) {
void operator()(const char *, OtherObject &obj) {
obj.state(*this);
}
// Drop all names/labels we're given
template<class V>
void operator ()(const char *, V &v) {
(*this)(v);
}
template<class Fn>
void group(const char *, Fn fn) {
fn(*this);
@ -311,7 +408,7 @@ namespace stfx {
std::vector<int> auxInputs, auxOutputs;
int maxBlockSize = 256;
bool operator ==(const Config &other) {
bool operator ==(const Config &other) const {
return sampleRate == other.sampleRate
&& inputChannels == other.inputChannels
&& 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)`.
/// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc.
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?
double fadeSamples = EffectClass::paramFadeMs()*0.001*config.sampleRate;
// 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) {
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) {
return percent*0.01;
}
static double ratioToPc(double linear) {
return linear*100;
}
static double kHzToHz(double kHz) {
return kHz*1000;
}
static double hzToKHz(double hz) {
return hz*0.001;
}
}} // namespace
#endif // include guard