SOLVED:Arduino Due Delay Effect - Noisy playback *only* with variable delay time

Hi All,

Longtime reader, first time poster. I've been working with Arduino Uno for a while. I first started working with audio / dsp by reverse engineering the DigDugDIY "Lofi Dreams" sampler and working with NooTropics shield. I've been wanting to learn more about delay and reverb effects and the Uno just doesn't seem up to the task - so I upgraded to the Due for a bit more processing power and the onboard ADC & DAC.

I found this YouTube tutorial that describes a simple Reverb using a ringbuffer. It's really more of a two tap delay and less of a reverb - so I've been messing with the code to see if I could get a full 3-knob delay (Effect Level, Delay Time, Feedback). I've had no issues implementing Effect Level and Feedback - but the Delay Time has proven to be more of a challenge.

Here is my current code:

//Setup the ring buffer
#define BSIZE 20000 //Buffer size, sets the maximum delay time.
int ring_buff[BSIZE]; //Ring buffer

void setup() {
  
  //Initialize the buffer contents to all zero
  for (int i=0; i<BSIZE; i++){
    ring_buff[i] = 0;
  }

  //Enable the ADC and DAC pins
  pinMode(A0, INPUT); // Define A0 as ADC input
  pinMode(A1, INPUT); // Define A1 as a multi-purpose Pot (currently used for testing variable delay times)
  analogReadResolution(12); //Override 10 bit default. We can go to 12 bits because the board used is a Due
  analogWriteResolution(12); //Override default 8 bit since we'll use DAC1 output on Due board

  //Open serial monitor transcript
  Serial.begin(9600);
  }

void loop() {
  
  int analogin; //stores ADC data
  float old_value1; // values read from the tap point
  int analogout; //value to be output and stored into buffer at current_tap
  int analogout_buf; //duplicate of analogue out taken before effect mix attenuation
  int current_tap = 0; //The current tap point - incremented after each sample
  int tap; //Temp variable used in indexing into older tap points
  byte effectMix; // variable for setting the effect vs. input mix
  int delayTime; // sets delay time

  //While loop to improve effect timing
  while (1) {
    analogin = analogRead(A0) - 2048; //Making audio values bi-polar
    effectMix = 0; //map(analogRead(2), 0, 4095, 0, 8);
    delayTime = map(analogRead(1), 0, 4095, 225,5000)*4; //sets delay time, mapped usable values between 900 - 20000

    tap = current_tap - delayTime; // sets temp tap value and grabs values earlier in the ring buffer
    if (tap < 0){                  //if tap is negative number, pushes it back to the top of the ring buffer
      tap += BSIZE; 
    }
    old_value1 = ring_buff[tap];   //grabs delayed audio for playback
    
    //Scale values by a specfic divisor to determine feedback amount
    old_value1 /= 2;

    analogout_buf =  analogin + old_value1; //add passthrough audio from ADC to scaled delayed audio

    //Limit the outputs to mimic normal overload distortion
    //avoid digital number wraparound - might not be needed for the buffer?
    if(analogout_buf > 2047){ analogout_buf = 2047;}
    if(analogout_buf < -2047){ analogout_buf = -2047;}
    
    //Effect Level for repeats (i.e. turn down input signal)
    //analogin = analogin >> effectMix; //commented out for testing
    
    analogout = analogin + old_value1; //add passthrough audio from ADC to scaled delayed audio
    
    //Limit the outputs to mimic normal overload distortion
    //avoid digital number wraparound
    if(analogout > 2047){ analogout = 2047;}
    if(analogout < -2047){ analogout = -2047;}
    
    //store duplicated output in buffer for "effect only" playback
    ring_buff[current_tap] = analogout_buf;
    
    //increment (circular) tap-point
    current_tap++;
    if(current_tap > BSIZE){ //if tap point goes beyond BSIZE, start back at 0
      current_tap = 0;
    }

    //Scale the value back to an unsigned value in preparation for output to DAC
    analogout += 2048;
    
    //Write to DAC1 output pin on Due board
    analogWrite(DAC1, analogout);

   }
}

