Vardelay

Vardelay

This is a sndkit algorithm. A more up-to-date version can be found here.

Overview

Vardelay is an implementation of a variable-length delay line, using third order Langrange Interpolation. This delay time of this delay line can be adjusted over time. Because it uses interpolation, any adjustments can cause pitch shifting to occur. This itself is often a desirable artifact, and is the basis for audio effects like flanging and chorusing.

In addition to delay time, a feedback parameter will also be supplied, as it is so often used with delay lines.

Tangled Files

vardelay.c and vardelay.h are the tangled files.

<<vardelay.c>>=
#include <math.h>
#include <stdlib.h>
#define SK_VARDELAY_PRIV
#include "vardelay.h"
<<funcs>>

If SK_VARDELAY_PRIV is defined, private structs are made public.

<<vardelay.h>>=
#ifndef SK_VARDELAY_H
#define SK_VARDELAY_H

#ifndef SKFLT
#define SKFLT float
#endif

<<typedefs>>
<<funcdefs>>
#ifdef SK_VARDELAY_PRIV
<<structs>>
#endif
#endif

Initialization

Initialized with sk_vardelay_init. must supply your own buffer, with buffer size. This way, no internal memory allocation is required.

<<funcdefs>>=
void sk_vardelay_init(sk_vardelay *vd, int sr,
                      SKFLT *buf, unsigned long sz);

<<funcs>>=
void sk_vardelay_init(sk_vardelay *vd, int sr,
                      SKFLT *buf, unsigned long sz)
{
    <<init>>
}

Anything buffer size less than 4 samples is not enough to do third-order interpolation, so it returns an error.

Struct

Called sk_vardelay.

<<typedefs>>=
typedef struct sk_vardelay sk_vardelay;

<<structs>>=
struct sk_vardelay {
    <<sk_vardelay>>
};

Constants and Variables

Sample rate

a copy of the sampling rate is stored in the struct. It is used to convert delay time seconds to samples.

<<sk_vardelay>>=
int sr;

<<init>>=
vd->sr = sr;

Buffer, Buffer Size

The delay buffer is the chunk of memory where samples will be read/written. It is assumed to be neatly pre-allocated and zeroed out externally. The size is also assumed to be known ahead of time and correct.

If the buffer size is less than 4, the delay line is disabled, as there aren't enough samples to do the interpolation.

<<sk_vardelay>>=
SKFLT *buf;
unsigned long sz;

<<init>>=
if (sz < 4) {
    vd->buf = NULL;
    vd->buf = 0;
} else {
    vd->buf = buf;
    vd->sz = sz;
}

Previous output

A variable is used to store the output of the previous delay in what is known as a single-sample delay or a unit delay. This delay is used to introduce feedback into the system. In discrete digital systems like this one, unit delays are inevitable for feedback, which is why they are referred to as implicit delays.

<<sk_vardelay>>=
SKFLT prev;

<<init>>=
vd->prev = 0;

Write Position

The write position is used to store the current index position in the buffer being written to. The write position starts at zero, moves forward until it reaches the end of the buffer, and then goes back to the beginning again. The delayed samples start the write position, and move backwards in time. If that position is a negative, it wraps around.

Notably, the write position is a signed long to compensate for negative values. This is needed for the delay to read stuff back in time.

<<sk_vardelay>>=
long writepos;

<<init>>=
vd->writepos = 0;

Parameters

Delay Time

Set with sk_vardelay_delay, In units of seconds.

<<funcdefs>>=
void sk_vardelay_delay(sk_vardelay *vd, SKFLT delay);

<<funcs>>=
void sk_vardelay_delay(sk_vardelay *vd, SKFLT delay)
{
    vd->delay = delay;
}

<<sk_vardelay>>=
SKFLT delay;

The initial delay time is set to be half the delay time, in units of seconds.

<<init>>=
sk_vardelay_delay(vd, ((SKFLT)sz / sr) * 0.5);

Feedback

Set with sk_vardelay_feedback. Should be a value between 0 and 1.

<<funcdefs>>=
void sk_vardelay_feedback(sk_vardelay *vd, SKFLT feedback);

