VOLTAreference

Reference / Work in Studio

Patch Bay and Rack

Work in Studio

Open in the workbench →
On this page · 5 sections

Explore how a sound is connected and edit its modules, parameters and signal flow in the visual patch workspace.

Held from the first release. The whole-song Patch Bay board (a Bench view) has no menu, header or shortcut entry in this release; the Rack and its case described here are unaffected.

The modular-rack view of a synth patch (native: ui/src/rack.rs; formerly the TypeScript patchbay.ts, deleted 2026-08-09), opened with the header rack toggle. Each IR node of the selected synth is a module box, each connection a patch cable; chrome follows signal rate (audio → --pri, control → --mid, event → --sec, pattern → --acc) on theme tokens, and cables/jacks pulse with live node values off the VizBus. Field-root Sum cards carry the per-summand dominance readout (Mixing tracks and buses §7); named-effect groups collapse to one block (Generate and transform musical structures §11), double-click to expand.

See a patch as a case#

The Rack opens on case: the selected patch drawn as the panels you wrote, the way a Eurorack case shows modules. Each builtin call (an oscillator, an envelope, a filter, an effect), each call of one of your own functions, and each named field is one panel; a top-level macro is a knob on the Controls column at the left. Arithmetic is not a module:

  • cutoff = 2000Hz + motion * 800Hz is a cutoff knob at 2 kHz. The motion LFO's cable lands on it, and the knob's coloured band shows how far the 800 Hz depth sweeps it.
  • (a |> gain 0.85) + (b |> gain 0.34) is a mix panel with a level knob per channel; x |> gain (ampEnv * vel * 0.34) is a VCA whose level knob reads 0.34 and whose legend says level × vel. A classic voice's gain = vel * 0.32 reads the same way: a gain knob at 0.32, × vel.
  • 1 - follower kick … * 0.85 is an offset panel: a knob at 1 with the follower's ring on it.
  • A freq pitch argument is the oscillator's pitch normal; pitch + 12Semitones puts a 12-semitone transposition on its knob.

A |> chain is a row, read left to right, with the patch's output at the end. Panels whose output feeds only the next panel are normalled: side by side with no cable. Patch cables appear only for modulation (a ring or CV on a knob) and for an output that feeds more than one place; a source that modulates three or more knobs puts a coloured tag on each instead of a fistful of cables, and its own face says → 6 knobs. Envelopes, LFOs and other modulators sit in rows above the signal path. A knob greyed out holds a constant expression (0 - 2), a value that is not a number, or no literal at all. Anything the case cannot draw exactly as panels — an if … then … else, a function value applied to a signal — is a source panel that shows its text, with jacks for the signals it reads.

The case looks like a case: modules sit on rails along every row, each a whole number of HP wide (from what it carries) and as tall as what it carries (a one-knob amp is a short module, a four-knob envelope a full one; every module on a line takes that line's tallest height, so the rails, the jack line and the cable gutters stay level), with a coloured band along its top for its kind — sound sources, filters, modulators, your own modules and the sequencer, effects. Knob legends take up to two lines and abbreviate long names (resonance → res); legends and the other names you wrote (ampEnv, ← kickTrack, track chips) print as written, never in capitals (a filter's q reads q). Cables run in the gutters between rows and down the case's margins, pass under a module's face (never across it), in the colour of their rate, and plug into the side of the knob they move. The case fits itself to the pane: a small patch grows (with the same rows) and a tall one shrinks a little before it scrolls. A row never ends in one module alone on a line of its own: the case shrinks a little so it keeps its line, or, if that would go below the smallest zoom, the module before it wraps with it. One of your functions with nothing to turn (wreck = \s -> glass (rust s)) shows a small strip of what is inside it — a glyph and name for each of its modules — and a track's voice panel shows its patch's oscillators, filters, envelopes and modules, and its voice count when the patch sets one. The Controls column's knobs use the same face as the Macros pane: they are the same knobs, and the column is where a macro's cable starts.

The header is two rows: the patch (its ⟲ opens the patch's versions across the song's history, lit when it differs from your last idea — Save ideas, compare versions and record takes § Find a remembered change), ⏵ audition (its note and velocity behind the C4 · 0.80 chip), the A/B reference, where you are (‹ back · keys › rust, inside a module) and compare (Compare sound, as a popup); then the views. The case's own chips sit on its bottom rail beside the status line: the track chips, knobs ▾ (save knobs, recall knobs, save A, save B, and save C / save D once A and B exist), + module and, once A and B exist, the morph (a slider, or the four-corner pad). The status line says what a held cable, a refusal or a turn is doing (wrapped, never cut off); otherwise it shows the case's size and its cost, with the how-to sentence until your first gesture — only when it fits whole on the line, and always in the counts' hover. A rail too narrow for everything wraps to a second line between counts, never inside one.

