Adds the Faust DSP toolchain infrastructure: placeholder additive and FM DSP files, build.sh (faust -lang wasm per .dsp), faustJsonToParamMeta() to convert Faust JSON UI trees into the standard paramMeta format, FaustEngineBase (SynthEngine subclass wiring init/setParam/noteOn/noteOff through AudioWorklet messages), and FaustWorkletProcessor base class for concrete engine processors.
154 lines
4.9 KiB
Markdown
154 lines
4.9 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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 by `faustJsonToParamMeta`)
|
|
- `<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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```js
|
|
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:
|
|
|
|
```js
|
|
[
|
|
{ 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:
|
|
|
|
```js
|
|
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:
|
|
1. Fetches `jsonUrl` → calls `loadFaustParamMeta()` → populates `engine.paramMeta`
|
|
2. Fetches `wasmUrl` → transfers bytes to AudioWorklet
|
|
3. Registers `workletUrl` with `audioCtx.audioWorklet.addModule()`
|
|
4. Creates an `AudioWorkletNode` and connects it to `audioCtx.destination`
|
|
5. Sends `{ type: 'init', wasmBytes, sampleRate }` to the worklet
|
|
6. 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()`.
|