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.
#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.
#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.
void sk_vardelay_init(sk_vardelay *vd, int sr,
SKFLT *buf, unsigned long sz);
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.
<<vardelay_struct>>
<<clkdel_struct>>
Main Struct
Called sk_vardelay
.
typedef struct sk_vardelay sk_vardelay;
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.
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.
SKFLT *buf;
unsigned long sz;
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.
SKFLT prev;
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.
long writepos;
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
.
void sk_vardelay_delay(sk_vardelay *vd, SKFLT delay);
void sk_vardelay_delays(sk_vardelay *vd, SKFLT delay);
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;
}
SKFLT dels;
The initial delay time is set to be half the delay time, in units of seconds.
sk_vardelay_delay(vd, ((SKFLT)sz / sr) * 0.5);
Feedback
Set with sk_vardelay_feedback
. Should be a value between
0 and 1.
void sk_vardelay_feedback(sk_vardelay *vd, SKFLT feedback);
void sk_vardelay_feedback(sk_vardelay *vd, SKFLT feedback)
{
vd->feedback = feedback;
}
SKFLT feedback;
No feedback by default.
sk_vardelay_feedback(vd, 0);
Computation
Done with sk_vardelay_tick
.
SKFLT sk_vardelay_tick(sk_vardelay *vd, SKFLT in);
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.
if (vd->buf == NULL || vd->sz == 0) return 0;
Write to buffer with feedback.
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.
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.
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.
/* 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.
{
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) $$
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.
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.
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.
typedef struct sk_clkdel sk_clkdel;
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.
void sk_clkdel_init(sk_clkdel *cd, int sr,
SKFLT *buf,
unsigned long sz);
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
.
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.
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);
}