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).
Add serializeShapeSeqState/restoreShapeSeqState for complete ShapeSeq
state capture (chain config, primitive states, projection knobs, voice
mode, MLP weights, freeze state, clock config). SessionPresetManager
lazily imports the session module when a ShapeSeq engine is registered,
including ShapeSeq state in save/load without a hard dependency.
Resolved conflicts: merged all 6 module imports in eoc-chain-ui.js and index.js.
All moduleFactory cases now return real Faust WASM implementations — no stubs remain.
Faust DSP sources, compiled WASM + JSON descriptors, AudioWorklet processors,
and EOCModule subclasses for all three effects.
- meml-4b4: 4-band parametric EQ (fi.low_shelf, fi.peak_eq × 2, fi.high_shelf)
10 params: freq/gain per band, Q for the two mid bell bands
- meml-cpe: stereo feed-forward compressor (co.compressor_mono × 2)
7 params: threshold, ratio, attack, release, knee, makeup, mix (parallel)
- meml-wwc: zita reverb (re.zita_rev1_stereo) with pre-delay and M/S width
8 active params + mod_rate placeholder (zita internal mod not yet exposed)
All three DSPs compile cleanly under Faust 2.83.1. moduleFactory in
eoc-chain-ui.js now returns real instances for eq/compressor/reverb.
- Wire EOCChain into audio graph after c15.start() (both start-btn and
quick-play paths); guarded by _eocInited flag so init happens once per
AudioContext lifetime
- getOutputNode() on C15Adapter now returns limiter (last node before
destination) so EOC inserts correctly between limiter and destination
- Expose limiterNode getter on C15Bridge
- Add getCurrentParamValue(i) to EOCModule base class, backed by _paramValues
array that setParam() writes to
- Add _buildParamSliders() to EOCChainUI: collapsible ▶ params toggle per
module row, one range slider per paramMeta entry, calls module.setParam()
on input; labelled "manual" when nispsMode=bypass
- eoc:change listener in a-app.js logs paramCount (hook point for future modes)
- saveState()/loadState() persist eocModules (id, enabled, params) and
eocNispsMode; restore re-adds modules via moduleFactory and re-applies values
- CSS: eoc-params-section, eoc-params-toggle, eoc-param-row/label/slider;
eoc-module-row gains flex-wrap to accommodate params section below controls
Adds EngineSwitcher module with clickable engine cards (active highlight,
coming-soon greyed state, loading bar animation, confirm-on-switch when
training data exists). Wired into the Engine drawer; C15 active,
Additive/FM marked coming-soon with toast. Dock title reflects active
engine; engineId persisted to/restored from localStorage.
- input-pipeline.js: guard circular clamp against div-by-zero when
input is exactly at center (0.5, 0.5) — dist=0 produced NaN
- snapshot-stack.js: jumpTo() used slice(0, index) which excluded the
target snapshot; fixed to slice(0, index + 1)
- a-app.js: _lastSentParams not resized when MLP output count changes
during mode switch, causing stale throttle state and potential param
flood on first frame after switch
InputHeatmap.update() now accepts options.inferBatchFn to evaluate
all grid points (plus the divergence center point) in a single WASM
call instead of 256 separate round-trips. The per-point inferFn path
is preserved as a fallback when inferBatchFn is not provided.
Hand tracking via webcam as alternative to joystick input:
- Right hand tracks 14 derived features (palm XY, finger curls, spread,
roll, pitch, pinch) through a separate 14-input MLP
- Left hand gesture recognition (1 finger = thumbs up, 2 = thumbs down)
with 400ms debounce hold
- Split-zone PIP display (no camera feed, skeleton only) with dashed
divider and cross-zone dimming
- Dual IML architecture: independent imlJoy (2 inputs) and imlHand
(14 inputs) with pointer swap, preserving training data per mode
- Dev panel (?devmode=true): draggable/collapsible floating panel with
sliders for MediaPipe confidence thresholds, smoothing, gesture hold
time, world landmarks toggle, and live feature bar monitor
- Visual presets always route to joystick IML (prevents dimension mismatch)
- Race condition guard on input mode switching
- Map A=Train, B=Clear Examples, X=Randomize, LB=Thumbs Down, RB=Thumbs Up
- Add periodic gamepad polling fallback for environments where
gamepadconnected event doesn't fire reliably (Steam Deck, some Linux browsers)
- Show "Press any gamepad button to connect" hint on page load
Merge Examples/RL tabs into single unified toolbar across all four UIs.
Users can now freely mix supervised learning (Add Example + Train) with
RL feedback (thumbs up/down) without switching modes. Param bar dragging
is always enabled.
Add Web MIDI input module (js/synth/midi-input.js) for external MIDI
controllers — routes note on/off to C15 synth, CC 1/2 to joystick.
Add gamepad module (js/ui/gamepad.js) with auto-detection, deadzone,
and axis normalization across all UIs.
Fix synth parameter sync: routeOutputs() now called after trainModel()
in onThumbsUp and loadState to prevent stale params on first joystick
move. Add separate Clear Examples button (dataset only, keeps weights).
Control all sonically meaningful C15 parameters through NISPS ML outputs.
Excludes hardware routing, macros, scale/tuning, key tracking, velocity,
envelope mod depths, discrete switches, and dangerous volume/pitch params.
- Expand param-map.js with 126 curated params grouped by synthesis section
- Widen MLP architecture from [3,10,10,14,20] to [3,32,48,64,126]
- Add ParamDisplay.rebuild() for mode-dependent bar count (20 visual, 126 synth)
- Add scrollable compact layout for synth mode param bars
- Pad visual presets and old saves to 126 outputs for backward compatibility
Adds a secondary Synth mode (toggled via collapsible left side panel)
where the NISPS ML engine's 20 outputs control curated C15 synthesizer
parameters (oscillator PM, shapers, filters, reverb, echo, flanger,
output mixer) through a WASM AudioWorklet bridge.
Includes a chord progression arpeggiator with controls for tempo (BPM),
octave range, octave offset, and 4 selectable progressions. The C15
engine runs in an AudioWorklet with lock-free SharedArrayBuffer ring
buffer communication.
New files: c15/ (WASM assets), js/synth/ (bridge, arpeggiator, param map),
serve-coop.py (COOP/COEP headers for SharedArrayBuffer support).
Faithful JS port of nisps-core MLP + IML engine with a touch-friendly
UI for exploring neural parameter mapping visually. Two learning modes:
example-based (set slider targets) and RL feedback (thumbs up/down with
exploration noise). Flow field particle system controlled by 8 MLP outputs.