diff --git a/delay.h b/delay.h index 946c597..0eeba6a 100644 --- a/delay.h +++ b/delay.h @@ -4,6 +4,8 @@ Released under the Boost Software License (see LICENSE.txt) */ #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" @@ -16,6 +18,10 @@ namespace signalsmith { namespace basics { 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) { @@ -42,7 +48,7 @@ namespace signalsmith { namespace basics { ParamRange ms{0}; ParamRange gain{0.4}; - ParamRange highpass{200}; + ParamRange highpass{100}; ParamRange lowpass{12000}; ParamSteps steps{2}; @@ -50,16 +56,16 @@ namespace signalsmith { namespace basics { template void state(Storage &storage) { storage.param("ms", ms) - .info("delay", "echo spacing (fixed time)") + .info("time", "echo spacing (fixed time)") .range(0, 150, maxDelayMs) .unit("ms", 0); storage.param("steps", steps) - .info("delay", "echo spacing (tempo-dependent)") + .info("tempo", "echo spacing (tempo-dependent)") .range(0, 8) .unit("steps", 0); storage.param("gain", gain) .info("gain", "echo decay") - .range(0, 0.2, 0.99) + .range(0, 0.4, 0.99) .exact(0, "off") .unit("dB", 1, db_gain, gain_db) .unit("", 2); @@ -70,7 +76,7 @@ namespace signalsmith { namespace basics { .unit("Hz", 0); storage.param("lowpass", lowpass) .info("lowpass", "lowpass") - .range(10, 8000, 20000) + .range(10, 4000, 20000) .unit("kHz", 1, kHz_hz, hz_kHz, 1000, 1e10) .unit("Hz", 0); } @@ -78,9 +84,27 @@ namespace signalsmith { namespace basics { EchoSpec initial; EchoSpec feedback; - ParamRange wobbleRate{0.4}; - ParamRange wobbleDepth{0}; - ParamRange wobbleVariation{0.4}; + 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}; @@ -90,6 +114,36 @@ namespace signalsmith { namespace basics { 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) { @@ -108,18 +162,8 @@ namespace signalsmith { namespace basics { 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.param("wobbleRate", wobbleRate) - .info("wobble", "LFO rate") - .range(0.1, 1, 10) - .unit("Hz", 1); - storage.param("wobbleDepth", wobbleDepth) - .info("depth", "LFO detuning") - .range(0, 10, 50) - .unit("cents", 0); - storage.param("wobbleVariation", wobbleVariation) - .info("variation", "LFO variation") - .range(0, 0.25, 1) - .unit("%", 0, pc_linear, linear_pc); + + storage("wobble", wobble); } template @@ -130,40 +174,84 @@ namespace signalsmith { namespace basics { 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 std::max(1, std::min(maxDelaySamples, ms*0.001*config.sampleRate + steps*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], initialDelayTo); - Sample feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayTo); + Sample initialDelayed = reader.read(multiBuffer[c], initialDelayToLfo); + Sample feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayToLfo); if (delayChanging) { - Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFrom); + Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFromLfo); initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed); - Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFrom); + 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); }