All effect parameters, ranges, and implementation notes pulled directly from
the source headers in audio/.
Four-stage allpass filter chain with LFO modulation and feedback. Each stage is a biquad allpass with the same ω₀ swept by the LFO — this creates the phase notches that give a phaser its sound. Uses a triangle LFO (not sine) for the sweep.
| pot | range | unit | description |
|---|---|---|---|
| LFO | 25 – 2000 | ms | Sweep period. 25ms = very fast (40 Hz), 2s = slow |
| Feedback | 0 – 0.75 | — | How much of the output feeds back into the first stage |
| Freq | 220 – 6460 | Hz | Center frequency of the allpass sweep |
| Q | 0.25 – 2 | — | Width of the phase notches. Higher Q = narrower, sharper |
// From phaser_step() — the actual per-sample loop
float phaser_step(float in)
{
float lfo = lfo_step(&phaser.lfo, lfo_triangle);
float freq = pow2(lfo * phaser.octaves) * phaser.center_f;
_biquad_allpass_filter(&phaser.coeff, _w0(freq), phaser.Q);
float out = in + phaser.feedback * phaser.s3[0];
out = biquad_step_df1(&phaser.coeff, out, phaser.s0, phaser.s1);
out = biquad_step_df1(&phaser.coeff, out, phaser.s1, phaser.s2);
out = biquad_step_df1(&phaser.coeff, out, phaser.s2, phaser.s3);
return limit_value(in + out);
}
Comb filter effect via a short LFO-modulated delay line. Based on the MIT-licensed DaisySP library (Electrosmith), which itself is based on Soundpipe by Paul Batchelor. The delay buffer is 1024 samples (~21 ms at 48 kHz), supporting delays up to 4 ms.
| pot | range | unit | description |
|---|---|---|---|
| Freq | 0 – 10 | Hz | LFO rate (pot² scaled, so low end is finer) |
| Delay | 0 – 4 | ms | Base delay time |
| Depth | 0 – 1 | — | LFO modulation amount on the delay time |
| Feedback | 0 – 1 | — | Regeneration — comb resonance intensity |
float flanger_step(float in)
{
float d = 1 + flanger.delay * (1 + lfo_step(&flanger.lfo, lfo_sinewave)
* flanger.depth);
float out = sample_array_read(d, &flanger.idx, flanger.samples);
sample_array_write(limit_value(in + out * flanger.feedback),
&flanger.idx, flanger.samples);
return (in + out) / 2;
}
Output is the average of dry and wet — no separate mix pot.
Feedback capped implicitly by limit_value().
A ring-buffer echo with smoothed delay, variable depth, and feedback.
The buffer is 65536 samples — at 48 kHz that is about 1.37 seconds.
Delay target is smoothed sample-by-sample (linear()) to
avoid clicking when the pot changes.
| pot | range | unit | description |
|---|---|---|---|
| Delay | 0 – 1000 | ms | Echo delay time. Smoothed to prevent clicks on change |
| Depth | 0 – 1 | — | Mix between dry signal and echo output |
| Feedback | 0 – 1 | — | How much echo feeds back (trail length) |
// 65536 samples ≈ 1.37s at 48kHz
float array[65536];
float echo_step(float in)
{
echo.delay = linear(0.001, echo.delay, echo.target_delay); // smooth
float out = sample_array_read(1 + echo.delay, &echo.idx, echo.array);
sample_array_write(limit_value(in + out * echo.feedback), &echo.idx, echo.array);
return linear(echo.depth, in, out);
}
Gain stage with HPF and LPF biquad shaping, followed by a wavefolding clipper.
When the signal exceeds the output level ceiling, it folds back rather than
hard-clipping — the fold() function bounces the excess off the ceiling
and floor repeatedly until it lands in range.
| pot | range | unit | description |
|---|---|---|---|
| Boost | 0 – 40 | dB | Pre-filter gain. 40 dB = 100× amplitude |
| Level | −40 – 0 | dB | Output ceiling. Below this the signal passes clean; above it folds |
| Basscut | 10 – 200 | Hz | High-pass filter cutoff — rolls off bass before boost |
| Highcut | 1 – 20 | kHz | Low-pass filter cutoff — removes harshness after boost |
| Mix | 0 – 1 | — | Dry/wet blend |
// Wavefolder — bounces signal between +level and -level
static float fold(float in, float level)
{
float fold_scale = 0.5;
for (;;) {
float over = (in - level) * fold_scale;
in = level - over;
if (in >= -level) return in;
over = (in + level) * fold_scale;
in = -level - over;
if (in <= level) return in;
}
}
Envelope follower compressor. The envelope tracks peak absolute value with
separate attack and release time constants. When the envelope exceeds the
threshold, the gain multiplier is reduced by
(level / env) ^ (1 − 1/ratio) and then smoothed to prevent clicks.
| pot | range | unit | default | description |
|---|---|---|---|---|
| Level | −40 – 0 | dB | −20 dB | Compression threshold |
| Attack | 2 – 100 | ms | ~15 ms | Envelope rise time |
| Release | 50 – 500 | ms | ~150 ms | Envelope fall time |
| Ratio | 1 – 20 | :1 | ~4.8 | Compression ratio above threshold |
| Boost | 0 – 24 | dB | ~6 dB | Makeup gain after compression |
// target = (level / env) ^ (1 - 1/ratio)
// implemented as pow2(log2(level/env) * (1 - 1/ratio))
static inline float mypow(float a, float b)
{
return pow2(log2f(a) * b);
}
// Gain smoothed to prevent clicks when compression changes suddenly
compressor.compression = linear(0.01f, compressor.compression, target);
Parametric EQ built from biquad band types: peaking, low shelf, high shelf, LPF, HPF. Coefficients are computed from the Audio EQ Cookbook formulas — ω₀ = 2π·f/Fs, α = sin(ω₀)/(2Q), normalized by a₀ = 1 + α. The peaking filter gets a small optimization: since a₁ = b₁ in a peaking biquad, one multiply is eliminated in the DF1 update.
// Peaking EQ — a1 == b1, saves one multiply per sample
static inline float biquad_peaking_step_df1(
const struct biquad_coeff *c, float in, float x[2], float y[2])
{
float out = c->b0*in + c->b1*(x[0]-y[0]) + c->b2*x[1] - c->a2*y[1];
x[1] = x[0]; x[0] = in;
y[1] = y[0]; y[0] = out;
return out;
}
A 32-bit phase accumulator split into a 2-bit quarter index and 30-bit phase.
Supports triangle, sinewave, and sawtooth shapes.
The quarter flip on overflow inverts direction without a branch.
The sine lookup table (quarter_sin[]) is pre-generated at build time
by scripts/quarter_sine.py with linear interpolation between entries.
| shape | description |
|---|---|
| triangle | Linear ramp 0→1→0→−1→0. Used by phaser. |
| sinewave | Interpolated quarter-sine lookup. Used by flanger. |
| sawtooth | Linear 0→1 ramp (upper 32 bits of counter, normalized). |
// F_STEP = 2^32 / 48000 ≈ 89478.48
// freq_hz = lfo_step / F_STEP
// period_ms = 1000 * F_STEP / lfo_step
#define F_STEP (TWO_POW_32 / SAMPLES_PER_SEC)
// Set LFO by period in milliseconds
void set_lfo_ms(struct lfo_state *lfo, float ms) {
if (ms < 0.1) ms = 0.1; // cap at 10 kHz
set_lfo_step(lfo, 1000 * F_STEP / ms);
}