Previous fix pulled matrix cells out of paramMeta to avoid the
default-patch-clobbering silence issue. Side effect: paramCount dropped
from 512 to 32, the heatmap strip and synth visualizer shrank
dramatically, and moving the joystick no longer animated matrix cells
(the user's two most recent complaints).
Better approach: put matrix cells back in paramMeta (512 outputs), but
when iml.exampleCount === 0 substitute the normalised default-patch
vector for the raw MLP output in routeOutputs. The engine sees the
default patch exactly, audio works, and the user still sees 512 cells
in the heatmap / matrix grid. As soon as they capture their first
training example the MLP resumes driving everything normally.
ModularEngine.getDefaultNormalizedOutputs() returns the normalised
default value per paramMeta entry, reading from _lastRawByLabel first
(so user edits via click or _setRawByLabel propagate) and falling back
to the walk-entry init field.
modular-ui setCell now prefers engine.setParam over _setRawByLabel when
the cell is in paramMeta, so writes flow through the normal tracking
path and are visible to getDefaultNormalizedOutputs on the next tick.
Verified in headless Chromium: switching to modular, cold start, reading
engine._lastRawByLabel after routeOutputs ticks shows ampRaw=1,
attackRaw=0.01, enableRaw=1 — the default patch is preserved. paramCount
is 512 again. 0 console errors.
The matrix-seeding edit from the previous commit added a second
`const destNames = engine.destNames || [];` inside rebuildMatrixGrid
while the same name was already declared earlier in the function body.
Chrome threw SyntaxError at parse time, which in turn prevented
a-app.js from wiring window.__nisps at all, which is what made the
earlier FaustWorkletProcessor error look like the root cause — it
was just the next thing that broke once the parse error let partial
modules load.
Verified end-to-end in headless Chromium via Playwright: engine
switches to modular, paramCount=32, audioCtx running, worklet node
live, zero console errors.
Also updates the Modular engine card description to reflect the new
default (32 params = 4 ADSR × 4 + 8 LFO × 2, matrix cells opt-in).
Two fixes driven by user reports:
1. MLP wasn't affecting the sound. Matrix cells are now opt-in to the
MLP output vector rather than always-driven. Default modular paramMeta
is 32 mod-source params (4 ADSR * A/D/S/R + 8 LFO * rate/morph), down
from 512. The default patch's MM_Matrix/s00_d08_amp=1.0 now survives
the first inference tick because it's not in paramMeta — ADSR1 stays
routed to amp and the MLP drives envelope shape per joystick position.
Matrix cells still clickable as direct-DSP knobs in modular-ui: setCell
now routes through engine._setRawByLabel(). A later UI pass can add a
"expose to MLP" menu entry that calls engine.setExposeMatrixCell(s,d).
Also removes the earlier exampleCount-based routing gate; no longer
needed now that the default patch is stable.
2. Per-group curve drawer (hover over section labels on the synth
visualizer) is now available for all synth engines, not just C15.
Refactored showGroupDrawer behind a getSectionView(sectionIndex)
helper that returns a uniform view for either C15 (via SYNTH_SECTIONS
+ groupOverrides) or non-C15 (via nonC15Sections + nonC15GroupCurves
+ engineParamOverrides).
Group-level curve persists across sub-engine swaps by group name, so
e.g. tuning the "ADSR 1" curve survives a switch between subtractive
and fm without being reset.
ModularEngine: adds setExposeMatrixCell(s,d,exposed) +
getExposedMatrixCells() + clears exposed cells on sub-engine swap.
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.
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).