Contents

Pattern Modulation

So far patterns have driven note pitches. They can also be used as values: feeding any DSP slot from a pattern, modulating per-event properties, and combining via arithmetic.

Patterns are values

A pattern is a stream of events stepping through values over time. When you write:

osc("sin", n"c4 e4 g4")

the compiler takes the pattern’s frequency buffer and feeds it to osc’s freq slot. Patterns coerce to scalar Signals automatically.

Numeric patterns with v”…”

Sometimes you want raw numbers, not mtof’d notes. Use v"…":

// Three frequencies, no mtof, just the literal Hz values
osc("sin", v"<220 440 880>") |> out(%, %)

// Filter cutoff sweeping through three values per cycle
sig = osc("saw", 440)
lp(sig, v"<200 800 2000>", 0.7) |> out(%, %)

// Amplitude scrubber
osc("saw", 220) * v"<0.2 0.5 1.0 0.5>" |> out(%, %)

Atoms in v"…" must be numeric; v"c4" is a parse error.

Per-event modulation

Pattern-valued bend depth: each note bends by the corresponding pattern value.

n"c4 e4 g4" |> bend(%, v"<0 0.5 -0.5>") as e
  |> osc("sin", e.freq + e.bend * 12)
  |> out(%, %)

The same shape works for aftertouch() and dur():

n"c4 e4 g4 b4" |> aftertouch(%, v"<0 0.25 0.5 1.0>")  // crescendo
n"c4 e4 g4"    |> dur(%, v"<0.25 0.5 1.0>")           // pattern-driven note length

Constant args still work; bend(notes, 0.5) is unchanged.

Custom-property accessor

A note can carry arbitrary record-suffix keys; the binding as e exposes each as a Signal:

n"c4{cutoff:0.3} e4{cutoff:0.7} g4{cutoff:0.5}" as e
  |> osc("saw", e.freq)
  |> lp(%, 200 + e.cutoff * 4000)
  |> out(%, %)

This is more compact than calling bend() / aftertouch() separately for each property.

Scalar arithmetic

Patterns combine naturally with numbers and signals:

v"<60 64 67>" + 12          // still a Pattern (Pattern + Number)
v"<0 0.5>" + sig            // Signal (Pattern + Signal coerces)
n"c4 e4 g4" + v"<0 0 12>"   // Pattern + Pattern (combined)

When coerce fails

Polyphonic chord patterns can’t silently degrade to one voice:

osc("sin", c"Am")     // ❌ E160: chord pattern in scalar slot

The fix is to consume them with poly(), which fans out per voice:

c"Am C G Em" |> poly(4, fn (e) -> osc("saw", e.freq) * ar(e.trig)) |> out(%, %)

Sample patterns route through SAMPLE_PLAY and produce audio. Pipe them to out() directly:

s"bd ~ bd ~" |> out(%, %)

scalar(): explicit cast

scalar(p) is the explicit form of auto-coerce. Useful for clarity, or for arithmetic outside a DSP slot:

let detune = scalar(v"<0 -10 10 0>")
osc("saw", n"c4 e4 g4 b4" + detune)

scalar() errors on sample / polyphonic patterns (E161). It’s idempotent on Signals: scalar(scalar(p)) is safe.