The minimum delayTime value (900) and maximum delayTime value (20000) sound acceptable. There is slight noise on the maximum delay time - no noise on the minimum. I can change the delay time on the fly and get the expected pitch warping - BUT! any values in between the minimum and maximum introduce noise. If I just set it at around 8000 (i.e. don't touch the pot) - the repeats are noisy. As if there is white noise mixed into the signal of the repeat.

If I remove the pot from the equation - set the delayTime to a constant 8000, there is no noise.

I've got examples of the playback at different setting here. See the comments for which pieces of audio represent which settings:
[edit]sample removed[edit]

What can I do to eliminate the noise? Why is the noise being introduced? Any help would be much appreciated!

What is analogRead(1) ?

ard_newbie:
What is analogRead(1) ?

The A1 pin is connected to a 10k linear potentiometer. That's what I'm using to vary the delay time.

analogRead(1) = analogRead(A1) ?

ard_newbie:
analogRead(1) = analogRead(A1) ?

Yes. As far as I can tell when using analogRead() it doesn't matter whether you specify the pin as "1" or "A1".

I did try this change - but I still get the same functionality (pot works either way) and still hear the noise on the output at intermediate delay times.

It seems that the pot (channel 6 = A1) introduces some noise in channel 7 (A0) conversion. You could try to suppress this noise this way:

1/ Replicate the exact same schematic as in the youtube video (this one works fine), and the exact same code as the one showed in the video.

2/ Add a pot to A2 just to produce a noise and see if the sound is still correct or not,

3/ If you can hear the noise, I guess this is a sort of EMI and you should convert this noise from both A0 and A1.

Do analogin = analogRead(A0) – analogRead(A1); This should suppress most of the noise. See if the result is correct.

Hi Ard_Newbie,

Thanks for your replies so far.

1 - I have done this. This was the very first thing I did. I agree - it works fine - no noise on my side, either.

2 - If I just connect A2, but don't include it in the code - The delay is noise free, but I can't vary the delay time. Once I add it into the code to alter the delay time (either in my code or the original code - [i.e. tap = current_tap - (T1 - map(analogRead(A2),0,4095,1,20000)) ;]) I still get the noise.

  1. I tried this and it has no effect other than limiting the range of the delay - at high values it subtracts (up to 20,000) from A0 which can only be a range from -2048 to 2048. Not sure if I'm executing this incorrectly though. How else would I convert the noise other than your example?

I took your idea concerning EMI a step further and just moved the pot all the way down to A8 and even tried moving the power supply to the 5V pin & grounding it with a ground pin further from the 3.3V pin. All instances had the exact same result as my initial example. With that result - I don't think it's EMI.

Ok, this is not due to EMI. The only explanation I can think of is that noise comes from the map() usage:

map(analogRead(A2),0,4095,1,20000))

If analogRead(A2) = 0, the result should be 0 (0 is the minimum buffer Index)
If analogRead(A0) = 4095, the result should be 19999 (19999 is the maximum buffer Index)

You should Serial.print the result of the map() function in your code to see where is the issue while you turn the pot. BTW, select Serial.begin(250000).

Hi Ard_Newbie,

This is exactly what I described doing in my initial post. I scrubbed from 1 to 20,000. There aren't really individual values that are mapped that return different levels of noise. Essentially any value above 1000 produces noise and any value below 19,500 produces noise.

But the behavior is different when the delayTime can be varied vs. when it is constant.

I guess I'm wondering at this point whether the design is flawed? Have I set it up in a way that would grab empty values or those that sound like noise when the delay time is reduced from the maximum?

The code includes an if statement that prevents the ring buffer from going negative - but it also, kind of arbitrarily, pushes the tap up by the max buffer size (BSIZE). I'm wondering whether this pushes the tap too far up in the ring buffer? See the snippet below:

