There are many reasons to want to use a time-delayed copy of a sound: for creating an echo effect, for simulating room reverberation, for filtering (changing the frequency content of a sound), and for a variety of other delay-based effects such as flanging, chorusing, Doppler shift, etc. Let's take a look at the generally-accepted way to delay a sound.
Audio delay is achieved by simultaneous recording and playback; while we play back recorded sound from the recent past, we keep recording the current sound so that there's always sound to play back. Since we don't have an infinite amount of memory in which to store recorded sound, it makes sense to store a recording only of the most recent time period—up to the amount of delay we expect to need—and throw away recorded sound from the more distant past. We do that by recording the newest (current) sound over the oldest (no longer needed) sound in a loop. We record the sound data into an array, and when we reach the end of the array we wrap around to the beginning and continue recording. An array that we treat as a loop in this manner is called a circular buffer or a ring buffer.
For example, suppose we wanted to store the most recently recorded one second of audio. At an audio sampling rate of 44,100 samples per second, we would need an array with a length of at least 44,100. We'd first set a counter variable n to 0, to point to the beginning of the array. To record, we put the current audio amplitude into array location n, increment n, and 1/44100 of a second later do that again, over and over. When n is determined to be greater than or equal to the length of the array, we wrap n around to stay within the array, and continue.
Here's some pseudo-code to show that.
var samplerate = 44100.; // samples per second
var seconds = 1.; // duration of the buffer
var arrayofsamples = new Float32Array( samplerate*seconds ); // create buffer
var n = 0; // initialize the counter
while ( recording ) { // at the sampling rate
arrayofsamples[n] = inputvalue; // store current audio sample
n = (n+1) % arrayofsamples.length; // wrap to stay within the array
}
In order to play back the sound that happened a precise amount of time ago, we need to keep track of the current time. The counter n used in the recording process is always the marker of "the current time" in the array. Some time in the past—we'll call it d, for delay in samples—will thus be point n-d in the array. If n-d is less than 0, we'll have to wrap that back into the proper range to stay within the array; as long as d is less than the length of the array, that will still point to the proper time in the past.
In the Max Cookbook of programming examples, you can find an implementation of a DIY ring buffer in Max/MSP. The MSP object delay~ does the same thing with a single object.
This page is by Christopher Dobrian, dobrian@uci.edu.
Last modified March 31, 2019.