Hover a panel to light its source; click to move the caret there. The other way round, the Rack follows the caret: move the caret in the focused editor into a patch declaration and the Rack shows that patch; into a track that plays a patch, that patch — opened as the track's case when the case is up (the song case stays put: it already holds every track); into one of your functions the case on show draws, that function's own case. Only a move counts, so a patch you pick on the Rack stays until the caret moves again; nothing moves while a hand is on the Rack (a turn, a held cable, a dragged panel, a Routes or wiring gesture), and the caret a panel click placed is not followed back. In an import tab the caret opens the library case of the patch it is in when the song plays that patch, and otherwise does nothing. Notebook docks keep their own cell's patch. The panel whose source holds the caret wears a dashed outline in the caret's ink with a text-caret I-beam on its left edge; with the caret inside a knob's number the knob gets a dashed ring (the panel a quieter outline), and inside a modulation term that term's cable is traced dashed as well. The innermost span wins, and only the buffer that declares the case counts: an import tab's caret never lights the song's panels, nor the song's a library case. Assistive tech hears holds the source caret as the panel's or knob's description. Double-click one of your own functions to open it as its own case; ‹ back returns. track chips on the bottom rail open a whole track: its sequencer (its events — see Sequencer face below), pattern transforms such as fast 2 as panels, the voice (double-click to open the patch, an inline patch { … } too: its own case, knobs and all), a tinted seam marked Σ voices sum here, then the track effects and the out strip: the track's mixer { } entry — level, pan, width and each send (send hall). A functor instance (k909.kick from k909 = drumKit { punch = 0.85 }) opens the module's patch with a panel per module parameter whose knob is the instance's own value. A parameter the instance leaves out shows the module's default (tone: Normalized = 0.2) on a greyed knob whose legend says default; turning it writes the field into the instance (k909 = drumKit { punch = 0.85, tone = 0.3 }) — one edit, one undo step — and never changes the module's default. An imported instrument (import "drums" (kick)) opens as the case of its library declaration: the header says from drums · shared definition, and every gesture edits the library's own buffer (the Rack composer's rule — the change reaches every song that imports it), checked by compiling the song against the edited library. Such a turn is heard on release, not while it turns, and Source ↗ opens the library's tab. The header's x-ray views (graph, wire, modules, chain, grid) show and edit the compiled program; routes is the patch sheet.

Live faces#

While the song plays, panels show what the engine is doing, not what the source says:

  • An adsr panel has a small window that draws its envelope — the times and sustain the playing voice latched at its note-on, so the dot stays on the curve when they are modulated. A dot and a cursor show where the playing voice is on it: the stage (attack, decay, sustain, release) and the envelope's actual level. The sustain plateau has no length of its own, so while a note is held the cursor eases along it as the hold goes on and never reaches the release corner; the reading says how long (sustain · held 1.5 s · level 0.08). Stopped, the window draws the envelope the source writes: the rack reads each inline adsr's numbers from the IR's attack/decay/sustain/ release fields (RackNode::env, ui/src/moduleinfo.rs), and the node inspector reads them as A 4ms · D 580ms · S 0.08 · R 950ms. An envelope whose stages are signals, not numbers, reads zero there; the panel's caption still names it.
  • An LFO's window draws one cycle of its wave, with a dot at its current phase and its actual value. A sample-and-hold (lfoSH), drift or wander has no cycle to draw: its window shows the last two seconds of its value, newest at the right (value 0.30 · step 25%).
  • An oscillator's window draws its wave. The plain shapes and pulse get it, and so do as3340 (the wave of its chip output), fmOp and pulsar; a shape the case cannot draw (a wavetable's table) is a level band, never a stand-in sine. The trace's height follows what is heard: the patch's output after its envelope and VCA, summed over the sounding voices; the reading gives both (out -12.0 dB · osc -3.0 dB).
  • A filter's window draws its response — the slope of its type (a ladder is 4-pole) and the peak its resonance knob makes — with a cursor at the cutoff. While the song plays, the curve follows the modulated cutoff and an automation lane that drives it. When stopped, it sits at the knob's value.
  • A VCA, the patch's out, other envelopes, an effect or noise source (a delay, a reverb, a distortion, a chorus) and a patch's own mix have a level meter: the node's output summed over the patch's sounding voices, with an instant attack, a ~300 ms release and a peak that holds for a second. In a track's case, each insert after the voice seam meters its own slot's output, and a compressor-family panel (compress, limiter, ott, …) also shows its own gain reduction, by the slot the case's analysis ties it to; the strip's meter shows the track's level and its chain's total gain reduction. In the song case every row's line meters its own strip — a track's, a bus's, the master's, with gain reduction eating in from the right (a VCA group has no audio of its own, so no meter) — and an unfolded row's strip and inserts read their own chain.
  • A sampler lights the lanes of the samples its track is playing now and meters the track's level beside them.
  • A modulated knob's ring follows its control's signed value, so a pan or a detune swings both ways.

