Skip to content

Signals & Time

A signal is a value that moves. Anything you set with << — an effect parameter, a track’s volume or pan, a send level — can take a signal instead of a number, and the parameter follows it. In DAW terms, one operator covers both the modulation matrix and the automation lane. This page is about how a signal keeps time, because that decides what you hear when you re-run a line, pause, or seek.

The engine evaluates a bound signal as the graph runs, not once per event, so a parameter glides instead of stepping. Signal arithmetic — Sine(1) + Tri(3) * 0.5, -Sine(1) — runs sample by sample with no allocation, so layering and nesting modulators costs little.

A signal’s numbers are counted in a unit: its timebase. By default the unit is one cycle, so Sine(4) sweeps four times per cycle and an LFO stays locked to the tempo when the tempo changes. A change mid-play bends its speed and never jumps its phase.

.set_timebase() sets another unit, given as a duration. Musical durations follow the tempo: Sine(1).set_timebase(note(1, 8).dotted()) sweeps once per dotted eighth, Saw(1).set_timebase(cycles(2)) once every two bars. Seconds do not: Sine(2).set_timebase(seconds(1)) runs at 2 Hz at any tempo, like an LFO with its sync switched off. The same unit applies to everything the signal counts, so an automation curve’s breakpoint times and a signal() pattern’s steps are in its timebase too.

The unit belongs to the signal, not to the number. That is the difference from converting: a rate computed from the tempo of the moment goes stale when the tempo changes, and a timebase never does.

A signal has one clock, and it belongs to the signal — not to any particular binding. Its local time starts at zero on the next cycle boundary after you bind it, and binding it to the same parameter again restarts it. That’s what you want for one-shots: re-running a signal_ramp(0, 1, 4) line restarts the ramp cleanly.

A restart lands on the grid, so it can be up to a cycle away — and until it lands, the signal keeps running exactly as it was. Nothing drops out or jumps to the bottom of its range while it waits, the way a launch-quantized clip keeps playing until the downbeat. A signal you have just written, with no phase to carry yet, waits at its start value.

Because the clock lives on the signal, everything reading that signal sees the same phase — every parameter it drives, and scope() too. Point one LFO at two parameters and they stay locked together. Binding a running signal to one more parameter doesn’t restart it; rebinding it where it is already bound does, and every parameter it drives restarts with it. When you want two modulators that drift independently, make two signals.

Two settings on the signal change this. .continuous() puts it on the global clock instead, so you can change its speed without its phase jumping. .set_retrigger() resets its phase every period you give it: .set_retrigger(cycles(1)) on every cycle, .set_retrigger(beats(1)) on every beat, .set_retrigger(note(1, 8).dotted()) every dotted eighth. Without one, a signal never resets. Like .set_timebase(), both configure the signal itself, so they hold wherever it is used and reach it through .range() and other wrappers in any order.

The transport moves signal time, binding restarts it

Section titled “The transport moves signal time, binding restarts it”

A restart is a binding gesture — << is the note-on. The transport does not retrigger anything: PAUSE freezes signal time along with the playhead, and the PLAY after it picks every signal up on exactly the phase it froze on.

Moving the playhead is different. Signal time is the playhead, so SEEK takes it along — every signal restarts at the position you seeked to, right there rather than a cycle later. STOP is a seek to 0 and behaves the same way, including while the transport is stopped: SEEK 8 then PLAY starts at cycle 8 with every signal already positioned there.

The upshot is that phase depends only on where the playhead is, never on how it got there. Seek to the same place twice and you hear the same thing — which is also what makes a bounce reproducible.

A modulator loops, an automation lane is placed

Section titled “A modulator loops, an automation lane is placed”

automation(#[0, 0], #[4, 1]) bound with << is a signal like any other: its breakpoint times are measured from the moment its clock starts. It is a shape you attach to a parameter.

An automation curve written into a section lane is placed instead: song[drums].volume.at(8, 8) << curve sits at cycles 8–16 of the song, in absolute cycles, exactly like a clip on an arrangement track. It is bound the same way whether the song is playing or stopped, it masks the curve beneath it, and when the song reaches cycle 8 the swell is there. That is the difference between a modulator you perform and a lane you arrange.

A few parameters take a plain number only, because changing them is not a change of level but a change to the graph. track.set_delay(ms) — the calibration for outboard gear — re-plans the graph’s latency compensation when it is set, so it cannot be driven by a signal.