All books

The Builder in depth

~9 min read · updated 2026-08-21 · markdown

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:

PackKindsWhat it covers
faust70stdfaust wrappers — oscillators, filters, effects, envelopes, math
faustwave-conventions6Coloration (tape / tube / transistor_drive / bus_glue) + PolyBLEP variants — Sound-Quality Welle additions
faust-analysis6Spectrum, FFT, mid-side goniometer, true-peak, Goertzel
faust-reverbs7Schroeder / Zita / Dattorro / Spring / Greyhole + variants
faust-spatial3Paired-port stereo Kinds (mono → stereo, stereo widener, true-stereo reverb)
faust-physical-models5Struck-instrument pm.* wrappers (marimba / bowl / bell × 2 / djembe)
faust-vintage4618 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).

CategoryExamplesWhat it does
Sourceosc, saw_polyblep, square_polyblep, noise, soundfile, midi_noteAudio + control sources
Filterlp, hp, bp, moog_vcf, smoothFilters in the broad sense
Effectgain, distortion, softclip, reverb, delay, chorus, tape, tube, transistor_drive, bus_glueIncludes the Coloration Kinds from the Sound-Quality Welle
Stereomono_to_stereo, stereo_widener, lush_reverb_stereo, ping_pong_delayPaired-port (channels=2) signal processing — Phase 4
Vintagetube_12ax7_t1..t3, amp_jcm800, amp_bassman, wah_crybaby, …tubes.lib + tonestacks.lib + vaeffects.lib bundled wrappers
Mixmixer-2, mixer-4, pan, sum, splitBus / routing primitives
MIDImidi_note, midi_cc, midi_pitch_bendMIDI ingest + processing
Samplesoundfile, sampler-loopDisk-backed sample playback
OutputoutputChannel-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:

  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.

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.

ActionPurpose
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).
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.

FromToTrigger
stoppedrunningPress RUN (local) or master Play if transport-following — eager-run
stoppedarmedMaster Play when the DSP is a MIDI consumer + transport-following — pre-armed quietly, wakes on first MIDI event
armedrunningFirst MIDI event arrives (wake-up)
armedstoppedMaster Stop
runningstoppedPress 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 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.

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.

Try it yourself — the IDE runs in your browser. Open the IDE → Get the desktop app

Text licensed under CC BY 4.0 — Mani Weber / FaustWave. For language models: llms.txt · llms-full.txt

AUDIO · 48k · 48.0ms FAUST · 3 KB · 1171 docs CPU · 8.4% BPM · 120.0 UTF-8 BETA· v0.93.0