7.12.9. STRUDEL-09 — Signals & Modulation

So far every effect parameter has been a constant — lpf(800), gain(0.5). This tutorial introduces signals: patterns whose Haps carry numbers instead of sounds, and the pattern-valued setters that let a signal drive any effect parameter over time. A filter that sweeps, a gain that swells, a pan that drifts — all of it is one idea: feed a signal where you used to write a constant.

7.12.9.1. A signal is just a pattern

A signal is an ordinary Pattern; the difference is what its Haps carry. Query one by hand and you get a number, not a drum hit. sine() is a continuous signal — one query window yields one Hap holding the wave’s value at the start of that window:

let sig <- sine()
var h <- invoke(sig, TimeSpan(start = 0.0lf, stop = 1.0lf))
print("value = {h[0].value.note}\n")
// output: value = 0.5

At phase 0 a strudel sine sits at its midpoint, 0.5 — it sweeps 0 -> 1 -> 0 across each cycle. run(n) is a discrete signal: n equal steps valued 0, 1, ... n-1:

var hr <- invoke(run(4), TimeSpan(start = 0.0lf, stop = 1.0lf))
// four haps: 0  1  2  3   (one per quarter cycle)

The full signal zoo: continuous sine, cosine, saw, isaw, tri, itri, square, perlin, rand (all running 0..1); discrete run(n) and irand(n).

7.12.9.2. range: remap into a useful span

The continuous signals all run 0..1. range(lo, hi) stretches that into the span you actually want — a cutoff in Hz, a gain, a pan position. run() can drive pitch directly; run(8) |> add(48) is a chromatic ramp from MIDI 48 (C3) upward:

let pat <- run(8) |> add(48.0) |> sound("sine") |> sustain(0.3)
play(pat, 0.5lf)

7.12.9.3. Pattern-valued setters: a moving filter

This is the headline. Every per-event setter (lpf, gain, pan, speed, …) has an overload that takes a Pattern instead of a constant. Feed it a signal and the parameter tracks that signal over time:

let pat <- (
    atom("sawtooth") |> note(48.0) |> sustain(1.0)
    |> lpf(sine() |> range(200.0, 4000.0) |> slow(4.0lf))
)
play(pat, 0.5lf, 8.0lf)

sine() |> range(200, 4000) |> slow(4) sweeps the cutoff from 200 Hz to 4 kHz and back over four cycles — the classic filter-sweep pad. Note the surrounding parens: a multi-line pipe chain at statement level must be wrapped in (...).

7.12.9.4. Modulating gain and pan

The same trick works for any setter. A slow sine on gain is a smooth volume swell; a slow sine on pan drifts the sound across the stereo field. Stack them and one sustained tone breathes and moves:

let pat <- (
    atom("sine") |> note(48.0) |> sustain(1.0)
    |> gain(sine() |> slow(2.0lf))
    |> pan(sine() |> slow(4.0lf))
)
play(pat, 0.5lf, 8.0lf)

7.12.9.5. The noisy signals: perlin and rand

rand() is white-noise random — a fresh value every query, good for scattering pan or velocity. perlin() is smooth/organic random — it wanders instead of jumping, ideal for slow, living modulation:

// perlin drives a slow, unpredictable gain swell on a pad
let pad <- (
    atom("sawtooth") |> note(48.0) |> sustain(1.0) |> lpf(800.0)
    |> gain(perlin() |> range(0.1, 0.8) |> slow(4.0lf))
)
// rand scatters each note to a random stereo position
let mel <- note("c4 e4 g4 a4 g4 e4", "sine") |> sustain(0.5) |> pan(rand())

7.12.9.6. Where next

You now know:

  • A signal is a Pattern whose Haps carry numbers (in the note field)

  • range(lo, hi) remaps a 0..1 signal into any span

  • Every setter has a pattern-valued overload — feed it a signal to make the parameter move over time

  • slow() stretches a signal’s motion across several cycles

  • perlin wanders smoothly; rand jumps

Tutorial 10 covers ADSR envelopes — shaping the amplitude of each note over its lifetime, the other half of “sound that changes over time”.

See also

Full source: tutorials/daStrudel/daStrudel_09_signals_modulation.das

Previous tutorial: STRUDEL-08 — Effects & Filters

Next tutorial: STRUDEL-10 — ADSR & Envelope Shaping

Related: STRUDEL-08 — Effects & Filters — the constant-valued setters these signals replace