/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff Released under the Boost Software License (see LICENSE.txt) */ #ifndef SIGNALSMITH_BASICS_DELAY_H #define SIGNALSMITH_BASICS_DELAY_H #include "./dsp/delay.h" #include "./dsp/filters.h" #include "./dsp/envelopes.h" SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0) #include "./stfx-library.h" #include namespace signalsmith { namespace basics { template class DelaySTFX : public BaseEffect { using typename BaseEffect::ParamRange; using typename BaseEffect::ParamSteps; static constexpr double initialFilterBandwidth = 1.9; // Butterworth static constexpr double feedbackFilterBandwidth = 3; // Softer edge static constexpr Sample feedbackLfoMultiplier = 0.5; // Feedback should wobble less // Unit conversions static double kHz_hz(double kHz) { return kHz*1000; } static double hz_kHz(double hz) { return hz*0.001; } static double db_gain(double db) { return std::pow(10, db*0.05); } static double gain_db(double gain) { return std::log10(std::max(gain, 1e-10))*20; } static double pc_linear(double percent) { return percent*0.01; } static double linear_pc(double linear) { return linear*100; } struct EchoSpec { double maxDelayMs = 0; ParamRange ms{0}; ParamRange gain{0.4}; ParamRange highpass{100}; ParamRange lowpass{12000}; ParamSteps steps{2}; template void state(Storage &storage) { storage.param("ms", ms) .info("time", "echo spacing (fixed time)") .range(0, 150, maxDelayMs) .unit("ms", 0); storage.param("steps", steps) .info("tempo", "echo spacing (tempo-dependent)") .range(0, 8) .unit("steps", 0); storage.param("gain", gain) .info("gain", "echo decay") .range(0, 0.4, 0.99) .exact(0, "off") .unit("dB", 1, db_gain, gain_db) .unit("", 2); storage.param("highpass", highpass) .info("highpass", "highpass") .range(10, 500, 20000) .unit("kHz", 1, kHz_hz, hz_kHz, 1000, 1e10) .unit("Hz", 0); storage.param("lowpass", lowpass) .info("lowpass", "lowpass") .range(10, 4000, 20000) .unit("kHz", 1, kHz_hz, hz_kHz, 1000, 1e10) .unit("Hz", 0); } }; EchoSpec initial; EchoSpec feedback; struct Wobble { ParamRange rate{2}; ParamRange detune{0}; ParamRange variation{0.4}; template void state(Storage &storage) { storage.param("rate", rate) .info("rate", "LFO rate") .range(0.1, 1, 10) .unit("Hz", 1); storage.param("detune", detune) .info("detune", "LFO detuning") .range(0, 20, 200) .unit("cents", 0); storage.param("variation", variation) .info("variation", "LFO variation") .range(0, 0.25, 1) .unit("%", 0, pc_linear, linear_pc); } } wobble; enum {steps4, steps4t, steps8, steps8t, steps16, steps16t, steps32, steps32t, stepEnumCount}; static constexpr Sample stepFactors[stepEnumCount] = {1.0, 2.0/3, 0.5, 1.0/3, 0.25, 0.5/3, 0.125, 0.25/3}; ParamSteps beatSteps = steps16; int channels = 0; int maxDelaySamples = 0; signalsmith::delay::MultiBuffer multiBuffer; signalsmith::delay::Reader reader; struct FilterAB { signalsmith::filters::BiquadStatic lowpassFrom, lowpassTo; signalsmith::filters::BiquadStatic highpassFrom, highpassTo; void swap() { std::swap(lowpassFrom, lowpassTo); std::swap(highpassFrom, highpassTo); } Sample process(Sample v) { return highpassTo(lowpassTo(v)); } Sample process(Sample v, Sample fade) { double vFrom = highpassFrom(lowpassFrom(v)); double vTo = highpassTo(lowpassTo(v)); return vFrom + (vTo - vFrom)*fade; } void reset() { lowpassFrom.reset(); lowpassTo.reset(); highpassFrom.reset(); highpassTo.reset(); } }; std::vector initialFilters; std::vector feedbackFilters; std::vector channelLfos; public: DelaySTFX(double maxDelayMs=2000) { initial.maxDelayMs = feedback.maxDelayMs = maxDelayMs; } template void state(Storage &storage) { storage.info("Basics: Delay", "A delay with feedback, filters and modulation"); int version = storage.version(2); if (version < 2) return; storage("initial", initial); storage("feedback", feedback); storage.param("beatSteps", beatSteps) .info("beat step", "How long a tempo-dependent \"step\" is") .names("1/4", "1/4 T", "1/8", "1/8 T", "1/16", "1/16 T", "1/32", "1/32 T"); storage("wobble", wobble); } template void configure(Config &config) { channels = config.outputChannels = config.inputChannels; config.auxInputs.resize(0); config.auxOutputs.resize(0); maxDelaySamples = std::ceil(initial.maxDelayMs*0.001*config.sampleRate); multiBuffer.resize(channels, maxDelaySamples + reader.inputLength + 1); initialFilters.resize(channels); feedbackFilters.resize(channels); channelLfos.resize(channels); } void reset() { multiBuffer.reset(); for (auto &f : initialFilters) f.reset(); for (auto &f : feedbackFilters) f.reset(); for (int c = 0; c < channels; ++c) { channelLfos[c] = {c*1000}; // use channel number as seed channelLfos[c].set(0, 0, 0); } } template void process(Io &io, const Config &config, const Block &block) { Sample lfoRate = wobble.rate/config.sampleRate; Sample lfoDepthSamples = wobble.detune/lfoRate*0.00015; // Magic tuning factor for cents Sample depthFactor = 0.5 + std::min(0.5, wobble.variation); for (auto &c : channelLfos) { c.set(lfoDepthSamples*-depthFactor, lfoDepthSamples*depthFactor, lfoRate, wobble.variation, wobble.variation); } Sample stepSamplesFrom = 60*config.sampleRate/this->bpm.from()*stepFactors[beatSteps.from()]; Sample stepSamplesTo = 60*config.sampleRate/this->bpm.to()*stepFactors[beatSteps.to()]; auto samplesDelay = [&](double ms, double steps, double stepSamples) { return maxDelaySamples, ms*0.001*config.sampleRate + steps*stepSamples; }; Sample initialDelayFrom = samplesDelay(initial.ms.from(), initial.steps.from(), stepSamplesFrom); Sample initialDelayTo = samplesDelay(initial.ms.to(), initial.steps.to(), stepSamplesTo); Sample feedbackDelayFrom = samplesDelay(feedback.ms.from(), feedback.steps.from(), stepSamplesFrom); Sample feedbackDelayTo = samplesDelay(feedback.ms.to(), feedback.steps.to(), stepSamplesTo); bool delayChanging = (initialDelayFrom != initialDelayTo) || (feedbackDelayFrom != feedbackDelayTo); bool filtersChanging = block.fading(initial.highpass, initial.lowpass, feedback.highpass, feedback.lowpass); block.setupFade(filtersChanging, [&](){ for (auto &f : initialFilters) { f.swap(); f.lowpassTo.lowpass(initial.lowpass.to()/config.sampleRate, initialFilterBandwidth); f.highpassTo.highpass(initial.highpass.to()/config.sampleRate, initialFilterBandwidth); } for (auto &f : feedbackFilters) { f.swap(); f.lowpassTo.lowpass(feedback.lowpass.to()/config.sampleRate, feedbackFilterBandwidth); f.highpassTo.highpass(feedback.highpass.to()/config.sampleRate, feedbackFilterBandwidth); } }); auto smoothInitialGain = block.smooth(initial.gain); auto smoothFeedbackGain = block.smooth(feedback.gain); for (int i = 0; i < block.length; ++i) { for (int c = 0; c < channels; ++c) { Sample lfoDelay = channelLfos[c].next(); Sample initialDelayToLfo = std::max(1, std::min(maxDelaySamples, initialDelayTo + lfoDelay)); Sample initialDelayFromLfo = std::max(1, std::min(maxDelaySamples, initialDelayFrom + lfoDelay)); Sample feedbackDelayToLfo = std::max(1, std::min(maxDelaySamples, feedbackDelayTo + lfoDelay*feedbackLfoMultiplier)); Sample feedbackDelayFromLfo = std::max(1, std::min(maxDelaySamples, feedbackDelayFrom + lfoDelay*feedbackLfoMultiplier)); Sample value = io.input[c][i]; multiBuffer[c][0] = value; Sample initialDelayed = reader.read(multiBuffer[c], initialDelayToLfo); Sample feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayToLfo); if (delayChanging) { Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFromLfo); initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed); Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFromLfo); feedbackDelayed = block.fade(i, feedbackDelayedFrom, feedbackDelayed); } if (filtersChanging) { initialDelayed = initialFilters[c].process(initialDelayed, block.fade(i)); feedbackDelayed = feedbackFilters[c].process(feedbackDelayed, block.fade(i)); } else { initialDelayed = initialFilters[c].process(initialDelayed); feedbackDelayed = feedbackFilters[c].process(feedbackDelayed); } multiBuffer[c][0] = value + feedbackDelayed*smoothFeedbackGain.at(i); io.output[c][i] = value + initialDelayed*smoothInitialGain.at(i); } ++multiBuffer; } } }; using Delay = stfx::LibraryEffect; }} // namespace #endif // include guard