Phaser PSR

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.

potrangeunitdescription
LFO25 – 2000msSweep period. 25ms = very fast (40 Hz), 2s = slow
Feedback0 – 0.75How much of the output feeds back into the first stage
Freq220 – 6460HzCenter frequency of the allpass sweep
Q0.25 – 2Width 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);
}

Flanger FLN

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.

potrangeunitdescription
Freq0 – 10HzLFO rate (pot² scaled, so low end is finer)
Delay0 – 4msBase delay time
Depth0 – 1LFO modulation amount on the delay time
Feedback0 – 1Regeneration — 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().

Echo ECHO

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.

potrangeunitdescription
Delay0 – 1000msEcho delay time. Smoothed to prevent clicks on change
Depth0 – 1Mix between dry signal and echo output
Feedback0 – 1How 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);
}

Boost BO

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.

potrangeunitdescription
Boost0 – 40dBPre-filter gain. 40 dB = 100× amplitude
Level−40 – 0dBOutput ceiling. Below this the signal passes clean; above it folds
Basscut10 – 200HzHigh-pass filter cutoff — rolls off bass before boost
Highcut1 – 20kHzLow-pass filter cutoff — removes harshness after boost
Mix0 – 1Dry/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;
    }
}

Compressor CMPR

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.

potrangeunitdefaultdescription
Level−40 – 0dB−20 dBCompression threshold
Attack2 – 100ms~15 msEnvelope rise time
Release50 – 500ms~150 msEnvelope fall time
Ratio1 – 20:1~4.8Compression ratio above threshold
Boost0 – 24dB~6 dBMakeup 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);

EQ

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;
}

LFO

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.

shapedescription
triangleLinear ramp 0→1→0→−1→0. Used by phaser.
sinewaveInterpolated quarter-sine lookup. Used by flanger.
sawtoothLinear 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);
}