Two ways to build a DSP — Builder vs .dsp, version control, sharing
A DSP is the thing that makes sound: a compiled Faust worklet, running. Two kinds of document produce one, and FaustWave keeps them apart on purpose — a Builder (.builder) is a node graph you wire, a Faust DSP (.dsp) is source text you write. Both compile through the same libfaust pipeline and run as the same worklet; what differs is the editing surface and the file that ends up on disk.
That distinction runs through the whole product. The two documents are builder and faust-dsp, and the actions follow: builder.* edits a graph, dsp.* drives whatever is running — either kind. This section covers when to use which, how to evolve one, and how to share it.
Builder vs .dsp — when to use which
| You're… | Use |
|---|---|
| Wiring effects + filters in a signal flow you can SEE | Builder (.builder) |
| Translating a Faust paper / online example into something runnable | .dsp |
| Iterating on a synth with a few oscillators + filter | Builder |
| Writing a custom Master FX slot | .dsp source pasted into master.fx.slot.add |
| Sharing with someone unfamiliar with Faust | Builder — they can SEE the structure |
| Sharing with a Faust dev who wants to fork the source | .dsp — they get text they can read + edit |
You can move between them: Builder canvas → Show Faust source → paste into a .dsp file. A .dsp with a process = ... body → builder.import it into an empty Builder (planned; manual paste-and-cleanup works today).
The canonical Builder loop
The short round-trip from idea to running DSP:
actions:
- action_id: documents.add
input: { type: builder, title: "My first Builder" }
save_as: b
label: "▶ 1. Spawn a Builder"
- action_id: builder.graph.node.add
input: { module_id: "{{b.module_id}}", kind: osc }
save_as: osc
label: "▶ 2. Add an oscillator"
- action_id: builder.graph.node.add
input: { module_id: "{{b.module_id}}", kind: lp }
save_as: lp
label: "▶ 3. Add a low-pass filter"
- action_id: builder.graph.node.add
input: { module_id: "{{b.module_id}}", kind: output }
save_as: out
label: "▶ 4. Add the output"
- action_id: builder.graph.connect
input:
module_id: "{{b.module_id}}"
source_node_id: "{{osc}}"
target_node_id: "{{lp}}"
label: "▶ 5. Wire osc → lp"
- action_id: builder.graph.connect
input:
module_id: "{{b.module_id}}"
source_node_id: "{{lp}}"
target_node_id: "{{out}}"
target_port: in_l
label: "▶ 6. Wire lp → output"
- action_id: builder.graph.node.param.set
input:
module_id: "{{b.module_id}}"
node_id: "{{lp}}"
param: cutoff
value: 800
label: "▶ 7. Cutoff = 800 Hz"
- action_id: dsp.run
input: { module_id: "{{b.module_id}}" }
label: "▶ 8. Compile + run"
- action_id: documents.save
input: { module_id: "{{b.module_id}}" }
save_as: saved
label: "■ 9. Save to assets/"
The .dsp workflow
.dsp documents are plain text. Open one via Documents → Open from disk, or:
documents.add { ref: "asset:assets/<your patch>.dsp" }
Each .dsp:
- Has a
process = ...definition (the audio chain). - Compiles to a worklet on Run (same pipeline the Builder takes).
- Exposes Faust sliders / bargraphs as live params, accessible via
dsp.param.set/dsp.param.getand the Inspector panel.
You can import("stdfaust.lib"); at the top to pull in the Faust standard library plus everything mounted under custom .lib files.
Compile vs Run
The .dsp editor's footer carries two buttons when stopped: Compile and Run.
- Compile (Hammer icon) — calls libfaust only. Reports syntax / semantic errors and stops there. Use when you want to validate a paste without starting audio.
- Run (Play icon) — compile + start the worklet. Requires the source to be a generator (
process = …, no audio input). An FX-shape source (process(in)orprocess(in_l, in_r)) compiles cleanly but Run bails at start with a friendly error: "DSP modules are generators with no audio input. For FX-shape sources use master.fx.slot.add or the Master panel's 'Add FX' picker."
The Compile split lands clean when you paste a Builder-generated source — Builder graphs always emit process(in_l, in_r) = …, which is FX-shape. Use Compile to confirm the paste survived transit, then mount it into a Master FX slot (or back into an empty Builder via builder.import once that's wired) instead of trying to Run it standalone.
For AI & automation · MCP
dsp.compile, documents.list, dsp.run, dsp.stop, dsp.param.set, dsp.param.get work on either kind — the Builder's graph and a .dsp's text compile to the same worklet. See palette.list { category: "DSP" } for the full set.
documents.save vs builder.export
Two write-to-disk paths that look similar but differ in one critical way:
| Action | What it does | When to use |
|---|---|---|
documents.save { module_id, path? } | Writes the .builder to disk AND links it to the module so the project reopens it on next load. Without path: auto-place in the active project's assets/. | When building content that should persist in the project. |
builder.export { module_id, path? } | Writes the .builder to disk (or returns the JSON if no path). Does NOT link to the module — the graph would reopen empty next session. | When sharing a one-off file. |
The trap: builder.export followed by hub.publish works, but next time you open the project the Builder is empty (no linked file). Use documents.save for project-resident documents, then either hub.publish directly with the saved file, or builder.export to a separate share location.
Importing a .builder
1. documents.add { type: "builder", title: "Import target" } → module_id
2. builder.import { module_id, path: "asset:assets/<name>.builder" } (or an absolute OS path for an external file)
builder.import accepts EITHER path (loads from disk + KB re-indexes it) OR doc (an in-memory BuilderDocument object / JSON string). Pass exactly one.
Evolving one — version control without the ceremony
.builder and .dsp documents are text / JSON files in your project's assets/ folder. That means git tracks them naturally:
cd ~/your-project-as-git-repo
git diff assets/my-saturator-lead.builder
Two patterns:
- Per-project git repo — your whole FaustWave project is a git repo. DSPs, samples (gitignore the hashed bytes if you don't want them in repo), sequencer state — everything.
- One repo per instrument — share an evolving Builder via a tiny repo + a
.builder+ aREADME.mddescribing what it does. Other usersbuilder.importfrom disk.
Plus the Hub for one-click distribution when git isn't the right granularity.
Sharing one
Three escalating channels:
- Send the file —
.builderor.dsp. Recipient opens it; if there are sample refs they don't have, FaustWave prompts on compile. Works for one-off shares. - Push to a git repo — best for "I'm iterating with one collaborator and I want diffs."
- Publish to the Hub —
hub.publish { kind: "builder" | "dsp", file_path, slug, title, ... }. Best for "anybody can install with one click." See FaustWave — Hub § 3.
Templates from the Hub
Before starting from scratch, check the Hub:
actions:
- action_id: hub.search
input: { kind: builder, q: bass }
save_as: hits
label: "▶ Look for bass templates"
- action_id: hub.install
input: { item_id: "{{hits.data.0.id}}" }
label: "▶ Install the top hit"
Install a starter, fork it, evolve. Faster than learning the Builder from a blank canvas; you see how an existing graph puts the nodes together.
Where to go from here
- FaustWave — Builder § 1 — the canvas + node-kinds + lifecycle.
- FaustWave — Builder § 3 — loading samples into a
soundfilenode. - FaustWave — Hub § 3 — publishing them as
hub.publish kind: "builder"/kind: "faust-dsp". - FaustWave — Mixer & Master § 2 — turning a
.dspbody into a Master FX insert. - FaustWave — Node-Pack Authoring — the Box-DSL syntax that pack-author
toBoxtemplates use, per-kindimportsdeclaration, and paired-port stereo for community packs.