This document describes Option A where the Node worker + rt-node-ui.html remain in place. The alternative architecture (Option B) is documented separately in: plan-mcp-llm-python-pwa.md.
- rt-node-ui frontend moved into
ui/rt-node-ui.jsand served by the worker at/rt-node-ui.js. - Node-side MIDI backend is handled by a git submodule at
external/node-midi.make setup-midiinitializes submodule + builds TS (npm run build:ts).- MIDI devices are listed via
/midi/inputsand selected via/midi/select.
faustwasmnow exposessetInputParamHandler; the worker caches input param changes so UI polling shows MIDI-driven updates.- rt-node-ui has a MIDI device selector and status feedback.
- Web MIDI + poly:
- https://github.com/grame-cncm/faust-web-component/blob/main/src/common.ts
- accessMIDIDevice() (Web MIDI), midiInputCallback() -> node.midiMessage(...)
- extractMidiAndNvoices() (parse [midi:on] + [nvoices:n])
- https://github.com/grame-cncm/faust-web-component/blob/main/src/faust-editor.ts
- compile mono then poly if nvoices > 0, createNode(audioCtx, nvoices)
- https://github.com/grame-cncm/faust-web-component/blob/main/src/common.ts
- MIDI + poly in PWA:
- https://github.com/grame-cncm/faustwasm/blob/master/assets/standalone/faust-pwa.js
- startMIDI()/stopMIDI() -> faustNode.midiMessage(event.data)
- create() drives voices (poly) via createFaustNode(...)
- https://github.com/grame-cncm/faustwasm/blob/master/assets/standalone/faust-pwa.js
Goal: compile mono or poly based on [nvoices:n] metadata or an explicit param.
flowchart LR
Client[MCP client] -->|SSE/stdio| Server[faust_node_server.py]
Server -->|stdin/stdout JSON| Worker[faust_node_worker.mjs]
Worker -->|AudioWorklet| WebAudio[node-web-audio-api + faustwasm]
UI[rt-node-ui.html] -->|HTTP JSON| Worker
UI -->|POST /midi| Worker
MCP client
| SSE/stdio
v
faust_node_server.py (Python)
| stdin/stdout JSON
v
faust_node_worker.mjs (Node)
| AudioWorklet -> node-web-audio-api + faustwasm
|
+-- HTTP JSON <---- rt-node-ui.html (browser)
+-- POST /midi <--- rt-node-ui.html (browser)
- MCP client process: MCP tool (LLM client, script, etc.) talking SSE/stdio to the server.
- Python process: faust_node_server.py exposes MCP API and launches/controls the Node worker.
- Node.js process: faust_node_worker.mjs runs in a separate Node process, handles WebAudio, faustwasm, and the HTTP UI server.
- Browser process: rt-node-ui.html runs in a browser tab, calls the worker over HTTP and sends MIDI to /midi. Notes:
- The UI HTTP server is inside the Node process (not a separate process).
- Real-time audio output comes from the Node process, not the browser.
- Load FaustPolyDspGenerator in FaustCompilerManager (faust_node_worker.mjs):
- Extend ensureReady() to capture FaustPolyDspGenerator alongside FaustMonoDspGenerator.
- Provide createPolyGenerator() similar to createGenerator().
- Add extractMidiAndNvoices(jsonMeta) (new helper):
- Place in a small helper module (e.g. faust_dsp_utils.mjs) next to extractBargraphUnits.
- Logic based on https://github.com/grame-cncm/faust-web-component/blob/main/src/common.ts.
- Two-phase compile in WorkerRuntime.compileAndStart:
- Compile once with mono generator to read meta/json.
- Determine nvoices + midiEnabled from JSON meta.
- Create the node:
- If nvoices > 0 (or force_poly), use FaustPolyDspGenerator + createNode(audioContext, nvoices).
- Otherwise, keep mono.
- Store runtime state in WorkerRuntime:
- midiEnabled, polyNvoices, polyMode.
- Expose in UI server
/status:- Add midi_enabled, poly_nvoices, poly_mode in UiServer response.
- Add metadata parser in
faust_dsp_utils.mjs:- Implement
extractMidiAndNvoices(jsonMeta)returning{ midiEnabled, nvoices }. - Parse
json.metaforoptionsentry; read[midi:on]and[nvoices:n].
- Implement
- Extend compiler manager in
faust_node_worker.mjs:- In
FaustCompilerManager.ensureReady(), captureFaustPolyDspGenerator. - Add
createPolyGenerator()method.
- In
- Two‑phase compile in WorkerRuntime.compileAndStart:
- Compile with mono generator to read JSON/meta.
- Call
extractMidiAndNvoices(faustJson.meta); ifnvoices > 0, recompile with poly generator.
- Create node based on meta:
- When
nvoices > 0, callpolyGenerator.createNode(audioContext, nvoices). - Otherwise use mono generator (current path).
- When
- Track runtime state:
- Add
this.polyNvoicesandthis.midiEnabledinWorkerRuntime.resetState(). - Populate from meta in compile path.
- Add
- Expose status:
- Extend
/statusresponse withmidi_enabledandpoly_nvoices.
- Extend
- UI polish:
- In
ui/rt-node-ui.js, show “Poly: N voices” or “Mono”.
- In
- Manual test:
- Compile a DSP with
[nvoices:8]and confirm/status+ audio are polyphonic.
- Compile a DSP with
Goal: allow Faust DSPs that define a global effect to work correctly in poly mode.
Example pattern:
process = voice * 0.3 <: _, _;
effect = _, _ : + : fi.lowpass(2, 8000) : ef.reverb_mono(0.3, 0.5, 0.5, 1) <: _, _;Problem: the current wrapper nests user code in environment { ... } and only exposes
mcp_dsp.process, so a top-level effect is hidden from the poly compiler (or not
metered correctly).
Steps:
- Detect
effectin DSP source:- Add a simple parser/regex in
faust_dsp_utils.mjsto detect top-leveleffect =. - Keep it lightweight (string scan) and treat it as a best-effort flag.
- Add a simple parser/regex in
- Expose
effectat top level in the wrapper:- Keep the
environmentwrapper but re-exportprocessandeffect:process = mcp_dsp.process;effect = mcp_dsp.effect;(only ifeffectexists).
- Keep the
- Apply meters at the right stage:
- If
effectexists, apply output metering toeffect, notprocess, so meters reflect post‑FX audio:effect = mcp_dsp.effect : mcp_output_meters(mcp_dsp.effect);
- If
effectdoes not exist, keep the currentprocess : mcp_output_meters(process)path.
- If
- Input metering stays pre‑voice:
- For
input_sourcepaths, keepmcp_input_meters(mcp_dsp.process)before the voice and avoid duplicating meters on the effect.
- For
- Poly compile still sees
process+effect:- With
effectre‑exported at top level, the poly generator should apply it after voice mixing (standard Faust poly behavior).
- With
- Add a test DSP:
- Add a
poly_fx.dspexample that definesprocess+effectand uses[nvoices:n]. - Verify audio output (FX audible) and metering matches post‑FX output.
- Add a
- Polyphony is enabled only when
[nvoices:n]is present in the DSP metadata. - If
[nvoices:n]is missing, the node stays mono (no implicit defaults). compile_and_startdoes not override this rule for now.
Goal: allow rt-node-ui.html to send MIDI.
- Node-side MIDI input (current path):
- MIDI device list:
GET /midi/inputs. - MIDI device select:
POST /midi/select({ index } or { name }). - Node worker forwards events to
faustNode.midiMessage(...).
- MIDI device list:
- Optional Web MIDI POST path (if you want a browser-only fallback):
POST /midiwith { data: [144, 60, 100] }.- Only needed if you want browser Web MIDI without node-midi.
- Guards:
- If no DSP running -> 409/400.
- If midiEnabled is false -> 403 or ignore.
- JSON response:
- { status: "ok" } or { error: "..." }.
Goal: expose poly controls on MCP.
- Extend compile_and_start in https://github.com/grame-cncm/faust-mcp/blob/main/faust_node_server.py:
- nvoices?: number
- force_poly?: boolean
- Forward these params to the worker.
- Update https://github.com/grame-cncm/faust-mcp/blob/main/README.md to document these options.
Goal: a simple, robust MIDI interface.
- UI:
- MIDI input selector is in
ui/rt-node-ui.jsand polls/midi/inputs. - Show only if midi_enabled is true (via
/status).
- MIDI input selector is in
- Optional Web MIDI (if POST /midi is kept):
- navigator.requestMIDIAccess() + access.onstatechange to list devices.
- input.onmidimessage = (e) => postMidi(e.data).
Goal: clear feedback.
- Show "Poly: N voices" or "Mono" in the UI header (via /status).
- Optional: warn if [nvoices:n] found but node is mono (if force_poly is false).
- Poly MIDI DSP:
// [midi:on][nvoices:8] import("stdfaust.lib"); process = pm.pluckString;
- Start faust_node_server.py + UI.
- Verify:
- status returns poly_nvoices and midi_enabled: true.
- MIDI UI lists devices.
- Note On/Off produces sound.
- Web MIDI requires https or localhost -> OK on 127.0.0.1.
- midiMessage() must exist on the node (confirmed by faust-pwa.js).
- If FaustPolyDspGenerator does not work in node-web-audio-api, provide a mono fallback.
- Web MIDI via HTTP POST can add latency/jitter (browser event loop + HTTP overhead).
- OK for casual control, but not “hard” real-time.
- For tighter timing, consider a more integrated path (node MIDI input or a low-overhead binary socket).