7.10.7. STRUDEL-07 — Per-Voice FX & Combinators

This tutorial covers the transforming combinators — those that take a @(Pattern) => Pattern lambda and use it to derive a second copy of the input — and the daslang-specific per-voice FX group (phaser, tremolo, compressor, shape, crush).

7.10.7.1. Part A: jux — stereo splitting via a transform

jux(pat, fn) plays the original pattern in the left channel and fn(pat) in the right. The classic move is to reverse the right side, producing the trademark live-coding stereo wobble:

let pat <- jux(note("c4 e4 g4 c5", "sine") |> sustain(0.4), @(p) => rev(p))

Or transpose the right side up a fifth for instant harmony:

let pat <- jux(note("c4 e4 g4 c5", "sine") |> sustain(0.4),
               @(p) => transpose(p, 7.0))

Any function Pattern Pattern works as the transform — including combinators built out of stack, cat or fast.

7.10.7.2. Part B: off — canon, delayed transformed copy

off(pat, time, fn) plays the original AND a copy of fn(pat) delayed by time cycles. With time = 0.25 and an octave-up transpose you get a textbook canon at the octave:

let pat <- off(note("c4 e4 g4 b4", "sine") |> sustain(0.4), 0.25lf,
               @(p) => transpose(p, 12.0))

The delayed voice obeys whatever the transform does to it — try @(p) => fast(p, 2.0lf) to layer a faster echo, or rev for a retrograde answer.

7.10.7.3. Part C: chunk — apply a transform to one slice at a time

chunk(pat, n, fn) divides each cycle into n equal slices and applies fn to slice 0 on cycle 0, slice 1 on cycle 1, and so on. Use it to add evolving variation without rewriting the pattern:

let pat <- chunk(note("c4 e4 g4 c5", "sine") |> sustain(0.4), 4,
                 @(p) => fast(p, 2.0lf))

Each beat in turn doubles in speed for one cycle, then settles back.

7.10.7.4. Part D: per-voice FX (a daslang divergence)

Most live-coding systems treat phaser, tremolo, compressor, shape, crush as effects on a shared bus. In daslang these are per-voice — each playing voice carries its own FX chain, applied before the voice mix is summed into the orbit. Two simultaneous notes in a single stack can therefore use entirely different FX parameters without any cross-talk:

let pat <- stack([
    note("c4", "sawtooth") |> sustain(1.0) |> phaser(0.5) |> gain(0.4),
    note("g4", "sawtooth") |> sustain(1.0) |> phaser(2.0) |> gain(0.4)
])

Above, the C voice carries a slow phaser sweep while the G voice carries a fast one. On a shared-bus system both voices would have to share the same rate.

The per-voice setters available today: lpf, hpf, phaser / ph, tremolo / trem, compressor, shape, crush, coarse. The shared-bus setters (room, delay, chorus) are covered in the next tutorial.