A control is a named, settable parameter of the synth. Controls are
event-rate sources: the engine delivers value changes as events, and only the
subgraph that depends on a changed control recomputes. Set them from Tzopilotl with the
audio_engine bridge — setControl(nodeID, controlID, value),
where controlID is the control's index in declaration order — or let the
app's UI generate widgets from them — the control's spec and kind determine the widget
(see Live Controls and Notebooks).
| Function | Kind | Description |
|---|---|---|
control(name, spec, chans = 1) | continuous | Ranged value; UIs render a slider. |
trigger(name, chans = 1) | trigger | Momentary event (0..1, init 0). A nonzero arrival is a one-shot edge — retrigger an envelope, reset a phase. UIs render a button. |
toggle(name, init = 0.0, chans = 1) | boolean | Latched 0/1 state. UIs render a toggle. |
choice(name, numChoices, init = 0, chans = 1) | select | Integer choice 0 .. numChoices-1 in unit steps. UIs render a number box. Pairs naturally with switch (§11). |
noteParam(name, spec, chans = 1) | — | Per-note parameter inside a voicer body (§12). |
gate() | — | The implicit per-note gate: noteParam("gate", …), high while the note is held. |
struct ControlSpec { lo Float, hi Float, init Float, warp ControlWarp }
enum ControlWarp { linear, exponential, step Float, signedSquare, cubed }
The spec gives the control's range, initial value, and warp — the
curve a UI uses to map slider position to value (exponential for frequencies
and gains, step(sz) for quantized values, signedSquare /
cubed for finer resolution near zero). The warp does not change what your graph
receives; it shapes the widget.
lag to avoid
zipper noise. This is the standard output-gain idiom used by the examples:
fn gainOutlet(x S) S {
let gain = control("gain",
ControlSpec{0.001, 1.0, 0.2, ControlWarp.exponential}) lag(0.1);
x * gain |> outlet
}
A control change is a message: it is bundled, scheduled, and delivered to one node.
For values that change continuously and matter to every playing synth — the
mouse position — the engine keeps a small shared-input table instead:
the app writes it in the background (~60× a second for the mouse), and any graph
reads it at audio rate with sharedIn. No messages are sent at all.
| Function | Description |
|---|---|
sharedIn(slot, rate = Rate.audio) |
Reads one table slot (a single f32 channel). Rate.init samples it
once at synth start. Slots 0..2 are the mouse (x, y, button); slots 3..15 are free
for your own values. |
mouseX(lo = 0.0, hi = 1.0, lagTime = 0.2) |
(common_ugens) Mouse x mapped linearly: lo at the left screen
edge, hi at the right. |
mouseY(lo = 0.0, hi = 1.0, lagTime = 0.2) |
(common_ugens) Mouse y mapped linearly: lo at the bottom of the
screen, hi at the top. |
mouseXExp(lo, hi, lagTime = 0.2)mouseYExp(lo, hi, lagTime = 0.2) |
(common_ugens) Exponential mapping, for frequencies and gains.
lo and hi must be nonzero and the same sign. |
mouseButton() |
(common_ugens) 1 while the primary button is held, else 0. Lag it to soften the edges. |
-- theremin: x = pitch, y = amplitude; every instance follows the mouse
fn theremin() S =
sinosc(mouseXExp(110.0, 1760.0)) * mouseY(0.0, 0.5) |> outlet;
From the language side, setSharedInput(slot, value) writes a free slot and
getSharedInput(slot) reads one back (audio_engine module) — a lightweight
channel for any continuously varying value that would otherwise be a stream of
setControl messages. Two things to keep in mind: shared-input changes are
not sample-accurately scheduled (use controls when timing must be exact), and the
raw value steps at the writer's update rate, so smooth it with lag as the
mouse ugens do.