memlnaut-nisps/playground/faust
monkey-w1n5t0n afff406d92 feat(playground): add Modular audio mode with shared mod pool
New "Modular" engine in a-immersive with three hot-swappable Faust
sub-engines (subtractive/additive/fm) sharing a common modulation pool:
16 ADSR slots + 32 LFO slots (single-knob sine->tri->square->saw
wavemorph) routed through a 48-source x 10-destination matrix per
engine. Per-connection scalar amounts in [-1, 1], summed at each
destination. Default MLP output count is 512 (32 mod-source params +
480 matrix cells); model reinits on sub-engine swap, count change, or
engine-param exposure toggle.

Faust layer:
- mod-pool.lib: shared ADSR/LFO/source-bus library
- gen-modular-dsp.py: byte-reproducible generator (source of truth)
- modular-subtractive: faithful Minimoog (3 osc, ladder filter, no envs)
- modular-additive: 64-partial, spectral shape + formants, no envs/LFOs
- modular-fm: 4-op matrix + self-feedback, no envs
- All three share d08=amp, d09=pan conventions
- MODULAR_DESTINATIONS.md: authoritative destination table

JS layer:
- ModularEngine: self-contained SynthEngine with getState/setState,
  setSubEngine, setModSourceCount, setExposeEngineParam
- modular-ui: drawer with sub-engine toggle, ADSR/LFO count steppers,
  per-slot enable switches, matrix grid editor (tap-cycle, long-press
  precise, right-click menu, negative amounts), preset overlay
- modular-presets: 6 named presets (Slow pad, Plucky bass, Crystal,
  DX bell, Morphing drone + default)
- a-app.js: Modular mode registered, paramMeta:change -> resizeMLP,
  modular DSP state persisted under modularDspState, window.__nisps
  debug hooks for programmatic control

Tests: tests/e2e/modular-mode.spec.js (11 Playwright tests, all passing
including DSP state survives reload, sub-engine swap keeps paramCount,
preset apply verification).

Also fixes a pre-existing build.sh bug where the -e flag caused faust
to overwrite .wasm outputs with expanded DSP source text, leaving
additive/fm-matrix/eoc-* committed as invalid WebAssembly. Rebuilt all
affected engines with the corrected script. Added an early-message
buffer to faust-worklet-processor.js so setParam calls arriving before
wasm instantiation are queued rather than dropped (needed when the user
configures modular state before clicking Start Audio).
2026-04-11 07:34:41 +02:00
..
additive-processor.js feat(playground/faust): full 48-param additive synth DSP (meml-pj4) 2026-04-03 17:52:07 +01:00
additive.dsp feat(playground/faust): full 48-param additive synth DSP (meml-pj4) 2026-04-03 17:52:07 +01:00
additive.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
additive.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
build.sh feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-compressor-processor.js feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) 2026-04-03 17:57:36 +01:00
eoc-compressor.dsp feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) 2026-04-03 17:57:36 +01:00
eoc-compressor.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-compressor.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-delay-processor.js feat(playground/eoc): Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm) 2026-04-03 18:19:32 +01:00
eoc-delay.dsp feat(playground/eoc): Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm) 2026-04-03 18:19:32 +01:00
eoc-delay.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-delay.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-eq-processor.js feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) 2026-04-03 17:57:36 +01:00
eoc-eq.dsp feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) 2026-04-03 17:57:36 +01:00
eoc-eq.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-eq.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-master-processor.js feat(playground/eoc): Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm) 2026-04-03 18:19:32 +01:00
eoc-master.dsp feat(playground/eoc): Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm) 2026-04-03 18:19:32 +01:00
eoc-master.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-master.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-reverb-processor.js feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) 2026-04-03 17:57:36 +01:00
eoc-reverb.dsp feat(playground/eoc): EQ + Compressor + Reverb modules (meml-4b4, meml-cpe, meml-wwc) 2026-04-03 17:57:36 +01:00
eoc-reverb.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-reverb.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-saturation-processor.js feat(playground/eoc): Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm) 2026-04-03 18:19:32 +01:00
eoc-saturation.dsp feat(playground/eoc): Delay + Saturation + Master Bus modules (meml-2sl, meml-2zm) 2026-04-03 18:19:32 +01:00
eoc-saturation.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
eoc-saturation.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
faust-worklet-processor.js feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
fm-matrix-processor.js feat(playground/faust): full 56-param FM matrix synth DSP (meml-wgg) 2026-04-03 17:41:54 +01:00
fm-matrix.dsp feat(playground/faust): full 56-param FM matrix synth DSP (meml-wgg) 2026-04-03 17:41:54 +01:00
fm-matrix.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
fm-matrix.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
gen-modular-dsp.py feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
mod-pool.lib feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-additive-processor.js feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-additive.dsp feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-additive.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-additive.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-fm-processor.js feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-fm.dsp feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-fm.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-fm.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-subtractive-processor.js feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-subtractive-smoke.html feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-subtractive.dsp feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-subtractive.json feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
modular-subtractive.wasm feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
MODULAR_DESTINATIONS.md feat(playground): add Modular audio mode with shared mod pool 2026-04-11 07:34:41 +02:00
README.md feat(playground): Faust WASM build pipeline + param auto-discovery (meml-5s3) 2026-04-03 17:30:23 +01:00

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 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:

{
  "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:

  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().