Skip to content

Commit

Permalink
Add Floaty Delay
Browse files Browse the repository at this point in the history
  • Loading branch information
Andreya-Autumn authored Sep 24, 2024
1 parent 9142611 commit be525d9
Show file tree
Hide file tree
Showing 2 changed files with 358 additions and 1 deletion.
352 changes: 352 additions & 0 deletions include/sst/effects/FloatyDelay.h
Original file line number Diff line number Diff line change
@@ -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 <cstring>
#include <cmath>
#include <utility>
#include <iostream>

#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 <typename FXConfig> struct FloatyDelay : core::EffectTemplateBase<FXConfig>
{
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<FXConfig>(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<max_delay_length>;
line_t delayLineL{sincTable};
line_t delayLineR{sincTable};

float min_delay_length = static_cast<float>(sincTable.FIRipol_N);

using lfo_t = sst::basic_blocks::modulators::SimpleLFO<FloatyDelay, FXConfig::blockSize>;
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<FXConfig::blockSize, false> 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 <typename FXConfig> inline void FloatyDelay<FXConfig>::initialize()
{
// ringout_time = 100000;
inputFilter.init();
feedbackFilter.init();
DCfilter.init();
DCfilter.template setCoeffForBlock<FXConfig::blockSize>(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 <typename FXConfig>
inline void FloatyDelay<FXConfig>::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<FXConfig::blockSize>(
sst::filters::CytomicSVF::LP, freqL, freqR, res, res, sampleRateInv, 0.f, 0.f);
feedbackFilter.template setCoeffForBlock<FXConfig::blockSize>(
sst::filters::CytomicSVF::LP, freqFB, freqFB, .55f, .55f, sampleRateInv, 0.f, 0.f);
DCfilter.template retainCoeffForBlock<FXConfig::blockSize>();

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
7 changes: 6 additions & 1 deletion tests/create-effect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -124,8 +125,12 @@ TEST_CASE("Can Create Types")
SECTION("Reverb2") { Tester<sst::effects::reverb2::Reverb2<TestConfig>>::TestFX(); }
SECTION("TreeMonster") { Tester<sst::effects::treemonster::TreeMonster<TestConfig>>::TestFX(); }
SECTION("Nimbus") { Tester<sst::effects::nimbus::Nimbus<TestConfig>>::TestFX(); }
SECTION("Floaty Delay")
{
Tester<sst::effects::floatydelay::FloatyDelay<TestConfig>>::TestFX();
}
SECTION("RotarySpeaker")
{
Tester<sst::effects::rotaryspeaker::RotarySpeaker<TestConfig>>::TestFX();
}
}
}

0 comments on commit be525d9

Please sign in to comment.