All books

Two ways to build a DSP — Builder vs .dsp, version control, sharing

~6 min read · updated 2026-09-10 · markdown

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 SEEBuilder (.builder)
Translating a Faust paper / online example into something runnable.dsp
Iterating on a synth with a few oscillators + filterBuilder
Writing a custom Master FX slot.dsp source pasted into master.fx.slot.add
Sharing with someone unfamiliar with FaustBuilder — 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.get and 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) or process(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:

ActionWhat it doesWhen 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 + a README.md describing what it does. Other users builder.import from disk.

Plus the Hub for one-click distribution when git isn't the right granularity.

Sharing one

Three escalating channels:

  1. Send the file — .builder or .dsp. Recipient opens it; if there are sample refs they don't have, FaustWave prompts on compile. Works for one-off shares.
  2. Push to a git repo — best for "I'm iterating with one collaborator and I want diffs."
  3. 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 soundfile node.
  • FaustWave — Hub § 3 — publishing them as hub.publish kind: "builder" / kind: "faust-dsp".
  • FaustWave — Mixer & Master § 2 — turning a .dsp body into a Master FX insert.
  • FaustWave — Node-Pack Authoring — the Box-DSL syntax that pack-author toBox templates use, per-kind imports declaration, and paired-port stereo for community packs.
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.90.0