A browser-native workbench for the Faust DSP
language. Edit Faust source with live syntax highlighting, compile in
the page via libfaust-wasm, watch the run-time UI, oscilloscope and
spectrum analyser update at every save, and optionally drive the
workflow from an MCP client (Claude Desktop, Claude Code, …) through
a small local Go bridge.
No backend. No Docker. The webapp runs entirely client-side ; only the optional MCP bridge runs locally on your machine.
The latest build is live at https://orlarey.github.io/faustcode/.
For local development :
./scripts/serve.py # serves the repo on http://localhost:8080
# then open http://localhost:8080/webapp/The dev server sends Cache-Control: no-store on every response so you
don't have to fight Chrome's ES module cache while iterating.
-
Drop a
.dspfile anywhere on the page, or paste Faust code into the empty-state pane. -
The header dropdown switches between five views :
- dsp — live CodeMirror editor (syntax-highlighted Faust,
Cmd/Ctrl-Sto compile, click a line number to select that whole line, drag across line numbers for multi-line selections) - svg — block diagram, clickable from the outer
processdown to every nested expression - run — audio engine with the classic Faust UI on the left, an orbit-style knob layout on the right, polyphony / MIDI / download selectors, and an oscilloscope + spectrum analyser at the bottom
(The signals and tasks views, DOT graphs of the compiled program, were removed in 0.14.0: of little use to most users, and libfaust 2.88 to 2.90 cannot draw the signals graph of recursive programs.)
- dsp — live CodeMirror editor (syntax-highlighted Faust,
-
✎ in the header toggles a floating CodeMirror overlay : drag the title bar to move, drag the bottom-right corner to resize, position and size persist across reloads. The overlay mirrors the dsp-view editor keystroke by keystroke (and vice-versa) when both are visible, so you can iterate on the code while watching the Run view update underneath at every
Cmd/Ctrl-S. -
Files (0.16.0,
spec_files_store.md) : the File menu has New file, Open… (Cmd/Ctrl-O), All files… (search, trash), the recent files, Save (Cmd/Ctrl-S), Save as… (Shift-Cmd/Ctrl-S), Rename… and Move to trash. A file is a name and its versions : each save adds one, the header showsWahoo.dsp ● · v3/5(● : unsaved changes) and ◀ ▶ step through the versions. In Chromium, a file opened from the disk is written back there by Save. Submitting a code under the name of a file (drop, MCPsubmit) makes a new version of it. -
Stores (0.17.0,
spec_files_store.md) : GitHub repositories of Faust modules and scenes, the same as in faustmodular (same browser, same list : a store followed in one is followed in the other). File ▸ Stores… follows one ; File ▸ Open from a store lists its modules, and one opens as a file linked to the store. A link opens any.dspof GitHub :?file=<its address on GitHub>(or?store=owner/repo&file= <path>&ref=<commit>) ;?store=owner/reposhows the modules of a store. Share offers the link of the store (short, at the commit read) for a module unchanged since it was opened. -
Publishing (0.18.0) : a click on the read badge of a store (File ▸ Stores…) asks for a token of GitHub (fine-grained, this repository only, Contents read and write), checked before it is kept ; faustmodular uses it too. File ▸ Publish to … publishes the current version of the open file as a module of the store, in one commit, the index made again : a new module, or a new version of the module it was opened from. It warns when someone else published since, offers another name when the path holds another module, and refuses a code that does not compile, a
.lib, or a code that imports a.libof faustcode. The link to share it, at the commit published, is copied.⇡in the header : the file changed since the store. -
Sessions hydrate from the browser's Origin Private File System on every load, so closing the tab doesn't lose your patches.
The contract exposes 34 tools (cf. tools.json) covering session
management, audio transport, run-parameter manipulation, MIDI events,
spectrum / audio-quality capture, error retrieval and Faust library
documentation lookup. A local Go bridge (faustcode-mcp) speaks MCP
over stdio and brokers every tool call to the browser tab over
ws://127.0.0.1:7777/ws.
Download the prebuilt binary for your platform from
https://github.com/orlarey/faustcode/releases/latest —
darwin-arm64, darwin-amd64, linux-amd64, linux-arm64 or
windows-amd64.exe. The webapp's "Setup MCP" panel (open it from the
MCP pill in the header) detects your platform and offers the
matching link.
On macOS the binary is unsigned ; strip Gatekeeper's quarantine flag after the first download :
chmod +x ./faustcode-mcp-darwin-arm64
xattr -d com.apple.quarantine ./faustcode-mcp-darwin-arm64
mv ./faustcode-mcp-darwin-arm64 ~/bin/faustcode-mcp # or anywhere on PATH(On Linux, chmod +x is enough. On Windows, double-click — Defender
SmartScreen will flag the unsigned .exe once ; click "More info → Run
anyway".)
Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json)
or any MCP-aware tool that consumes the standard config :
{
"mcpServers": {
"faustcode": {
"command": "/Users/<you>/bin/faustcode-mcp"
}
}
}Claude Code (one-liner) :
claude mcp add faustcode /Users/<you>/bin/faustcode-mcp
claude mcp list # verify it shows ✓ ConnectedMake sure https://orlarey.github.io/faustcode/ is open in a browser
tab while you talk to your MCP client. The bridge needs a webapp tab on
the other end to actually execute tool calls — without it, calls return
a friendly no_webapp error that tells the assistant what to do.
Only one tab can hold the bridge at a time. A second tab opening
sends the first one a 4001 superseded-by-new-tab close code ; the MCP
pill of the displaced tab goes red and stops reconnecting until you
click Connect in the side drawer to take the seat back.
The webapp recognises these query strings :
?mcp=ws://127.0.0.1:7777/ws— auto-connect to the bridge without needing the user to click Connect. Handy for automated browsers, embedded contexts or persistent setups.?token=<shared-secret>— sent in theWsReady.tokenfield of the handshake (SC-4). The bridge must have been launched with the same--tokenflag, otherwise the connection is rejected.?code=…&name=…&view=…&poly=…— open a shared DSP (cf.spec_share_url.md).?link=modular— this window edits the modules of the faustmodular (formerly FaustPlayground) page that opened it (see below).?faustmodular=<url>— the faustmodular that the faustmodular button opens (defaulthttps://orlarey.github.io/faustmodular/; a local one, tests).
The pencil of a faustmodular module opens faustcode in a window
reused for all the modules (?link=modular). The module's code
becomes a session, shown in the dsp view once ENTER is clicked. Each
save of that session that compiles (Cmd/Ctrl-S in the dsp view or the
floating editor, or an MCP submit while it is active) is sent back to
the playground, which recompiles the module in place : the sound goes
on, the slider values and the connections are kept. A code with errors
stays in faustcode, with its errors ; the module keeps playing its last
valid version.
The other way, the faustmodular button of the header opens the
session in faustmodular, as a module of its open document : faustmodular
is opened if needed (?link=faustcode, a window reused), or the
faustmodular that opened faustcode is used. The session is then linked
to the module : each save that compiles updates it, as above, and the
button sends the new code of a linked session to its module instead of
adding another one.
The two pages talk with postMessage (they may have different
origins) ; faustcode only accepts messages from the faustmodular window
(the one that opened it, or the one it opened), and faustmodular only
takes code from the official faustcode, or a local one. Protocol and
behaviour : spec_playground_link.md and webapp/playground-link.js.
webapp/ the browser app (HTML, ES modules, CodeMirror 6 bundle,
Faust UI vendor, libfaust-wasm runtime)
mcp/ the Go MCP bridge ; speaks MCP over stdio, brokers
tool calls to the browser tab over WebSocket
tools.json the shared contract (34 tools, JSON Schemas inlined per
tool so MCP clients can validate without resolving $defs)
SPECIFICATION.md architecture and protocol document
webapp/vendor/fauststore/ the library of the stores, a copy built by
faustmodular (scripts/update-fauststore.sh)
spec_files_store.md files, File menu and stores
scripts/serve.py a no-cache dev server
.github/workflows/ Pages publish + cross-build releases
┌─────────────────────────┐ stdio JSON-RPC ┌─────────────────┐
│ MCP client │ ◄───────────────────►│ faustcode-mcp │
│ (Claude Desktop, …) │ │ (Go bridge) │
└─────────────────────────┘ └────────┬────────┘
│ WS 7777
▼
┌──────────────────────┐
│ browser tab │
│ (faustcode webapp) │
│ │
│ • libfaust-wasm │
│ • WebAudio engine │
│ • spectrum / OPFS │
└──────────────────────┘
The webapp is just static files served from GitHub Pages (or the local
dev server). All compilation and audio happen in-page via WebAssembly.
The bridge is the only host-side process and only listens on 127.0.0.1.
cd mcp
make build # outputs ./faustcode-mcp
make test # vet + go test ./...
make probe # round-trip MCP↔bridge↔fake-webapp e2e checkCross-platform release builds are produced by the release.yml
workflow on every v*.*.* tag and uploaded to GitHub Releases ; the
webapp's "Setup MCP" panel always points to releases/latest so users
get the newest binary regardless of how often you cut tags.
./webapp/vendor/codemirror/build-cm6.shRuns npx esbuild inside a transient build directory ; no
node_modules is committed. The resulting cm6.js (~290 KB) lives
in webapp/vendor/codemirror/.
faustcode and faustmodular embed the same libfaust and Faust libraries (stdfaust.lib...): a module
edited here must compile and sound the same there. Both use @grame/faustwasm 0.20.1 (libfaust 2.90.19),
copied here from the npm package:
npm pack @grame/faustwasm@0.20.1 && tar xzf grame-faustwasm-0.20.1.tgz
cp package/dist/esm/index.js webapp/faustwasm/index.js
cp package/dist/esm/index.js webapp/vendor/faustwasm/index.js
cp package/libfaust-wasm/libfaust-wasm.{js,data,wasm} webapp/libfaust-wasm/
node scripts/build-doc-index.mjsThe last command rebuilds webapp/faust-doc-index.json, the documentation of the libraries used by the
completion and the library tools (search_faust_lib, get_faust_symbol, list_faust_module...), from
the .lib files of the embedded libfaust; webapp/doc-index.test.mjs fails when it is out of date
(node scripts/build-doc-index.mjs --check tells the same).
Update both applications together. When faustcode is opened by faustmodular, its ready message carries
its version of libfaust, and faustmodular warns if it differs from its own. (Known issue of libfaust
2.88.0 to 2.90.19, reproduced with process = _ : + ~ *(0.5);: -sg fails on an assertion in
sigtyperules.cpp:209 for any recursive program. faustcode no longer uses -sg.)
- macOS :
killed: 9— Gatekeeper rejected the binary, usually becausecom.apple.quarantineis still attached. Re-runxattr -d com.apple.quarantine ./faustcode-mcp-darwin-arm64. - macOS : binary works once, then
killed: 9after replacing the file in place — AMFI caches the code signature per path. Eithermv newfile oldpathorrm oldpath && cp newfile oldpathto force a new inode. - MCP pill keeps cycling between green and red — two tabs are fighting for the bridge. Close the spare ones, then click Connect in the drawer.
- MCP pill stays grey ("idle") with the webapp open — the auto-
connect only fires when
?mcp=…is in the URL or a saved WS URL exists in OPFS. Click MCP → Connect once. - Audio is locked — visit the run view once and click anywhere in it. WebAudio refuses to start without a user gesture per tab.
MIT (see LICENSE).