Fractional Delay And Smoothed Time Control

Introduction

The delay code in the previous posting quantizes the delay value to the sample. This can cause problems when modulating the delay time, as many modulated delay effects (doppler shift, comb filtering, chorusing, flanging) depend on sub-sample time accuracy. It is also necessary to smooth the time control to decrease parameter change noise (aka “zipper noise”). When adjusting the delay time with a mouse (in a plugin or app), the time parameter will only be updated once per sample block. This sporadic parameter change needs to be smoothed through interpolation or other filtering techniques. Finally, if your delay time is being controlled by an analog control, there is often noise or jitter in this control. This also needs to be smoothed. In this section a few of these techniques will be presented.

Fractional Delay

A fractional delay estimates possible values between actual samples, by using a interpolation or curve fitting technique.

possible curve for 7 samples

If the sample rate is 48k, and a delayed sample at time .0729ms is needed, the sample at location 48 * 0.0729 = 3.5 is needed. Of course, there is no sample 3.5, but an estimate can be made. There are several common techniques to finding this value (these are the same techniques used in a table lookup oscillator):

  • truncation
  • rounding
  • linear interpolation
  • hermite interpolation
  • other interpolation functions

Truncation is the simplest of these techniques. Simply truncate any number right of the decimal point and 3.5 becomes 3. This is the fastest, and worst sounding method. Rounding involves finding the closest integer. It is the same as adding 0.5 to the floating point sample number and then truncating – 3.5 becomes 4. It sounds as bad as truncation but involves more computation. Linear interpolation is a much better estimate. This involves calculating the value between 3 and 4 by a simple crossfade.

The value given by linear interpolation will have error, but is much better than the value given by truncation.

Even more accurate are curve fitting functions like the 4 point hermite cubic interpolator which is discussed in great detail in the oscillator section of this website. It uses the 4 points around 3.5 (2, 3, 4, 5) to estimate the value at 3.5 giving more weight to points 3 and 4. There are many interpolators, using more or less points, and with varying accuracy and CPU performance. A good discussion of these is in this paper by Olli Neimitalo.

Implementation

To implement linear interpolation, the sample points around the fractional point are needed. The the first sample number (3 in the example) is subtracted from the desired sample number (3.5) to get the fraction (0.5). We can use this fraction to estimate the value at 3.5: y = x_{3} * (1 - frac) + x_{4} * frac or y = x_{3} + (x_{4} - x_{3}) * frac .

// The sample reading code is modified for linear interpolation

float readPointer;
long readPointerLong;
float fraction;
float x0, x1;

readPointer = writePointer + (delayTime*sampleRate);
readPointerLong = (long)readPointer;
fraction = readPointer - readPointerLong;
// both points need to be masked to keep them within the delay buffer
x0 = *(delayBuffer + (readPointerLong & bufferMask));
x1 = *(delayBuffer + ((readPointerLong + 1) & bufferMask));
outputSample = x1 + (x2 - x1) * fraction;

Using the hermite polynomial is a little more involved, temporary variables c0 – c4 are used to make the code easier to read. This is using James McCartney’s code from musicdsp.org:

// The sample reading code is modified for cubic interpolation

float readPointer;
long readPointerLong;
float fraction;
float xm1, x0, x1, x2;
float c0, c1, c2, c3;

readPointer = writePointer + (delayTime*sampleRate);
readPointerLong = (long)readPointer;
fraction = readPointer - readPointerLong;
// all points need to be masked to keep them within the delay buffer
xm1 = *(delayBuffer + ((readPointerLong - 1) & bufferMask))
x0 = *(delayBuffer + (readPointerLong & bufferMask));
x1 = *(delayBuffer + ((readPointerLong + 1) & bufferMask));
x2 = *(delayBuffer + ((readPointerLong + 2) & bufferMask));

c0 = x0;     
c1 = 0.5 * (x1 - xm1);     
c3 = 1.5 * (x0 - x1) + 0.5f * (x2 - xm1);     
c2 = xm1 - x0 + c1 - c3;     

outputSample = ((c3 * fraction + c2) * fraction + c1) * fraction + c0;

Smoothed Time Control

When processing samples in blocks, it is necessary to provide an interpolated delay time parameter for every sample. We can do this by simply calculating the change in parameter value for each sample as follows.

float currentDelayTime;

void Delay(float *input, float *output, float delayTime, long samples)
{
  float delayTimeIncrement;
  long i;

  delayTimeIncrement = (delayTime - currentDelayTime)/samples;
  
  for(i = 0; i < samples; i++)
  {
    // DELAY CODE GOES HERE USING currentDelayTime
    currentDelayTime = currentDelayTime + delayTimeIncrement;
  }
  currentDelayTime = delayTime;
}

This takes care of smoothing out parameter change between blocks, however, additional smoothing could be needed for external controllers. A mouse position can be updated as slowly as 60 times a second. Other external sources (knobs, MIDI, network) can also have noise or jitter. Generally it is necessary to design a smoothing filter that works for each source, smoothing the jitter, but quick enough to feel responsive. A simple low pass filter is one common approach.

currentTimeControl  = 0.99 * (currentTimeControl - timeControl) + timeControl;
delayTime = currentTimeControl;

The coefficient (0.99) can be varied for responsiveness. At 48k, this comes within 1% of the new control position in just under 10ms.

Dopper Shift Limiting

The other factor one might consider, especially when there is a large range for the delay time, is to limit the change of delay time per sample. If the delay time moves quickly across a large time span (i.e. from 10ms to 5s), it can create high frequency doppler shift. In this case, it is helpful to limit the change by sending it through a saturation function (a sigmoid, s shaped, function). In the following code this is done with atan().

float delayTimeChange = delayTime - delayTimeLimited; 
delayTimeChange = 4.0 * atan(delayTimeChange * 0.25);
delayTimeLimited += delTimeChange;

The factors 0.25 and 4.0 are chosen here to allow the delay time change to be fairly linear under 1.0 (1 sample of delay time change per 1 sample played), but increasingly limited as so that the delay time never changes more than 2π per sample. This factor is a doppler shift of about 2.5 octaves. Changing the factors around atan() will change this doppler shift limit.

Leave a Reply

Your email address will not be published.