# The Builder in depth

The Builder is FaustWave's visual graph editor. You build node graphs; the Builder generates Faust source from them in real time, compiles to WebAssembly, and runs the resulting worklet at audio-thread priority. It's the centre of gravity for most users.

This section is the canvas + node model. The next section covers the workflow (Builder vs `.dsp` source, version control, sharing) and the third covers sample-loading.

## Canvas anatomy

A Builder tab is split into:

- **The canvas** — infinite-pan, zoom-in / zoom-out. Hosts nodes and the cables between them. Right-click empty area opens the node picker.
- **The footer** — toolbar with Save / Open / Auto-arrange / Compile / Run + Stop / Polyphony / DSP-mode / Inspector buttons.
- **Per-node inspector** (right-click node → Inspect, or via the Inspector toggle) — fine-grained view of node params + modulation rings.
- **Minimap** — toggle on for a thumbnail navigator (useful past ~20 nodes). **Default OFF** (0.64.5+) — the canvas + Hub-Discovery rail already compete for real estate; flip on via the canvas-controls minimap toggle when you want it.

## Nodes, by category

Node kinds come from registered node-packs. The IDE ships **seven bundled packs** today (≈ 143 kinds total), auto-restored on launch:

| Pack | Kinds | What it covers |
|---|---:|---|
| `faust` | 70 | stdfaust wrappers — oscillators, filters, effects, envelopes, math |
| `faustwave-conventions` | 6 | Coloration (tape / tube / transistor_drive / bus_glue) + PolyBLEP variants — Sound-Quality Welle additions |
| `faust-analysis` | 6 | Spectrum, FFT, mid-side goniometer, true-peak, Goertzel |
| `faust-reverbs` | 7 | Schroeder / Zita / Dattorro / Spring / Greyhole + variants |
| `faust-spatial` | 3 | Paired-port stereo Kinds (mono → stereo, stereo widener, true-stereo reverb) |
| `faust-physical-models` | 5 | Struck-instrument `pm.*` wrappers (marimba / bowl / bell × 2 / djembe) |
| `faust-vintage` | 46 | 18 tube stages (12AX7/AT7/AU7/6V6/6DJ8/6C16 × T1-T3) + 25 amp tonestacks + 3 wah pedals |

