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) |
A live-catalog chain
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"
Adding nodes
Four equivalent ways:
- 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.
- Open the Node Library via the footer's Library button. Same picker, always-visible.
- AI ask: "add an osc to my Builder". The assistant runs
builder.graph.node.add { module_id, kind }for you. - 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 companionbuilder.graph.node.param.getreads 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
soundfilenode-kind sha256 references. Set viabuilder.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.
For AI & automation · 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).
For AI & automation · 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/keyOffAPI. - The graph must contain at most one
midi_notenode — multiplemidi_noteinstances all collapse onto the same shared magic-name sliders (would be ambiguous in poly mode).
For AI & automation · 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.
For AI & automation · 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
hsliderdeclaration in the pasted source). - Lifting a Builder graph into a
.dspto 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.
For AI & automation · 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.