Audio Effect Delay Pops & Clicks when changing tap times

houtson

Well-known member
Hi

I'm using the AudioEffectDelay with a Teensy4 and AudioBoard.

When I change tap times I get pops and clicks. I'm changing a few tap time together (like changing between different presets if that makes sense).

Question is whether this is as expected and if so are there any approaches to minimise the noise?

I've attached some simple code that switches between two sets of tap delay times (it expects some audio input through the audioboard).

Any advice appreciated, Cheers Paul




Code:
#include <Audio.h>
#include <SD.h>
#include <SPI.h>
#include <SerialFlash.h>
#include <Wire.h>

// GUItool: begin automatically generated code
AudioInputI2S i2s1;       // xy=206,94
AudioMixer4 mixer1;       // xy=523,76
AudioEffectDelay delay1;  // xy=538,236
AudioMixer4 mixer2;       // xy=766,197
AudioOutputI2S i2s2;      // xy=1099,196
AudioConnection patchCord1(i2s1, 0, mixer1, 0);
AudioConnection patchCord2(i2s1, 1, mixer1, 1);
AudioConnection patchCord3(mixer1, delay1);
AudioConnection patchCord4(mixer1, 0, mixer2, 2);
AudioConnection patchCord5(delay1, 0, mixer2, 0);
AudioConnection patchCord6(delay1, 1, mixer2, 1);
AudioConnection patchCord7(delay1, 2, mixer1, 2);
AudioConnection patchCord8(mixer2, 0, i2s2, 0);
AudioConnection patchCord9(mixer2, 0, i2s2, 1);
AudioControlSGTL5000 codec;  // xy=377,390
// GUItool: end automatically generated code


void setup(void) {
  Serial.begin(9600);
  AudioMemory(150 * (128 / AUDIO_BLOCK_SAMPLES));
  codec.enable();
  codec.volume(0.5);

  mixer1.gain(0, 0.7);
  mixer1.gain(1, 0.7);
  mixer1.gain(2, 0.5);
  mixer1.gain(3, 0.0);
}

void loop(void) {
  Serial.println("Delay 1");
  delay1.delay(0, 50);
  delay1.delay(1, 75);
  delay1.delay(2, 400);
  delay(2000);
  Serial.println("Delay 2");
  delay1.delay(0, 150);
  delay1.delay(1, 250);
  delay1.delay(2, 400);
  delay(2000);
}
 
Hi

I'm using the AudioEffectDelay with a Teensy4 and AudioBoard.

When I change tap times I get pops and clicks. I'm changing a few tap time together (like changing between different presets if that makes sense).

Question is whether this is as expected and if so are there any approaches to minimise the noise?

Yes, its completely expected as you are making discontinuous changes to a signal on the fly.

One approach often used is to switch on a zero-crossing, but the Audio lib processes in blocks so there's no
easy way to do this, and it won't work for several taps at once as the zero crossings happen at different
times.

Using linear interpolation between old and new taps might work, so you'd need two sets of taps, and fade between them in
a few ms.
 
Hi Mark,

I thought that might the be the expected behaviour, thanks for confirming and your suggestion - I'll have a go at your second suggestion (cross fading two sets of taps)

cheers, Paul
 
Thanks again MarkT for the ideas, I made a new delay object to get round the clicks and pops when you change delay times dynamically.
It seems to work well letting me adjust multiple taps. It's not the most efficient code but works for my situation.

A few details:
- the new object works sample by sample rather than by block, fading between old and new delay times per tap to avoid clicks and pops
- it's less efficient than the standard block based effect but easier to work with
- it doesn't use the AudioMemory for the delay line, you need to pass it a basic array. this means it won't re-size if you don't use the full delay
- it's multi-tap with 10 independent taps
- when you want to change delay time you call delayfade(uint8_t channel, float milliseconds, float transition_time); transition time is the time to fade in millis.
- it's a good basis for other tape delay type effects

if anyone is looking for something similar let me know and I'll post the code and an example

cheers, Paul
 
Hi ETMoody3

I've pasted below:
- example file (just like the first one in this post that changes a few taps over and over)
- effect_tapedelay10tap.h which is the header file for the effect object
- effect_tapedelay10tap.cpp which is the main file for the effect object