The engine measures meters, node values and probes at the block it has just rendered, a device buffer ahead of what the speaker plays; the case reads them from a frame that much older, so a meter peaks with the hit (the playheads already follow the audible position). A shell that reports no device latency (the browser, a headless shot) reads the current frame. When stopped, the windows show their shapes with no dot. The live values are sampled from one voice of the selected patch: the most recently started voice still held, or the newest releasing one when every voice is releasing — never an older note's tail. When the patch is sounding but its readings did not fit the engine's budget for live values, the panel says live slots full instead of going blank or showing stale values. It waits 150 ms first: right after a patch opens (or Play is pressed), the engine has not yet had a block to sample it, so a miss that short shows the plain shape instead.

In the Bay, a patch card shows the same window beside its chain: its adsr, else its first LFO, else its first oscillator. The Bay asks the engine for no live values of its own, so a card moves only while its patch is being sampled anyway (the one open in Compose). Every other card shows its plain shape, and a card never says live slots full. For screen readers, each panel's accessible value is its reading (decay 25% · level 0.60, cutoff 1.2 kHz, level -12.0 dB, live slots full). A skin restyles these windows and meters through the display and meter control faces (Skin control faces); the eurorack-case fixture pack (ui/tests/fixtures/skins/eurorack-case/) binds a davies-style knob, a trimmer for macros, a phosphor window and an LED meter that way.

Sequencer face#

A track's sequencer panel draws its pattern's events, picked by what they are:

  • a step grid for a drum line — events with no pitch, or one repeated pitch, on a grid of up to 32 steps a cycle (c4(5,16,1) is 16 steps with five lit), one row per sample, a velocity bar under each hit;
  • a compact roll for a pitched line — time across, pitch up, a velocity bar per onset;
  • a wheel when the pattern's period runs against the bar (a three-beat cycle in 4/4) or its onsets sit on no grid: the attacks around a circle, joined, 12 o'clock the downbeat.

A sequencer that feeds its voice directly draws the compiled track's events (the same events the Timeline and pattern_events read, <a b> and arrange included); a pattern longer than four cycles (or 32 steps) shows the page the playhead is on. While the transport runs, a playhead follows the engine's clock for that track and the events sounding now light up; stopped, the face is static. Under pattern transforms the sequencer draws the pattern as written (its whole period), and each transform panel (fast 2, every 4 rev, humanize 0.08) adds a small in / out pair — the pattern before and after it, one cycle at a time, both evaluated in the song's scope off the UI thread (one probe per text gives the whole period, cached). When the period is longer than a cycle (every 4 rev: four) the strip says which cycle it shows (3/4): while playing, the cycle the track's playhead is in; stopped, a click on the strip steps to the next cycle. The transform the voice reads from carries the playhead. The panel's hover still shows the pattern's text, and its accessible value says what the face shows (16 steps · 5 hits · as played · playhead 0.42, fast: in 2 → out 4, every: in 4 → out 4 · cycle 3 of 4 (playhead)).

Listen, count, diagnose#

