From be525d9a5598e3bd0ed2a2fd59d7ea925b450120 Mon Sep 17 00:00:00 2001 From: Andreya-Autumn <105538426+Andreya-Autumn@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:34:58 +0200 Subject: [PATCH] Add Floaty Delay --- include/sst/effects/FloatyDelay.h | 352 ++++++++++++++++++++++++++++++ tests/create-effect.cpp | 7 +- 2 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 include/sst/effects/FloatyDelay.h diff --git a/include/sst/effects/FloatyDelay.h b/include/sst/effects/FloatyDelay.h new file mode 100644 index 0000000..7113719 --- /dev/null +++ b/include/sst/effects/FloatyDelay.h @@ -0,0 +1,352 @@ +/* + * sst-effects - an open source library of audio effects + * built by Surge Synth Team. + * + * Copyright 2018-2023, various authors, as described in the GitHub + * transaction log. + * + * sst-effects is released under the GNU General Public Licence v3 + * or later (GPL-3.0-or-later). The license is found in the "LICENSE" + * file in the root of this repository, or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * The majority of these effects at initiation were factored from + * Surge XT, and so git history prior to April 2023 is found in the + * surge repo, https://github.com/surge-synthesizer/surge + * + * All source in sst-effects available at + * https://github.com/surge-synthesizer/sst-effects + */ + +#ifndef INCLUDE_SST_EFFECTS_FLOATY_DELAY_H +#define INCLUDE_SST_EFFECTS_FLOATY_DELAY_H + +#include +#include +#include +#include + +#include "EffectCore.h" +#include "sst/basic-blocks/params/ParamMetadata.h" + +#include "sst/basic-blocks/dsp/Lag.h" +#include "sst/basic-blocks/dsp/BlockInterpolators.h" + +#include "sst/basic-blocks/mechanics/simd-ops.h" +#include "sst/basic-blocks/mechanics/block-ops.h" + +#include "sst/basic-blocks/tables/SincTableProvider.h" +#include "sst/basic-blocks/dsp/SSESincDelayLine.h" + +#include "sst/filters/CytomicSVF.h" +#include "sst/basic-blocks/modulators/SimpleLFO.h" +#include "sst/basic-blocks/dsp/RNG.h" + +namespace sst::effects::floatydelay +{ +namespace sdsp = sst::basic_blocks::dsp; +namespace mech = sst::basic_blocks::mechanics; + +template struct FloatyDelay : core::EffectTemplateBase +{ + enum floaty_params + { + fld_mix = 0, + fld_time, + fld_feedback, + fld_pitch_warp_depth, + fld_warp_rate, + fld_warp_width, + fld_filt_warp_depth, + fld_cutoff, + fld_resonance, + fld_playrate, + + fld_num_params, + }; + + static constexpr int numParams{fld_num_params}; + static constexpr const char *effectName{"Floaty Delay"}; + + basic_blocks::dsp::RNG rng; + + FloatyDelay(typename FXConfig::GlobalStorage *s, typename FXConfig::EffectStorage *e, + typename FXConfig::ValueStorage *p) + : core::EffectTemplateBase(s, e, p) + { + } + + void suspendProcessing() { initialize(); } + int getRingoutDecay() const { return -1; } + void onSampleRateChanged() { initialize(); } + + void initialize(); + void processBlock(float *__restrict L, float *__restrict R); + + basic_blocks::params::ParamMetaData paramAt(int idx) const + { + using pmd = basic_blocks::params::ParamMetaData; + + switch (idx) + { + case fld_mix: + return pmd().withName("Mix").asPercent().withDefault(0.3f); + + case fld_time: + return pmd() + .asEnvelopeTime() + .withRange(-5.64386f, 3.f) // 20ms to 8s + .withDefault(-1.73697f) // 300ms + .withName("Time"); + + case fld_pitch_warp_depth: + return pmd().asPercent().withName("Pitch Warp"); + + case fld_feedback: + return pmd().asPercent().withDefault(.5f).withName("Feedback"); + + case fld_warp_rate: + return pmd().asLfoRate(-3, 4).withName("Warp Rate"); + + case fld_warp_width: + return pmd().asPercent().withDefault(0.f).withName("Warp Width"); + + case fld_filt_warp_depth: + return pmd().asPercent().withName("Filter Warp"); + + case fld_cutoff: + return pmd().asAudibleFrequency().withDefault(20.f).withName("Cutoff"); + + case fld_resonance: + return pmd().asPercent().withName("Resonance").withDefault(.5f); + + case fld_playrate: + return pmd().asFloat().withRange(-5, 5).withName("Playrate").withDefault(1); + } + return {}; + } + + int samplerate = this->sampleRate(); + const float sampleRateInv = 1 / this->sampleRate(); + inline float envelope_rate_linear_nowrap(float f) { return this->envelopeRateLinear(f); } + + protected: + static constexpr int max_delay_length{1 << 19}; + + const sst::basic_blocks::tables::SurgeSincTableProvider sincTable; + using line_t = sst::basic_blocks::dsp::SSESincDelayLine; + line_t delayLineL{sincTable}; + line_t delayLineR{sincTable}; + + float min_delay_length = static_cast(sincTable.FIRipol_N); + + using lfo_t = sst::basic_blocks::modulators::SimpleLFO; + lfo_t sineLFO{this, rng}; + lfo_t noiseLFO1{this, rng}; + lfo_t noiseLFO2{this, rng}; + typename lfo_t::Shape sine = lfo_t::Shape::SINE; + typename lfo_t::Shape noise = lfo_t::Shape::SMOOTH_NOISE; + + sst::filters::CytomicSVF inputFilter; + sst::filters::CytomicSVF feedbackFilter; + sst::filters::CytomicSVF DCfilter; + + sst::basic_blocks::dsp::lipol_sse timeLerp, modLerpL, modLerpR, + rateLerp, feedbackLerp, mixLerp; + + // int ringout_time; + + inline float rateToSeconds(float f) { return std::pow(2, f); } + + inline void softClip(float &L, float &R) + { + L = std::clamp(L, -1.5f, 1.5f); + L = L - 4.0 / 27.0 * L * L * L; + + R = std::clamp(R, -1.5f, 1.5f); + R = R - 4.0 / 27.0 * R * R * R; + } + + float readHeadMove{0}; + int test{0}; + float priorrate{1.f}; +}; + +template inline void FloatyDelay::initialize() +{ + // ringout_time = 100000; + inputFilter.init(); + feedbackFilter.init(); + DCfilter.init(); + DCfilter.template setCoeffForBlock(sst::filters::CytomicSVF::HP, 30.f, .5f, + sampleRateInv, 0.f); + timeLerp.instantize(); + modLerpL.instantize(); + modLerpR.instantize(); + rateLerp.instantize(); + feedbackLerp.instantize(); + mixLerp.instantize(); + delayLineL.clear(); + delayLineR.clear(); + sineLFO.attack(sine); + noiseLFO1.attack(noise); + noiseLFO2.attack(noise); +} + +template +inline void FloatyDelay::processBlock(float *dataL, float *dataR) +{ + float wr = this->floatValue(fld_warp_rate); + float ww = this->floatValue(fld_warp_width); + float pd = this->floatValue(fld_pitch_warp_depth); + float fd = this->floatValue(fld_filt_warp_depth); + + sineLFO.process_block(wr, 0.f, sine); + noiseLFO1.process_block(wr + 1, 0.f, noise); + noiseLFO2.process_block(wr + 1, 0.f, noise); + float sine = sineLFO.lastTarget; + float noise1 = noiseLFO1.lastTarget; + float noise2 = noiseLFO2.lastTarget; + + float mL = sine + noise1; + float mR = sine + (noise1 * (1 - ww)) + (noise2 * ww); + + // input filter is modulated, feedback filter is static 2 1/2 octaves above the input one + + auto freqL = + 440 * this->noteToPitchIgnoringTuning(this->floatValue(fld_cutoff) + mL * fd * 24.f); + auto freqR = + 440 * this->noteToPitchIgnoringTuning(this->floatValue(fld_cutoff) + mR * fd * 24.f); + freqL = std::clamp(freqL, 20.f, 20000.f); + freqR = std::clamp(freqR, 20.f, 20000.f); + auto freqFB = 440 * this->noteToPitchIgnoringTuning(this->floatValue(fld_cutoff) + 31.02f); + auto res = this->floatValue(fld_resonance); + inputFilter.template setCoeffForBlock( + sst::filters::CytomicSVF::LP, freqL, freqR, res, res, sampleRateInv, 0.f, 0.f); + feedbackFilter.template setCoeffForBlock( + sst::filters::CytomicSVF::LP, freqFB, freqFB, .55f, .55f, sampleRateInv, 0.f, 0.f); + DCfilter.template retainCoeffForBlock(); + + float baseTime = + std::clamp(rateToSeconds(this->floatValue(fld_time)), .002f, 8.f) * this->sampleRate(); + timeLerp.set_target(baseTime); + float time alignas(16)[FXConfig::blockSize]; + timeLerp.store_block(time); + + mL *= pd; + mR *= pd; + // FIXME: This tries and fails to make pitch warp depth consistent between long and short times + mL *= .01225f * (baseTime - .002f) + .002f; + mR *= .01225f * (baseTime - .002f) + .002f; + + modLerpL.set_target(mL); + modLerpR.set_target(mR); + float modL alignas(16)[FXConfig::blockSize]; + float modR alignas(16)[FXConfig::blockSize]; + modLerpL.store_block(modL); + modLerpR.store_block(modR); + + rateLerp.set_target(this->floatValue(fld_playrate)); + float playrate alignas(16)[FXConfig::blockSize]; + rateLerp.store_block(playrate); + + float fb = this->floatValue(fld_feedback); + feedbackLerp.set_target(fb); + float feedback alignas(16)[FXConfig::blockSize]; + feedbackLerp.store_block(feedback); + + float dBufferL alignas(16)[FXConfig::blockSize]; + float dBufferR alignas(16)[FXConfig::blockSize]; + + float smooth{1.f}; + float smoothWindow = 256.f; + + for (int i = 0; i < FXConfig::blockSize; i++) + { + auto absrate = std::fabs(playrate[i]); + auto absRHM = std::fabs(readHeadMove); + auto adjustedTime = baseTime * absrate; + + if (absRHM >= adjustedTime) + { + readHeadMove = 0; + } + + auto readPos = adjustedTime; + readPos -= (playrate[i] >= 0) ? readHeadMove : readPos - readHeadMove; + // The increment determines the playback speed. + // Write head advances 1 each sample, and we're setting read positions relative to it, + // hence the -1 in fwd and +1 in rev. + float increment = (playrate[i] >= 0) ? absrate - 1 : absrate + 1; + readHeadMove += increment; + + auto readPosL = readPos + modL[i]; + auto readPosR = readPos + modR[i]; + + // Clamp at min_delay lest bad things happen + readPosL = std::max(readPosL, min_delay_length); + readPosR = std::max(readPosR, min_delay_length); + + /* + Smoothing stragegy + In any playrate except 1, turn the read head signal down around each clicky jump + // TODO: Improve... + Either try a 2-head strategy or make the window size relative to the speed + */ + if (playrate[i] == 1) + { + smooth = 1.f; // no smoothing needed + + if (readHeadMove > 1) // but delay time will be wrong + { + readHeadMove -= 1; // so wind the head back towards zero + } + else if (readHeadMove < 1) + { + readHeadMove += 1; + } + // (yeah yeah -1...1 != 0 but close enough here) + } + else + { + if (playrate[i] < 0) + { + smoothWindow = 512.f; // lengthen the window in reverse + } + // this slightly cursed nest of mins answers "how far are we from a jump" + auto s = std::min(std::min(smoothWindow, absRHM), + std::min(smoothWindow, adjustedTime - absRHM)); + // the answer has no business outside these bounds + s = std::clamp(s, 0.f, smoothWindow); + // divide it by the window total to get the smoothing amount + smooth = s / smoothWindow; + } + + auto fromLineL = delayLineL.read(readPosL) * smooth; + auto fromLineR = delayLineR.read(readPosR) * smooth; + + // lest very slow speeds get a little unwieldy + DCfilter.processBlockStep(fromLineL, fromLineR); + + dBufferL[i] = fromLineL; + dBufferR[i] = fromLineR; + + auto inL = dataL[i]; + auto inR = dataR[i]; + inputFilter.processBlockStep(inL, inR); + + auto toLineL = inL + feedback[i] * dBufferL[i]; + auto toLineR = inR + feedback[i] * dBufferR[i]; + + feedbackFilter.processBlockStep(toLineL, toLineR); + softClip(toLineL, toLineR); + + delayLineL.write(toLineL); + delayLineR.write(toLineR); + } + + mixLerp.set_target(this->floatValue(fld_mix)); + mixLerp.fade_2_blocks_inplace(dataL, dBufferL, dataR, dBufferR, this->blockSize_quad); +} +} // namespace sst::effects::floatydelay +#endif // FLOATYDELAY_H diff --git a/tests/create-effect.cpp b/tests/create-effect.cpp index cf25823..82ba19e 100644 --- a/tests/create-effect.cpp +++ b/tests/create-effect.cpp @@ -24,6 +24,7 @@ #include "simd-test-include.h" #include "sst/effects/Delay.h" +#include "sst/effects/FloatyDelay.h" #include "sst/effects/Flanger.h" #include "sst/effects/Reverb1.h" #include "sst/effects/Bonsai.h" @@ -124,8 +125,12 @@ TEST_CASE("Can Create Types") SECTION("Reverb2") { Tester>::TestFX(); } SECTION("TreeMonster") { Tester>::TestFX(); } SECTION("Nimbus") { Tester>::TestFX(); } + SECTION("Floaty Delay") + { + Tester>::TestFX(); + } SECTION("RotarySpeaker") { Tester>::TestFX(); } -} \ No newline at end of file +}