Delay Effects – Circular Buffers
Delay effects exploit a DSP programming concept called circular buffering. A buffer is really just an array of values. When you use an index to step through the array, you can move in the forward (positive) or reverse (negative) direction by incrementing the index up or down. But when you get to a boundary of the array at the top or bottom and you increment or decrement the index, you move outside the array, often ending with a crash. In a circular buffer, movement over a boundary loops back into the array. When you try to move past the end of the array, the circular buffer automatically wraps the index around to the starting point.
In the opposite direction, the same thing applies but in reverse. This is shown below, where we are using a pointer and index off set to access a circular buffer. The index off set is +5, so the pointer moves in steps of five buffer locations, skipping through the array. When it skips over the end boundary, it wraps back to the top and off sets the same number of buffer locations.
Suppose we have been using the pointer to write audio samples into this buffer and that this has been going on for some time so that the pointer has wrapped back around the top a few times. If we freeze time during a write-event and think about the contents of the buffer, we can see how the samples line up in time. Let’s assume we have been using an index value called m_nWrite to keep track of the writing location in the buffer, incrementing it by one each sample period. We are going to use another index value m_nRead to read values from the array. In the figure below, you can see that the oldest sample in the buffer is the one we are writing over. The youngest sample is one location before the write location. The other delayed values are between the oldest and youngest. If we want to delay the input signal by 100 samples, we off set the read index 100 samples before the write index; this might mean that the index would wrap around the top of the buffer. Thus as the figure below shows, the distance between the read and write index determines the delay time in samples.
So, by implementing a circular buffer and reading/writing audio samples from/to it, we can create a basic delay. But this delay time is set in samples, not seconds or milliseconds. And as it turns out, we need more precision than the sample period has to offer. For example, at 44.1 kHz, the sample period is about 23μSec. Th at might seem like a small value, but if that is the resolution of our delay effect, we will have problems if the user tries to synchronize the delay time to the beats per minute (BPM) of the song. If the delays do not fall precisely on the required intervals, the delay will drift over time. It might take a while, but during a long song, the delay could drift significantly. Therefore we need to be able to specify—and implement—a fractional delay time with the delay value in milliseconds. This means we need to be able to read out values that are technically between two actual values; the delay is a fraction of the distance of one sample period. A simple way to do this is with interpolation. While interpolators are inherently lowpass filters, the convenience and ease of implementation makes them attractive. See the Bibliography for more information on fractional delays with and without filtering.
Since linear interpolation is the simplest form, let’s look at that operation. There are several ways to implement linear interpolation, but the easiest method is to treat it like a DSP filter rather than y = mx + b. You can also view interpolation as a weighted sum operation. For example, if the interpolation point is 0.5 between samples 1 and 2, then the interpolated value is made up of 50% of sample 1 plus 50% of sample 2. Suppose we want a fractional delay of 23.7186 samples. Th e interpolated distance is 0.7183 between samples 23 and 24. We call the fractional component frac and the integer part int. The scheme is shown below. We can re-map the samples 23 and 24 to index values 0 and 1, then just interpolate the frac distance between them. We can then write the interpolated output as:
interp_output = (frac)(Sample 24) + (1-frac)(Sample 23)
interp_output = (0.7183)(Sample 24) + (0.2187)(Sample 23)
Here is a linear interpolation function you can use; it is already declared in your pluginconstants.h file:
float dLinTerp (float x1, float x2, float y1, float y2, float x);
You give it a pair of data points, (x1,y1) and (x2,y2) plus a distance between them on the x-axis (x) and it returns the interpolated value using the weighted sum method.
Interestingly, this interpolator can be viewed as a kind of filter—a first order feed-forward variety as shown below.
Excerpt from Designing Software Synthesizer Plug-Ins in C++ by Will Pirkle © 2014 Taylor & Francis Group. All Rights Reserved.
About the Author
Will Pirkle is an Assistant Professor of Music Engineering Technology at the University of Miami Frost School of Music and is the author of Designing Audio Effects Plug-Ins in C++. He teaches classes in C++ Audio Programming, Signal Processing and Audio Synthesis Theory, and Mobile App Programming. In addition to ten years of teaching at the University of Miami, Mr. Pirkle has twenty years of experience in the audio industry working and consulting for such names as Korg Research and Development, SiriusXM Radio, Diamond Multimedia, Gibson Musical Instruments, and National Semiconductor Corporation. An avid guitarist and studio owner, Mr. Pirkle still seeks projects that combine all his skills. www.willpirkle.com