Community packs install via `hub.install kind:"node-pack"` (or the Hub module's Browse → Install path).

| Category | Examples | What it does |
|---|---|---|
| **Source** | osc, saw_polyblep, square_polyblep, noise, soundfile, midi_note | Audio + control sources |
| **Filter** | lp, hp, bp, moog_vcf, smooth | Filters in the broad sense |
| **Effect** | gain, distortion, softclip, reverb, delay, chorus, **tape, tube, transistor_drive, bus_glue** | Includes the Coloration Kinds from the Sound-Quality Welle |
| **Stereo** | mono_to_stereo, stereo_widener, lush_reverb_stereo, ping_pong_delay | Paired-port (channels=2) signal processing — Phase 4 |
| **Vintage** | tube_12ax7_t1..t3, amp_jcm800, amp_bassman, wah_crybaby, … | tubes.lib + tonestacks.lib + vaeffects.lib bundled wrappers |
| **Mix** | mixer-2, mixer-4, pan, sum, split | Bus / routing primitives |
| **MIDI** | midi_note, midi_cc, midi_pitch_bend | MIDI ingest + processing |
| **Sample** | soundfile, sampler-loop | Disk-backed sample playback |
| **Output** | output | Channel-aware sink (`in_l`, `in_r` paired ports — channels=2) |

> 🟦 **DISCOVER FROM CODE**: the live catalog is always one MCP call away — `builder.kinds.list {}` returns id / label / category / I/O / param counts for every registered kind (no `module_id` needed; it's a global catalog). `builder.kind.describe { kinds: [...] }` returns the full schema (up to 10 kinds per call). The catalog in this section may drift; the MCP catalog never does.

## A live-catalog chain

```yaml
actions:
  - action_id: builder.kinds.list
    input: {}
    save_as: kinds
    label: "▶ 1. List every registered node kind"
  - action_id: builder.kind.describe
    input: { kinds: [osc, lp, output] }
    save_as: details
    label: "▶ 2. Get full schema for osc + lp + output"
```

> 🟦 **HANDS-FREE PATH**: step 1 enumerates the whole catalog; step 2 pulls the port + param schemas for three specific kinds. This is what the assistant does behind the scenes when you ask "add an oscillator and a low-pass filter".

## Adding nodes

Four equivalent ways:

1. **Right-click the canvas** at the position you want the node. Picker opens at cursor, searches across name + category + description. Hit Enter on the first match.
2. **Open the Node Library** via the footer's Library button. Same picker, always-visible.
3. **AI ask**: *"add an osc to my Builder"*. The assistant runs `builder.graph.node.add { module_id, kind }` for you.
4. **MCP direct**: `builder.graph.node.add { module_id, kind, x?, y?, params? }` — same action all paths route through.

## Editing nodes

Each node carries two kinds of properties:

- **Params** — numeric values (frequency, cutoff, gain, ...). Edit via the on-canvas knob (drag), via the Inspector panel, or via `builder.graph.node.param.set { module_id, node_id, param, value }`. The companion `builder.graph.node.param.get` reads the running engine's live post-modulation value — useful for confirming an LFO is actually moving the param.
- **Refs** — string-typed bindings, currently only used for `soundfile` node-kind sha256 references. Set via `builder.graph.node.ref.set { module_id, node_id, param, value }`.

The distinction matters because param changes are typically smooth (the worklet ramps internally), while ref changes are topology-relevant (a new sha means a new file in the worklet's slot map) and trigger a `stop → recompile → start` cycle.

### Duplicate

`builder.graph.node.duplicate { module_id, node_id }` clones a node — its kind, param values, soundfile refs, bypass state — at a small offset. Returns the new node id. Groups are not duplicated; if you duplicate a node inside a group, the clone lands top-level.

### Bypass

Effect-kinds and filter-kinds declare a bypass mapping; flipping bypass either re-routes the chain around the node or triggers a recompile (when the worklet's I/O shape changes). Bypass-able kinds today: `gain`, `smooth`, `lp`, `hp`, `moog_vcf`, `distortion`, `softclip`, `reverb`, `delay`. Other kinds ignore the flag.

> 🔘 **MCP**: `builder.graph.node.bypassed.set { module_id, node_id, bypassed: true | false }`. Bypass is topology-level: with audio running the Builder transparently stops + recompiles + restarts (brief gap). With audio stopped, the flag stages for the next `dsp.run`.

## Cables

Coloured by signal kind so you can read flow at a glance:

- **Cyan** — audio
- **Purple** — control / parameter (e.g. envelope → gain)
- **Orange** — MIDI trigger (gate, velocity)

Connection rules are kind-aware: cables only stick between compatible port types. Wrong combos refuse to connect (the canvas highlights illegal targets while you drag).

> 🔘 **MCP**: `builder.graph.connect { module_id, source_node_id, target_node_id, source_port?, target_port? }` adds an edge. `builder.graph.disconnect { module_id, edge_id }` removes one.

## Groups

A group is a labeled visual frame around N nodes. Purely organisational — no audio impact, no source change. Useful past ~15 nodes when "what's the bass section?" stops being instantly obvious.

| Action | Purpose |
|---|---|
| `builder.graph.group.create { module_id, node_ids, label? }` | Wrap selected nodes into a group. Skips ids that are already grouped. |
| `builder.graph.group.label.set { module_id, group_id, label }` | Rename. |
| `builder.graph.group.ungroup { module_id, group_id }` | Dissolve. Children keep their absolute positions and wirings. |
| `builder.graph.groups.list { module_id }` | Snapshot of every group + its members. |

## Mono vs polyphonic compilation

By default a Builder compiles as **mono** — one voice, no polyphony. A single MIDI key-press steals the previous voice's pitch.

Switch to **polyphonic** via the **Polyphony** footer toggle (cycles Off → 4 → 8 → 16 voices → Off). When polyphonic:

- The source compiles with `declare options "[nvoices:N]"` — the Faust poly generator handles voice allocation.
- Incoming MIDI routes per-voice via the worklet's `keyOn` / `keyOff` API.
- The graph must contain **at most one** `midi_note` node — multiple `midi_note` instances all collapse onto the same shared magic-name sliders (would be ambiguous in poly mode).

> 🔘 **MCP**: `dsp.polyphonic.set { module_id, voices: 0 | 4 | 8 | 16 }`. The engine auto-stops if running so the next `dsp.run` compiles cleanly.

## Compile + Run lifecycle

The Builder runtime is a 3-state machine: `stopped` → `armed` → `running`.

| From | To | Trigger |
|---|---|---|
| stopped | running | Press RUN (local) or master Play if transport-following — **eager-run** |
| stopped | armed | Master Play when the DSP is a MIDI consumer + transport-following — **pre-armed** quietly, wakes on first MIDI event |
| armed | running | First MIDI event arrives (wake-up) |
| armed | stopped | Master Stop |
| running | stopped | Press STOP / Master Stop |

The **eager-run** path is the per-Builder Play (and Builders that don't declare themselves MIDI consumers). The **pre-arm** path lets dozens of MIDI-consumer Builders sit quiet during transport.play without firing N parallel libfaust compiles at once — they wake only when the sequencer actually delivers them a note.

> 🔘 **MCP**: `dsp.compile { module_id }`, `dsp.run { module_id }`, `dsp.stop { module_id }`. The compile path is autonomous — `dsp.run` recompiles if the graph drifted since the last compile.

## Transport-follow toggle

Default: every Builder follows the global transport. Toggle off via the Builder footer's **Link** button (or `dsp.transport.follow.set { module_id, enabled: false }`) for a Builder you want isolated — e.g. an FX-only utility DSP, or one you're auditioning while the rig plays. The opt-out persists in the `.builder`.

## Paired-port stereo — Phase 4

Output + Input kinds expose **paired-port stereo**: each side declares two ports (`in_l` / `in_r` on Output, `out_l` / `out_r` on Input) carrying `channels: 2` metadata. The Builder's wire validator is paired-port-aware (PR #286) — connecting one half of a paired port surfaces a hint to wire its partner. The cable's stroke-width doubles when both halves are connected, so a stereo path reads at a glance.

A typical stereo chain: `midi_note → adsr → osc → softclip → mono_to_stereo → stereo_widener → output`. The chain stays mono until `mono_to_stereo` (one input, two outputs); from there both sides flow through `stereo_widener` (paired in, paired out) into Output's `in_l` + `in_r`. Stereo-aware Kinds live in **`faust-spatial`** (`mono_to_stereo`, `stereo_widener`, `lush_reverb_stereo`) + a few in `faust-reverbs` / `faust-vocal`. Mono Kinds work unchanged — fan-out to stereo happens at Output (`<: _, _`).

## FX as graph Kinds (not built into Output)

The Output node-kind is a slim **channel-aware sink** — paired `in_l` + `in_r` input ports, the Unison config (voices / detune / stereo-spread), and nothing else. FX (saturation, chorus, reverb, width) live as eigenständige Kinds you place in the graph: `softclip`, `chorus`, `reverb`, `mono_to_stereo`, `stereo_widener`, `lush_reverb_stereo`, `ping_pong_delay`. A common sound-quality chain looks like: instrument → `softclip` → `chorus` → `reverb` → output.

This is consistent with the "Effekte = Kinds" Builder convention — wire what you want, where you want it. Per-Builder CPU is naturally low because nothing runs unless you wire it. The pre-Phase-4 auto-injected FX-Bus (drive / chorus / reverb defaults baked into every Output compile) was dissolved in #284 — its sonic recipe lives on as the upcoming Master-Control rack (see TODOS).

## Inspecting the generated Faust source

Click the **`</>`** button in the Builder toolbar — the live Faust source copies to your clipboard (icon swaps to ✓ for ~1.2s as confirmation). Paste into a `.dsp` editor + click **Compile** there to validate, or into any text editor for inspection.

Useful for:

- Confirming what your knob writes (find the `hslider` declaration in the pasted source).
- Lifting a Builder graph into a `.dsp` to keep evolving outside the Builder.
- Filing bugs.

The source is also available via `builder.source.get { module_id }` for MCP-driven workflows. Per-kind imports (e.g. `import("tubes.lib")` when a Vintage Kind is in the graph) are auto-collected + emitted by `faustGen` after the default `import("stdfaust.lib")` header — see **FaustWave — Node-Pack Authoring** § 2 for the mechanism.

## Modulation rings

In the Inspector view, every editable param shows a **modulation ring** around the knob: it's the visual surface for connecting an LFO / external modulation source to that param. Right-click the ring to set the source + amount.

LFOs live in the **Modulators** rail panel (right rail — "+ Add" → *Modulators*). See **FaustWave — Patchbay** § 3 for the modulation cable semantics (`amount` instead of `gain`, per-param Sink ports).

## MIDI channel filter

Every Builder has a MIDI channel filter (0 = Omni, 1..16 = listen on that channel). Sequencer tracks dispatch to a specific channel; matching channels receive the events.

> 🔘 **MCP**: `dsp.midi.channel.set { module_id, channel: 0..16 }`. The filter applies to every `midi_note` node in the DSP.

## AI parity

Every gesture in the Builder routes through the action registry — palette, click, drag, keyboard shortcut. The assistant + the MCP server see the same actions, same input schemas. There's no "limited subset"; what you can do, the AI can do.

The hard-required `module_id` on all DSP actions (every `dsp.*` call must specify which Builder) means external MCP clients must thread the Builder id through every call — no silent fallback to "the active tab". Discover via `documents.list` or capture the return of `documents.add`.

Next section: the workflow side — Builder vs `.dsp` source, version control, sharing.
