Two fixes for Modular mode: 1. faust-worklet-processor.js: explicitly attach FaustWorkletProcessor to globalThis. Class declarations at the top of a classic script are lexically scoped to that script's evaluation context and do not propagate across separate addModule() calls, so the base class was invisible to subclass processors when they loaded in AudioWorklet- GlobalScope. Symptom: "FaustWorkletProcessor is not defined" at the extends clause of modular-subtractive-processor.js. 2. modular-ui.js + a-app.js: wire live matrix cell updates. Phase C wired matrix cells for writes (click cycles, precise editor) but not for reads — the MLP-driven values never propagated to the DOM. modular-ui now exposes updateLive(outputs), called from routeOutputs on every inference tick. Throttled to ~20 fps internally to avoid DOM thrashing. Only visible sources (within adsrCount/lfoCount) are updated; muted slots stay dark. |
||
|---|---|---|
| .. | ||
| additive-processor.js | ||
| additive.dsp | ||
| additive.json | ||
| additive.wasm | ||
| build.sh | ||
| eoc-compressor-processor.js | ||
| eoc-compressor.dsp | ||
| eoc-compressor.json | ||
| eoc-compressor.wasm | ||
| eoc-delay-processor.js | ||
| eoc-delay.dsp | ||
| eoc-delay.json | ||
| eoc-delay.wasm | ||
| eoc-eq-processor.js | ||
| eoc-eq.dsp | ||
| eoc-eq.json | ||
| eoc-eq.wasm | ||
| eoc-master-processor.js | ||
| eoc-master.dsp | ||
| eoc-master.json | ||
| eoc-master.wasm | ||
| eoc-reverb-processor.js | ||
| eoc-reverb.dsp | ||
| eoc-reverb.json | ||
| eoc-reverb.wasm | ||
| eoc-saturation-processor.js | ||
| eoc-saturation.dsp | ||
| eoc-saturation.json | ||
| eoc-saturation.wasm | ||
| faust-worklet-processor.js | ||
| fm-matrix-processor.js | ||
| fm-matrix.dsp | ||
| fm-matrix.json | ||
| fm-matrix.wasm | ||
| gen-modular-dsp.py | ||
| mod-pool.lib | ||
| modular-additive-processor.js | ||
| modular-additive.dsp | ||
| modular-additive.json | ||
| modular-additive.wasm | ||
| modular-fm-processor.js | ||
| modular-fm.dsp | ||
| modular-fm.json | ||
| modular-fm.wasm | ||
| modular-subtractive-processor.js | ||
| modular-subtractive-smoke.html | ||
| modular-subtractive.dsp | ||
| modular-subtractive.json | ||
| modular-subtractive.wasm | ||
| MODULAR_DESTINATIONS.md | ||
| README.md | ||
playground/faust — Faust DSP Build Pipeline
This directory contains Faust DSP source files and the toolchain for compiling them to WebAssembly for use in the MEMLNaut playground.
Required Tools
| Tool | Version | Purpose |
|---|---|---|
faust |
>= 2.60 | Compiles .dsp → .wasm + .json |
emcc |
>= 3.1.x | Optional — needed only if linking extra C++ code |
emcc is already available at /usr/lib/emscripten/emcc on this system.
faust is not currently installed — use nix-shell -p faust for a one-off
build, or nix profile install nixpkgs#faust to install permanently.
How to Compile
cd playground/faust
./build.sh
This compiles every .dsp file in the directory, producing alongside it:
<name>.wasm— the compiled DSP binary (loaded as an AudioWorkletNode)<name>.json— the Faust UI descriptor (consumed byfaustJsonToParamMeta)<name>.js— JS glue / AudioWorklet wrapper generated by faust
DSP Files
| File | Status | Description |
|---|---|---|
additive.dsp |
Placeholder | 4-harmonic sine bank (2 params). Full engine: meml-pj4 |
fm-matrix.dsp |
Placeholder | 2-op FM synth (4 params). Full engine: meml-wgg |
Output Format — .json Descriptor
Faust's -json flag emits a UI descriptor tree. Example:
{
"name": "additive",
"version": "2.75.7",
"options": "-vec",
"size": "0",
"inputs": "0",
"outputs": "2",
"meta": [...],
"ui": [
{
"type": "vgroup",
"label": "additive",
"items": [
{
"type": "hslider",
"label": "freq",
"address": "/additive/freq",
"meta": [{"unit": "Hz"}],
"init": 220,
"min": 20,
"max": 4000,
"step": 0.1
},
{
"type": "hslider",
"label": "amp",
"address": "/additive/amp",
"init": 0.5,
"min": 0,
"max": 1,
"step": 0.001
}
]
}
]
}
How faustJsonToParamMeta Consumes the JSON
playground/js/synth/faust-param-meta.js exports:
import { faustJsonToParamMeta, loadFaustParamMeta } from './faust-param-meta.js';
// From a pre-parsed object:
const paramMeta = faustJsonToParamMeta(faustJson);
// Or fetch + parse in one step:
const paramMeta = await loadFaustParamMeta('faust/additive.json');
faustJsonToParamMeta recursively walks the ui tree, collects all
hslider / vslider / nentry items, and returns:
[
{ id: 'freq', name: 'freq', min: 20, max: 4000, init: 220, curve: 0.5, group: 'additive' },
{ id: 'amp', name: 'amp', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'additive' },
]
This is the standard paramMeta format used throughout the playground
(SynthEngine.paramMeta, preset system, group drawer, etc.).
FaustEngineBase Loading Pattern
playground/js/synth/faust-engine-base.js provides a base class for any
engine compiled with this pipeline:
import { FaustEngineBase } from './js/synth/faust-engine-base.js';
class AdditiveEngine extends FaustEngineBase {
constructor() {
super({
id: 'additive',
displayName: 'Additive',
wasmUrl: 'faust/additive.wasm',
jsonUrl: 'faust/additive.json',
workletUrl: 'faust/additive-processor.js', // AudioWorklet file
processorName: 'additive-processor', // registerProcessor() name
});
}
}
const engine = new AdditiveEngine();
await engine.init(audioCtx); // fetches JSON + WASM, loads worklet
engine.noteOn(69, 0.8); // A4, velocity 0.8
engine.setParam(0, 0.6); // normalized [0,1] → maps to param range
engine.noteOff(69);
The init() call:
- Fetches
jsonUrl→ callsloadFaustParamMeta()→ populatesengine.paramMeta - Fetches
wasmUrl→ transfers bytes to AudioWorklet - Registers
workletUrlwithaudioCtx.audioWorklet.addModule() - Creates an
AudioWorkletNodeand connects it toaudioCtx.destination - Sends
{ type: 'init', wasmBytes, sampleRate }to the worklet - Waits for the worklet to reply
{ type: 'ready' }(10 s timeout)
AudioWorklet Processor Base
faust-worklet-processor.js (this directory) defines FaustWorkletProcessor,
a base class for concrete engine worklets. It handles:
{ type: 'init', wasmBytes, sampleRate }— calls_initWasm(){ type: 'setParam', index, value }— calls_onSetParam(){ type: 'noteOn', freq, vel }— calls_onNoteOn(){ type: 'noteOff', freq }— calls_onNoteOff()- Replies
{ type: 'ready' }or{ type: 'error', message }to main thread
Each concrete engine processor overrides _initWasm() and _renderBlock().