8.12.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).

8.12.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.

8.12.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.

8.12.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.

8.12.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 full split (the same table appears in tutorial 08):

Per-voice vs per-orbit FX

Scope

Setters

Per-voice (independent per note)

gain, pan, speed, lpf, hpf, bpf, phaser, tremolo, compressor, shape, crush, coarse, djf, fm, vowel

Per-orbit (one shared bus per orbit)

room / roomsize, delay / delaytime / delayfeedback, chorus

8.12.7.5. Part E: probabilistic combinators — sometimes, degradeBy

sometimes(pat, fn) applies fn to a random ~50% of events; degradeBy(prob) randomly drops events with the given probability. Both turn a rigid loop into something that breathes:

let pat <- sometimes(s("bd sd hh cp"), @(x) => fast(x, 2.0lf))
// and: s("hh*8") |> degradeBy(0.4)   // thin a hi-hat run

8.12.7.6. Part F: cycle-conditional — every, when_cycle

every(n, pat_on, pat_off) plays pat_on on cycles 0, n, 2n, … and pat_off otherwise. Note daslang’s every takes two patterns, not a transform — build the variant explicitly:

let pat <- every(4, note("c4 e4 g4 c5", "sine") |> sustain(0.4) |> rev(),
                    note("c4 e4 g4 c5", "sine") |> sustain(0.4))

when_cycle(pat, cond, fn) applies fn only on cycles where cond(cycle) is true (cond is a lambda<(cycle:int):bool>):

let pat <- when_cycle(note("c4 e4 g4 c5", "sine") |> sustain(0.4),
                      @(c) => c % 2 == 0, @(x) => fast(x, 2.0lf))

8.12.7.7. Part G: reordering & gating — shuffle, scramble, mask

shuffle(n) cuts the cycle into n slices and plays them in a shuffled order (a permutation — each slice once); scramble(n) picks n slices at random (repeats allowed). mask(pat, bool_pat) gates events through a 1/~ pattern:

note("c4 d4 e4 f4 g4 a4 b4 c5", "sine") |> sustain(0.4) |> shuffle(8)
note("c4 d4 e4 f4", "sine") |> sustain(0.4) |> mask(s("1 ~ 1 ~"))