Four readouts sit on top of the panels (ui/src/rackcase/monitor.rs):

  • Listen at a point. Each panel with an audio output has a small ◉ at its top right. Hold it to hear the signal at that panel's output; let go and the mix returns. It is the chain scrubber's monitor: a held preview note of the patch at the audition pitch, soloed at that panel's compiled node — not the song's own voices. A classic voice's implied amp and a track case's Voice panel solo the voice's output. Envelopes, LFOs and sequencers have no ◉. The browser routes the same commands to its engine, so ◉ sounds there too.
  • Listen after an insert. In a track case each insert panel (a builtin effect, or a user function whose body is effects) has a ◉ as well. Holding it sends the held note through the track — its fader, strip and chain — and replaces the whole mix with the track's signal right after that insert, so later inserts, other tracks and buses drop away until you let go (if the song is playing, the track's own notes are heard there too). A function panel listens after its last effect. An insert the case cannot tie to a panel (a run of function effects with no single panel to own them) gets no ◉. In a song whose mix uses staged routing, the held note is not routed through the track; only the track's own notes are heard.
  • Scope on hover. Hovering an output jack, or an input jack patched from another panel, shows that signal: its level history (block RMS over the last two seconds of engine time — an insert's from its slot meter) while the song plays, and while a monitor or audition preview runs, the tapped waveform (its last ~20 ms) and its spectrum. An insert panel's jack shows the tapped window after its insert while a held ◉ (or its ~3 s tail) runs; otherwise it says to hold ◉.
  • ×N voices. The patch's output panel (a track case: its Voice panel) says, on its own line just above the jacks (so it never prints over a jack's name), the declared cap (×6 for voices = 6, ×1 for mono or glide, poly with no cap) and how many song voices of the patch sound now (×6 · 3 on); the hover says how many are releasing. The count is the engine's own (a PROBE_KIND_VOICES record next to the panel faces' probes), so it appears only while the patch is sampled for the case.
  • Doctor on panels. When Doctor: trace a silent signal finds this patch or track silent or suspect, the panel its finding lands in gets an outline and a ! badge in Doctor's colour (red for silent, accent for suspect), and its output jack a ring; a finding on an input's expression rings that jack. The hover is Doctor's own sentence and culprit chain; a chain step that lands in another panel marks that panel too. The case's bottom rail says doctor: N findings with every sentence in its hover, including findings that land on no panel.
  • Cost. Each panel's top left reads its static cost from the Understand audio processing cost, in units of one baseline oscillator (1.0u): the IR nodes its source compiled to, each node counted once in the narrowest panel holding its span (a user function's body splits over its calls). In a track case the Voice panel carries the patch's per-voice cost and each effect its insert's cost. The bottom rail shows the total (≈ 14u /voice, + 85u track) and, in its hover, the share of a core at the declared voice count. Cost from a library's function body cannot be placed by span and is reported as off-panel. The song case owns no voice, so its rail shows the compiler's whole-song estimate instead (≈ 663u peak: every track's voices at the busiest moment they sound together, plus every insert, bus and master chain — the figure the DSP-load advisory reads), or nothing when the model has no figure (a song of only sample tracks).

The analysis runs once per change of the source, on a worker (inline in the browser). A library-read case shows no marks and no cost.

Turn, patch and rearrange#

Every gesture below is one source edit, checked by the compiler before it lands, one undo step, and live.

  • Turn a knob: drag up or down (Shift for fine), then release; the new literal keeps your unit and spelling (580ms becomes 612ms; a kHz value turned below 1 kHz is written in Hz, 445Hz). The sound follows the knob while it turns: the engine moves the numbers the literal compiled to in place, with no rebuild — a filter sweep on the notes already sounding, an effect's mix, an envelope time from the next note. Release writes the literal and installs the song once; Escape puts the sound and the text back. This works while the buffer is what is playing (run it first); a number that shapes the program (a voice count, a mode) or that an effect reads only when it is built is heard when you let go, and the status row says so. Each knob shows which before you grab it: a small filled dot at its upper right means the sound follows it as it turns, and a hollow ring means the change applies on release, with the reason in the hover text. Screen readers get the same as the knob's description (heard live as you turn, applies on release: this number shapes the program). The workbench works this out off the UI thread, once per change of the source, and only while the buffer is what is playing. Until then knobs show no dot. The browser has no worker for this, so it takes the same plans one compile per frame (the installed program, then each knob's two probes) and pauses while a knob turns; the dots fill in over a few frames. A macro knob moves the sound too, and writes where the macro takes effect — the sounding scene's with value when that scene sets it, else its default, as the outline's bar does; under a scene ramp it has nothing to turn. Alt-drag turns the knob's first modulation depth instead. A clicked knob takes the arrow keys — each step heard at once, the steps landing as one edit when they pause. Click a knob's number to type an exact value: a bare number keeps the unit (620 over 580ms writes 620ms), a number with its own unit is written as typed (1.2s), Enter writes it.
  • Turn the strip: an out-strip knob written as a plain number turns like any other and writes that one value inside the mixer { } block (level = -6dB becomes level = -4.5dB). An expression (level = fader) is read-only, and a track with no mixer entry shows no strip knobs — set them in the mixer or Source. If the source changes under a turn, the turn is dropped and nothing is written.
  • Patch a cable: drag from a panel's output jack (or a macro's) onto a knob or an input jack, or click the output and then the target. On a knob it adds a modulation term with a starting depth in the knob's unit (+ motion * 1000Hz); on an input it repatches that input. A signal without a name is named once, as a patch field, so one oscillator feeds both places. While a cable is held, targets that take it light (dashed when the depth carries a unit) — first from their types, then, as a background compile of every target lands, from the compiler's own verdict (a target whose candidate does not compile turns red); hovering one shows the compiler's sentence. A per-voice signal cannot move a shared macro; a macro dropped on a macro writes modulate. Right-click a knob to Remove a modulation term.
  • Select a cable: hover a cord (or a tag standing for one) and it thickens and lights its term in the editor; click it to select it — the other cords dim, both its ends ring, the term stays lit — and a card opens beside it: what it connects (motion → filter · cutoff), its kind (modulation, CV, patch, send, member, return, sidechain) and rate with the rate colour, the depth or level with its sign and, for a modulation term, the range it sweeps (cutoff 2000Hz ± 800Hz → 1200Hz … 2800Hz, clamped where the knob clamps), the term as written, the live level of its source (the node's value; a strip's track meter in the song case) and Doctor's marks. On the card: depth — drag sideways to scrub (one edit on release, Escape cancels) or click to type (Enter writes); Remove cable — a modulation term leaves the knob's base; a CV factor leaves the rest of its product (ampEnv * vel * 0.34 without vel reads ampEnv * 0.34; the last signal factor leaves the constant, or 1 where there was none; a VCA keeps its audio input; a factor carrying the product's unit, like filtEnv * (900Hz + …), refuses); a send deletes its sends.<bus> (or presends) field from the track's mixer { } entry, taking an emptied sends = { }, an emptied entry and an emptied block with it, and a field alone on its line goes with its line and its comment; a member leaves the group/submix list (the last leaves []); a duck trigger's card says Remove duck and takes the whole |> duck … stage out (a follower read as a term removes that term). Still refused, with the reason: a processor's own input (it always takes a signal — repatch it), a master return (a bus returns by itself), and other cross-track readers (tap, pitchOf, onsets). Every removal is one guarded edit compiled before it lands — a scene that automates the removed send makes the compiler refuse it — and one Undo step; hold to listen (the ◉ seam, at the source's output; a control source has none); show in Source (moves the caret — selecting does not, so the keyboard stays with the selection); pin probe (a small level tile clipped to the cable, several at once, kept while the case shows the same owner; × unpins). Delete or Backspace removes the selected cable (one Undo step), Escape or a click on empty board lets go, a panel click selects the panel instead. A knob, jack or ◉ on top of a cord keeps its own click. Each cord is a named button for assistive tech (cable motion → filter · cutoff, selected = toggled).
  • Start from nothing: a song with no patch shows + new patch in the Rack; on any patch's case, + patch on the bottom rail adds another. Either writes voice = patch { source = saw (freq pitch), gain = vel * 0.3 } (or voice2 …) before the song's first track — plus voiceT = play voice !"c3 e3 g3 e3" when the song plays nothing yet — and opens it in the case.
  • Add a module (+ module opens a palette on the case's bottom rail, by kind — the case gives up that height, so the palette never covers its lower rows; it stays open for the next module, Esc closes it; a patch is offered no track effect, a track's case only its track effects, and the song case none; each is written the way you would write it by hand, without extra parentheses, and a patch laid out the way volta fmt lays it out stays that way): a source mixes into the voice (saw (freq pitch) * 0.5 + sine (freq pitch) * 0.5, a mix panel with a level knob per oscillator); an lpf on a classic voice with no filter writes its cutoff = 2kHz, resonance = 0.7; any other filter or effect goes after the selected panel, or at the end of the voice's source chain, and stays selected so the next one goes after it; an envelope is the voice's ampEnv, then its filtEnv, then env2; an LFO is a field named lfo, ready to cable. On a track, an effect goes at the end of its chain (… |> reverb 0.25). An edit to the whole song toasts as a sentence (new patch voice).
  • Rearrange: right-click a panel for Insert after, Replace with, Bypass/Enable (writes bypassed; the panel stays, dimmed), Move earlier / later, Remove and Source. Drag a pipeline stage along its row to move it (a bar marks where it lands; one edit), and drag a module out of + module onto a panel to insert it after that panel.
  • Place by hand: drag any other panel (or Alt-drag a stage) to put it where you like. The place is kept in your workbench preferences, never in the source; Reset position (panel menu) puts it back where the patch's structure places it.
  • Make performance knob (knob menu): a new macro on the Controls column at the knob's current position, read across the knob's travel (cutoff = (20Hz + cutoff1 * 19980Hz)).
  • save knobs / recall knobs (in knobs ▾): write the macros' current positions onto the patch as @knobs { … }, and move the macros back to them.
  • save A / save B, then morph (in knobs ▾): each writes a whole snapshot — every macro and every literal knob of the patch — as @knobs a { … } / @knobs b { … } (a literal is keyed by its panel and knob, "lpf:source.cutoff": 1200, in the unit it is written in). Once both exist, the morph A → B control slides every value that differs: literals heard in place while it moves (a frequency travels in ratios), macros moved live; release writes all the literals as one edit, Escape puts everything back. Macros are moved, not written (like recall knobs).
  • save C / save D: the morph pad. Once A and B exist, save C and save D appear in knobs ▾; a third snapshot turns the slider into a four-corner morph pad (A top left, B top right, C bottom left, D bottom right — the letters sit in the pad's margin, outside the handle's travel; an unsaved corner is the case as it is when the drag begins). Dragging the handle blends every value the corners disagree on — bilinearly, a frequency or time knob in ratios — heard in place like the slider; release writes the literals as one edit, Escape puts everything back.
  • Snapshots bound to scenes: name a snapshot after a scene (@knobs drop { energy: 0.9, "lpf.cutoff": 2400 }) and it is recalled whenever that scene launches — from the scene buttons or the arrangement walk. Its macros move (like recall knobs); its literals are heard in place but not written (the source keeps its values, so the next Run plays those again), and only while the buffer is what is playing. When the scene leaves — another scene launches, the arrangement moves on, the transport stops — the moved literals go back to their source values and the moved macros to where they were before the scene (the engine's reported value at launch), unless the incoming scene's own snapshot places them. The status row says what was recalled or put back. Any patch of the song can carry one; no new syntax — it is a named @knobs snapshot.
  • Lane badge: a macro knob that an automate lane or a shred (ride = do { bright := 0.8; … }) drives, and an effect knob that an automate "owner.effect.param" lane drives — a track or bus insert, or a user effect … on voice inside a patch (automate "keys.grit.drive"; its { drive = 0.3 } fields are the panel's knobs) — wear a small badge; while the song plays its ring follows the value the lane is writing. Its accessible name says what drives it (bright (shred ride), tape mix (lane haze)).
  • Shared by N: open one of your own functions from a caller and, when more than one call uses it, its panels say shared by N — turning a number inside it changes every call. A literal knob's menu then offers Edit here only (caller): the function grows a parameter that the number reads, the caller you opened it from passes its value and every other call passes the old one (rust = \s cutoff -> s |> lpf cutoff 0.3, |> rust 1200Hz at each call) — one edit over the song, compiled before it lands, one undo step; the case returns to the caller, where the new knob is. Edit the module (all N calls)… types a value into the function itself. A function passed as a value somewhere (not called) is refused with the line, and a library function is edited as the shared definition it is.
  • Record a knob into a capture: while Capture Performance as Code is armed, turning a literal knob is heard as it turns but not written on release — the take records it. The review says which literals it promotes (keys lpf.cutoff → macro cutoff), and insert makes each one a performance knob and writes its ride as that macro's lane, in one edit and one undo step. The capture only drives macros, so promotion is the path; discarding the take puts the sound back. Arrow-key steps still write as usual during a take.
  • MIDI learn (knob menu): the next CC moved on a controller turns this knob — a macro, or any literal. The binding is kept by the literal's identity (case:keys/lpf.cutoff, saved with the macro learns), found again after edits, and works with the Rack closed. A CC ride is heard in place (planned once per source) and the literal is written once the stream pauses: one install, one undo step. Forget MIDI drops it.
  • Make module: shift-click consecutive stages of a chain, right-click, Make module…, name it and tick the knobs to expose. The stages become module1 = \s amount -> s |> lpf 900Hz 0.7 |> drive amount, called in their place with the current values, so the sound is unchanged.

See the whole song as a case#

The song chip in the case header (announced to screen readers as song case) opens the whole song as one case. Each row starts as one line: a thumbnail of its pattern (a tick per hit, a dash per note, the playhead while playing), its name (click it to open the track's own case), what it plays (wide panes), a small chip per effect stage, a chip per send (→ room -15dB) or group it belongs to (→ drums) and per track it hears (← kickTrack), then the strip's level as a fader — drag it either way or type its number; it is heard while it moves, like the knob — and a meter. Rows that do not fit their chips say how many more (+2). An 18-track song fits the pane this way.

The lines form a tree: the master first, the tracks that go straight to it, then each bus or VCA group with the tracks under it — a track sits under the submix or group it is a member of, else the first bus it sends to, so a send stays next to its bus. A bus whose strip sends to another bus sits under that bus (a pair sending to each other keeps only the first link). A row with no level in the mixer shows + level where its fader would be: click it to write level = 0dB into its mixer entry (or a new entry), then turn the fader. A bus has no mixer entry, so no fader. Click a line (or its ▸) to unfold it into the full row below; click again to fold it. Several rows can be open at once and stay open across sessions. An unfolded row is only as tall as its tallest panel. When the rows overflow the pane, a minimap at the right shows every row (unfolded ones taller, buses and the master in their colours) and the part on screen as a frame; click or drag it to scroll.

An unfolded row shows, as before:

  • A row per track, under its name: the sequencer, any pattern transforms (fast 2, chop 8), the voice — or, for a sound track, a sampler — the dashed voices sum here seam, the track's effects, and its out strip (level, pan, width).
  • A row per bus: its first panel lists the sends, one line each: the sending track, its amount as written (-18dB) and a slim bar you drag like a knob. A long list grows a column at a time (up to three), then shows pages (1/2 ▸); hover the panel for every send. A short list's sends arrive as cables at the panel's left edge; a long list marks each line with a dot instead, and the track's strip says where it goes (→ hall). Then come the bus's effects and its strip. A submix row lists its member tracks the same way.
  • A row per VCA group (group drums = [kickT, snareT]): its members' strips, then the group's own strip with its level from the mixer.
  • The master: the returns listed, the master inserts = [ … ], and the master strip.

When one track's audio is used in another, a cable runs from that track's strip to where it is used: follower kickTrack or tap voice in a patch (the jack is on the voice that plays the patch), |> duck kickTrack 0.6 on a track, or a shared control such as kickSense = tapLive kickT … read by duckBy. The jack reads ← kickTrack. A track heard in three or more places gets a coloured dot on each jack instead of a cable, and its strip's output says how many (→ 6). A cable is drawn when both rows are unfolded; on a folded row the same link is its ← kickTrack or → room chip. In a single track's or patch's case, the same source is a labelled jack (← kickTrack) with no cable. Long cables share one side of the case, and cables from one source share one lane.

A sampler shows the samples it plays, one lane each (up to four), with the waveform from the loaded sample bank. When the bank has not loaded a sample, its lane shows a dim placeholder outline, and the panel's accessible value says not loaded. Slice markers appear when a chop or slice cuts it. sample "name" inside a patch is a sampler panel too. A song with no patches, such as breaks.volta, opens straight on its song case. A file with no tracks or buses (a library of patches or effects) has no song case.

Every knob is the same one-literal edit as in a track's case, checked by the compiler and undone in one step. A send amount or a master insert writes inside the mixer { } block, and a bus effect writes its bus hall = … line. Click a row's name, or double-click a track's strip, to open that track's own case; ‹ back returns to the song. Tracks the case cannot draw as rows (an imported song's entries, for example) are listed in the header after not drawn.

Patch new routing. Drag a track's strip output — on an unfolded row its strip's jack, on a folded line the jack at the line's right end, so neither row needs unfolding:

  • onto a bus's + send (on its unfolded sends list, or the ⊕ beside bus on its folded line): the track gets sends = { hall = -18dB } in its mixer entry (the entry, or the whole mixer { } block, is created when missing);
  • onto a group's or submix's + member (likewise): the track joins its list;
  • onto an effect's knob: a sidechain, written as (follower kickT 5ms 120ms) * depth beside the knob's value — or as the whole control of a duckBy;
  • onto a jack that already names a track (duck kickT): that name changes.

Each target lights only once the change compiles; a refused drop says why (a bus's output cannot send to another bus: the mixer takes sends from tracks only). One undo removes it.

Expand and rewire a patch#

Double-click a local patch card in Bay, or select it and press Enter. Its Rack opens inside Bay, using the full canvas; ‹ Bay collapses it and restores the prior task mode and camera. Alt-click still opens the patch's source. The existing audition, Envelope, graph, chain, scopes and modulation-route tools remain available. wire also appears in the standalone Rack and notebook inspectors.

wire shows the complete compiled signal graph: oscillators, note inputs, envelopes, filters, arithmetic and output. Classic patches show the implicit source → low-pass → envelope → gain stages too. Signal reads left to right: the output sits alone in the rightmost column, each module one column left of the stage it feeds, and each column is ordered to minimise cable crossings. Only a patch too long to fit the canvas even at the readable zoom floor wraps its columns into stacked bands (a return cable hands each band to the next); one that fits never wraps just to zoom in. A module's face shows its title, its folded values (kick909 over 0.31 · 0.29 · 0.62) and, once zoomed in far enough, a range chip (±1 bipolar, 0–1 unipolar, Hz); hover names the range, the op and the compiler's node id. Cables stay visible while playback is stopped; live energy brightens them. The expanded canvas has fewer navigation rows and uses the available height.

Lit, outlined jacks are source-backed patch points. Drag a named output to an input, click the output then the input, or click an input to choose a signal from its menu. Dragging an input to another output changes its source. A swap changes only the corresponding reference: cutoff = 800Hz + slowLfo * 1200Hz becomes cutoff = 800Hz + fastLfo * 1200Hz, retaining the base value and depth. The wiring toolbar groups what to patch (Inputs ▾, Signals ▾), the joined undo / redo pair and Source, after the WIRING title. Repeated uses have separate sockets. Note inputs pitch and vel participate alongside named fields and local macros.

The classic voice's implicit input jacks replace their authored field value; for example, repatching its amplitude input replaces ampEnv. Inputs exposes every field and reference, including constant values absent from the graph. Signals includes unused macros and named sources. Hover identifies the exact input and whether a swap replaces a reference or the whole value. Anonymous/internal cables remain visible; their nodes open source for editing.

Changes compile before committing. Invalid units or a dependency cycle leave the source intact; a compiler rejection reports its diagnostic. Accepted changes use one guarded source splice and the normal live-update path. undo/redo and their source-history shortcuts restore the text and update the sound. Escape cancels a held cable before it closes the expanded patch; dropping on empty space cancels it too. Live cables read the existing node meters.

Alt-click a Bay card at any zoom level to open its inline source editor, including over pattern lanes and faders. Source and Alt-click on a module also open the patch's source while its Rack is expanded inside Bay. The popover retains its normal apply/discard and conflict behavior.

The wire view's cable editing supports local patch { … } declarations. Imported/derived patches retain their complete graph and show the editing limitation; a local derived declaration can still open its source. References inside lexical binders or qualified projections are not exposed as editable cables in this view; use modules for authored-expression editing and extraction. Neither view inserts automatic unit conversions. A missing compiled patch reports that state rather than silently showing another patch.

Implementation: ui/src/patchwire.rs parses with the compiler AST, guards the gesture's owning declaration, and re-resolves a minimal splice at commit. rack.rs hosts the view; Bench and the standalone shell share the same plan/analyze/splice seam. Native regressions: patchwire::tests and ui/tests/patch_wiring.rs.

Compose the signal graph#

Choose modules in Rack to work with the expressions you wrote. Function calls, combinators, lists, records, arithmetic and lambda bodies have their own modules and argument jacks. The wire and graph views still show the lowered DSP and its scopes. Modules that do not feed the output are marked not in output; they remain available for connection. Polyphony and glide directives are marked voice setting.

Click a module to open its inspector. Drag a numeric parameter to change it in its authored unit, or choose expr to replace it with a signal or any valid expression. Parameter names and documentation come from the compiler's builtin catalogue; Info opens the selected module's description and signature. Lists, lambdas, records and case-branch expressions can be edited directly. Changes apply only after the candidate compiles.

The canvas fits again when its size or graph changes. Dense patches wrap to keep their output visible. Scroll to zoom and drag the background to pan; double-click a module or choose Focus to inspect it at full size, then Fit to restore the overview. A narrow window puts the inspector below the graph, with parameter controls before the longer expression editor.

  • Insert after adds processing at a module's output. Select an argument and use Insert before input to process only that input.
  • Replace opens a searchable browser of sources, modulation, filters, effects, functions and combinators. Ready-to-run recipes include mix, map, crossfade, shape, process and feedback. Local functions also appear; the expression editor accepts custom calls.
  • Earlier / Later reorder pipeline stages with their parameters intact. Bypass/Enable too; Replace with input swaps a module for an input. Remove deletes a named field; the compiler refuses removal when it would leave a broken dependency.
  • + module adds a named patch field. Connect it to the audible path using its output jack. Drag output-to-input or input-to-output, or click the two jacks in sequence. Share names an anonymous expression once. Anonymous cable fan-out also extracts a shared field or lexical let, preserving one oscillator/effect instance rather than duplicating its state.

Classic patches have a voice output module representing their implicit source/filter/envelope/gain path. Make output editable writes the equivalent explicit out expression. This preserves rendered audio and makes every output processing stage available for editing.

Functions and Context enter local definitions, showing function parameters as inputs. Editing a definition affects every call. Track FX opens a track's expression, where reverb and compression operate after voice summing. Libraries opens definitions in their owning buffers, including browser-shipped instruments and nested constructor patches such as anchorKick.voice. A loaded instance offers Edit … definition directly. The library and full song are checked before an edit becomes live, and library undo/redo uses that buffer's history. These edits remain unsaved buffer changes until the ordinary Save operation; browser changes use an override and preserve the shipped library's original contents.

For a derived patch, the module view shows its base and explicit overrides. Add a field to override it locally, or enter the base definition through Context/Libraries to change the shared instrument. An instantiated library patch keeps its complete DSP graph visible until a definition is selected; Rack never treats an imported byte offset as local source ownership.

Source and Alt-click open the current definition. Local definitions use Bay's inline source editor when the patch is expanded. Floating graphs close so the source editor is visible. Every committed module edit is one source-history operation, and undo/redo updates the sound.

Implementation: rackedit.rs projects compiler AST/group/pipe tables and plans guarded declaration edits; rackcompose.rs hosts the graph and inspector. The existing Bench transaction owns local compile/commit/history; app/rackmodules.rs validates and publishes import edits. Regression coverage includes rack_composer.rs, the compiler-backed model tests, library-buffer history, and identical rendered samples for explicit classic outputs.

Editing gestures (historical — deleted 2026-08-09, re-port retired 2026-08-24)#

  • Patch in: drag any module's output jack — or a macro chip from the SOURCES strip, or the ⊕ macro well (declares a fresh macro) — onto a field card or a PORTS-strip well. The field's expression gains the term, unit-wrapped as needed (* 800Hz depth into an Hz port, |> scale [0, 0.5] into a mismatched normalized port, an * ride onto the audio path); the editor selects the inserted depth literal so the first thing you type tunes it. Ports that would take the signal directly highlight solid during the drag; ports that need a wrap highlight dashed. Dropping on an undeclared standard field (cutoff / resonance / gain / pitchMod) adds the field to the patch.
  • Unpatch: right-click a cable feeding a field-root mix (Sum) or ride (Product) to remove exactly that term; right-click a single-term field card to zero it (const 0). The sound source itself (source/out) refuses to be unpatched.
  • Field-root modules patch onward by name (shared); dragging an anonymous module's jack duplicates its expression at the drop site — name the expression in source when you want one instance feeding two ports.

Historical ground rules#

  • Only synths with a local patch { … } (or template) block are editable; imported and transform-derived synths render read-only with a note — their IR spans index another file's text (the signal-overlay containment rule).
  • A gesture that fails the pure edit layer or the compile check toasts the reason and leaves the source byte-for-byte untouched.

The historical TypeScript implementation never shipped (spec §4–6): node-creation palette, track-effect targets, extract-to-share for anonymous expressions, un-patching text that doesn't split 1:1 into the root's terms. The current module-composition workflow is described above.