7.10.8. STRUDEL-08 — Effects & Filters

Strudel splits effects into two groups, distinguished by where they live in the signal graph:

Group

Setters

Lifetime

Per-orbit bus FX

room, delay, chorus

one shared instance per orbit; voices send wet / dry into it

Per-voice FX

lpf, hpf, phaser, tremolo, compressor, shape, crush, coarse

baked into each individual voice; runs before mixdown into the orbit

This split lets you give each layer of a stack its own reverb and delay character (per-orbit), while still personalising every note’s filter and modulation (per-voice).

7.10.8.1. Part A: per-voice filters — lpf and hpf

lpf(freq) cuts harmonics above freq Hz; hpf(freq) cuts below. Compare a muffled sawtooth (lpf 200) against a thin one (hpf 2000):

// Dark, all-bass:
let pat_lpf <- atom("sawtooth") |> note(48.0) |> sustain(1.0) |> lpf(200.0)

// Thin, all-treble:
let pat_hpf <- atom("sawtooth") |> note(48.0) |> sustain(1.0) |> hpf(2000.0)

Each voice carries its own filter coefficients, so two stacked voices can be filtered differently.

7.10.8.2. Part B: per-orbit reverb — room and roomsize

room(amount) sets the wet send to the orbit’s reverb bus; roomsize(N) controls the room dimensions. Both live on the bus, so all voices on the same orbit share one reverb instance:

let pat <- note("c4 ~ c4 ~", "sine") |> attack(0.001) |> decay(0.15) |> sustain(0.0) |> release(0.01) |> room(0.8) |> roomsize(2.0)

7.10.8.3. Part C: per-orbit delay — tempo-aware by default

delay(amount), delaytime(seconds) and delayfeedback(0..1) drive the orbit’s delay line. delaytime defaults to a tempo-aware 3/16 cycle (resolved from delaysync and the current cps), so plain delay(0.5) already locks to the tempo even without a delaytime setter. Tutorial 09 covers the resolver in detail.

let pat <- note("c4 ~ e4 ~ g4 ~ c5 ~", "sine") |> attack(0.001) |> decay(0.15) |> sustain(0.0) |> release(0.01) |> delay(0.5) |> delaytime(0.5) |> delayfeedback(0.4)

7.10.8.4. Part D: per-voice phaser

phaser(rateHz) sweeps a notch through the spectrum at the given rate. Because it is per-voice, every note carries its own phase — two stacked voices stay independent, which is not how shared-bus live-coding systems behave.

let pat <- run(8) |> scale("D:pentatonic") |> sound("sawtooth") |> sustain(1.0) |> release(0.5) |> phaser(2.0)

7.10.8.5. Part E: orbits — give each layer its own bus FX

Different orbits get independent bus FX instances. Below, orbit 0 sits in a tight small room while orbit 1 sits inside a cathedral:

let pat <- stack([
    note("c4 ~ e4 ~", "sine") |> attack(0.001) |> decay(0.15) |> sustain(0.0) |> release(0.01) |> room(0.8) |> roomsize(0.5) |> orbit(0),
    note("c3", "triangle") |> attack(0.5) |> decay(0.2) |> sustain(0.6) |> release(0.5) |> room(0.8) |> roomsize(8.0) |> orbit(1)
])

The pluck on orbit 0 sounds tight; the pad on orbit 1 swims in reverb — neither bleeds into the other.