hi @lucian_dusk
I've stripped out and added lots of comments as below - if anything doesn't make sense let me know.
I've included an example that is expecting an audio input through line in and a pot on pin A0 to control delay time.
A few notes:
- the code is not optimised, it runs fine on a Teensy 4, not tried it on anything else.
- the new delaySmooth method smoothly moves from one time to another incrementing by fractions of a sample.
- it picks the size of increment based on how 'far' you are looking to move (i.e. delay time of 100ms to a new delay time of 800ms) and looks to do think is semitone increments (1 to and octave)
- you can easily re-write the code to move at a specific speed or pass a speed to it or have another input modulate it if that suits your application.
- if you are sweeping a pot to change the time and checking that pot regularly (the example checks every 20ms) you will get a number of calls to the effect over one sweep of the pot speeding up and slowing down as you move.
- I updated the delayFade bit also (which fades between delay times) changing it form linear to exponential curves - don't think it makes that much difference.
Let me know how you get on with it. Cheers Paul
delayExample.ino
Code:
// example of effect_delay10tap
//
#include <Audio.h>
#include <ResponsiveAnalogRead.h>
#include <SD.h>
#include <SPI.h>
#include "effect_delay10tap.h"
// GUItool: begin automatically generated code
AudioInputI2S i2s_in; // xy=196,180
AudioMixer4 mixer1; // xy=434,190
AudioEffectDelay10tap delay1; // xy=638,182
AudioOutputI2S i2s_out; // xy=1038,218
AudioConnection patchCord1(i2s_in, 0, mixer1, 0);
AudioConnection patchCord2(i2s_in, 1, mixer1, 1);
AudioConnection patchCord3(mixer1, delay1);
AudioConnection patchCord4(delay1, 0, mixer1, 2);
AudioConnection patchCord5(delay1, 0, i2s_out, 0);
AudioConnection patchCord6(delay1, 0, i2s_out, 1);
AudioControlSGTL5000 sgtl5000_1; // xy=407,367
// GUItool: end automatically generated code
// delay line
#define DELAYLINE_MAX_LEN 45159 // number of samples at 44100 samples a second
int16_t delay_line[DELAYLINE_MAX_LEN] = {};
// main timing loop
#define LOOP0_DURATION 20 // interval time in millis
elapsedMillis loop0_timer;
// pot to control delaytime
const int DELAY_TIME_KNOB_PIN = A0; // A12 gain
ResponsiveAnalogRead delayTimeKnob(DELAY_TIME_KNOB_PIN, true);
void setup() {
Serial.begin(9600);
// Enable the audio shield and set the output volume.
sgtl5000_1.enable();
sgtl5000_1.inputSelect(AUDIO_INPUT_LINEIN);
sgtl5000_1.volume(0.5);
// alocate some audio memory, don't need much as passing an array to the effect
AudioMemory(15);
// set up the mixer including some feedback
mixer1.gain(0, 0.5);
mixer1.gain(1, 0.5);
mixer1.gain(2, 0.6);
// start up the effect and pass it an array to store the samples
delay1.begin(delay_line, DELAYLINE_MAX_LEN);
}
void loop() {
// loop timer
if (loop0_timer >= LOOP0_DURATION) {
// update and check the pot
delayTimeKnob.update();
if (delayTimeKnob.hasChanged()) {
//Serial.printf("Delay Time Knob:%d\n", delayTimeKnob.getValue());
delay1.delaysmooth(0, delayTimeKnob.getValue());
}
loop0_timer = 0;
}
}
effect_delay10tap.h
Code:
/* Audio Library for Teensy 4.x
* Modified to extend to 10 taps PMF 16-03-2020
* Modified for single samples delay line (rather than blocks) and tape delay like behaviour PMF 02-09-2020
* added delayfade to fade between old and new delay time with expo cross fade PMF 04-09-2020
* added delaysmooth to smoothly delay from old to new time PMF 14-10-2020
*/
#ifndef effect_delay10tap_h_
#define effect_delay10tap_h_
#include "Arduino.h"
#include "AudioStream.h"
#define DELAY_NUM_TAPS 10 // max numer of fixed delay taps / channels
#define DELAY_INC 0.19 // delaysmooth, default increment per samples for delaysmooth
class AudioEffectDelay10tap : public AudioStream {
public:
AudioEffectDelay10tap(void) : AudioStream(1, inputQueueArray) {}
// initialise the delay line
void begin(int16_t *delay_line, uint32_t max_delay_length);
// activate a tap and/or change time with a fade between old and new time (no clicks), transition time in millis
uint32_t delayfade(uint8_t channel, float milliseconds, float transition_time);
uint32_t delaysmooth(uint8_t channel, float milliseconds);
void setDelayIncPerSample(uint8_t channel, float _DELAYINC);
// disable a tap
void disable(uint8_t channel);
// main update routine
virtual void update(void);
void setBufferFreeze(bool _FREEZE) { freezeBuffer = _FREEZE; };
bool returnBufferFreeze() { return freezeBuffer; };
void inspect(void) { dump_samples = true; };
private:
// linear interpolation between two samples (frac = 0 - 1)
int16_t lerpSamples(int16_t sample1, int16_t sample2, float frac) {
return static_cast<int16_t>(static_cast<float>(sample1) + static_cast<float>(sample2 - sample1) * frac);
};
// all pass interpol sample 1= current sample, 2= next sample, 3= last sample
int16_t allPassInterpolSamples(int16_t sample1, int16_t sample2, int16_t sample3, float frac) {
return int16_t(sample2 + (1 - frac) * sample1 - (1 - frac) * sample3);
};
// convert milliseconds to number of samples at sample rate
int32_t millisToSamples(float milliseconds) { return milliseconds * (AUDIO_SAMPLE_RATE_EXACT / 1000.0) + 0.5; };
audio_block_t *inputQueueArray[1];
uint32_t max_delay_length_samples; // lenght of the delay line in samples
uint32_t write_index; // write head position
uint16_t activemask; // which taps/channels are active
int16_t *delay_line; // pointer to delay line
// delay modes
enum delay_modes { DELAY_MODE_NORMAL, DELAY_MODE_SMOOTH, DELAY_MODE_FADE };
// struct for tap
typedef struct {
int32_t current_delay; // actual # of sample delay for each channel
int32_t desired_delay; // desired # of sample delay for each channel
uint32_t fade_to_delay_samples;
uint32_t fade_transition_time;
uint32_t fade_samples_to_complete_transition;
int16_t last_sample;
double fade_multiplier_out;
double fade_multiplier_in;
double fade_expo_multiplier;
int16_t inc_direction = 1; // direction (+/-) to increment if delaysmooth
float inc = 0.0; // cummulative increment if delaysmooth
float inc_per_sample = 0.0; // increment per sample if delaysmooth
int16_t delay_mode = DELAY_MODE_NORMAL;
} tap_struct;
tap_struct tap[DELAY_NUM_TAPS];
// smooth delay increments for changing delay times in semitones
float delay_inc_per_semitone[13] = {0.0000000, 0.0594631, 0.1224620, 0.1892071, 0.2599210, 0.3348399, 0.4142136,
0.4983071, 0.5874011, 0.6817928, 0.7817974, 0.8877486, 1.0000000};
uint32_t temp_timer = 0;
boolean freezeBuffer = false;
boolean dump_samples = false;
};
#endif
effect_delay10tap.cpp
Code:
/* Audio Library for Teensy 4.x
* Modified to extend to 10 taps PMF 16-03-2020
* Modified for single samples delay line (rather than blocks) and tape delay like behaviour PMF 02-09-2020
* added delayfade to fade between old and new delay time with expo cross fade PMF 04-09-2020
* added delaysmooth to smoothly delay from old to new time PMF 14-10-2020
*/
#include "effect_delay10tap.h"
#include <Arduino.h>
void AudioEffectDelay10tap::begin(int16_t *delay_l, uint32_t max_delay_length) {
delay_line = delay_l;
max_delay_length_samples = max_delay_length - 1;
write_index = 0;
}
// activate a tap and/or change time with a fade between old and new time (no clicks), transition time in millis
uint32_t AudioEffectDelay10tap::delayfade(uint8_t channel, float milliseconds, float transition_time) {
if (channel >= DELAY_NUM_TAPS) return 0;
if (milliseconds < 0.0) {
milliseconds = 0.0;
}
if (transition_time < 0.0) transition_time = 0.0;
uint32_t delay_length_samples = millisToSamples(milliseconds);
if (delay_length_samples > max_delay_length_samples) delay_length_samples = max_delay_length_samples;
__disable_irq();
// enable disabled channel
if (!(activemask & (1 << channel))) {
// if channel not active then activate and delay as normal, no fade
tap[channel].fade_to_delay_samples = tap[channel].current_delay = tap[channel].desired_delay = delay_length_samples;
tap[channel].fade_samples_to_complete_transition = 0;
activemask |= (1 << channel);
tap[channel].delay_mode = DELAY_MODE_NORMAL;
} else {
// if already active check if currently fading
if (tap[channel].fade_samples_to_complete_transition == 0) {
// not currently fading, set up for a expo fade over transition time millis
tap[channel].fade_to_delay_samples = tap[channel].desired_delay = delay_length_samples; // where in the delay are we fading to
tap[channel].fade_transition_time = transition_time; // fade over xx milis
tap[channel].fade_samples_to_complete_transition = millisToSamples(tap[channel].fade_transition_time); // counter to fade over xx samples
double tau = -1.0 * transition_time / log(1.0 - 1.0 / 1.01); // calculate a multiplier for a exponential fade (with 1.01 overshoot)
tap[channel].fade_expo_multiplier = pow(exp(-1.0 / tau), 1.0 / (AUDIO_SAMPLE_RATE_EXACT / 1000.0));
tap[channel].fade_multiplier_out = 1.0; // initialise the multiplier to be applied as gain on outgoing tap
tap[channel].fade_multiplier_in = 0.01; // initialise the multiplier to be applied as gain on incoming tap
tap[channel].delay_mode = DELAY_MODE_FADE;
} else {
// currently fading, want to let that play out then fade again to desired so set desired as new target
tap[channel].desired_delay = delay_length_samples;
}
}
__enable_irq();
return tap[channel].current_delay;
}
// activate a tap and/or change time without fading but smoothly changing delaytime
uint32_t AudioEffectDelay10tap::delaysmooth(uint8_t channel, float milliseconds) {
long temp;
if (channel >= DELAY_NUM_TAPS) return 0;
if (milliseconds < 0.0) milliseconds = 0.0;
uint32_t delay_length_samples = millisToSamples(milliseconds);
if (delay_length_samples > max_delay_length_samples) delay_length_samples = max_delay_length_samples;
__disable_irq();
// enable disabled channel
if (!(activemask & (1 << channel))) {
// if not previously activie just move straight to it
tap[channel].delay_mode = DELAY_MODE_NORMAL;
tap[channel].current_delay = tap[channel].desired_delay = delay_length_samples;
tap[channel].fade_samples_to_complete_transition = 0;
activemask |= (1 << channel);
} else {
// if already active...set desired and current delay
tap[channel].desired_delay = delay_length_samples;
tap[channel].current_delay = tap[channel].current_delay + (static_cast<int32_t>(tap[channel].inc) * tap[channel].inc_direction);
tap[channel].fade_samples_to_complete_transition = 0;
// if desire and current different set a rate to increment to desired
if (tap[channel].current_delay != tap[channel].desired_delay) {
// set direction to increment in, initialise and set the rate from 1 semiton to 1 octave based on size of change required
if (tap[channel].current_delay > tap[channel].desired_delay) tap[channel].inc_direction = -1;
if (tap[channel].current_delay < tap[channel].desired_delay) tap[channel].inc_direction = 1;
tap[channel].inc = 0.0;
tap[channel].last_sample = 0;
// set the speed of increment based on how much delaytime has change - this is completely arbitary/tune to your needs
// the delay_inc_per_semitone[] is an array of calculated increments to semitone pitch changes when incrementing
temp = map(abs(tap[channel].current_delay - tap[channel].desired_delay), 0, (static_cast<float>(max_delay_length_samples) * .8), 1, 12);
tap[channel].inc_per_sample = delay_inc_per_semitone[constrain(temp, 0, 12)];
tap[channel].delay_mode = DELAY_MODE_SMOOTH;
// Serial.printf("delaySmooth time:%d increment:%d (semitones)\n", tap[channel].desired_delay, temp);
} else {
// desired and current are equal so normal delay and no change
tap[channel].delay_mode = DELAY_MODE_NORMAL;
tap[channel].current_delay = tap[channel].desired_delay;
}
}
__enable_irq();
return tap[channel].current_delay;
}
void AudioEffectDelay10tap::disable(uint8_t channel) {
if (channel >= DELAY_NUM_TAPS) return;
// disable this channel
activemask &= ~(1 << channel);
};
void AudioEffectDelay10tap::update(void) {
audio_block_t *input, *output;
int16_t *input_data_pointer, *output_data_pointer;
uint32_t read_index, start_index;
uint32_t fade_to_read_index = 0;
uint8_t channel;
int16_t next_sample = 0;
int16_t sample = 0;
int32_t inc_samples;
float inc_frac;
if (delay_line == NULL) return;
// reading and wriitng the block separately so grab a copy of the write_index starting poisition
start_index = write_index;
// write incoming block of samples to buffer if not freezing buffer
input = receiveReadOnly();
if (input) {
if (!freezeBuffer) {
input_data_pointer = input->data;
for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
delay_line[write_index++] = *input_data_pointer++;
if (write_index >= max_delay_length_samples) write_index = 0;
}
} else {
// if buffer frozen move write index on a block
write_index += AUDIO_BLOCK_SAMPLES;
if (write_index >= max_delay_length_samples) write_index = (write_index + max_delay_length_samples) % max_delay_length_samples;
}
release(input);
}
// delay
// process each tap and write out delayed samples
for (channel = 0; channel < DELAY_NUM_TAPS; channel++) {
// check if channel is active
if (!(activemask & (1 << channel))) continue;
output = allocate();
if (!output) continue;
output_data_pointer = output->data;
// if fading between current delay to desired , position desired read head
if (tap[channel].fade_samples_to_complete_transition > 0)
fade_to_read_index = ((start_index - tap[channel].fade_to_delay_samples + max_delay_length_samples) % max_delay_length_samples);
// position the main read head (current_delay_) for this channel / tap
read_index = ((start_index - tap[channel].current_delay + max_delay_length_samples) % max_delay_length_samples);
// process each sample in the audio block
for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
//
// if fading, cross mix in fade_length_samples steps
//
if (tap[channel].delay_mode == DELAY_MODE_FADE) {
// update the fade multiplier for expo fade curves
tap[channel].fade_multiplier_out *= tap[channel].fade_expo_multiplier;
tap[channel].fade_multiplier_in /= tap[channel].fade_expo_multiplier;
// read the two points from the delay line, crossfade and send to the output block
*output_data_pointer++ = (int16_t)(delay_line[read_index] * tap[channel].fade_multiplier_out) + (int16_t)(delay_line[fade_to_read_index] * tap[channel].fade_multiplier_in);
tap[channel].fade_samples_to_complete_transition--;
if (tap[channel].fade_samples_to_complete_transition == 0) { // got to end of fade
// make the current_delay the fade_to_
tap[channel].current_delay = tap[channel].fade_to_delay_samples;
read_index = fade_to_read_index;
tap[channel].delay_mode = DELAY_MODE_NORMAL;
// if stil not at desired delay then start another fade to it
if (tap[channel].desired_delay != tap[channel].current_delay) {
tap[channel].fade_to_delay_samples = tap[channel].desired_delay;
tap[channel].fade_samples_to_complete_transition = millisToSamples(tap[channel].fade_transition_time); // counter to fade over xx samples
fade_to_read_index = ((start_index - tap[channel].fade_to_delay_samples + max_delay_length_samples) % max_delay_length_samples);
// re-set fade multipliers
tap[channel].fade_multiplier_out = 1.0; // initialise the multiplier to be applied as gain on outgoing tap
tap[channel].fade_multiplier_in = 0.01; // initialise the multiplier to be applied as gain on incoming tap
tap[channel].delay_mode = DELAY_MODE_FADE;
}
}
// increment and wrap around the fade_to_read_index
fade_to_read_index++;
if (fade_to_read_index >= max_delay_length_samples) fade_to_read_index = 0;
}
//
// smooth transition from one delay time to another
//
if (tap[channel].delay_mode == DELAY_MODE_SMOOTH) {
// move the delay time by a small increment (inc), split inc into numbers of sample + frac apply direction +-
tap[channel].inc += tap[channel].inc_per_sample;
inc_samples = static_cast<int32_t>(tap[channel].inc);
inc_frac = tap[channel].inc - static_cast<float>(inc_samples);
inc_samples *= tap[channel].inc_direction;
// check if reached desired delay
if (tap[channel].current_delay + inc_samples == tap[channel].desired_delay) {
// reached desire delay, re-set current and re-position index and change to normal delay
tap[channel].current_delay = tap[channel].desired_delay;
tap[channel].inc = 0.0;
read_index = ((start_index + i - tap[channel].current_delay + max_delay_length_samples) % max_delay_length_samples);
tap[channel].delay_mode = DELAY_MODE_NORMAL;
} else {
// not reached desired yet so get next sample and interpolate
sample = delay_line[(read_index - inc_samples + max_delay_length_samples) % max_delay_length_samples];
next_sample = delay_line[(read_index - inc_samples - tap[channel].inc_direction + max_delay_length_samples) % max_delay_length_samples];
if (tap[channel].last_sample == 0) tap[channel].last_sample = sample; // if just starting help the allpass tune in quickly
*output_data_pointer++ = tap[channel].last_sample = allPassInterpolSamples(sample, next_sample, tap[channel].last_sample, inc_frac);
}
}
//
// normal delay
//
if (tap[channel].delay_mode == DELAY_MODE_NORMAL) {
// read delay line and send sample to the output block
*output_data_pointer++ = delay_line[read_index];
}
// increment and wrap around the read index
read_index++;
if (read_index >= max_delay_length_samples) read_index = 0;
}
transmit(output, channel);
release(output);
}
// if (dump_samples) dump_samples = false;
}
// set the increment for the smooth delay
void AudioEffectDelay10tap::setDelayIncPerSample(uint8_t channel, float _DELAYINC) {
tap[channel].inc_per_sample = _DELAYINC;
if (tap[channel].inc_per_sample < 0.1) tap[channel].inc_per_sample = 0.1;
if (tap[channel].inc_per_sample > 1.0) tap[channel].inc_per_sample = 1.0;
};