DJ Pi 6: Delay
November 19, 2017
After the unprecedented success of making sound in the last post, it’s now time to actually process the input in some way. I’ll look at two types: fixed length and variable length.
The basic idea of a delay is that you write the output into a buffer and then read it out again after some delay, adding the delayed output to the main output. This gets saved again and the process repeats, thereby creating an echoed fade out sound.
You can find the code for this post here.
Fixed length delay
Here’s some example code from the
PluginProcessor demo on the JUCE Github:
delayLevel represents the level of the output when it’s written into the delay buffer. A lower value will cause the delayed sound to fade out more quickly. The code also refers to a
delayPosition variable, which is initialised to zero.
The code relies on a buffer like this below:
It is an array initialised to a size equal to the delay length multiplied by the number of samples per second. Whenever the index reaches the end of the array it wraps round to the beginning lines 23-24), making this a circular buffer.
In the code’s main loop we first read from the delay buffer and add it to the output (line 20). We then write the scaled output into the buffer and move on.
Let’s walk through what happens from the start of the loop. For simplicity, let’s imagine that an entire beat is written into each array position, rather than just a sample:
- Read delay buffer, nothing there. First beat is outputted unchanged
- Scale the first beat and write into delay buffer
- Increment and repeat
- We reach the end of the buffer and wrap back to start
- Read first beat out from delay, add to output
- Scale current beat and first delayed beat, write into delay buffer
- Repeat ad infinitum
What’s subtle about this code example is that it is using one position, the buffer index, as both read and write position. But because we are reading before we write we will have to make an entire pass over the array before we start hearing any delayed output.
Most delay implementations I’ve seen used two indexes or pointers, one for read and one for write. I was quite confused about how this implementation was only using one index until I realised that it should be thought of as two completely different positions.
In fact, I find it easier to think of the read position being behind the write. So far behind, in fact, that it’s almost being lapped. I think this makes it clearer to see that the length of the delay is the same as the length of the delay buffer - the read position has to go round the whole thing and catch up before it can read what the write position has just put in.
Variable length delay
An obvious thing to want to do with a delay effect is to change the length of the delay, so here is my implementation of a variable length delay:
The key change is to separate the read and write indexes into two different variables. This allows the position of the read index to change relative to the write, thereby changing the length of the delay. We calculate by how many samples the read should lag behind the write (line 9) and then calcuate the read index from that, wrapping as necessary (lines 22-24).
An important thing to note is that the required read index might not be an integer, depending on the value of the delay length parameter. We therefore use linear interpolation to calculate an intermediate value.
For example, let’s say our read index turns out to be
19.25 at a given point. This means we are a quarter of the way towards the 21st sample, so we calculate the difference between the 21st and 20th sample and scale by a quarter.
We do a similar thing with the parameters themselves. My initial implementation of the variable length delay worked fine until I changed the delay length. This caused crackling in the output until I settled on a new value.
Two very helpful Redditors pointed out that I was allowing the read index to jump all over the place and so causing audio discontinuities. Changing the delay length parameter might cause the read index to jump from being 50 to being 500 samples behind the write position, for example. I had been handling the interpolation for when the read index fell between two integers but not the interpolation needed to move between two delay lengths.
Of course, this seems pretty obvious now in retrospect and is common enough that Juce provides a utility class, LinearSmoothedValue, to handle just this problem.
I switched the parameters to use a
LinearSmoothedValue rather than a pure float and this solved the problem. It does mean that the parameters take some time to respond to changes but this seems unavoidable.
You can use a delay to create other effects like chorus and flange but, well, who actually likes flange? So in the next post I will start diving into the world of filters.