<<funcs>>=
void sk_vardelay_feedback(sk_vardelay *vd, SKFLT feedback)
{
    vd->feedback = feedback;
}

<<sk_vardelay>>=
SKFLT feedback;

No feedback by default.

<<init>>=
sk_vardelay_feedback(vd, 0);

Computation

Done with sk_vardelay_tick.

<<funcdefs>>=
SKFLT sk_vardelay_tick(sk_vardelay *vd, SKFLT in);

<<funcs>>=
SKFLT sk_vardelay_tick(sk_vardelay *vd, SKFLT in)
{
    SKFLT out;
    SKFLT dels;
    SKFLT f;
    long i;
    SKFLT s[4];
    unsigned long n[4];
    SKFLT a, b, c, d;

    out = 0;
    <<return_if_empty>>
    <<write_to_buffer>>
    <<calculate_read_position>>
    <<wrap_and_flip>>
    <<read_samples>>
    <<calculate_interpolation_coefficients>>
    <<interpolate>>
    <<update_position>>
    <<update_feedback>>

    return out;
}

If buffer is NULL or size is 0, return 0.

<<return_if_empty>>=
if (vd->buf == NULL || vd->sz == 0) return 0;

Write to buffer with feedback.

<<write_to_buffer>>=
vd->buf[vd->writepos] = in + vd->prev * vd->feedback;

Calculate the read position. This is two parts: a fractional component, and an integer component.

Get floating point + integer components of delay time. The delay time position is set to be the write position dels samples in the past, where dels is the delay time in samples.

<<calculate_read_position>>=
dels = vd->delay * vd->sr;
i = floor(dels);
f = i - dels;
i = vd->writepos - i;

Because we're looking backwards, the fractional component is backwards too. A fractional value here is set to be negative (reaching back in time).

If there is a fractional component that is negative, or the integer component is negative, these must be corrected.

The fractional value is flipped to be positive by adding 1 to itself. The integer position is set back in time one sample. This sets the interpolation up so that instead of taking a sample and interpolating backwards, you start with the previous sample and move forwards. The integer position is then set to be in bounds.

By default, the position is wrapped around using addition until the sample is in bounds.

<<wrap_and_flip>>=
if ((f < 0.0) || (i < 0)) {
    /* flip fractional component */
    f = f + 1.0;
    /* go backwards one sample */
    i = i - 1;
    while (i < 0) i += vd->sz;
} else while(i >= vd->sz) i -= vd->sz;

Read samples. This includes the current sample, the previous sample, and two samples in the future.

<<read_samples>>=
/* x(n) */
n[1] = i;

/* x(n + 1) */
if (i == (vd->sz - 1)) n[2] = 0;
else n[2] = n[1] + 1;

/* x(n - 1) */
if (i == 0) n[0] = vd->sz - 1;
else n[0] = i - 1;

if (n[2] == vd->sz - 1) n[3] = 0;
else n[3] = n[2] + 1;

{
    int j;
    for (j = 0; j < 4; j++) s[j] = vd->buf[n[j]];
}

Calculate interpolation coefficients. These four coefficients correspond with the four samples read.

<<calculate_interpolation_coefficients>>=
{
    SKFLT tmp[2];

    d = ((f * f) - 1) * 0.1666666667;
    tmp[0] = (f + 1.0) * 0.5;
    tmp[1] = 3.0 * d;
    a = tmp[0] - 1.0 - d;
    c = tmp[0] - tmp[1];
    b = tmp[1] - f;
}

Interpolate. This follows the following equation:

$$ y(n) = (a x(n - 1) + b x(n) + c x(n + 1) + d x(n + 2)) \cdot f + x(n) $$

<<interpolate>>=
out = (a*s[0] + b*s[1] + c*s[2] + d*s[3]) * f + s[1];

Update position. Increment the write position, and wrap back to zero if it reaches the end of the delay buffer.

<<update_position>>=
vd->writepos++;
if (vd->writepos == vd->sz) vd->writepos = 0;

Update feedback. The current output is set to be the prev value in the delay line.

<<update_feedback>>=
vd->prev = out;