Info

The Delay Line

In the next couple of posts, we will be going over some fundamental building blocks used to build more complex effects like reverb. We will start off with the delay line.

We have come across delay lines before in FIR filters (SAP 5). However, we implemented an encapsulation of the delay line effect rather than a standalone version of it.

The purpose of the delay line is to delay the propagation of an audio sample by M samples. The diagram below illustrates the process:

The dashed box is a delay line with delay length, M=5. Its internal storage buffer is shown. Note that in implementation, this buffer can take on a number of different forms. At the start, the delay line is at some state preloaded with random values: 4, 5, 6, and 7. The value 3 is then loaded into the delay line. As new samples are shifted in, our first value, 3 is shifted closer to the end of the buffer until finally at time n=5, it is shifted out of the delay line.

In other words, the input is not immediately moved to the output. It is delayed by 5 samples.

For the more mathematically inclined, this process can be described by the expression:

Implementation

Here is a simple implementation of a delay line in C.

typedef struct
{
  float32_t *buffer;
  size_t currentPtr;
  size_t M;
}DelayLine;

In this implementation, the delay line is essentially a circular buffer. buffer points to a block of memory that holds the delay line samples, currentPtr points to the location in memory that holds the sample for output and the location to store the input. M is the delay length (which is also the size of the buffer).

Creating Delay Lines

Creating a delay line instance involves allocating memory for the delay line itself as well as its buffer. This buffer is then initialized with all zeros:

DelayLine *createDelayLine(size_t M)
{
  DelayLine *d = (DelayLine *)malloc(sizeof(DelayLine));
  if (d == NULL)
    return NULL;

  if (M != 0)
  {
    d->buffer = (float *)malloc(sizeof(float) * M);
    if (d->buffer == NULL)
    {
      free(d);
      return NULL;
    }

    for (int i = 0; i < M; ++i)
      d->buffer[i] = 0;
  }
  else
    d->buffer = NULL;

  d->M = M;
  d->currentPtr = 0;

  return d;
}

Delay Line Operation

Next we will want to implement the ability to input a sample into the delay line and get a delayed sample out of it:

int delayLineShift(DelayLine *d, float32_t x, float32_t *y)
{
  if (d == NULL) return -1;
  if (d->M != 0)
  {
    if (d->currentPtr >= d->M)
      d->currentPtr = 0;

    //  Get the output first before writing the input into the delay line
    *y = d->buffer[d->currentPtr];
    d->buffer[d->currentPtr] = x;

    //  Increment the index and wrap around if it points past the buffer
    d->currentPtr += 1;
    if (d->currentPtr >= d->M)
      d->currentPtr = 0;
  }

  //	Pass-through case (M = 0)
  else
    *y = x;

  return 0;
}

This code combines the delay line input and output process into one function. As the underlying buffer is a circular one, the location where the incoming sample will reside and the location of the outgoing sample are the same. The outgoing sample is extracted first (assigned to the parameter, y) before the incoming sample is written in.

This delay line allows for a delay length of 0, which would simply act as a pass-through. In this case, the incoming sample is the outgoing sample.

In some cases, there is the need to “peek” at what the delay output is without having to shift in an input sample. The delayLinePeek function implements this:

int delayLinePeek(DelayLine *d, float32_t *y)
{
  if (d == NULL) return -1;
  if (d->currentPtr >= d->M) return -1;

  *y = d->buffer[d->currentPtr];

  return 0;
}

Putting it all Together

The following code creates a delay line and uses it to delay the incoming audio samples by 10 samples. Note that this delay is on top of the latency introduced when using buffers to transport audio samples from the ADC, processing and DAC.

This code snippet assumes that the DelayLine typedef is defined in a header file somewhere called DelayLine.h.

One important thing to note is that since we allocated memory to create the DelayLine instance, it is important that we free this memory when we are finished with it. This is done by calling the deleteDelayLine function (its implementation can be found in DelayLine.c in the Github repository referenced below).

An example of delay line use is available on Github.

// main.c

#include "DelayLine.h"

#define NUM_BUFFERS 4
#define BUFFER_SIZE 256
#define QUEUE_SIZE (NUM_BUFFERS)
#define DELAY_LENGTH 10

//  All the peripheral initializations, defines, interrupts, and necessary variables are omitted here.  
//  Refer to SAP 4 for this part of the code OR the examples in Github

int main(void)
{
  // ...

  //  Create a delay line.  If creation fails then exit
  DelayLine *delay = createDelayLine(DELAY_LENGTH);
  if (delay == NULL)
    return;

  while(1)
  {
    //  Check to make sure there is a buffer available for processing
    if (processingQueue[processingQueueHead] != NULL)
    {
      //  Fancy processing code goes here
      for (int i = 0; i < BUFFER_SIZE; ++i)
      {
        float32_t y;
        float32_t x = processingQueue[processingQueueHead][i];
        int error = delayLineShift(delay, x, &y);

        if (error)
          //  Error handling

        processingQueue[processingQueueHead][i] = y;
      }

      transferBufferToQueue(processingQueue[processingQueueHead], dacQueue, &dacQueueTail);
    
      processingQueue[processingQueueHead] = NULL;
      processingQueueHead = (processingQueueHead + 1) % QUEUE_SIZE;
    }
  }

  deleteDelayLine(delay);
}