Add detuning to reverb
This commit is contained in:
parent
6e41b252a7
commit
713f3a309a
111
reverb.h
111
reverb.h
@ -20,21 +20,23 @@ namespace signalsmith { namespace basics {
|
|||||||
using typename BaseEffect::ParamRange;
|
using typename BaseEffect::ParamRange;
|
||||||
using typename BaseEffect::ParamSteps;
|
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.25};
|
ParamRange dry{1}, wet{0.5};
|
||||||
ParamRange roomMs{100};
|
ParamRange roomMs{80};
|
||||||
ParamRange rt20{3};
|
ParamRange rt20{1};
|
||||||
ParamRange early{1};
|
ParamRange early{1};
|
||||||
|
ParamRange detune{2};
|
||||||
|
|
||||||
ReverbSTFX(double maxRoomMs=200) : maxRoomMs(maxRoomMs) {}
|
ReverbSTFX(double maxRoomMs=200, double detuneDepthMs=2) : maxRoomMs(maxRoomMs), detuneDepthMs(detuneDepthMs) {}
|
||||||
|
|
||||||
template<class Storage>
|
template<class Storage>
|
||||||
void state(Storage &storage) {
|
void state(Storage &storage) {
|
||||||
using namespace signalsmith::units;
|
using namespace signalsmith::units;
|
||||||
|
|
||||||
storage.info("[Basics] Reverb", "An FDN reverb");
|
storage.info("[Basics] Reverb", "An FDN reverb");
|
||||||
int version = storage.version(3);
|
int version = storage.version(4);
|
||||||
if (version != 3) return;
|
if (version != 4) return;
|
||||||
|
|
||||||
storage.param("dry", dry)
|
storage.param("dry", dry)
|
||||||
.info("dry", "dry signal gain")
|
.info("dry", "dry signal gain")
|
||||||
@ -59,14 +61,19 @@ namespace signalsmith { namespace basics {
|
|||||||
|
|
||||||
storage.param("rt20", rt20)
|
storage.param("rt20", rt20)
|
||||||
.info("decay", "RT20: decay time to -20dB")
|
.info("decay", "RT20: decay time to -20dB")
|
||||||
.range(0.1, 3, 30)
|
.range(0.01, 2, 30)
|
||||||
.unit("seconds", 2, 0, 1)
|
.unit("seconds", 2, 0, 1)
|
||||||
.unit("seconds", 1, 1, 1e100);
|
.unit("seconds", 1, 1, 1e100);
|
||||||
|
|
||||||
storage.param("early", early)
|
storage.param("early", early)
|
||||||
.info("early", "Early reflections")
|
.info("early", "Early reflections")
|
||||||
.range(0, 1, 2)
|
.range(0, 1, 2.5)
|
||||||
.unit("%", 0, pcToRatio, ratioToPc);
|
.unit("%", 0, pcToRatio, ratioToPc);
|
||||||
|
|
||||||
|
storage.param("detune", detune)
|
||||||
|
.info("detune", "Detuning rate (inside feedback loop)")
|
||||||
|
.range(0, 5, 50)
|
||||||
|
.unit("", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<class Config>
|
template<class Config>
|
||||||
@ -74,13 +81,15 @@ namespace signalsmith { namespace basics {
|
|||||||
config.outputChannels = config.inputChannels = 2;
|
config.outputChannels = config.inputChannels = 2;
|
||||||
config.auxInputs.resize(0);
|
config.auxInputs.resize(0);
|
||||||
config.auxOutputs.resize(0);
|
config.auxOutputs.resize(0);
|
||||||
|
|
||||||
|
detuneDepthSamples = detuneDepthMs*0.001*config.sampleRate;
|
||||||
double maxRoomSamples = maxRoomMs*0.001*config.sampleRate;
|
double maxRoomSamples = maxRoomMs*0.001*config.sampleRate;
|
||||||
|
|
||||||
delay1.configure(maxRoomSamples, 0.125);
|
delay1.configure(maxRoomSamples, 0.125);
|
||||||
delay2.configure(maxRoomSamples, 1);
|
delay2.configure(maxRoomSamples, 1);
|
||||||
delay3.configure(maxRoomSamples, 0.5);
|
delay3.configure(maxRoomSamples, 0.5);
|
||||||
delay4.configure(maxRoomSamples, 0.25);
|
delay4.configure(maxRoomSamples, 0.25);
|
||||||
delayFeedback.configure(maxRoomSamples*2, 1);
|
delayFeedback.configure(maxRoomSamples*1.36 + detuneDepthSamples, 1);
|
||||||
delayEarly.configure(maxRoomSamples, 0.25);
|
delayEarly.configure(maxRoomSamples, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +99,8 @@ namespace signalsmith { namespace basics {
|
|||||||
delay3.reset();
|
delay3.reset();
|
||||||
delayFeedback.reset();
|
delayFeedback.reset();
|
||||||
delayEarly.reset();
|
delayEarly.reset();
|
||||||
|
|
||||||
|
detuneLfoPhase = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int latencySamples() const {
|
int latencySamples() const {
|
||||||
@ -109,14 +120,14 @@ namespace signalsmith { namespace basics {
|
|||||||
auto smoothedDryGain = block.smooth(dry);
|
auto smoothedDryGain = block.smooth(dry);
|
||||||
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
|
Sample scalingFactor = stereoMixer.scalingFactor2()*0.015625; // 4 Hadamard mixes
|
||||||
auto smoothedWetGain = block.smooth(wet.from(), wet.to());
|
auto smoothedWetGain = block.smooth(wet.from(), wet.to());
|
||||||
auto smoothedInputGain = block.smooth( // tuned by ear: smaller feedback loops with longer decays sound louder
|
|
||||||
scalingFactor*std::sqrt(roomMs.from()/(rt20.from()*50 + roomMs.from())),
|
|
||||||
scalingFactor*std::sqrt(roomMs.to()/(rt20.to()*50 + roomMs.to()))
|
|
||||||
);
|
|
||||||
using signalsmith::units::dbToGain;
|
using signalsmith::units::dbToGain;
|
||||||
auto smoothedDecayGain = block.smooth(
|
double decayGainFrom = dbToGain(getDecayDb(rt20.from(), roomMs.from()));
|
||||||
dbToGain(getDecayDb(rt20.from(), roomMs.from())),
|
double decayGainTo = dbToGain(getDecayDb(rt20.to(), roomMs.to()));
|
||||||
dbToGain(getDecayDb(rt20.to(), roomMs.to()))
|
auto smoothedDecayGain = block.smooth(decayGainFrom, decayGainTo);
|
||||||
|
auto smoothedInputGain = block.smooth( // scale according to the number of expected echoes in the first 100ms
|
||||||
|
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) {
|
auto smoothedEarlyGain = block.smooth(early, [&](double g) {
|
||||||
return g*0.35; // tuned by ear
|
return g*0.35; // tuned by ear
|
||||||
@ -126,15 +137,29 @@ namespace signalsmith { namespace basics {
|
|||||||
updateDelays(roomMs.to()*0.001*config.sampleRate);
|
updateDelays(roomMs.to()*0.001*config.sampleRate);
|
||||||
});
|
});
|
||||||
bool fading = block.fading();
|
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) {
|
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 = {inputLeft[i], inputRight[i]};
|
||||||
|
|
||||||
Array samples;
|
Array samples;
|
||||||
std::array<Sample, 2> stereoIn = {inputLeft[i]*inputGain, inputRight[i]*inputGain};
|
std::array<Sample, 2> stereoInScaled = {stereoIn[0]*inputGain, stereoIn[1]*inputGain};
|
||||||
stereoMixer.stereoToMulti(stereoIn, samples);
|
stereoMixer.stereoToMulti(stereoInScaled, samples);
|
||||||
|
|
||||||
|
double lfoCos = std::cos(detuneLfoPhase*2*M_PI), lfoSin = std::sin(detuneLfoPhase*2*M_PI);
|
||||||
|
Array3 lfoArray = {
|
||||||
|
(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) {
|
if (fading) {
|
||||||
Sample fade = block.fade(i);
|
Sample fade = block.fade(i);
|
||||||
@ -143,10 +168,13 @@ namespace signalsmith { namespace basics {
|
|||||||
samples = delay2.write(samples).read(fade);
|
samples = delay2.write(samples).read(fade);
|
||||||
Hadamard::unscaledInPlace(samples);
|
Hadamard::unscaledInPlace(samples);
|
||||||
|
|
||||||
Array feedback = delayFeedback.read(fade);
|
Array feedback = delayFeedback.readDetuned(lfoArray, fade);
|
||||||
Householder::inPlace(feedback);
|
Householder::inPlace(feedback);
|
||||||
Array feedbackInput;
|
Array feedbackInput;
|
||||||
for (int c = 0; c < 8; ++c) feedbackInput[c] = samples[c] + feedback[c]*decayGain;
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
int c2 = (c + 3)&7;
|
||||||
|
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
|
||||||
|
}
|
||||||
delayFeedback.write(feedbackInput);
|
delayFeedback.write(feedbackInput);
|
||||||
|
|
||||||
Array earlyReflections = delayEarly.write(samples).read(fade);
|
Array earlyReflections = delayEarly.write(samples).read(fade);
|
||||||
@ -162,10 +190,13 @@ namespace signalsmith { namespace basics {
|
|||||||
samples = delay2.write(samples).read();
|
samples = delay2.write(samples).read();
|
||||||
Hadamard::unscaledInPlace(samples);
|
Hadamard::unscaledInPlace(samples);
|
||||||
|
|
||||||
Array feedback = delayFeedback.read();
|
Array feedback = delayFeedback.readDetuned(lfoArray, lfoSin);
|
||||||
Householder::inPlace(feedback);
|
Householder::inPlace(feedback);
|
||||||
Array feedbackInput;
|
Array feedbackInput;
|
||||||
for (int c = 0; c < 8; ++c) feedbackInput[c] = samples[c] + feedback[c]*decayGain;
|
for (int c = 0; c < 8; ++c) {
|
||||||
|
int c2 = (c + 3)&7;
|
||||||
|
feedbackInput[c2] = samples[c] + feedback[c]*decayGain;
|
||||||
|
}
|
||||||
delayFeedback.write(feedbackInput);
|
delayFeedback.write(feedbackInput);
|
||||||
|
|
||||||
Array earlyReflections = delayEarly.write(samples).read();
|
Array earlyReflections = delayEarly.write(samples).read();
|
||||||
@ -185,11 +216,15 @@ namespace signalsmith { namespace basics {
|
|||||||
outputLeft[i] = stereoIn[0]*dryGain + stereoOut[0]*wetGain;
|
outputLeft[i] = stereoIn[0]*dryGain + stereoOut[0]*wetGain;
|
||||||
outputRight[i] = stereoIn[1]*dryGain + stereoOut[1]*wetGain;
|
outputRight[i] = stereoIn[1]*dryGain + stereoOut[1]*wetGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detuneLfoPhase -= std::floor(detuneLfoPhase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int channels = 0;
|
int channels = 0;
|
||||||
double maxRoomMs;
|
double maxRoomMs, detuneDepthMs;
|
||||||
|
double detuneLfoPhase = 0;
|
||||||
|
double detuneDepthSamples = 0;
|
||||||
|
|
||||||
static Sample getDecayDb(Sample rt20, Sample loopMs) {
|
static Sample getDecayDb(Sample rt20, Sample loopMs) {
|
||||||
Sample dbPerSecond = -20/rt20;
|
Sample dbPerSecond = -20/rt20;
|
||||||
@ -236,7 +271,7 @@ namespace signalsmith { namespace basics {
|
|||||||
rangeSamples *= delayScale;
|
rangeSamples *= delayScale;
|
||||||
delayOffsetsPrev = delayOffsets;
|
delayOffsetsPrev = delayOffsets;
|
||||||
std::mt19937 engine(seed);
|
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) {
|
for (int i = 0; i < 8; ++i) {
|
||||||
delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i]/2)));
|
delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i]/2)));
|
||||||
std::uniform_int_distribution<int> indexDist(0, i);
|
std::uniform_int_distribution<int> indexDist(0, i);
|
||||||
@ -269,6 +304,32 @@ 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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user