/* 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 #include namespace signalsmith { namespace basics { template struct ReverbSTFX : public BaseEffect { using typename BaseEffect::ParamRange; using typename BaseEffect::ParamSteps; using Array = std::array; ParamRange dry{1}, wet{0.25}; ParamRange roomMs{100}; ParamRange rt20{3}; ParamRange early{1}; ReverbSTFX(double maxRoomMs=200) : maxRoomMs(maxRoomMs) {} template void state(Storage &storage) { using namespace signalsmith::units; storage.info("[Basics] Reverb", "An FDN reverb"); int version = storage.version(3); if (version != 3) 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.1, 3, 30) .unit("seconds", 2, 0, 1) .unit("seconds", 1, 1, 1e100); storage.param("early", early) .info("early", "Early reflections") .range(0, 1, 2) .unit("%", 0, pcToRatio, ratioToPc); } template void configure(Config &config) { config.outputChannels = config.inputChannels = 2; config.auxInputs.resize(0); config.auxOutputs.resize(0); 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); delayEarly.configure(maxRoomSamples, 0.25); } void reset() { delay1.reset(); delay2.reset(); delay3.reset(); delayFeedback.reset(); delayEarly.reset(); } int latencySamples() const { return 0; } template void processSTFX(Io &io, Config &config, Block &block) { using Hadamard = signalsmith::mix::Hadamard; using Householder = signalsmith::mix::Householder; 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()); 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(); for (int i = 0; i < block.length; ++i) { Sample inputGain = smoothedInputGain.at(i); Sample decayGain = smoothedDecayGain.at(i); Sample earlyGain = smoothedEarlyGain.at(i); Array samples; std::array stereoIn = {inputLeft[i]*inputGain, inputRight[i]*inputGain}; stereoMixer.stereoToMulti(stereoIn, samples); 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.read(fade); Householder::inPlace(feedback); Array feedbackInput; for (int c = 0; c < 8; ++c) feedbackInput[c] = 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.read(); Householder::inPlace(feedback); Array feedbackInput; for (int c = 0; c < 8; ++c) feedbackInput[c] = 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 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; } } private: int channels = 0; double maxRoomMs; static Sample getDecayDb(Sample rt20, Sample loopMs) { Sample dbPerSecond = -20/rt20; Sample secondsPerLoop = loopMs*Sample(0.001); return dbPerSecond*secondsPerLoop; } signalsmith::mix::StereoMultiMixer stereoMixer; struct MultiDelay { signalsmith::delay::MultiBuffer buffer; double delayScale = 1; std::array 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 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 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.125, -0.125, 0.375, -0.375, 0.625, -0.625, 0.875, -0.875}; for (int i = 0; i < 8; ++i) { delayOffsets[i] = int(-std::floor(rangeSamples*std::pow(2, ratios[i]/2))); std::uniform_int_distribution 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; } }; 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; }} // namespace #endif // include guard