Delay Lines
A delay line is used to store samples and then retrieve them for use some number of sample times later. Delay lines have a wide variety of uses in synthesizers. We have already seen one use in the development of filters. We accomplished a phase shift in the waveform by storing a sample in a variable and then using it one and two sample times later. A more generic delay line allows storing samples for any number of sample times.
Input samples are stored into the buffer at one end and taken out from the other. In programming terminology, a delay line is nothing other than a FIFO queue. The length of the delay line buffer is simply the delay time in seconds multiplied by the sample rate.
The total time delay accomplished by the delay line is equal to the length multiplied by the sample time (n·t). However, in addition to taking samples from the end of the delay line, we can create multiple "taps" that remove samples from the middle. This allows us to get multiple delay times from one buffer. The delay time for a tap is equal to one sample time multiplied by the difference between the tap position and the input position.
For short delay buffers, we can implement the delay line by moving the samples from one "bucket" to the next. However, as the length of the delay buffer increases, we have to spend an increasing amount of time shifting the samples in the buffer. A quicker method is to leave the samples in place and move a buffer index that indicates the current input/output position. Every time we add a sample to the buffer, we first extract the oldest sample, store the new sample back in its place, then increment the index modulo the buffer length. We must increment any tap positions as well. The result is a "ring buffer".
delayLen = delayTime * sampleRate;
delayBuf = new float[delayLen];
out = delayBuf[delayIndex];
delayBuf[delayIndex] = in;
if (++delayIndex >= delayLen)
delayIndex = 0;
for (n = 0; n < numTaps; n++) {
if (++delayTaps[n] >= delayLen)
delayTaps[n] = 0; }
We can optimize the delay line code slightly by using pointers rather than array indexing. However, with good complier optimization, the difference is minimal.
ptrIn = delayBuf;
ptrEnd = &delayBuf[delayLen];
out = *ptrIn;
*ptrIn = in;
if (++ptrIn >= ptrEnd)
ptrIn = delayBuf;
A delay line can also be set up to provide a regenerating signal. If we add the input value to the current contents of the delay buffer rather than replacing the value, the signal in the delay line will re-circulate. Obviously, we must attenuate the old value so that it does not accumulate and produce the effect of a positive feedback loop.
out = delayBuf[delayIndex] * decay;
delayBuf[delayIndex] = in + out;
if (++delayIndex >= delayLen)
delayIndex = 0;
When the decay variable is set to a value less than one, the signal will be reduced in amplitude on each cycle through the buffer and thus slowly fade to an inaudible level over time. In acoustical terms, this type of delay line acts like a resonator. The decay value is typically calculated to produce an exponential decay:
decay = pow(minVal, delayTime/decayTime);
The minVal argument is set to the level we want to reach after decayTime seconds. For obvious reasons, minVal must be > 0. A value of 0.001 is commonly used as this represents a -60dB level when amplitude is normalized to the [0,1] range. |