Commit graph

6 commits

Author SHA1 Message Date
monkey-w1n5t0n
01c1346dfd fix(modular): restore matrix in paramMeta; amp floor via positive-only mod_amp
b290144 made matrix cells opt-in to prevent joystick-silences-voice,
but that broke modular-ui.updateLive(): the matrix DOM stopped
reflecting live MLP outputs because matrixIndexCache was empty when
buildMatrixIndex() walked paramMeta. This was the same regression
6072fe8 had previously fixed.

Fix it structurally at the DSP layer instead: amp_val now computes
as `clamp(base_amp + max(0, mod_amp)) * level * vel_gain`, so matrix
d08_amp cells can only boost the amp floor — never cut it. base_amp
defaults to 1.0 (always audible), and presets that want classic
envelope-gated voices (slow pad, plucky bass, crystal, morphing
drone) drop base_amp to 0 and layer a positive ADSR→amp route on top.

With the DSP guard in place, all 480 matrix cells can safely live
in paramMeta again, and updateLive() gets its live visual feedback
back. Revert the opt-in gate in _rebuildParamMeta and the 32-param
test counts, and add a regression test asserting that every matrix
destination has 48 cells in paramMeta — that's what updateLive needs.
2026-04-11 09:39:58 +02:00
monkey-w1n5t0n
b290144670 fix(modular): base_amp floor + opt-in matrix to keep voice audible
Modular sub-engines computed amp_val as a pure function of mod_amp (the
matrix d08_amp destination sum), so once the MLP drove the matrix cells
every joystick movement had a chance to silence the voice: matrix cells
have signed range [-1, 1], sigmoid outputs near 0.5 denormalise to 0,
and the amp gate collapsed. Additive survived in scattered regions
because it only has one kill-switch (d08_amp); subtractive and fm were
almost always dead because they also have d05_cutoff and d01_op1_level.

Two changes:

1. DSP: each sub-engine gets a base_amp hslider (default 1.0) so
   amp_val = clamp(base_amp + mod_amp) * level * vel_gain. At the
   default the voice is always fully open and d08_amp modulation is
   purely additive decoration; drop base_amp to 0 for classic
   ADSR-gated VCA behaviour.

2. ModularEngine._rebuildParamMeta: restore the _exposedMatrixCells
   gate (default empty). paramCount drops from 512 to 32 (4 ADSR * 4
   + 8 LFO * 2); matrix cells are opt-in via setExposeMatrixCell.
   _applyDefaultPatch no longer writes s00_d08_amp since base_amp
   keeps the voice audible without routing.

Tests updated for the new 32-param baseline; matrix-cell persistence
test now calls setExposeMatrixCell(1, 5, true) before asserting the
cell lands in paramMeta. Drive-by: engine-switching test bumped from
3 to 4 engine cards (stale since the modular engine was added).
2026-04-11 09:27:19 +02:00
monkey-w1n5t0n
6072fe80ac fix(modular): restore matrix in paramMeta + bypass MLP when untrained
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.
2026-04-11 08:50:59 +02:00
monkey-w1n5t0n
fbeedba2f3 fix(modular): register worklet via concatenated blob url
The globalThis.FaustWorkletProcessor attachment on the base class isn't
enough in practice: Chrome's AudioWorkletGlobalScope isolates top-level
class declarations between separate addModule() calls, so a subclass
script loaded via a second addModule() still throws ReferenceError on
its 'extends FaustWorkletProcessor' clause.

Workaround: fetch both files, concatenate, and addModule() a single
blob URL. The base class and subclass end up in the same script
evaluation context and the extends clause resolves. Processor names
are memoised on a static Set so repeated sub-engine swaps don't try
to re-register (which would throw).
2026-04-11 08:33:29 +02:00
monkey-w1n5t0n
4507db00c8 fix(modular): matrix cells opt-in + per-group drawer for non-C15 engines
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.
2026-04-11 08:31:19 +02:00
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