From 45e50c53833a46ee0d52ed1448800b4e666ffddc Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Sun, 4 Aug 2024 07:41:20 -0700 Subject: [PATCH] Delay: Add crossfade mode for pitch-shift-less delay time changes --- software/src/AudioDelayExt.h | 20 ++++--- software/src/AudioParam.h | 108 ++++++++++++++++++++++++++++++++++- software/src/applets/Delay.h | 40 ++++++++++++- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/software/src/AudioDelayExt.h b/software/src/AudioDelayExt.h index bad1c6ef3..eba699b98 100644 --- a/software/src/AudioDelayExt.h +++ b/software/src/AudioDelayExt.h @@ -5,14 +5,14 @@ #include "dsputils.h" #include "stmlib_utils_dsp.h" #include -#include template (AUDIO_SAMPLE_RATE), size_t Taps = 1> class AudioDelayExt : public AudioStream { public: AudioDelayExt() : AudioStream(1, input_queue_array) { - delay_secs.fill(AudioParam(0.0f, 0.0002f)); + delay_secs.fill(OnePole(Interpolated(0.0f, AUDIO_BLOCK_SAMPLES), 0.0002f)); + fb.fill(Interpolated(0.0f, AUDIO_BLOCK_SAMPLES)); } void delay(size_t tap, float secs) { @@ -22,8 +22,8 @@ class AudioDelayExt : public AudioStream { } void cf_delay(size_t tap, float secs) { - auto& t = target_delay[tap]; - if (t.phase == 0.0f) { + auto &t = target_delay[tap]; + if (t.phase == 0.0f && t.target != secs) { t.phase = crossfade_dt; t.target = secs; } @@ -59,7 +59,7 @@ class AudioDelayExt : public AudioStream { int16_t ReadNext(size_t tap) { float d = delay_secs[tap].ReadNext(); - auto& target = target_delay[tap]; + auto &target = target_delay[tap]; if (target.phase > 0.0f) { int16_t target_val = buffer.ReadSample(target.target * AUDIO_SAMPLE_RATE); int16_t source_val = buffer.ReadSample(d * AUDIO_SAMPLE_RATE); @@ -78,7 +78,11 @@ class AudioDelayExt : public AudioStream { } private: - static constexpr float crossfade_dt = 100.0f / AUDIO_SAMPLE_RATE; + // 20 hz to be just below human hearing frequency. + // Empirically most stuff sounds pretty good at this, but high pitched sine + // waves will crackle a bit with modulation + // TODO: Make this auto-adjust to delay time to better support karplus-strong + static constexpr float crossfade_dt = 20.0f / AUDIO_SAMPLE_RATE; struct CrossfadeTarget { float target; float phase; @@ -86,7 +90,7 @@ class AudioDelayExt : public AudioStream { audio_block_t *input_queue_array[1]; std::array target_delay; - std::array, Taps> delay_secs; - std::array, Taps> fb; + std::array, Taps> delay_secs; + std::array fb; ExtAudioBuffer buffer; }; diff --git a/software/src/AudioParam.h b/software/src/AudioParam.h index 19ca9cf1d..f76f2be88 100644 --- a/software/src/AudioParam.h +++ b/software/src/AudioParam.h @@ -29,9 +29,7 @@ template class AudioParam { return lpf; } - T Read() { - return lpf; - } + T Read() { return lpf; } void Reset() { inc = 0; @@ -46,3 +44,107 @@ template class AudioParam { float lpf_coeff; }; +template class Param { +public: + Param() : Param(0.0f) {} + Param(T value) : value(value) {} + inline T ReadNext() { return value; } + inline T Read() { return value; } + inline Param &operator=(const T &newValue) { + value = newValue; + return *this; + } + inline void Reset() {} + +private: + T value; +}; + +template class OnePole { +public: + OnePole() : OnePole(P(), 1.0f) {} + + OnePole(float value, float coeff) : OnePole(Param(value), coeff) {} + + OnePole(P param, float coeff) + : param(param), coeff(coeff), lp_value(param.Read()) {} + inline OnePole &operator=(float new_value) { + param = new_value; + return *this; + } + + inline float ReadNext() { + ONE_POLE(lp_value, param.ReadNext(), coeff); + return lp_value; + } + + inline void Reset() { + param.Reset(); + lp_value = param.Read(); + } + + inline float Read() { return param.Read(); } + +private: + P param; + float coeff; + float lp_value; +}; + +class Interpolated { +public: + Interpolated() : Interpolated(0, 1) {} + + Interpolated(float value, size_t steps) + : value(value), target(value), steps(steps), inc(0) {} + + inline Interpolated &operator=(float new_value) { + target = new_value; + inc = (target - value) / steps; + return *this; + } + + inline float ReadNext() { + value += inc; + // check if the sign on inc and value-prev are different to see if we've + // gone too far + if (inc * (target - value) <= 0.0f) { + inc = 0.0f; + value = target; + } + return value; + } + + inline float Read() { return value; } + + inline void Reset() { value = target; } + +private: + float value; + float target; + size_t steps; + float inc; +}; + +// Based on +// https://github.com/oamodular/time-machine/blob/aaf3410759f43a828c8350bcacb10828af13f3c2/TimeMachine/dsp.h#L109-L146 +struct NoiseSuppressor { + float value; + float lpf_coeff; + float noise_floor; + size_t settle_threshold; + size_t settle_count; + + float Process(float new_val) { + float d = new_val - value; + if (abs(d) < noise_floor) { + if (settle_count < settle_threshold) + settle_count++; + else + d = 0; + } else { + settle_count = 0; + } + return value += lpf_coeff * d; + } +}; diff --git a/software/src/applets/Delay.h b/software/src/applets/Delay.h index 696991ddf..e8ac32594 100644 --- a/software/src/applets/Delay.h +++ b/software/src/applets/Delay.h @@ -24,12 +24,19 @@ class Delay : public HemisphereAudioApplet { clock_base_secs = clock_count / 16666.0f; clock_count = 0; } - float d = DelaySecs(delay_exp + In(0)); + float d = DelaySecs(delay_exp + delay_cv.Process(In(0))); float f = 0.01f * feedback / taps; for (int tap = 0; tap < taps; tap++) { + float t = d * (tap + 1.0f) / taps; CONSTRAIN(d, 0.0f, MAX_DELAY_SECS); - // delay.delay(tap, d * (tap + 1) / taps); - delay.cf_delay(tap, d * (tap + 1) / taps); + switch (delay_mod_type) { + case CROSSFADE: + delay.cf_delay(tap, t); + break; + case STRETCH: + delay.delay(tap, t); + break; + } delay.feedback(tap, f); } for (int tap = taps; tap < 8; tap++) { @@ -47,6 +54,7 @@ class Delay : public HemisphereAudioApplet { wet_dry_mixer.gain(WD_WET_CH_2, w); wet_dry_mixer.gain(WD_DRY_CH, 1.0f - w); } + void View() { // gfxPrint(0, 15, delaySecs * 1000.0f, 0); switch (time_rep) { @@ -70,10 +78,16 @@ class Delay : public HemisphereAudioApplet { gfxIcon(6 * 6, 15, CLOCK_ICON); break; } + if (delay_mod_type == CROSSFADE) + gfxIcon(6 * 8, 15, CHECK_OFF_ICON); + else + gfxIcon(6 * 8, 15, CHECK_ON_ICON); if (cursor == TIME) gfxCursor(0, 23, 6 * 6); if (cursor == TIME_REP) gfxCursor(6 * 6, 23, 2 * 6); + if (cursor == TIME_MOD) + gfxCursor(6 * 8, 23, 8); gfxPrint(0, 25, "FB: "); gfxPrint(feedback); @@ -121,6 +135,10 @@ class Delay : public HemisphereAudioApplet { time_rep += direction; CONSTRAIN(time_rep, 0, TIME_REP_LENGTH - 1); break; + case TIME_MOD: + delay_mod_type += direction; + CONSTRAIN(delay_mod_type, 0, 1); + break; case FEEDBACK: feedback += direction; CONSTRAIN(feedback, 0, 100); @@ -194,6 +212,7 @@ class Delay : public HemisphereAudioApplet { enum Cursor { TIME, TIME_REP, + TIME_MOD, FEEDBACK, WET, TAPS, @@ -207,6 +226,11 @@ class Delay : public HemisphereAudioApplet { TIME_REP_LENGTH, }; + enum TimeMod { + CROSSFADE, + STRETCH, + }; + int cursor = TIME; int16_t delay_exp = 0; @@ -215,12 +239,22 @@ class Delay : public HemisphereAudioApplet { int8_t wet = 50; int8_t feedback = 0; int8_t taps = 1; + int8_t delay_mod_type = CROSSFADE; PackLocation delay_loc{0, 16}; PackLocation time_rep_loc{16, 4}; PackLocation wet_loc{32, 7}; PackLocation fb_loc{39, 7}; PackLocation taps_loc{46, 3}; + NoiseSuppressor delay_cv{ + 0.0f, + 0.05f, // This needs checking against various sequencers and such + // 16 determined empirically by checking typical range with static + // voltages + 16.0f, + 64, // a little less than 4ms + }; + uint32_t clock_count = 0; float clock_base_secs = 0.0f;