Cookbook

An effects chain

Several processors wired into one graph — and what the wires mean.

Anyone whose patch has outgrown a single processor.

An effects chain is easier to understand when each processor has one job. Here the input passes through drive, filtering, delay and tremolo. The graph describes those connections; each processor still owns its own calculations and memory.

Read the graph at the bottom first. It is a map of the patch. Then return to the processor definitions to inspect the stages that interest you.

Controls exposed by the graph#

ParameterDefaultRangeDestination
drive2.01–12dist.amount: gain into the soft clipper.
cutoff2,400 Hz60–7,500 Hzflt.cutoff: filter frequency.
resonance0.450–0.95flt.resonance: emphasis near cutoff.
echo0.30–1dly.mix: amount of delayed audio.
wobble0.350–1trem.depth: tremolo intensity.

The graph's in and out are mono. Unconnected internal parameters keep their defaults, so you can expose a small set of useful controls without copying every internal knob to the host.

The complete patch#

An effects chainPlayground
processor Drive
{
    input  stream float in;
    param  float amount = 2.0f [1.0f, 12.0f] smooth 20.0f;
    output stream float out;

    void main()
    {
        loop
        {
            let x = in * amount;
            let c = x > 1.0f ? 1.0f : (x < -1.0f ? -1.0f : x);
            let odd = c * 1.5f - c * c * c * 0.5f;
            out <- (odd + c * c * 0.12f) * 0.7f;
            advance();
        }
    }
}
processor Filter
{
    input  stream float in;
    param  float cutoff = 1200.0f [60.0f, 7500.0f] smooth 15.0f;
    param  float resonance = 0.6f [0.0f, 0.95f];
    output stream float out;

    float lp = 0.0f;
    float bp = 0.0f;

    void main()
    {
        let sr = processor.frequency;
        loop
        {
            let fc = 6.2831853f * min(cutoff, sr * 0.18f) / sr;
            let q = 1.02f - resonance;
            lp = lp + fc * bp;
            let hp = in - lp - q * bp;
            bp = bp + fc * hp;
            out <- lp;
            advance();
        }
    }
}
processor Delay
{
    input  stream float in;
    param  float time = 0.25f [0.02f, 0.33f];
    param  float feedback = 0.4f [0.0f, 0.85f];
    param  float mix = 0.3f [0.0f, 1.0f] smooth 30.0f;
    output stream float out;

    float buf[32768];
    int   pos = 0;

    void main()
    {
        let sr = processor.frequency;
        loop
        {
            let d = clamp(int(time * sr), 1, 32767);
            let readPos = pos - d;
            let echo = buf[readPos];
            buf[pos] = in + echo * feedback;
            pos = pos + 1;
            out <- in * (1.0f - mix) + echo * mix;
            advance();
        }
    }
}
processor Tremolo
{
    input  stream float in;
    param  float rate = 4.5f [0.05f, 20.0f];
    param  float depth = 0.5f [0.0f, 1.0f] smooth 25.0f;
    output stream float out;

    float phase = 0.0f;

    void main()
    {
        let sr = processor.frequency;
        loop
        {
            let step = rate / sr;
            let p = phase + step;
            phase = p > 1.0f ? p - 1.0f : p;
            let lfo = 0.5f + 0.5f * sin(6.2831853f * phase);
            out <- in * (1.0f - depth + depth * lfo) * 0.7f;
            advance();
        }
    }
}

graph FxChain
{
    input  stream float in;
    param float drive     = 2.0f    [1.0f, 12.0f];
    param float cutoff    = 2400.0f [60.0f, 7500.0f];
    param float resonance = 0.45f   [0.0f, 0.95f];
    param float echo      = 0.3f    [0.0f, 1.0f];
    param float wobble    = 0.35f   [0.0f, 1.0f];

    output stream float out;
    node dist = Drive;
    node flt  = Filter;
    node dly  = Delay;
    node trem = Tremolo;

    connection
    {
        in       -> dist.in;
        dist.out -> flt.in;
        flt.out  -> dly.in;
        dly.out  -> trem.in;
        trem.out -> out;

        drive     -> dist.amount;
        cutoff    -> flt.cutoff;
        resonance -> flt.resonance;
        echo      -> dly.mix;
        wobble    -> trem.depth;
    }
}

Walk through the graph#

in -> dist.in sends the incoming sample to the drive stage. Each subsequent wire passes one stage's output into the next. The last wire, trem.out -> out, returns the result to the host. Pole schedules the stages from these wires; the order of the node declarations is not the execution order.

The other wires carry controls. echo -> dly.mix connects the graph's friendly public name to the delay's internal parameter. It does not create another audio path. The graph is the public interface, while its nodes are the implementation.

Try changing the order#

Move the tremolo before the delay by rewriting the audio connections. After the delay, tremolo changes the loudness of both the source and its existing echoes together. Before the delay, the echoes repeat the loudness changes already recorded in the buffer. The same four stages make a different effect because the signal reaches them in a different order.

If the result is too loud: remember that gains multiply in a serial chain, while multiple wires entering one input add. Leave headroom between stages, especially around resonance and feedback.

Next: graphs covers the rules this file demonstrates.