Compare commits
No commits in common. "b79063c97bf5fa5c179a58118e6e3474c147f0ca" and "6e41b252a7fa3b1ba2121b432882d2386d3b8640" have entirely different histories.
b79063c97b
...
6e41b252a7
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -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/
|
||||||
|
|||||||
23
LICENSE.txt
Normal file
23
LICENSE.txt
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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
32
README.md
@ -1,32 +0,0 @@
|
|||||||
# 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
171
crunch.h
@ -1,171 +0,0 @@
|
|||||||
/* 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
2
dsp
@ -1 +1 @@
|
|||||||
Subproject commit 618097bed9e7fb1b87a99592f78c9a8a964eda08
|
Subproject commit 763fd4751da69ce412878a31ffd383811e562f5f
|
||||||
59
limiter.h
59
limiter.h
@ -3,22 +3,34 @@ 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<class BaseEffect>
|
template<typename Sample, 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::ParamStepped;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
int channels = 0;
|
int channels = 0;
|
||||||
double maxDelayMs = 0;
|
double maxDelayMs = 0;
|
||||||
@ -26,53 +38,52 @@ namespace signalsmith { namespace basics {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
ParamRange inputGain{1};
|
ParamRange inputGain{1};
|
||||||
ParamRange outputLimit{signalsmith::units::dbToGain(-3)};
|
ParamRange outputLimit{db_gain(-3)};
|
||||||
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
|
ParamRange attackMs{20}, holdMs{0}, releaseMs{0};
|
||||||
|
|
||||||
ParamStepped smoothingStages{1};
|
ParamSteps 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.range("inputGain", inputGain)
|
storage.param("inputGain", inputGain)
|
||||||
.info("pre-gain", "amplifies the input before limiting")
|
.info("pre-gain", "amplifies the input before limiting")
|
||||||
.range(dbToGain(-12), 1, dbToGain(24))
|
.range(db_gain(-12), 1, db_gain(24))
|
||||||
.unit("dB", 1, dbToGain, gainToDb)
|
.unit("dB", 1, db_gain, gain_db)
|
||||||
.unit("");
|
.unit("");
|
||||||
storage.range("outputLimit", outputLimit)
|
storage.param("outputLimit", outputLimit)
|
||||||
.info("limit", "maximum output amplitude")
|
.info("limit", "maximum output amplitude")
|
||||||
.range(dbToGain(-24), dbToGain(-12), 1)
|
.range(db_gain(-24), db_gain(-12), 1)
|
||||||
.unit("dB", 1, dbToGain, gainToDb)
|
.unit("dB", 1, db_gain, gain_db)
|
||||||
// Extra resolution between -1dB and 0dB
|
// Extra resolution between -1dB and 0dB
|
||||||
.unit("dB", 2, dbToGain, gainToDb, dbToGain(-1), 1)
|
.unit("dB", 2, db_gain, gain_db, db_gain(-1), 1)
|
||||||
.unit("");
|
.unit("");
|
||||||
storage.range("attackMs", attackMs)
|
storage.param("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.range("holdMs", holdMs)
|
storage.param("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.range("releaseMs", releaseMs)
|
storage.param("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.stepped("smoothingStages", smoothingStages)
|
storage.param("smoothingStages", smoothingStages)
|
||||||
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
|
.info("smoothing", "smoothing filter(s) used for attack-smoothing")
|
||||||
.label(1, "rect", "double");
|
.names(1, "rect", "double");
|
||||||
storage.range("linkChannels", linkChannels)
|
storage.param("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, pcToRatio, ratioToPc);
|
.unit("%", 0, pc_linear, linear_pc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gain envelopes are calculated per-channel
|
// Gain envelopes are calculated per-channel
|
||||||
@ -145,7 +156,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;
|
int stages = smoothingStages.to();
|
||||||
|
|
||||||
for (auto &envelope : channelEnvelopes) {
|
for (auto &envelope : channelEnvelopes) {
|
||||||
envelope.peakHold.set(attackSamples + holdSamples);
|
envelope.peakHold.set(attackSamples + holdSamples);
|
||||||
|
|||||||
225
reverb.h
225
reverb.h
@ -5,7 +5,6 @@ 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"
|
||||||
@ -16,34 +15,28 @@ SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
|
|||||||
|
|
||||||
namespace signalsmith { namespace basics {
|
namespace signalsmith { namespace basics {
|
||||||
|
|
||||||
template<class BaseEffect>
|
template<typename Sample, 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::ParamStepped;
|
using typename BaseEffect::ParamSteps;
|
||||||
using Array = std::array<Sample, 8>;
|
using Array = std::array<Sample, 8>;
|
||||||
using Array3 = std::array<Sample, 3>;
|
|
||||||
|
|
||||||
ParamRange dry{1}, wet{0.5};
|
ParamRange dry{1}, wet{0.25};
|
||||||
ParamRange roomMs{80};
|
ParamRange roomMs{100};
|
||||||
ParamRange rt20{1};
|
ParamRange rt20{3};
|
||||||
ParamRange early{1.5};
|
ParamRange early{1};
|
||||||
ParamRange detune{2};
|
|
||||||
|
|
||||||
ParamRange lowCutHz{80}, highCutHz{12000};
|
ReverbSTFX(double maxRoomMs=200) : maxRoomMs(maxRoomMs) {}
|
||||||
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(5);
|
int version = storage.version(3);
|
||||||
if (version != 5) return;
|
if (version != 3) return;
|
||||||
|
|
||||||
storage.range("dry", dry)
|
storage.param("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)
|
||||||
@ -51,7 +44,7 @@ namespace signalsmith { namespace basics {
|
|||||||
.exact(0, "off")
|
.exact(0, "off")
|
||||||
.unit("");
|
.unit("");
|
||||||
|
|
||||||
storage.range("wet", wet)
|
storage.param("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)
|
||||||
@ -59,77 +52,35 @@ namespace signalsmith { namespace basics {
|
|||||||
.exact(0, "off")
|
.exact(0, "off")
|
||||||
.unit("");
|
.unit("");
|
||||||
|
|
||||||
storage.range("roomMs", roomMs)
|
storage.param("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.range("rt20", rt20)
|
storage.param("rt20", rt20)
|
||||||
.info("decay", "RT20: decay time to -20dB")
|
.info("decay", "RT20: decay time to -20dB")
|
||||||
.range(0.01, 2, 30)
|
.range(0.1, 3, 30)
|
||||||
.unit("seconds", 2, 0, 1)
|
.unit("seconds", 2, 0, 1)
|
||||||
.unit("seconds", 1, 1, 1e100);
|
.unit("seconds", 1, 1, 1e100);
|
||||||
|
|
||||||
storage.range("early", early)
|
storage.param("early", early)
|
||||||
.info("early", "Early reflections")
|
.info("early", "Early reflections")
|
||||||
.range(0, 1, 2.5)
|
.range(0, 1, 2)
|
||||||
.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) {
|
||||||
sampleRate = config.sampleRate;
|
config.outputChannels = config.inputChannels = 2;
|
||||||
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*1.6 + detuneDepthSamples, 1);
|
delayFeedback.configure(maxRoomSamples*2, 1);
|
||||||
delayEarly.configure(maxRoomSamples, 0.25);
|
delayEarly.configure(maxRoomSamples, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,23 +90,12 @@ 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>;
|
||||||
@ -166,51 +106,35 @@ 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;
|
||||||
double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from()));
|
auto smoothedDecayGain = block.smooth(
|
||||||
double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to()));
|
dbToGain(getDecayDb(rt20.from(), roomMs.from())),
|
||||||
auto smoothedDecayGain = block.smooth(decayGainFrom, decayGainTo);
|
dbToGain(getDecayDb(rt20.to(), roomMs.to()))
|
||||||
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
|
||||||
});
|
});
|
||||||
|
|
||||||
updateFilters(decayGainTo);
|
block.setupFade([&](){
|
||||||
|
updateDelays(roomMs.to()*0.001*config.sampleRate);
|
||||||
// Detuning LFO rate
|
});
|
||||||
double detuneCentsPerLoop = detune*std::sqrt(roomMs*0.001);
|
bool fading = block.fading();
|
||||||
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> stereoInScaled = {stereoIn[0]*inputGain, stereoIn[1]*inputGain};
|
std::array<Sample, 2> stereoIn = {inputLeft[i]*inputGain, inputRight[i]*inputGain};
|
||||||
stereoMixer.stereoToMulti(stereoInScaled, samples);
|
stereoMixer.stereoToMulti(stereoIn, 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);
|
||||||
@ -219,16 +143,10 @@ 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.readDetuned(lfoArray, fade);
|
Array feedback = delayFeedback.read(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) {
|
for (int c = 0; c < 8; ++c) feedbackInput[c] = samples[c] + feedback[c]*decayGain;
|
||||||
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);
|
||||||
@ -244,16 +162,10 @@ namespace signalsmith { namespace basics {
|
|||||||
samples = delay2.write(samples).read();
|
samples = delay2.write(samples).read();
|
||||||
Hadamard::unscaledInPlace(samples);
|
Hadamard::unscaledInPlace(samples);
|
||||||
|
|
||||||
Array feedback = delayFeedback.readDetuned(lfoArray);
|
Array feedback = delayFeedback.read();
|
||||||
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) {
|
for (int c = 0; c < 8; ++c) feedbackInput[c] = samples[c] + feedback[c]*decayGain;
|
||||||
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();
|
||||||
@ -268,41 +180,16 @@ 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 sampleRate = 1;
|
double maxRoomMs;
|
||||||
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;
|
||||||
@ -345,12 +232,16 @@ namespace signalsmith { namespace basics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateLengthsExponential(double rangeSamples) {
|
void updateLengthsExponential(int seed, double rangeSamples) {
|
||||||
rangeSamples *= delayScale;
|
rangeSamples *= delayScale;
|
||||||
delayOffsetsPrev = delayOffsets;
|
delayOffsetsPrev = delayOffsets;
|
||||||
constexpr double ratios[8] = {0.0625, -0.0625, 0.1875, -0.1875, 0.3125, -0.3125, 0.4375, -0.4375};
|
std::mt19937 engine(seed);
|
||||||
|
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])));
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,41 +269,15 @@ 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(0x6DD09EE5, roomSamples, false);
|
delay1.updateLengths(0x876753A5, 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(roomSamples);
|
delayFeedback.updateLengthsExponential(0xC6BF7158, roomSamples);
|
||||||
delayEarly.updateLengths(0x0BDDE171, roomSamples);
|
delayEarly.updateLengths(0x0BDDE171, roomSamples);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
145
stfx-library.h
145
stfx-library.h
@ -10,86 +10,8 @@
|
|||||||
|
|
||||||
#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 ¶m) {
|
|
||||||
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 ¶m) {
|
|
||||||
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 ¶m) {
|
|
||||||
return param
|
|
||||||
.unit("%", 0, pcToRatio, ratioToPc)
|
|
||||||
.unit("x");
|
|
||||||
}
|
|
||||||
template<class RangeParam>
|
|
||||||
RangeParam & rangeMs(RangeParam ¶m) {
|
|
||||||
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 ¶m) {
|
|
||||||
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)
|
||||||
@ -98,7 +20,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(double i) {
|
double at(int i) {
|
||||||
return offset + i*gradient;
|
return offset + i*gradient;
|
||||||
}
|
}
|
||||||
bool changing() {
|
bool changing() {
|
||||||
@ -220,7 +142,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, list.size()).split(std::forward<Others>(others)...);
|
return split(list).split(std::forward<Others>(others)...);
|
||||||
}
|
}
|
||||||
/// Base-case for templated recursion
|
/// Base-case for templated recursion
|
||||||
const Block & split() const {
|
const Block & split() const {
|
||||||
@ -248,7 +170,7 @@ namespace stfx {
|
|||||||
current = v;
|
current = v;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
// Return the current fade
|
// Return the
|
||||||
Value from() const {
|
Value from() const {
|
||||||
return _from;
|
return _from;
|
||||||
}
|
}
|
||||||
@ -256,11 +178,6 @@ 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;
|
||||||
@ -303,7 +220,7 @@ namespace stfx {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
template<typename ...Args>
|
template<typename ...Args>
|
||||||
PInfoPlaceholder & label(Args ...) {
|
PInfoPlaceholder & names(Args ...) {
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -311,35 +228,26 @@ 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 ParamStepped = LibraryParam<int>;
|
using ParamSteps = 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<BaseClass, ...ExtraConfig>`
|
/// The effect template takes `EffectTemplate<Sample, BaseClass, ...ExtraConfig>`
|
||||||
template<typename Sample, template <class, class...> class EffectTemplate, class ...ExtraConfig>
|
template<typename Sample, template <class, class, class...> class EffectTemplate, class ...ExtraConfig>
|
||||||
class LibraryEffect : public EffectTemplate<stfx::LibraryEffectBase<Sample>, ExtraConfig...> {
|
class LibraryEffect : public EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...> {
|
||||||
using EffectClass = EffectTemplate<stfx::LibraryEffectBase<Sample>, ExtraConfig...>;
|
using EffectClass = EffectTemplate<Sample, stfx::LibraryEffectBase, 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 {
|
||||||
@ -348,12 +256,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 range(const char *, LibraryParam<double> ¶m, const char *codeExpr=nullptr) {
|
PInfoPlaceholder param(const char *, LibraryParam<double> ¶m, const char *codeExpr=nullptr) {
|
||||||
(void)codeExpr;
|
(void)codeExpr;
|
||||||
rangeParams.push_back(¶m);
|
rangeParams.push_back(¶m);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
PInfoPlaceholder stepped(const char *, LibraryParam<int> ¶m, const char *codeExpr=nullptr) {
|
PInfoPlaceholder param(const char *, LibraryParam<int> ¶m, const char *codeExpr=nullptr) {
|
||||||
(void)codeExpr;
|
(void)codeExpr;
|
||||||
stepParams.push_back(¶m);
|
stepParams.push_back(¶m);
|
||||||
return {};
|
return {};
|
||||||
@ -361,28 +269,23 @@ 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()(const char *, bool) {}
|
void operator ()(bool) {}
|
||||||
void operator()(const char *, int) {}
|
void operator ()(int) {}
|
||||||
void operator()(const char *, long) {}
|
void operator ()(long) {}
|
||||||
void operator()(const char *, double) {}
|
void operator ()(double) {}
|
||||||
void operator()(const char *, float) {}
|
void operator ()(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()(const char *, OtherObject &obj) {
|
void operator ()(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);
|
||||||
@ -408,7 +311,7 @@ namespace stfx {
|
|||||||
std::vector<int> auxInputs, auxOutputs;
|
std::vector<int> auxInputs, auxOutputs;
|
||||||
int maxBlockSize = 256;
|
int maxBlockSize = 256;
|
||||||
|
|
||||||
bool operator ==(const Config &other) const {
|
bool operator ==(const Config &other) {
|
||||||
return sampleRate == other.sampleRate
|
return sampleRate == other.sampleRate
|
||||||
&& inputChannels == other.inputChannels
|
&& inputChannels == other.inputChannels
|
||||||
&& outputChannels == other.outputChannels
|
&& outputChannels == other.outputChannels
|
||||||
@ -464,7 +367,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
12
units.h
@ -13,24 +13,12 @@ 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user