1
0
basics/reverb.h
2022-12-05 22:37:28 +00:00

351 lines
11 KiB
C++

/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff
Released under the Boost Software License (see LICENSE.txt) */
#ifndef SIGNALSMITH_BASICS_REVERB_H
#define SIGNALSMITH_BASICS_REVERB_H
#include "dsp/delay.h"
#include "dsp/mix.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 3, 3)
#include "./stfx-library.h"
#include "./units.h"
#include <cmath>
#include <random>
namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect>
struct ReverbSTFX : public BaseEffect {
using typename BaseEffect::ParamRange;
using typename BaseEffect::ParamSteps;
using Array = std::array<Sample, 8>;
using Array3 = std::array<Sample, 3>;
ParamRange dry{1}, wet{0.5};
ParamRange roomMs{80};
ParamRange rt20{1};
ParamRange early{1};
ParamRange detune{2};
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(4);
if (version != 4) return;
storage.param("dry", dry)
.info("dry", "dry signal gain")
.range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb)
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("");
storage.param("wet", wet)
.info("wet", "reverb tail gain")
.range(0, 1, 4)
.unit("dB", 1, dbToGain, gainToDb)
.unit("%", 0, pcToRatio, ratioToPc)
.exact(0, "off")
.unit("");
storage.param("roomMs", roomMs)
.info("room", "room size (1ms ~ 1 foot)")
.range(10, 100, maxRoomMs)
.unit("ms", 0);
storage.param("rt20", rt20)
.info("decay", "RT20: decay time to -20dB")
.range(0.01, 2, 30)
.unit("seconds", 2, 0, 1)
.unit("seconds", 1, 1, 1e100);
storage.param("early", early)
.info("early", "Early reflections")
.range(0, 1, 2.5)
.unit("%", 0, pcToRatio, ratioToPc);
storage.param("detune", detune)
.info("detune", "Detuning rate (inside feedback loop)")
.range(0, 5, 50)
.unit("", 1);
}
template<class Config>
void configure(Config &config) {
config.outputChannels = config.inputChannels = 2;
config.auxInputs.resize(0);
config.auxOutputs.resize(0);
detuneDepthSamples = detuneDepthMs*0.001*config.sampleRate;
double maxRoomSamples = maxRoomMs*0.001*config.sampleRate;
delay1.configure(maxRoomSamples, 0.125);
delay2.configure(maxRoomSamples, 1);
delay3.configure(maxRoomSamples, 0.5);
delay4.configure(maxRoomSamples, 0.25);
delayFeedback.configure(maxRoomSamples*1.36 + detuneDepthSamples, 1);
delayEarly.configure(maxRoomSamples, 0.25);
}
void reset() {
delay1.reset();
delay2.reset();
delay3.reset();
delayFeedback.reset();
delayEarly.reset();
detuneLfoPhase = 0;
}
int latencySamples() const {
return 0;
}
template<class Io, class Config, class Block>
void processSTFX(Io &io, Config &config, Block &block) {
using Hadamard = signalsmith::mix::Hadamard<Sample, 8>;
using Householder = signalsmith::mix::Householder<Sample, 8>;
auto &&inputLeft = io.input[0];
auto &&inputRight = io.input[1];
auto &&outputLeft = io.output[0];
auto &&outputRight = io.output[1];
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
scalingFactor*std::sqrt((1 - decayGainFrom)/(1 - std::pow(decayGainFrom, 100/roomMs.from()))),
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
});
block.setupFade([&](){
updateDelays(roomMs.to()*0.001*config.sampleRate);
});
bool fading = block.fading();
// 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 = {inputLeft[i], inputRight[i]};
Array samples;
std::array<Sample, 2> stereoInScaled = {stereoIn[0]*inputGain, stereoIn[1]*inputGain};
stereoMixer.stereoToMulti(stereoInScaled, samples);
double lfoCos = std::cos(detuneLfoPhase*2*M_PI), lfoSin = std::sin(detuneLfoPhase*2*M_PI);
Array3 lfoArray = {
(0.5 + lfoCos*0.5)*detuneDepthSamples,
(0.5 + lfoCos*-0.25 + lfoSin*0.43301270189)*detuneDepthSamples,
(0.5 + lfoCos*-0.25 + lfoSin*-0.43301270189)*detuneDepthSamples
};
detuneLfoPhase += detuneLfoRate;
if (fading) {
Sample fade = block.fade(i);
samples = delay1.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
samples = delay2.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.readDetuned(lfoArray, fade);
Householder::inPlace(feedback);
Array feedbackInput;
for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read(fade);
Hadamard::unscaledInPlace(earlyReflections);
for (int c = 0; c < 8; ++c) samples[c] = feedback[c] + earlyReflections[c]*earlyGain;
samples = delay3.write(samples).read(fade);
Hadamard::unscaledInPlace(samples);
samples = delay4.write(samples).read(fade);
} else {
samples = delay1.write(samples).read();
Hadamard::unscaledInPlace(samples);
samples = delay2.write(samples).read();
Hadamard::unscaledInPlace(samples);
Array feedback = delayFeedback.readDetuned(lfoArray, lfoSin);
Householder::inPlace(feedback);
Array feedbackInput;
for (int c = 0; c < 8; ++c) {
int c2 = (c + 3)&7;
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
}
delayFeedback.write(feedbackInput);
Array earlyReflections = delayEarly.write(samples).read();
Hadamard::unscaledInPlace(earlyReflections);
for (int c = 0; c < 8; ++c) samples[c] = feedback[c] + earlyReflections[c]*earlyGain;
samples = delay3.write(samples).read();
Hadamard::unscaledInPlace(samples);
samples = delay4.write(samples).read();
}
std::array<Sample, 2> stereoOut;
stereoMixer.multiToStereo(samples, stereoOut);
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, detuneDepthMs;
double detuneLfoPhase = 0;
double detuneDepthSamples = 0;
static Sample getDecayDb(Sample rt20, Sample loopMs) {
Sample dbPerSecond = -20/rt20;
Sample secondsPerLoop = loopMs*Sample(0.001);
return dbPerSecond*secondsPerLoop;
}
signalsmith::mix::StereoMultiMixer<Sample, 8> stereoMixer;
struct MultiDelay {
signalsmith::delay::MultiBuffer<Sample> buffer;
double delayScale = 1;
std::array<int, 8> delayOffsets, delayOffsetsPrev;
void configure(double maxRangeSamples, double scale) {
delayScale = scale;
buffer.resize(8, std::ceil(maxRangeSamples*delayScale) + 1);
}
void reset() {
buffer.reset();
}
void updateLengths(int seed, double rangeSamples, bool minimise=true) {
rangeSamples *= delayScale;
delayOffsetsPrev = delayOffsets;
std::mt19937 engine(seed);
std::uniform_real_distribution<float> unitDist(0, 1);
for (int i = 0; i < 8; ++i) {
float unit = unitDist(engine);
delayOffsets[i] = int(-std::floor(rangeSamples*(unit + i)/8));
std::uniform_int_distribution<int> indexDist(0, i);
int swapIndex = indexDist(engine);
std::swap(delayOffsets[i], delayOffsets[swapIndex]);
}
if (minimise) { // Moves things along so the shortest delay is always 0
int maximumDelay = delayOffsets[0];
for (auto &d : delayOffsets) maximumDelay = std::max(d, maximumDelay);
for (auto &d : delayOffsets) d -= maximumDelay;
}
}
void updateLengthsExponential(int seed, double rangeSamples) {
rangeSamples *= delayScale;
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};
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]);
}
}
MultiDelay & write(const Array &arr) {
++buffer;
for (int i = 0; i < 8; ++i) {
buffer[i][0] = arr[i];
}
return *this;
}
Array read() {
Array result;
for (int i = 0; i < 8; ++i) {
result[i] = buffer[i][delayOffsets[i]];
}
return result;
}
Array read(Sample fade) {
Array result;
for (int i = 0; i < 8; ++i) {
Sample to = buffer[i][delayOffsets[i]];
Sample from = buffer[i][delayOffsetsPrev[i]];
result[i] = from + (to - from)*fade;
}
return result;
}
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);
delay2.updateLengths(0x876753A5, roomSamples);
delay3.updateLengths(0x5974DF44, roomSamples);
delay4.updateLengths(0x8CDBF7E6, roomSamples);
delayFeedback.updateLengthsExponential(0xC6BF7158, roomSamples);
delayEarly.updateLengths(0x0BDDE171, roomSamples);
}
};
using Reverb = stfx::LibraryEffect<float, ReverbSTFX>;
}} // namespace
#endif // include guard