I'm not a coder so any suggestions welcome - it uses a bit of float math for the fade which I'm sure could be optimised for integers. I've been using a Teensy 4.

Hopefully straight forward, any feedback or questions welcome.

Cheers Paul

example.ino
Code:
#include <Audio.h>
#include <SD.h>
#include <SPI.h>
#include <SerialFlash.h>
#include <Wire.h>
#include "effect_tapedelay10tap.h"

// GUItool: begin automatically generated code
AudioInputI2S i2s1;       // xy=206,94
AudioMixer4 mixer1;       // xy=523,76
AudioEffectTapeDelay10tap delay1;  // xy=538,236
AudioMixer4 mixer2;       // xy=766,197
AudioOutputI2S i2s2;      // xy=1099,196
AudioConnection patchCord1(i2s1, 0, mixer1, 0);
AudioConnection patchCord2(i2s1, 1, mixer1, 1);
AudioConnection patchCord3(mixer1, delay1);
AudioConnection patchCord4(mixer1, 0, mixer2, 2);
AudioConnection patchCord5(delay1, 0, mixer2, 0);
AudioConnection patchCord6(delay1, 1, mixer2, 1);
AudioConnection patchCord7(delay1, 2, mixer1, 2);
AudioConnection patchCord8(mixer2, 0, i2s2, 0);
AudioConnection patchCord9(mixer2, 0, i2s2, 1);
AudioControlSGTL5000 codec;  // xy=377,390
// GUItool: end automatically generated code

#define DELAY_MAX_LEN 22050  // buffer for samples @44100 samples per second, 22050 = 0.5s
int16_t sample_delay_line[DELAY_MAX_LEN] = {};

void setup(void) {
  Serial.begin(9600);
  AudioMemory(150 * (128 / AUDIO_BLOCK_SAMPLES));
  codec.enable();
  codec.volume(0.5);

  mixer1.gain(0, 0.7);
  mixer1.gain(1, 0.7);
  mixer1.gain(2, 0.5);
  mixer1.gain(3, 0.0);

  // initialise the delayline
  delay1.begin(sample_delay_line, DELAY_MAX_LEN);
}

void loop(void) {
  Serial.println("Delay 1");
  delay1.delayfade(0, 6, 3.0);  // (tap_number 0-9, millis of delay, milliseconds to tranistion from old to new time)
  delay1.delayfade(1, 12, 3.0);
  delay1.delayfade(2, 18, 3.0);
  delay(2000);
  Serial.println("Delay 2");
  delay1.delayfade(0, 150, 3.0);
  delay1.delayfade(1, 250, 3.0);
  delay1.delayfade(2, 350, 3.0);
  delay(2000);
}


effect_tapedelay10tap.h

Code:
/* Audio Library for Teensy 3.X
 * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * 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 PMF 04-09-2020
 */

#ifndef effect_tapedelay10tap_h_
#define effect_tapedelay10tap_h_
#include "Arduino.h"
#include "AudioStream.h"

#define DELAY_NUM_TAPS 10  // max numer of taps / channels

class AudioEffectTapeDelay10tap : public AudioStream {
 public:
  AudioEffectTapeDelay10tap(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);
  // disable a tap
  void disable(uint8_t channel);
  // main update routine
  virtual void update(void);

 private:
  audio_block_t *inputQueueArray[1];
  uint32_t delay_samples[DELAY_NUM_TAPS];          // actual # of sample delay for each channel
  uint32_t desired_delay_samples[DELAY_NUM_TAPS];  // desired # of sample delay for each channel
  int16_t fade_length_samples[DELAY_NUM_TAPS];     // how long to fade in samples
  int16_t fade_count[DELAY_NUM_TAPS];              // count down for fades
  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 *sample_delay_line;                      // pointer to delay line
};
#endif

effect_tapedelay10tap.cpp

Code:
/* Audio Library for Teensy 3.X
 * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 *
 * 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 PMF 04-09-2020
 */
#include "effect_tapedelay10tap.h"

#include <Arduino.h>

