Vardelay

Vardelay

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.

A variation of vardelay is also constructed called clkdel. which builds on top of vardelay to build a tempo-synced delay line.

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)
{
    vd->sr = sr;
    <<init>>
}

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

Struct Code Blocks Declaration

Some worgle-related structure to follow.

Below outlines the structs code block, which contains the named code blocks for both vardelay and clkdel, named vardelay_struct and clkdel_struct. Because clkdel depends on vardelay, the order which they are defined matters.

<<structs>>=
<<vardelay_struct>>
<<clkdel_struct>>

Main Struct

Called sk_vardelay.

<<typedefs>>=
typedef struct sk_vardelay sk_vardelay;

<<vardelay_struct>>=
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;

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.

The delay can also be set in units of samples, via sk_vardelay_delays.

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

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

void sk_vardelay_delays(sk_vardelay *vd, SKFLT delay)
{
    vd->dels = delay;
}

<<sk_vardelay>>=
SKFLT dels;

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->dels;
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 prevvalue in the delay line.

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

Tempo-Synced Delay Line (clkdel)

With some additional components, a variable delay line such as vardelay can be used as a tempo-synced delay line. This is achieved by controlling the delay time using some kind of external clock signal. Since it is a delay controlled by an external clock, it shall be called clkdel.

For a external clock signal, a phasor signal is expected. When used with a rephasor, it can make for a very flexible system for rhythmic control. This pairing is ultimately why a phasor signal was chosen instead of a simpler impulse-based clock signal such as metro.

The struct for clkdel is called sk_clkdel. This wraps an instance of sk_vardelay, along with a few other variables. Mainly, a timer, and a variable that keeps track of the phasor value, used for checking resets.

<<typedefs>>=
typedef struct sk_clkdel sk_clkdel;

<<clkdel_struct>>=
struct sk_clkdel {
    sk_vardelay vd;
    SKFLT phs;
    unsigned long timer;
    SKFLT isr;
};

clkdel is initialized with sk_clkdel_init, which passes in the same arguments as vardelay. A max delay time is still required, so a large enough value must be picked ahead of time.

<<funcdefs>>=
void sk_clkdel_init(sk_clkdel *cd, int sr,
                    SKFLT *buf,
                    unsigned long sz);

<<funcs>>=
void sk_clkdel_init(sk_clkdel *cd, int sr,
                    SKFLT *buf,
                    unsigned long sz)
{
    sk_vardelay_init(&cd->vd, sr, buf, sz);
    cd->phs = -1;
    cd->timer = 0;
    cd->isr = 1.0 / (SKFLT) sr;
    sk_vardelay_delays(&cd->vd, sz - 1);
}

A sample of audio is computed with sk_clkdel_tick, which takes in an input signal in and a phasor timing control signal phs.

<<funcdefs>>=
SKFLT sk_clkdel_tick(sk_clkdel *cd, SKFLT in, SKFLT phs);

Before the instance of vardelay is computed, the phasor is checked to see if it has been reset. If it has, the period is measured.

<<funcs>>=
SKFLT sk_clkdel_tick(sk_clkdel *cd, SKFLT in, SKFLT phs)
{
    if (phs < 0) {
        cd->phs = 0;
        cd->timer = 0;
        return sk_vardelay_tick(&cd->vd, in);
    }

    if (phs < cd->phs) {
        sk_vardelay_delay(&cd->vd, cd->timer * cd->isr);
        cd->timer = 0;
    }

    cd->phs = phs;
    cd->timer++;

    return sk_vardelay_tick(&cd->vd, in);
}