Tzopilotl
Docs
GitHub

Tzopilotl — Live Controls & Notebooks

Building performance surfaces with the ui module, and working with notebook documents in the app.

1. Overview

The app is both a code editor and a host for live control surfaces. The ui module creates widgets — sliders, XY pads, meters, scopes, spectrum analyzers — from code, and binds them to running audio. There are two places widgets can live:

The guiding rule of the whole design: code creates behavior; the document stores state. Widgets are always created and bound by running code. What the document persists is the widgets' names, ranges, and current values — never their bindings. Re-running the code is what brings a loaded document back to life (see §4.5).

Two sources of truth, one rule. If you wonder “where does X live?”: behavior (which node a slider drives, what a callback does) lives in code; state (the slider's position, the cell order) lives in the document.

2. Quick Start

Paste this into an editor tab and evaluate it (Cmd+Shift+Enter evaluates the whole file):

-- A synth with two declared controls
import synthdef.*;
import synthc.compile.*;
import common_ugens.*;
import audio_engine.*;
import ui.*;

fn demo() S {
    let freq = control("freq", ControlSpec { lo: 50.0, hi: 2000.0, init: 220.0, warp: ControlWarp.exponential });
    let amp  = control("amp",  ControlSpec { lo: 0.0,  hi: 1.0,    init: 0.2,   warp: ControlWarp.linear });
    freq sinosc(0) mul(amp) outlet
}

demo defSynthX("demo") await;
let node = play("demo");

-- One line: a slider per control, named, ranged, warped, and bound
-- from the synthdef's own ControlSpecs.
controls(node);

-- And an oscilloscope on its output.
scope("out", node);

A window named demo appears with a freq slider (exponential, 50–2000 Hz) and an amp slider — widgets derived from a synthdef default into a panel named after the def. The scope, created by hand without a panel, lands in the default Controls window. Drag the sliders — the sound follows immediately, even while other code is evaluating.

3. The ui Module

import ui.*; to use it. All constructors are idempotent upserts keyed by (panel, name): re-running the same code adopts the existing widget — it keeps its current value and refreshes its range/spec — and clears stale bindings so the code can attach fresh ones. Running your setup code twice never duplicates widgets; this is also how saved documents come back to life.

3.1 Widgets

ConstructorWidgetNotes
slider(name, spec)horizontal sliderspec is a synthdef.ControlSpec; the warp (linear / exponential / step / signedSquare / cubed) shapes the mapping
slider(name, lo, hi, init)horizontal sliderlinear convenience form
range(name, spec)range slidertwo ends (lo, hi) on one scale; sweep, slide, and end-adjust gestures in §3.3
range(name, lo, hi, initLo, initHi)range sliderlinear convenience form
number(name, init)drag/type number box
button(name)momentary buttonsends 1 while held, 0 on release (a gate)
toggle(name, init)checkboxsends 1 / 0
xy(name, xspec, yspec)2-D padtwo ControlSpecs; bind each axis separately
meter(name, node)level meterrms bar + peak marker, −60..0 dB; reads an engine tap
scope(name, node)oscilloscopeall channels of a node outlet, zero-cross triggered; the ch button cycles all / single channel (stacked lanes in “all” view share one trigger, preserving phase)
spectrum(name, node)spectrum analyzermagnitudes in dBFS on a log frequency axis (20 Hz–20 kHz), 2048-point Hann FFT; peaks fall ~21 dB/s so they stay readable. A full-scale sine reads 0 dB
masterMeter(name)master level meterthe master output bus — what the device actually plays
masterScope(name)master oscilloscopeas scope, on the master bus
masterSpectrum(name)master spectrumas spectrum, on the master bus
multislider(name, n, spec?)N vertical barsdrag across to paint; values mapped through the spec (default 0..1)
matrix(name, rows, cols)toggle gridclick cells on/off; a step-sequencer surface (values are 0/1, row-major)
buttonMatrix(name, rows, cols, labels?)labeled momentary gridmomentary buttons, like button(): a cell reads 1 while held, 0 on release (both edges dispatch); dragging onto another cell releases the held one and presses the new one. Per-cell labels are optional at construction and settable any time with setLabels(w, labels) / setLabel(w, row, col, label); read them back with labels(w). Cell changes deliver per-cell through onCell and whole-array through onChangeVec. Not stored by presets (its at-rest state is all zeros)
toggleMatrix(name, rows, cols, labels?)labeled toggle gridtoggle buttons, like toggle(): click flips a cell (0/1, row-major). Same labels and callbacks as buttonMatrix; values are stored by presets and saved with the document
pianoRoll(name, beats = 4.0, edo = 12)grid note editor16th grid; click to add/remove notes; rollRange(w, lowPitch, rows) sets the pitch window; notes are (pitch, startBeat, durBeats) triplets via notes()/setNotes(). Pitch is in steps of 1/edo octave — 12 (default) gives MIDI note numbers, 19/31/53 those equal temperaments, 1200 cents, 1 octaves. Fractional pitches are legal and drawn at their true (unquantized) position; clicking adds whole steps and deletes the nearest note within 1/24 octave
label(name, text)static text
plot(name, data)static line plotupdate with setData(w, data)
waveform(name, node, buf)audio file overviewshows the file loaded by audio_engine.loadBuffer

Every constructor returns a Widget handle used by the binding and value functions below.

Metering the master bus. meter, scope and spectrum tap a node outlet, which cannot reach the final mix: the built-in Audio Out node (node 0) has only an inlet, and the master gain and safety limiter are applied after the graph has run. The master… constructors tap the master bus itself — post-limiter, post-gain — so they show what leaves the application. They take a name only; there is no node, outlet or silo to choose:

masterMeter("out");
masterSpectrum("spec");

Because they read after the safety limiter, a master meter also tells you when the limiter is pulling the mix down — the level stops rising while a node meter upstream keeps climbing. The trade-off is that the limiter holds one audio block, so with it enabled the master readings lag node taps by one block (a few milliseconds, invisible in practice).

The app also has metering you don't have to write code for: the status bar along the bottom of the window carries a master level meter, DSP load, and a dropout indicator (see §6), and in the graph view you can right-click any node for Meter (level bars hung off its outlets), Scope, or Spectrum — the last two build the same widgets these constructors do, in the node's panel. Use the ui constructors when you want metering laid out as part of a notebook or control surface.

3.2 Binding to Sound

There are two binding paths, and a widget may use both at once:

let cutoff = slider("cutoff", ControlSpec { lo: 20.0, hi: 20000.0, init: 440.0, warp: ControlWarp.exponential });

-- FAST PATH: drive an engine control directly. The GUI thread sends
-- setControl without entering the VM, so the slider keeps working at
-- full rate even while a long eval (say, a synthdef compile) runs.
cutoff bindControl(node, "cutoff");

-- FLEXIBLE PATH: a callback, for logic (sequencing, bookkeeping, math).
-- Coalesced: fires with the LATEST value at most once per GUI frame.
cutoff onChange(fn(v Float) Void { "cutoff -> %^" fmt(v) println; });

bindControl resolves the control by name against the node's synthdef and returns an audio_engine error code (0 = ok; a wrong name prints errControlNotFound to the console). For an XY pad, bind each axis: bindControl drives X and bindControlY drives Y; onChangeXY receives both values. A range slider works the same way: bindControl drives the lo end, bindControlY the hi end, and onChangeRange receives (lo, hi).

Prefer bindControl for anything that shapes sound in real time. Use onChange when you need language-side logic. If an eval holds the VM busy, fast-path bindings keep sounding; callbacks queue their latest value and fire when the VM frees up.

When a node is freed (stop(node), freeNode, freeAllNodes), widgets bound to it are automatically unbound: they stay on screen with their values but drive nothing until code rebinds them (e.g. re-running the cell after playing the synth again). onChange callbacks are untouched — they're yours.

3.3 Mouse & Keyboard Control

Sliders, range sliders, xy pads, and multisliders are played with drag gestures and single-key hover commands. While a slider or range is hovered or dragged, a graduated tick scale appears on its background — decimal steps for linear ranges, decades (1–9 log-spaced, longer ticks at 1 and 5) for exponential ones.

Pointer

GestureSliderRange slider
dragpositions absolutelysweeps out a new range from the click point
option-dragfine: 1/10 rateslides the whole range (width preserved)
option+cmd-dragultra fine: 1/100 rate
shift-dragadjusts whichever end is nearer
cmd-clicktype the value (Enter commits, Esc cancels)type one end — left half = lo, right half = hi (clamped so the ends can't cross)
scroll wheelnudges by 0.01 per tick (up = higher)slides the whole range by 0.01 per tick

The fine modes are anchored at the value where you enter them, so you can slam close with a plain drag, then hold option and land it exactly — switching modes mid-drag never jumps. A range drag reads its modifiers every frame too: start a sweep, then hold option to slide it into place.

Hover keys

Hovering a widget enables quick single-key adjustments, all in the unmapped 0..1 position (so they respect the warp) except where noted. Keys auto-repeat while held, and key and wheel bursts alike commit one history entry when the adjustments stop.

KeyAction
ccenter — position 0.5
[jump to lo (position 0.0)
]jump to hi (position 1.0)
19jump to position 0.1–0.9 (sliders only)
runiform random position
jjitter by a random ±0.05, bouncing off the ends rather than clamping (hold to walk)
Jfine jitter, ±0.005
,step the position down by 0.05
.step the position up by 0.05
zset the mapped value to zero (clamped into range)
iset the spec's init value
-negate the mapped value (only when the result stays in range — handy on a ± range)
/reciprocal of the mapped value (when in range — handy on a ratio)

What a key acts on follows the widget:

WidgetKeys apply toWheel
sliderthe valuenudges the value
xy padboth axes (independent randoms for r/j)vertical wheel drives Y, horizontal drives X — a trackpad plays the pad in 2-D
multisliderall bars (independent randoms per bar — r scatters, held j makes the whole surface shimmer)adjusts the single bar under the cursor
range sliderboth ends: c, z, and i collapse the range; [ and ] send one end to its rail; r, j, and J randomize or jitter both ends; - negates and / inverts the range; , and . slide it, width preservedslides the whole range
toggle matrixevery cell at once: z and [ all off; ] all on; r a 50/50 coin flip per cell; j / J a 5% / 1% chance per cell of toggling (hold j for a slow churn); - inverts the pattern; 19 set each cell on with 10%–90% probability, else off. Other keys do nothing, and a momentary buttonMatrix ignores them all

A hovered control owns the keyboard. While the pointer rests on a slider, range, xy pad, multislider, or toggle matrix, typed keys act on that widget and go nowhere else — not to a focused cell editor, not to bindKey bindings. To type code or fire key bindings, hover somewhere else. (The one exception: an open cmd-click value edit keeps its keystrokes until you commit or cancel it.)

Key bindings

Buttons and toggles can be bound to the keyboard: kick bindKey("a") makes the a key press the button (momentary — down sends 1, up sends 0) and shows kick [a] on its face; on a toggle the key flips it. Accepted keys: a single character "a""z" / "0""9", or "space"; "" unbinds. Key bindings fire whenever no text field has focus — in the notebook, in perform mode, and over floating panels — and are saved with the document.

3.4 Widgets from Synthdefs

Compiled synthdefs carry each declared control's name and ControlSpec. The ui module can materialize widgets directly from them:

-- One widget, everything (name, range, warp, init, binding) from the def:
let c = control(node, "cutoff");

-- The node's whole interface at once:
let ws = controls(node);

This is the shortest path from “synth is playing” to “synth has a control surface.” The synthdef declaration is the single source of truth for ranges and warps — change the ControlSpec in the def and every derived widget follows on the next run.

Derived widgets default into a panel named after the node's synthdef — each synth automatically gets its own window (or notebook panel cell of that name). Pass an explicit panel name to place them elsewhere, e.g. a differently-named notebook cell (§4.4):

controls(node);                       -- into the panel named after node's def
controls(node, "mixer");            -- into the panel named "mixer"
control(node, "freq", "mixer");      -- one control, same destination

Widgets are keyed by (panel, name). Calling controls twice for the same node updates the same widgets — no duplicates, and different synths never collide (they default to different panels). The one case needing care is two instances of the same def: both default to the same panel, so the second call would rebind the shared widgets. Give each instance an explicit panel: controls(bass1, "bass 1"); controls(bass2, "bass 2");

3.5 Reading Values & Taps from Code

cutoff value;            -- current value (Float)
pad valueY;              -- Y of an xy pad
cutoff setValue(880.0);  -- move the widget AND fire its bindings

-- Tap-backed widgets can be read from scripts:
let m = meter("m", node);
m rmsLevel;              -- smoothed level, published every 512 samples
m peakLevel;
let s = scope("sc", node);
s tapChannels;           -- channels captured by the scope
let xs = s samples(4096);   -- raw samples: interleaved frames of
                         -- tapChannels(s) channels, whole frames only

-- The master widgets read the same way.
let out = masterMeter("out");
if (out peakLevel > 0.99) { println("clipping!"); }

All of these read a widget, so metering from a script means creating one. A script that wants a level but no visible display can park the widget in its own panel and leave that panel closed.

One consumer per sample stream. A scope's or spectrum's sample FIFO feeds either the on-screen display or samples() — whatever one drains, the other doesn't see. Levels (rmsLevel/peakLevel) are fine to read from both places. If you need both a display and script access to the same signal, make two widgets: each gets its own tap.

3.6 Panels

Every widget belongs to a named panel. Constructors place widgets into the current panel, set by panel(name); the default is "", shown as the floating Controls window:

panel("mixer");          -- subsequent widgets go to "mixer"
slider("ch1", 0.0, 1.0, 0.8);
slider("ch2", 0.0, 1.0, 0.6);
panel("");               -- back to the default Controls window

Where a panel appears depends on context: normally each panel is its own floating window; but if an open notebook has a panel cell with the same name, the widgets render inside that cell instead (§4.4).

Closing a floating panel window (its × button) removes its widgets — the window is just a view of them, and code recreates them by re-running (values reset to their inits, since floating panels aren't part of a document). It's the direct-manipulation equivalent of ui.clear() for that panel. Notebook panel cells are different: their widgets are document state, saved and covered by undo.

3.7 Function Reference

FunctionDescription
panel(name String) Void / currentPanel() Stringset / read the target panel for subsequent constructors
slider(name, spec) Widget
slider(name, lo, hi, init) Widget
slider (ControlSpec / linear form)
range(name, spec) Widget
range(name, lo, hi, initLo, initHi) Widget
range slider (ControlSpec / linear form); value/valueY read the ends, bindControl/bindControlY drive them
number(name, init) Widgetnumber box
button(name) Widgetmomentary gate button
toggle(name, init = false) Widgetcheckbox
xy(name, xspec, yspec) Widget2-D pad
meter(name, node, outlet = 0, silo = 0) Widgetlevel meter (engine tap)
scope(name, node, outlet = 0, silo = 0) Widgetoscilloscope (engine tap)
spectrum(name, node, outlet = 0, silo = 0) Widgetspectrum analyzer, dBFS on a log frequency axis (engine tap)
masterMeter(name) Widget
masterScope(name) Widget
masterSpectrum(name) Widget
the same three displays on the master output bus (post-limiter, post-gain) rather than a node outlet
plot(name, data [Float]) Widget / setData(w, data)static plot / replace its data
waveform(name, node, buf) Widgetoverview of the file loaded into a buffer slot
bindControl(w, node, control String, silo = 0) Intfast-path engine binding, by control name
bindControlY(w, node, control String, silo = 0) Intfast-path binding for an xy pad's Y axis
bindKey(w, key String)keyboard binding for a button (momentary) or toggle (flip): "a""z", "0""9", "space"; "" unbinds
onChange(w, f (Float) Void) / onChangeXY(w, f (Float, Float) Void) / onChangeRange(w, f (Float, Float) Void)coalesced value callback
value(w) Float / valueY(w) Float / values(w) [Float]read the widget
setValue(w, v) / setValueXY(w, x, y) / setRange(w, lo, hi) / setValues(w, vals)move the widget and fire its bindings
notes(w) [Float] / setNotes(w, ns)piano-roll notes as flat (pitch, startBeat, durBeats) triplets
onChangeVec(w, f ([Float]) Void)coalesced vector callback: multislider/matrix/buttonMatrix/toggleMatrix values, or roll note triplets
onCell(w, f (Int, Int, Float) Void)per-cell event callback for matrix/buttonMatrix/toggleMatrix: fired once per cell press, flip, or release with (row, col, value), in interaction order. Unlike onChangeVec it preserves both edges of a fast click and the ordering of multi-cell changes. GUI interaction fires it; whole-state syncs (setValues, preset recall, undo) fire only onChangeVec
labels(w) [String] / setLabels(w, labels [String]) / setLabel(w, row, col, label String)buttonMatrix/toggleMatrix cell labels (row-major; missing entries draw blank). Saved with the document
setFrame(w, x, y, wd, ht)layout frame within the panel (what arrange mode sets by dragging)
rollRange(w, lowPitch, rows)piano-roll pitch window, in steps of the roll’s edo
show(name, v)materialize a value as its natural widget: Float→number, Bool→toggle, [Float]→multislider (≤64) or plot, String→label
rmsLevel(w) / peakLevel(w) / tapChannels(w) / samples(w, max = 4096)read a tap widget from code; samples are interleaved frames of tapChannels(w) channels
control(node, name String, silo = 0) Widget
control(node, name, panel String, silo = 0)
one widget derived + bound from the def's ControlSpec; defaults into a panel named after the def
controls(node, silo = 0) [Widget]
controls(node, panel String, silo = 0)
materialize the node's whole interface; defaults into a panel named after the def
defName(node) Stringthe def name of a live node ("" if unknown)
remove(w) / clear()remove one widget / all widgets (taps are released)

4. Notebooks

4.1 Creating a Notebook

File > New Notebook (Cmd+Shift+N). The notebook takes over the editor pane, with the console as a resizable column on the right (the cell strip gets the full window height), and your .x editor tabs are untouched underneath — the Editor button at the right of the notebook toolbar (or Cmd+\, View > Toggle Notebook / Editor) swaps back to your editor tabs without closing the notebook — its widgets, history, and queued runs stay live (its panel controls are simply out of sight until you swap back; scheduled code and bindings keep running). Close (or Cmd+W) ends the document.

Only one notebook is open at a time: New Notebook or opening a .tzd replaces the current one (you are asked to save first if it has unsaved changes).

The toolbar across the top: + code / + prose / + panel / + presets insert a cell after the selected cell (or at the end if none is selected); Run All runs every code cell top to bottom (and becomes Stop (n) while runs are queued); Undo / Redo / History drive the document history (§4.6); Perform enters the locked stage view (§4.4); Editor swaps to the editor tabs (notebook stays open); Close ends the document. The title shows the file path, with * when modified.

Click anywhere in a cell to select it — the selected cell shows an accent bar along its left edge. Selection determines where new cells insert and which cell Cmd+Enter runs.

4.2 Cell Types

Every cell header starts with a collapse triangle that hides the body (code, output, or panel canvas), leaving just the header strip — collapsed state is saved with the document. Each header also has ↑ / ↓ (move) and x (delete) on the right. Collapsed code cells still run (Run, Run All, run-on-load).

With a code cell focused, File > Open (Cmd+O) reads the chosen file's text into that cell (one undoable history commit) — a quick way to pull an existing .x script into a notebook. With no code cell focused, Open behaves as usual (.tzd opens as a notebook; other files open in the editor).

4.3 Running Code

Cells share the one live session — the same globals, modules, and running audio as the editor and REPL. There is no hidden kernel: lets from one cell are visible to the next, and evaluation happens only when you ask.

Output printed while a cell runs appears under that cell; a returned value is shown as → value : Type. Errors underline the offending line in the cell and print the message below it. Output from timers, actors, and the engine (things that print when no cell is running) goes to the console column on the right, since it can't be attributed to any one cell.

4.4 Panel Cells

A panel cell embeds the widgets of the panel with the same name. The connection is by name, nothing more:

  1. Add a panel cell. Its name (editable right in the cell header — it starts as something like panel1) is what code targets. Naming it after your synthdef means controls(node) lands there with no extra arguments.
  2. In a code cell: controls(node); (cell named after the def) or controls(node, "panel1"); — or set a sticky target with panel("panel1") and create widgets individually.
  3. Run the cell — the widgets appear inside the panel cell.

Tabs within a panel cell

A panel named "<cell>/<tab>" renders as a tab inside the cell: panel("mixer/eq") and panel("mixer/sends") give the cell named mixer an eq and a sends page, one showing at a time (widgets in the bare "mixer" panel become a (main) tab). Which tab is selected is pure view state — never in history. Sub-panels follow their root everywhere: saved with the document, captured by undo, removed with the cell. Separate control sets per cell, created entirely from code.

Panels claimed by the document render only there, never as floating windows — hiding the notebook (Cmd+\) just puts its controls out of sight until it returns. Panels no cell claims (including the default "") float as windows, as always. Closing or replacing the notebook (Close, New Notebook, opening another file) removes its claimed panels' widgets along with it.

Arrange mode

To enter or leave it: click the arrange control in a panel cell's header, beside the cell's editable name and its reflow button. It is a toggle: click it again to leave. There is no menu item and no keyboard shortcut. Arrange is per cell, not per document: each panel cell has its own toggle, and only panel cells have one (code, prose, and preset cells do not). It also only works in the normal notebook at 1× scale — perform mode locks the layout, so you cannot rearrange from the stage view.

While arrange is on, widgets stop responding to input and instead become draggable, each outlined with a move border: drag a widget to move it, drag the small grip at its bottom-right corner to resize, both snapping to an 8-pixel grid as you drag. The bar under the canvas drags to change the panel's height. Layout is document state: frames save into the .tzd, are covered by undo (one history entry per drag), and survive re-running the creating code (upserts adopt position along with value). Widgets you never arrange flow top-to-bottom in creation order — and keep flowing: edit the creating code and re-run, and they re-lay out to match. Only an arrange drag (or setFrame(w, x, y, wd, ht) from code) pins a widget to a fixed frame. The panel header's reflow button unpins everything in the panel back to code-order flow (one undoable commit) — useful after experiments, and to heal notebooks saved by older builds that pinned auto-flowed positions.

Perform mode

The toolbar's Perform button (or Cmd+Shift+P) swaps the notebook for a stage view: one tab per panel cell, the selected panel filling the view at 1.5× scale for reliable targets, with the layout locked — no cell chrome, no arrange, no structural edits, nothing to grab by accident mid-set. Tabs give you separate control sets one page at a time (switching tabs is pure view state — not in history). Widgets stay fully live (bindings, taps, key bindings), and code keeps running; you just can't edit the document's shape. Esc or Exit Perform returns to the normal notebook.

4.5 Saving & Loading

Cmd+S saves the notebook to a .tzd file (a compact binary container in the TZB format, documented in FFI Guide §15). Saved: the cells (kind, text, order, run-on-load flags), for each panel cell its widgets' names, kinds, ranges, and current values — and the whole undo history tree (§4.6). Not saved: bindings, eval outputs, scope and spectrum data — anything that is behavior or transient display. A meter, scope or spectrum widget is saved (so the panel comes back with it in place) but reconnects to the engine only when the code that created it runs again.

On open, widgets reappear immediately with their saved positions and values, but unbound — they drive nothing until their creating code runs again. Because constructors are upserts, re-running the setup cell adopts each saved widget (keeping its saved value, which is pushed through the fresh binding) rather than resetting it. So the loading ritual is simply:

  1. Open the .tzd.
  2. Run the setup cells (or mark them run on load and they run themselves).
  3. Perform.

Why not save bindings? Bindings reference closures and engine node IDs that only exist in a live session. Rather than invent a second, weaker way to describe behavior, the document re-runs your code — the same code path as the first time, guaranteed to agree with it.

4.6 Undo & History

The notebook keeps one immutable history tree covering cell structure, code text, and widget values in document panels. Commits are coalesced so the tree stays readable: one entry per slider drag (committed when you release), one per pause in typing (≈1 s), one per cell run, one per structural edit.

History is saved with the document. The .tzd file stores the whole tree — every node, its branches, and where the cursor was — so after save and reopen, Undo/Redo and the History window pick up exactly where you left off (undoing a control move is still audible once the setup cells have re-run and rebound the widgets). Storage is content-addressed: each distinct cell, widget state, and preset is written once and shared by every history node that contains it, so a session of hundreds of small tweaks costs little more than its unique content. Files saved by older builds open normally (they simply carry no history), and older concerns about file bloat don't apply — a few hundred slider tweaks typically add only tens of kilobytes.

The same content addressing works in memory: recreating a state you were in before (toggling a control back, re-storing an identical preset, re-editing a branch to match another) shares the earlier objects instead of duplicating them, and committing a state identical to the current history tip is a no-op. The History window's footer shows the live tally — node count and the number of unique cells, widget states, and presets behind them.

The tree is capped at 500 nodes — past that, the oldest states fall off the root (along with any branches hanging off them) as new commits arrive.

5. Keyboard Shortcuts

KeyEditor modeNotebook mode
Cmd+Enterevaluate selection / blockrun focused cell
Shift+Enterevaluate linerun focused cell
Cmd+Shift+Enterevaluate filerun all cells
Cmd+Shift+Nnew notebook
Cmd+Ssave filesave notebook (.tzd)
Cmd+Wclose tabclose notebook
Cmd+\toggle notebook / editor view (notebook stays open)
Cmd+Shift+Ptoggle perform mode (Esc also exits)
Cmd+Z / Cmd+Shift+Ztext undo / redocharacter undo in the focused cell; document-history undo otherwise (text edits are in both — see §4.6)
Cmd+Kclear console

The View menu also holds the editor font size and the color theme (View > Theme: Dark, Corporate Gray, Dark 2, Cherry, Dark Grey).

6. Monitoring the Engine

Three surfaces show what the audio engine is doing. The ui constructors of §3.1 are the one you compose yourself; the other two are always there.

6.1 The Status Bar

A strip along the bottom of the window, always visible except in perform mode:

Click the XRUN readout to reset the counters and clear both latches; clicking the clip square clears just that one. Nothing else resets them — a dropout should never quietly disappear while you're looking away. Note that the device's own xrun counter belongs to the driver and cannot be zeroed, so what you see after a reset is the number of xruns since it.

Click anywhere else on the strip to expand a detail panel:

6.2 Meters in the Graph View

In the graph view (Cmd+Shift+\), right-click a node > Meter to hang a level bar off each of its outlets. A node with several outlets gets a submenu so you can meter just one; outlets carrying something other than audio are greyed out, since only audio can be metered. A dot in the node's title bar marks it as metered when you're zoomed too far out to read the bars.

The bars sit under the node, one per metered outlet, running left to right the width of the box — the same orientation as the master meter in the status bar. Each carries the outlet's name at the left and a peak-hold reading in dB at the right, faint ticks every 12 dB across a −60..0 dB span, a green fill for RMS (red when the outlet clips), and an orange peak-hold marker. The labels drop out when you zoom out far enough that they would be unreadable; the bars themselves stay.

Meters are per node and opt-in because each one costs a little real-time work, and each silo can carry only so many at once — if you hit the limit the console says so, and turning one off frees a slot. They last for the session: switching silos or leaving the graph view drops them, and they are not saved with a document.

The same context menu carries Scope and Spectrum (with a per-outlet submenu on a node with several outlets). These are not drawn in the graph: they create exactly the widgets scope() and spectrum() build, in the node's own panel, so a node ends up with one window holding its sliders and its displays. Unlike Meter they are not toggles — close the panel window to remove them and release their taps.

The menu also opens a node's Controls… panel (so does double-clicking the node). It is greyed out for a node whose synthdef declares no controls and no buffer slots — there would be nothing to put in the window.

6.3 Master Output in the Graph View

The right of the graph view's toolbar carries the two engine-wide output settings, next to the per-silo controls on the left:

Both are the same settings the masterGain and safetyLimiter functions write, and both live in the engine rather than in the window: the controls re-read the engine's values each time the graph view is shown, so a script that changes either is reflected here. They are engine-wide, so the silo selector does not affect them. On a narrow window the section is hidden rather than crowding the silo buttons.

7. Current Limitations