Skip to content

Commit

Permalink
Delay: Add crossfade mode for pitch-shift-less delay time changes
Browse files Browse the repository at this point in the history
  • Loading branch information
qiemem committed Aug 4, 2024
1 parent 915018b commit 45e50c5
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 14 deletions.
20 changes: 12 additions & 8 deletions software/src/AudioDelayExt.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
#include "dsputils.h"
#include "stmlib_utils_dsp.h"
#include <Audio.h>
#include <optional>

template <size_t BufferLength = static_cast<size_t>(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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -78,15 +78,19 @@ 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;
};

audio_block_t *input_queue_array[1];
std::array<CrossfadeTarget, Taps> target_delay;
std::array<AudioParam<float>, Taps> delay_secs;
std::array<AudioParam<float>, Taps> fb;
std::array<OnePole<Interpolated>, Taps> delay_secs;
std::array<Interpolated, Taps> fb;
ExtAudioBuffer<BufferLength> buffer;
};
108 changes: 105 additions & 3 deletions software/src/AudioParam.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ template <typename T> class AudioParam {
return lpf;
}

T Read() {
return lpf;
}
T Read() { return lpf; }

void Reset() {
inc = 0;
Expand All @@ -46,3 +44,107 @@ template <typename T> class AudioParam {
float lpf_coeff;
};

template <typename T> 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 <typename P> 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;
}
};
40 changes: 37 additions & 3 deletions software/src/applets/Delay.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ class Delay : public HemisphereAudioApplet<Mono> {
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++) {
Expand All @@ -47,6 +54,7 @@ class Delay : public HemisphereAudioApplet<Mono> {
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) {
Expand All @@ -70,10 +78,16 @@ class Delay : public HemisphereAudioApplet<Mono> {
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);
Expand Down Expand Up @@ -121,6 +135,10 @@ class Delay : public HemisphereAudioApplet<Mono> {
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);
Expand Down Expand Up @@ -194,6 +212,7 @@ class Delay : public HemisphereAudioApplet<Mono> {
enum Cursor {
TIME,
TIME_REP,
TIME_MOD,
FEEDBACK,
WET,
TAPS,
Expand All @@ -207,6 +226,11 @@ class Delay : public HemisphereAudioApplet<Mono> {
TIME_REP_LENGTH,
};

enum TimeMod {
CROSSFADE,
STRETCH,
};

int cursor = TIME;

int16_t delay_exp = 0;
Expand All @@ -215,12 +239,22 @@ class Delay : public HemisphereAudioApplet<Mono> {
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;

Expand Down

0 comments on commit 45e50c5

Please sign in to comment.