2026-04-03 18:30:23 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# build.sh — compile all Faust DSP files in this directory to WASM + JSON
|
|
|
|
|
#
|
|
|
|
|
# Required tools:
|
|
|
|
|
# faust >= 2.60.0 (https://faust.grame.fr / nix: faust)
|
|
|
|
|
# emcc >= 3.1.x (Emscripten, already present at /usr/lib/emscripten/emcc)
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# cd playground/faust && ./build.sh
|
|
|
|
|
#
|
|
|
|
|
# Outputs (alongside each .dsp):
|
|
|
|
|
# <name>.wasm — compiled audio DSP binary
|
|
|
|
|
# <name>.json — Faust UI descriptor (consumed by faustJsonToParamMeta)
|
|
|
|
|
# <name>.js — JS glue / AudioWorklet wrapper generated by faust2wasm
|
|
|
|
|
#
|
|
|
|
|
# The JSON descriptor is the key artifact: it contains the full UI tree
|
|
|
|
|
# (hslider / vslider / nentry / groups) that faustJsonToParamMeta.js parses
|
|
|
|
|
# into the playground's standard paramMeta format.
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Dependency checks
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-04-11 07:58:43 +02:00
|
|
|
#
|
|
|
|
|
# If faust isn't in PATH, automatically re-exec under `nix-shell -p faust`
|
|
|
|
|
# when nix-shell is available. NISPS_BUILD_IN_NIX_SHELL prevents infinite
|
|
|
|
|
# recursion if the nix-shell environment somehow still lacks faust.
|
2026-04-03 18:30:23 +02:00
|
|
|
|
|
|
|
|
if ! command -v faust &>/dev/null; then
|
2026-04-11 07:58:43 +02:00
|
|
|
if [ "${NISPS_BUILD_IN_NIX_SHELL:-0}" = "1" ]; then
|
|
|
|
|
echo "ERROR: 'faust' still not found inside nix-shell. Check your nixpkgs channel."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
if command -v nix-shell &>/dev/null; then
|
|
|
|
|
echo "faust not in PATH — re-exec under nix-shell -p faust ..."
|
|
|
|
|
exec env NISPS_BUILD_IN_NIX_SHELL=1 nix-shell -p faust --run "\"$0\" $*"
|
|
|
|
|
fi
|
2026-04-03 18:30:23 +02:00
|
|
|
echo ""
|
2026-04-11 07:58:43 +02:00
|
|
|
echo "ERROR: 'faust' not found in PATH (and nix-shell is unavailable)."
|
2026-04-03 18:30:23 +02:00
|
|
|
echo ""
|
|
|
|
|
echo "Install options:"
|
|
|
|
|
echo " nix-shell -p faust # one-off"
|
|
|
|
|
echo " nix profile install nixpkgs#faust # permanent"
|
|
|
|
|
echo " Or download from: https://faust.grame.fr/downloads/"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Required version: >= 2.60.0"
|
|
|
|
|
echo "Check: faust --version"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
FAUST_VER="$(faust --version 2>&1 | head -1)"
|
|
|
|
|
echo "faust: $FAUST_VER"
|
|
|
|
|
|
|
|
|
|
# emcc is optional — faust -lang wasm doesn't require it.
|
|
|
|
|
# It is needed if you want to link extra C++ into the WASM module.
|
|
|
|
|
if command -v emcc &>/dev/null; then
|
|
|
|
|
EMCC_VER="$(emcc --version 2>&1 | head -1)"
|
|
|
|
|
echo "emcc : $EMCC_VER"
|
|
|
|
|
else
|
|
|
|
|
echo "emcc : not found (not required for basic faust -lang wasm builds)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Compile each .dsp
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
DSP_FILES=("$SCRIPT_DIR"/*.dsp)
|
|
|
|
|
|
|
|
|
|
if [ ${#DSP_FILES[@]} -eq 0 ]; then
|
|
|
|
|
echo "No .dsp files found in $SCRIPT_DIR"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
for DSP in "${DSP_FILES[@]}"; do
|
|
|
|
|
NAME="$(basename "$DSP" .dsp)"
|
|
|
|
|
echo "Compiling $NAME.dsp ..."
|
|
|
|
|
|
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
|
|
|
# Faust -o is interpreted relative to -O (so we must cd into SCRIPT_DIR
|
|
|
|
|
# and pass just the filename). Class names (-cn) must be valid C identifiers,
|
|
|
|
|
# so dash characters are replaced with underscores.
|
|
|
|
|
CLASSNAME="$(echo "$NAME" | tr '-' '_')"
|
|
|
|
|
|
2026-04-03 18:30:23 +02:00
|
|
|
# Step 1: emit WASM binary + JS glue
|
|
|
|
|
# -lang wasm — target WebAssembly
|
|
|
|
|
# -cn <Name> — class name prefix in generated JS
|
|
|
|
|
# -O <dir> — output directory
|
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
|
|
|
#
|
|
|
|
|
# NB: do NOT pass -e here. In Faust 2.79, -e means "export expanded DSP
|
|
|
|
|
# (textual)" — it causes the .wasm output to be Faust source text, not a
|
|
|
|
|
# real WebAssembly binary. This script historically used -e by mistake.
|
|
|
|
|
( cd "$SCRIPT_DIR" && \
|
|
|
|
|
faust -lang wasm \
|
|
|
|
|
-cn "$CLASSNAME" \
|
|
|
|
|
-O "$SCRIPT_DIR" \
|
|
|
|
|
"${NAME}.dsp" \
|
|
|
|
|
-o "${NAME}.wasm" )
|
|
|
|
|
|
|
|
|
|
# Note: `faust -lang wasm -O . foo.dsp -o foo.wasm` already writes
|
|
|
|
|
# foo.json alongside foo.wasm with the wasm-native descriptor (including
|
|
|
|
|
# numeric `index` fields that workers use as setParamValue zone addresses).
|
|
|
|
|
# The old standalone `-json` pass produced a smaller JSON WITHOUT the
|
|
|
|
|
# `index` field, which is the wrong shape for workers — don't run it.
|
|
|
|
|
if [ ! -f "$SCRIPT_DIR/${NAME}.json" ]; then
|
2026-04-03 18:30:23 +02:00
|
|
|
echo " WARNING: ${NAME}.json not produced — check faust version"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -f "$SCRIPT_DIR/${NAME}.wasm" ]; then
|
|
|
|
|
WASM_SIZE="$(du -h "$SCRIPT_DIR/${NAME}.wasm" | cut -f1)"
|
|
|
|
|
echo " -> ${NAME}.wasm (${WASM_SIZE})"
|
|
|
|
|
fi
|
|
|
|
|
if [ -f "$SCRIPT_DIR/${NAME}.json" ]; then
|
|
|
|
|
echo " -> ${NAME}.json"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Done. Load engines in the playground via FaustEngineBase:"
|
|
|
|
|
echo " import { AdditiveEngine } from './js/synth/additive-engine.js';"
|