void AudioEffectTapeDelay10tap::begin(int16_t *delay_line, uint32_t max_delay_length) {
  sample_delay_line = delay_line;
  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 AudioEffectTapeDelay10tap::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 = (milliseconds * (AUDIO_SAMPLE_RATE_EXACT / 1000.0)) + 0.5;
  if (delay_length_samples > max_delay_length_samples) delay_length_samples = max_delay_length_samples;
  fade_length_samples[channel] = fade_count[channel] = (transition_time * (AUDIO_SAMPLE_RATE_EXACT / 1000.0)) + 0.5;
  // enable disabled channel
  if (!(activemask & (1 << channel))) {
    desired_delay_samples[channel] = delay_samples[channel] = delay_length_samples;
    fade_count[channel] = 0;
    activemask |= (1 << channel);
  } else {
    desired_delay_samples[channel] = delay_length_samples;
  }
  return delay_samples[channel];
}

void AudioEffectTapeDelay10tap::disable(uint8_t channel) {
  if (channel >= DELAY_NUM_TAPS) return;
  // disable this channel
  activemask &= ~(1 << channel);
};

void AudioEffectTapeDelay10tap::update(void) {
  audio_block_t *input, *output;
  int16_t *input_data_pointer, *output_data_pointer;
  uint32_t read_index, desired_read_index, start_index;
  uint8_t channel;
  float fade_multiplier;

  if (sample_delay_line == NULL) return;
  // grab a copy of the write_index starting poisition
  start_index = write_index;

  // write incoming block of samples to buffer
  input = receiveReadOnly();
  if (input) {
    input_data_pointer = input->data;
    for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
      sample_delay_line[write_index++] = *input_data_pointer++;
      if (write_index >= max_delay_length_samples) write_index = 0;
    }
    release(input);
  }

  // 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;
    // position the read head for this channel / tap
    read_index = ((start_index - delay_samples[channel] + max_delay_length_samples) % max_delay_length_samples);
    // if fading between current delay to desired , position desired read head
    if (fade_count[channel] > 0) desired_read_index = ((start_index - desired_delay_samples[channel] + max_delay_length_samples) % max_delay_length_samples);
    for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
      // if fading, cross mix in fade_length_samples steps
      if (fade_count[channel] > 0) {
        // calculate a multipier for the fade
        fade_multiplier = (float)fade_count[channel] / fade_length_samples[channel];
        // read the two points from the delay line, crossfade and send to the output block
        *output_data_pointer++ =
            (int16_t)(sample_delay_line[read_index] * fade_multiplier) + (int16_t)(sample_delay_line[desired_read_index] * (1 - fade_multiplier));
        fade_count[channel]--;
        if (fade_count[channel] == 0) {
          delay_samples[channel] = desired_delay_samples[channel];
          read_index = desired_read_index;
        }
        // increment and wrap around the desired_read_index
        desired_read_index++;
        if (desired_read_index >= max_delay_length_samples) desired_read_index = 0;
      } else {
        // read delay line and send sample to the output block
        *output_data_pointer++ = sample_delay_line[read_index];
      }
      // increment and wrap around the main read index
      read_index++;
      if (read_index >= max_delay_length_samples) read_index = 0;
    }
    transmit(output, channel);
    release(output);
  }
}
 
Nice.

I just put the delay array in external PSRAM on a Teensy4.1.

Code:
EXTMEM int16_t sample_delay_line[DELAY_MAX_LEN] = {};

Useful.

Thank you
 
I tested it today and just I can tell you double thank you 😆 for sharing this code.
It working just amazing. I tested it on my T4.1 equipped with 16mb psram with large buffer and even all 10 channels once and still working without any problems. I add potentiometer “delay” mapped in range 6-150 and in every next channel I added delay+6, delay+12, delay+18 and so on. All 10 channels controlled by one knob and working nice without any clicking. I settled lower fade time just around 1.0
Really cool object. Paul should add it to official Audio library 😋
 
This delay works fine, no clicks! (FINALLY) but I would like to have something similar to this using delayExt object with PSRAM of audioboard.
Is this possible?
 
I inserted two fade objects on each channel of the delayext, so that the new tempo is of a different channel, and by mixing the two channels with a 3ms fade the result is AMAZING.

One question, could someone help me create a new delayext object that has less total channels but internally fades between two channels every time the tempo is changed?

Currently for a multiatap delay you need a lot of fades, connections and mixers to make it work well and it's a bit awkward for very complex projects... I believe that 4 channels (with this fade function) are more than enough for many projects, but I don't know if it is possible to create an object that summarises the merging of several objects...
 
Back
Top