Chorus
Overview
This documents outlines an implementation for a chorus
effect.
A chorus can be generally described as an effect that takes some sound somehow makes it sound like multiple sounds playing the same note in unison.
A chorus is usually done with some kind of modulating delay line. This particular implementation will be no different. When the delay time is modulated, it results in an audible pitch shift due to the interpolation. This slight warbling pitch-shifted version of the signal is then added back to the original signal to create the illusion of two voices playing in unison.
A low-frequence oscillator (LFO) is typically used to
modulate the delay time. Some implementations I've seen
use a triangle wave, as they are computationally cheap and
simple to implement. However, the sharp edges when it
changes slope can cause a very unwanted artifact in the
chorus. In place of a triangle wave LFO, this implementation
will be using a sine wave. Normally, using a sine wave
modulator means either choosing between taking up memory
(via a table lookup oscillator), or
CPU cyles using sin
directly, but if the frequency is
expected to be fixed, a so-called
magic circle algorithm
can be used to produce a sinusoid requiring only 2 samples
of memory, 2 multiplies, and 2 adds. This magic circle
sinusoid will be used in the implementation to modulate
our chorus.
Before being mixed in with the input signal, the delay line output is filted with a one-pole low pass IIR filter. What this does is adds a bit more presence to the dry signal.
Tangled files
chorus.c
and chorus.h
. Defining SK_CHORUS_PRIV
will
expose the sk_chorus
struct.
#ifndef SK_CHORUS_H
#define SK_CHORUS_H
#ifndef SKFLT
#define SKFLT float
#endif
<<typedefs>>
#ifdef SK_CHORUS_PRIV
<<structs>>
#endif
<<funcdefs>>
#ifdef __plan9__
#pragma incomplete sk_chorus
#endif
#endif
#include <math.h>
#include <stdlib.h>
#define SK_CHORUS_PRIV
#include "chorus.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
<<funcs>>
Struct
All data is contained in a struct called sk_chorus
.
typedef struct sk_chorus sk_chorus;
struct sk_chorus {
<<sk_chorus>>
};
Setup and Cleanup
The function sk_chorus_new
and sk_chorus_del
will
dynamically allocate and free an instance of chorus
.
The sample rate sr
, and size of the delay line in units
of seconds delay
.
sk_chorus * sk_chorus_new(int sr, SKFLT delay);
void sk_chorus_del(sk_chorus *c);
sk_chorus * sk_chorus_new(int sr, SKFLT delay)
{
sk_chorus *c;
SKFLT *buf;
long sz;
c = malloc(sizeof(sk_chorus));
sz = floor(delay * sr);
buf = malloc(sizeof(SKFLT) * sz);
sk_chorus_init(c, sr, buf, sz);
return c;
}
void sk_chorus_del(sk_chorus *c)
{
free(c->buf);
free(c);
c = NULL;
}
sk_chorus_init
can be called directly if the memory
is intended to be managed externally. The buffer buf
and the buffer size sz
(in samples) is provided.
void sk_chorus_init(sk_chorus *c,
int sr,
SKFLT *buf,
long sz);
void sk_chorus_init(sk_chorus *c,
int sr,
SKFLT *buf,
long sz)
{
<<init>>
}
Setting Parameters
Rate
The rate of the LFO, in Hertz. Set it with
sk_chorus_rate
.
void sk_chorus_rate(sk_chorus *c, SKFLT rate);
void sk_chorus_rate(sk_chorus *c, SKFLT rate)
{
c->rate = rate;
}
This uses parameter caching to prevent coefficients from being needlessly updated.
SKFLT rate, prate;
prate
is set to be different from rate
so that
coefficients get updated on the first tick.
c->prate = -1;
sk_chorus_rate(c, 0.5);
Depth
Depth
controls how much the LFO modulates the delay line.
This is a value in range 0-1. Set it with sk_chorus_depth
.
void sk_chorus_depth(sk_chorus *c, SKFLT depth);
Because this is used with a delay line, some bounds checking is done here. If the value is not in the proper range, it could lead to segfaults.
void sk_chorus_depth(sk_chorus *c, SKFLT depth)
{
if (depth < 0) depth = 0;
if (depth > 1) depth = 1;
c->depth = depth;
}
SKFLT depth;
sk_chorus_depth(c, 1);
Mix
mix
controls the mix between the dry/wet signal. 1 is
all wet. 0 is all dry. 0.5 is a mix between the two. It
is helpful to have an all wet mix for chaining choruses
together.
void sk_chorus_mix(sk_chorus *c, SKFLT mix);
void sk_chorus_mix(sk_chorus *c, SKFLT mix)
{
c->mix = mix;
}
SKFLT mix;
sk_chorus_mix(c, 0.5);
Computing A Sample
A single sample is computed with sk_chorus_tick
, it will
take in a single sample as input, and return a single sample
of output.
SKFLT sk_chorus_tick(sk_chorus *c, SKFLT in);
SKFLT sk_chorus_tick(sk_chorus *c, SKFLT in)
{
SKFLT out;
SKFLT lfo;
SKFLT t;
SKFLT frac;
long p1, p2;
out = 0;
<<update_magic_circle>>
<<compute_lfo>>
<<calculate_delay>>
<<get_first_read_position>>
<<get_second_read_position>>
<<compute_fractional_component>>
<<interpolate_and_update>>
<<apply_lowpass_filter>>
<<write_input_sample>>
<<update_write_position>>
<<mix>>
return out;
}
Components
Sample Rate
A copy of the sample rate is needed to compute coefficients.
int sr;
c->sr = sr;
Delay
The delay line is buffer of floating-point samples.
The write position wpos
is stored in an integer. The
total buffer size sz
is used for bounds checking.
SKFLT *buf;
long sz;
long wpos;
c->buf = buf;
c->sz = sz;
c->wpos = sz - 1;
{
long i;
for (i = 0; i < sz; i++) c->buf[i] = 0;
}
For interpolation, a unit delay is used storing the previous
sample. This will be a variable called z1
, appropriately
labled since it is a 1-sample delay in the z-plane.
SKFLT z1;
c->z1 = 0;
1-pole lowpass filter
This one pole lowpass filter has 1-sample filter memory
of the previous sample ym1
(y minus 1), and alpha
coefficient a
.
SKFLT ym1;
SKFLT a;
c->ym1 = 0;
The a
filter coefficient is computed at init time to have
a cutoff frequency of 2.02kHz
. This cutoff value was found
empirically.
{
SKFLT b;
SKFLT freq;
freq = 2020;
b = 2.0 - cos(freq * (2 * M_PI / sr));
c->a = b - sqrt(b*b - 1);
}
Magic Circle Sinusoid
The magic circle requires 2 samples of memory stored
in mc_x
, in addition to a constant mc_eps
, where the
eps
is short for greek letter epsilon, the symbol
used in the original equation (TODO: create citation,
but see the link in overview for now).
SKFLT mc_x[2];
SKFLT mc_eps;
It's very important that the first sample input for the magic circle be set to be 1. This is the initial impulse which sets off the resonator.
c->mc_x[0] = 1;
c->mc_x[1] = 0;
c->mc_eps = 0;
The Process
Update magic circle.
if (c->prate != c->rate) {
c->prate = c->rate;
c->mc_eps = 2.0 * sin(M_PI * (c->rate / c->sr));
}
c->mc_x[0] = c->mc_x[0] + c->mc_eps * c->mc_x[1];
c->mc_x[1] = -c->mc_eps * c->mc_x[0] + c->mc_x[1];
Compute the LFO.
lfo = (c->mc_x[1] + 1) * 0.5;
Calculate the delay time t
(in samples).
t = (lfo * 0.9 * c->depth + 0.05) * c->sz;
Get first read position. Wrap around if needed.
p1 = c->wpos - (int)floor(t);
if (p1 < 0) p1 += c->sz;
Get second read position (used for linear interpolation). Wrap around if needed.
p2 = p1 - 1;
if (p2 < 0) p2 += c->sz;
Get fractional component from delay time.
frac = t - (int)floor(t);
Interpolate and update memory.
out = c->buf[p2] + c->buf[p1]*(1 - frac) - (1 - frac)*c->z1;
c->z1 = out;
Apply low pass filter.
c->ym1 = (1 - c->a) * out + c->a*c->ym1;
out = c->ym1;
Write input sample to buffer.
c->buf[c->wpos] = in;
Update write position. Wrap around if needed.
c->wpos++;
if (c->wpos >= c->sz) c->wpos = 0;
The final step is to mix the input signal with delay line signal.
out = c->mix * out + (1 - c->mix) * in;