tap = current_tap - delayTime; // sets temp tap value and grabs values earlier in the ring buffer
    if (tap < 0){                      //if tap is negative number, pushes it back to the top of the ring buffer
      tap += BSIZE;

I can get the noise issue to stop if I replace BSIZE in the above with delayTime - but then I lose the traditional "pitch warping" effect when the delay time is changed - which I want to keep.

Any thoughts about what value I could add when the value goes negative that would grab enough data to maintain the pitch effect while not grabbing data too far above in the buffer?

Maybe something like?:

tap = current_tap - delayTime; 
    if (tap < 0){                      
      tap += (delayTime - tap);

But the behavior is different when the delayTime can be varied vs. when it is constant.

Yes I would expect it to do.
By reading a pot you are adding a lot of overhead to the code and altering the overall sample rate that is achievable.

Grumpy_Mike:
Yes I would expect it to do.
By reading a pot you are adding a lot of overhead to the code and altering the overall sample rate that is achievable.

I understand that - but even without reading the pot I was getting well over 100k / sec. I would expect even with reading the pot every cycle it would only delay it by a couple microseconds - even if I lost 20us per cycle I would still be getting 40k / sec or so - which is a totally acceptable (essentially noise-less) sample rate.

And, in terms of symptoms, I'm still getting noise when the pot is left untouched at an intermediate value (i.e. not the min or max value). But the noise goes away (even when still reading the pot) at the max and min settings of the pot.

That would totally make sense to me as causation if not for the fact that the noise goes away at the max and min settings.

To help troubleshoot your sketch, check ring_buff Index each time you set or read ring_buff with assert.h.
Include assert.h at the beginning of your sketch, then before setting or reading ring_buff[Index], write:

assert(Index >= 0 && Index<BSIZE);

Your sketch will hang with the line number in your code if there is an issue with Index.

BTW, if(current_tap > BSIZE){ ....} is not correct, should be > (BSIZE - 1)

Hi Ard_Newbie,

Okay - Thanks! I will try the assert header and see if there is an issue.

I'll also make the other correction for putting the buffer back at 0. I did forget that with 20,000 units of the array that it would start at 0. Thanks!

But the noise goes away (even when still reading the pot) at the max and min settings of the pot.

In which case have you tried putting a serese resistor of about 20R between the top of the pot and +5V. Then put a 47uF capacitor from top to bottom of the pot. Finally put a 1uF capacitor from the wiper to ground. This would reduce the electrical noise from the pot and give a more stable reading.

Have you tested how much noise you have? Just read the pot and print it out, what sort of variations do you see? You could also try a running average on the readings from the pot. Any jitter on the delay will cause noise due to an inconsistent sample rate.

Grumpy_Mike:
In which case have you tried putting a serese resistor of about 20R between the top of the pot and +5V. Then put a 47uF capacitor from top to bottom of the pot. Finally put a 1uF capacitor from the wiper to ground. This would reduce the electrical noise from the pot and give a more stable reading.

Hi Mike,

I have tried smoothing the pot reading by using a running average. You're right - there is jitter on the reading from the pot. It wavers 5 or so clicks even when untouched. Unfortunately, the running average didn't appear to help.

I have not tried using resistors and caps to electrically smooth the reading - I will definitely try this next! The Due actually runs on 3.3V - should I use different cap / resistor values with this in mind? Or are these values pretty standard for this purpose?

Unfortunately, the running average didn't appear to help.

You could try a real average, take n samples and then create an average, then update your delay value. A running average will always reflect noise, if you are only using a few samples.

Grumpy_Mike:
You could try a real average, take n samples and then create an average, then update your delay value. A running average will always reflect noise, if you are only using a few samples.

YES!!!! That was it Mike!! The pot is just mega jittery.

Thank you! I ended up having to take an average of about 2000 samples - I may need to fine tune it more from here - but this works at least when the pot is static

Here is my code:

//Setup the ring buffer
#define BSIZE 20000 //Buffer size, sets the maximum delay time.
int ring_buff[BSIZE]; //Ring buffer

const int numReadings = 2000;

int readings[numReadings];      // the readings from the analog input
int readIndex = 0;              // the index of the current reading
int total = 0;                  // the running total
int average = 0;

void setup() {
  
  //Initialize the buffer contents to all zero
  for (int i=0; i<BSIZE; i++){
    ring_buff[i] = 0;
  }

   for (int thisReading = 0; thisReading < numReadings; thisReading++) {
    readings[thisReading] = 0;
   }

  //Enable the ADC and DAC pins
  pinMode(A0, INPUT); // Define A0 as ADC input
  pinMode(A1, INPUT); // Define A1 as a multi-purpose Pot (currently used for testing variable delay times)
  analogReadResolution(12); //Override 10 bit default. We can go to 12 bits because the board used is a Due
  analogWriteResolution(12); //Override default 8 bit since we'll use DAC1 output on Due board

  //Open serial monitor transcript
  Serial.begin(115200);
  }

void loop() {
  
  int analogin; //stores ADC data
  float old_value1; // values read from the tap point
  int analogout; //value to be output and stored into buffer at current_tap
  int analogout_buf; //duplicate of analogue out taken before effect mix attenuation
  int current_tap = 0; //The current tap point - incremented after each sample
  int tap; //Temp variable used in indexing into older tap points
  byte effectMix; // variable for setting the effect vs. input mix
  int delayTime; // sets delay time

  //While loop to improve effect timing
  while (1) {

    total = total - readings[readIndex];
    // read from the sensor:
    readings[readIndex] = analogRead(A1);
    // add the reading to the total:
    total = total + readings[readIndex];
    // advance to the next position in the array:
    readIndex = readIndex + 1;

    // if we're at the end of the array...
    if (readIndex >= numReadings) {
    // ...wrap around to the beginning:
    readIndex = 0;
  }

  // calculate the average:
    average = total / numReadings;
    analogin = analogRead(A0) - 2048; //Making audio values bi-polar

    delayMicroseconds(20);
    
    effectMix = 0; //map(analogRead(2), 0, 4095, 0, 8);
    delayTime = map(average, 0, 4095, 1,20000); //sets delay time, mapped usable values between 900 - 20000

    tap = current_tap - delayTime; // sets temp tap value and grabs values earlier in the ring buffer
    if (tap < 0){                  //if tap is negative number, pushes it back to the top of the ring buffer
      tap += BSIZE; 
    }
    old_value1 = ring_buff[tap];   //grabs delayed audio for playback
    
    //Scale values by a specfic divisor to determine feedback amount
    old_value1 /= 2;

    analogout_buf =  analogin + old_value1; //add passthrough audio from ADC to scaled delayed audio

    //Limit the outputs to mimic normal overload distortion
    //avoid digital number wraparound - might not be needed for the buffer?
    if(analogout_buf > 2047){ analogout_buf = 2047;}
    if(analogout_buf < -2047){ analogout_buf = -2047;}
    
    //Effect Level for repeats (i.e. turn down input signal)
    //analogin = analogin >> effectMix; //commented out for testing
    
    analogout = analogin + old_value1; //add passthrough audio from ADC to scaled delayed audio
    
    //Limit the outputs to mimic normal overload distortion
    //avoid digital number wraparound
    if(analogout > 2047){ analogout = 2047;}
    if(analogout < -2047){ analogout = -2047;}
    
    //store duplicated output in buffer for "effect only" playback
    ring_buff[current_tap] = analogout_buf;
    
    //increment (circular) tap-point
    current_tap++;
    if(current_tap > BSIZE-1){ //if tap point goes beyond BSIZE, start back at 0
      current_tap = 0;
    }
    
    //Scale the value back to an unsigned value in preparation for output to DAC
    analogout += 2048;
    
    //Write to DAC1 output pin on Due board
    analogWrite(DAC0, analogout);

   }
}

[EDITED]
Thanks for posting this thread.
Have you considered a hardware solution:Encoder wheel?
More expensive part but less supporting parts and very fast reads. Besides stability when not touching the wheel, you will be able to carefully control the interval, have very fine control, distinguish between a small change and a large, all sorts of smart possibilities.