Commit graph

429 commits

Author SHA1 Message Date
w1n5t0n
973455b158 feat(nisps/dsp): lean DSP primitives ported from maximilian (meml-1v6)
Header-only, heap-free, sample-rate-aware DSP primitives for stream 3:
- Biquad (LPF/HPF/BPF/Notch/Peak/LowShelf/HighShelf, denormal flush)
- Delay<N> + DynamicDelay<N> (fixed-length feedback + power-of-two
  ring with fractional read & smoothed offset)
- AllPass / Comb / LpComb (Schroeder-Moorer reverb sections, split per
  role rather than maximilian's one-class-many-roles maxiReverbFilters)
- DCBlocker (one-pole HPF)
- ChamberlinSVF + OnePoleSmoother<NCh> + EnvelopeFollower
- ADSR envelope generator
- SineOsc/SawOsc/SquareOsc, PAFOperator (port of maxiPAFOperator with
  static gauss/cauchy tables), FMOp single-operator FM building block
- PitchShifter<N> granular two-head crossfade (replaces daisysp PitchShifter)

Tests: biquad freq-domain attenuation, delay tap timing, reverb
boundedness, pitch-shifter ratio + finite output. All pass under
-Wall -Wextra -Werror -Wpedantic, C++20.
2026-04-29 16:08:49 +03:00
w1n5t0n
825ed6ad33 feat(nisps/ml): MLP library with fixed-architecture template + spread-aware RL (meml-wmh)
Stream 2 of the clean-slate rewrite: nisps/ml/ replaces src/memlp/ with a
header-only, heap-free MLP that satisfies nisps::core::MLEngine.

Files (nisps/ml/):
- activations.hpp — ReLU (leaky 0.01 for parity), sigmoid, tanh
- loss.hpp — MSE per-sample (fixes meml-ues double-scaling: returns the
  sample's MSE without an extra 1/N multiplication; the training loop
  averages explicitly)
- init.hpp — uniform/Xavier/spread-aware weight init
- training.hpp — gradient clip helper (±10.0 matches legacy)
- rl.hpp — move_weights with per-layer Xavier scaling, weight decay
  (10% * spread), gaussian noise via the deterministic Rng (matches the
  legacy JS sum-of-three-uniforms shape); draw_weights also spread-aware
- stats.hpp — per-layer mean/max/dead/saturating diagnostics
- mlp.hpp — 4-layer (3 hidden + sigmoid output) MLP class with
  std::array-backed weights, biases, gradient accumulators, dataset
  ring buffer (default 128 examples), loss history (default 4096 iters).
  Bias is a separate per-layer parameter — no input-vector mutation.
  Flat get_weights/set_weights layout: weights all layers (row-major,
  layer order), then biases all layers.

Tests (tests/cpp/, all 50 passing under -Wall -Wextra -Werror -Wpedantic):
- test_mlp_init.cpp — deterministic seeding, spread regimes,
  static_assert MLEngine concept satisfied
- test_mlp_inference.cpp — golden hand-computed forward pass match,
  sigmoid output range, set_input bounds
- test_mlp_training.cpp — XOR convergence (loss < 0.01 in <2k iters),
  ring-buffer eviction
- test_mlp_loss.cpp — meml-ues regression test: reported loss equals
  hand-computed average MSE without extra 1/N scaling; sample weights
  honoured
- test_mlp_rl.cpp — move_weights respects output_pin_mask (final-layer
  rows + biases preserved); spread regimes; grad clear after draw_weights
- test_mlp_serialize.cpp — get_weights/set_weights round-trip preserves
  inference exactly; eval_loss is non-mutating; infer_batch matches
  individual inference

Verification:
- Clean build, no warnings
- 50 tests pass (22 prior + 28 new)
- No std::vector / new / malloc in nisps/ml/
- All float literals .f-suffixed in code (comments excepted)
2026-04-29 15:55:43 +03:00
w1n5t0n
55e7bc9654 fix: wire generated C++ schemas to nisps::Curve from core/math.hpp
Stream 5 shipped a temporary local Curve enum (PascalCase) while stream 1
was in flight. Stream 1 has now landed nisps/core/math.hpp with the
authoritative lowercase Curve enum per architecture spec. Generated C++
headers now include core/math.hpp and re-export the enum into the
generated namespace via 'using Curve = ::nisps::Curve;'.

- codegen/generate.ts: emit lowercase enum values + include math.hpp
- regenerated all 8 mode schema headers
- updated golden snapshot to match

Verified all 8 headers compile clean with -std=c++20 -Wall -Wextra.
2026-04-29 15:43:42 +03:00
w1n5t0n
f59b12a056 merge stream 5: schemas + codegen + generated outputs (meml-7k6) 2026-04-29 15:41:44 +03:00
w1n5t0n
8911079212 merge stream 8: SolidJS scaffold + UI primitives (meml-911) 2026-04-29 15:41:40 +03:00
w1n5t0n
48dcf3fc6b merge stream 1: nisps/core foundation (meml-dn7) 2026-04-29 15:41:16 +03:00
w1n5t0n
c3fb63a113 feat(playground): UI primitive library + /dev/primitives showcase
Sixteen primitives at src/primitives/, each with its own .module.css and a
.demo.tsx that wires the primitive to live local state. All primitives
are typed (strict), pointer-event based (touch + mouse), keyboard-accessible
where it makes sense, and free of business logic.

Primitives:
- Slider, SliderBank: value sliders with optional curve mapping;
  collapsible sections.
- VirtualJoystick, XYPad: 2D pointer input, returning [0,1]^2.
- Heatmap: NxN canvas rendering with three color modes
  (luminance/variance/divergence) and a configurable palette.
- OutputDisplay: bar chart of N output values.
- TrainingControls: thumbs-up/down/undo + train/randomise + status.
- Drawer: portal'd slide-in panel from left or right; ESC + scrim close.
- ControlAxis: compound-axis slider (Boldness/Memory/Precision shape)
  with active-preset hint, endpoint labels, double-tap-to-relink.
- ProgressRing: SVG circular progress with optional inline label.
- PillToggle: segmented radio control.
- ParamEditor: min/max/curve/mute/pin/fixedValue editor for a single param.
- JoyMap: zoom minimap with adaptive grid + zoom window, vanishing trail
  with Catmull-Rom spline + tap-to-return, dual concentric noise rings,
  region pin overlays, frozen overlay.
- WeightHealth: 10-bin histogram + dead/saturating/healthy status glow.
- GradientFlow: per-layer bar chart with vanishing/exploding/converged
  color coding.
- LossPlot: log-scale line chart of training loss history.

App.tsx now wires the /dev/primitives route to a lazy-loaded
PrimitivesShowcase that renders all sixteen demos in a responsive grid.
Lazy import keeps the home page bundle ~20 kB while the showcase ships
its own ~42 kB chunk.

Stream 8 of the rewrite (meml-911).
2026-04-29 15:38:55 +03:00
w1n5t0n
add05ea554 feat(playground): solid stores + signal bus
Seven Solid stores wired up around the architecture's reactivity model
(§7.1). Each store uses createStore for object state + createSignal for
Float32Arrays where appropriate; setters mutate the store and schedule a
debounced (200ms) localStorage write through src/stores/persistence.ts.

- bus.ts: typed signal bus with prefix wildcards (ml.*, ui.*, mode.*,
  pin.*, snap.*). Singleton coreBus for app-wide events.
- ml-store.ts: shape final, methods stub-throw "not implemented" until
  stream 7 wires WASM. Outputs and weights are separate Float32Array
  signals so the store proxy doesn't run on every audio-rate tick.
- input-store.ts: full input pipeline config (zoom, anchor, deadzone,
  curve, smoothing, momentum, invert) + persisted live state.
- output-store.ts: globalCurve, smoothing, slewRate, freezeOutput,
  freezeMask. Mask not persisted (engine-specific).
- mode-store.ts: activeModeId + per-mode { paramName → ParamOverride }.
- control-store.ts: Boldness/Memory/Precision compound axes with
  interpolation tables and offset-based overrides (trim-pot model).
  Tables and 6 built-in CONTROL_PRESETS mirror legacy
  js/ui/control-surface.js exactly. interpolateAxis() exposed for
  testing. Stream 10 wires resolveParams() into other stores.
- session-store.ts: ring-buffered snapshot stack (max 20), A/B
  capture/toggle/accept/revert, region pins (max 5), param pins
  (toggle + mask builder), named session presets.
- index.ts: public re-exports for components and modes.

Stream 8 of the rewrite (meml-911).
2026-04-29 15:38:05 +03:00
w1n5t0n
665e224122 feat(playground): input/output pipelines + curve catalog
Pure-TS ports of the legacy input + output pipelines:
- src/input/pipeline.ts: deadzone → circular clamp → zoom → centered
  power curve → EMA smoothing → momentum-as-zoom. Exposed as a
  pure function processInput(raw, cfg, state, dt) so the input-store
  can hold the state. Math is intentionally bit-equivalent to the
  legacy js/ui/input-pipeline.js implementation.
- src/output/pipeline.ts: global power curve → EMA smoothing →
  slew-rate limit → freeze gate (global + per-output mask).
- src/output/curves.ts: named curve catalog (linear/exp/log/square/
  sqrt/sigmoid/cubic/centered_power) — the TS half of the contract
  defined in nisps/core/math.hpp (stream 1). Golden-vector tests
  in stream 11 will keep them in lockstep.

Stream 8 of the rewrite (meml-911).
2026-04-29 15:37:46 +03:00
w1n5t0n
a160b72295 feat(playground): scaffold vite + solid + ts
Fresh playground/ now sits on:
- Vite 5 + vite-plugin-solid + Solid.js 1.9
- TypeScript strict (jsx preserve, target ES2022)
- index.html → main.tsx → App.tsx with a tiny pushState/popstate router
- Design tokens (dark immersive palette, JetBrains Mono) at src/styles/tokens.css
- COOP/COEP headers in vite.config.ts (server + preview) so SharedArrayBuffer
  is available for the C15 + AudioWorklet wiring stream 7 will land.
- Debug-probe stub at src/debug/probe.ts: window.__nisps with placeholder
  methods + __ready=false marker. Stream 10 fills it in. Keeping the install
  point stable from day one means Playwright tests can rely on the global
  existing.

Stores, pipelines, and primitives ride in the next commits.

Stream 8 of the rewrite (meml-911).
2026-04-29 15:37:23 +03:00
w1n5t0n
d41e4c3e9f chore(playground): remove legacy a-immersive playground
Clears the old vanilla-JS / multi-app playground (a-immersive.html,
b-workbench.html, c-journey.html, designs.html, index.html, plus the
js/, css/, c15/, faust/, firmware/, osc-bridge/, wasm/ subtrees and the
serve/test scripts). The fresh SolidJS + Vite scaffold lands in the next
commits per the rewrite plan in .local/architecture.md.

Reference docs (SPEC-controls.md, SPEC-shapeseq.md,
PLAN-solidjs-migration.md, ARCHITECTURE.md, README.md, TODOS.md, devlog/)
have been moved to .local/playground-archive/ (out of git) so future
streams can still consult them.

Stream 8 of the rewrite (meml-911).
2026-04-29 15:35:58 +03:00
w1n5t0n
05f04a90e0 Add generated schema headers (C++) and modules (TS)
Output of \`bun run codegen/generate.ts\` for the 8 mode schemas.
These files are checked in so consumers don't need bun on every
build, but they remain regenerable and byte-identical.

C++ (nisps/modes/generated/):
  schema_types.hpp + 8 <mode_id>_schema.hpp files. All compile
  cleanly with g++ -std=c++20 -Wall -Wextra -fsyntax-only -I nisps.

TS (playground/src/modes/generated/):
  types.ts + index.ts + 8 <mode_id>_schema.ts files. Type-check
  cleanly with tsc --strict.
2026-04-29 15:29:15 +03:00
w1n5t0n
129e28b207 Add codegen tool: schemas -> C++ headers + TS modules
bun-runnable TypeScript script (codegen/generate.ts) that:
- Validates each schemas/modes/*.json against the meta-schema via
  ajv (Draft 2020-12).
- Cross-checks params.length == ml.output_size and
  ml.input_channels.length == ml.input_size.
- Emits constexpr C++ data into nisps/modes/generated/ (one
  schema_types.hpp + one <mode_id>_schema.hpp per mode). Uses
  std::string_view + std::array; no std::vector, no heap, .f
  suffixed float literals (perf contract §3.3).
- Emits TS modules into playground/src/modes/generated/ (one
  types.ts + one <mode_id>_schema.ts + index.ts barrel).
- Idempotent: re-running yields byte-identical output.
- Exits non-zero on schema validation failure.

Reference templates live in codegen/templates/ (not consumed at
codegen time -- for human reviewers).

Golden test (codegen/tests/golden_test.ts) snapshots
paf_synth_schema.{hpp,ts} and verifies regeneration matches the
golden + that a second run is idempotent.

Until stream 1 lands nisps/core/math.hpp, schema_types.hpp ships
its own minimal Curve enum with a TODO marker pointing at the
eventual include.
2026-04-29 15:29:08 +03:00
w1n5t0n
1bb0ec5eed Add mode schemas + meta-schema (Stream 5 / meml-7k6)
JSON Schema Draft 2020-12 meta-schema (schemas/schema.json) plus 8
per-mode schemas authored against the firmware sources:

- paf_synth (33 params, 7 voice spaces)
- channel_strip (24 params, 6 voice spaces)
- xiasri (24 params, direct mapping)
- verb_fx (47 params, 12 voice spaces)
- memlcelium (56 params, sequencer + dual PAF)
- breakor (56 params, 8-track ratio sequencer)
- elysiamorf (40 params, 8-track FM/CC sequencer)
- sound_analysis_midi (8 params, audio-feature -> MIDI CC)

schemas/modes/params_notes.md captures provenance and judgement
calls for each mode.
2026-04-29 15:28:49 +03:00
w1n5t0n
e5bf2aa055 feat: nisps build + host test harness
CMakeLists.txt:
- Native host build by default; Emscripten-target detection plumbed but
  WASM emit deferred to stream 7 (playground build script).
- Header-only INTERFACE library `nisps_core`.
- Host test executable `nisps_core_tests` compiled with -Wall -Wextra
  -Werror -Wpedantic (Chris's rules: clean build is non-negotiable).

tests/cpp/test_helpers.hpp:
- Minimal NISPS_TEST / NISPS_EXPECT / NISPS_EXPECT_NEAR macros, no external
  deps. Rationale documented in-file: Catch2/doctest would add ~10MB and 30s
  for what is currently <100 LOC of test runtime.

22 unit tests covering FixedBuffer (5), RingBuffer (5), Rng (7), math (5).
All green; verified via `cmake --build nisps/build && ./nisps/build/nisps_core_tests`.
2026-04-29 15:22:01 +03:00
w1n5t0n
4f60fc8405 feat: nisps/core foundation — perf, types, concepts, buffers, rng, math
Greenfield C++20 core for the unified firmware+WASM rewrite (architecture.md
streams, meml-dn7). Header-only, platform-agnostic, no heap, no virtual
dispatch.

Components:
- perf.hpp        memory section + inlining macros, RP2040/RP2350-aware,
                  inert on host/Emscripten
- types.hpp       stereosample_t (mirrors firmware AudioDriver API),
                  sample_t/param_t aliases, DriverConfig negotiation struct
- concepts.hpp    MLEngine, AudioEngine, Mode (architecture §4.1-4.3)
- fixed_buffer.hpp  std::array-backed cursor; replaces std::vector in hot paths
- ring_buffer.hpp   SPSC lock-free FIFO, power-of-two capacity, atomic
                    head/tail; replaces pico/util/queue in core
- rng.hpp         xoshiro256+ with splitmix64 seeding, uniform/signed/
                  gaussian-via-3-uniforms (matches legacy MoveWeights shape)
- math.hpp        clamp01, fast_sigmoid (tanh-Padé, ~1.2% max err on [-6,6]),
                  exact_sigmoid, fast_exp, named Curve catalog (linear/exp/
                  log/square/sqrt/sigmoid/cubic) — TypeScript twin lives in
                  playground/src/output/curves.ts (stream 5)

Performance discipline (Chris's rules):
- No heap, no std::vector, no malloc/new in core
- All float literals carry .f suffix
- Memory section attrs syntactically present, inert on non-firmware builds
2026-04-29 15:21:52 +03:00
w1n5t0n
01f6522fd5 docs: capture playground redesign intent from _rewound snapshot
Source: archive/playground-redesign-2026-snapshot branch (commit 287e3a1).
Reference-only — playground is being rewritten in SolidJS, so the redesign's
implementation will not be merged, but its design intent is preserved here
for the rewrite team.
2026-04-29 13:32:39 +03:00
w1n5t0n
d3373a4e44 Update memllib: fix DISLIKE and DRAG audible feedback
DISLIKE now calls joltNetworks() immediately for instant sound change.
DRAG release now outputs savedAction to audio queue immediately rather
than waiting for training to converge.
2026-04-17 23:23:47 -07:00
w1n5t0n
c7da7bf4f7 Improve UF2 bootloader detection 2026-04-16 00:52:39 +09:00
w1n5t0n
25b8f7ca72 Update beads interaction log 2026-04-16 00:46:17 +09:00
w1n5t0n
6b26168a29 Preserve firmware variant capitalization 2026-04-16 00:46:02 +09:00
w1n5t0n
d1c12dd2b1 Fix firmware variant prompt TTY detection 2026-04-16 00:44:52 +09:00
w1n5t0n
074d4b3005 Update beads interaction log 2026-04-16 00:38:12 +09:00
w1n5t0n
39312a0d1f Add firmware variant selection to build scripts 2026-04-16 00:37:58 +09:00
w1n5t0n
314408aba9 Add firmware helper scripts and docs 2026-04-16 00:32:32 +09:00
w1n5t0n
e12eaab9cb
Merge branch 'MusicallyEmbodiedML:main' into main 2026-04-15 12:56:41 +09:00
chriskiefer
6efbe9c992 voicing 2026-04-14 08:20:57 +01:00
chriskiefer
972d3e7961 focus 2026-04-13 17:07:31 +01:00
chriskiefer
d095688cd9 drum voice 2026-04-13 16:55:39 +01:00
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
575519df68 fix(modular): duplicate destNames declaration in modular-ui rebuildMatrixGrid
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).
2026-04-11 08:38:56 +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
70c858016f fix(a-app): gate modular MLP routing on example count to preserve default patch
Modular mode's default patch sets MM_Matrix/s00_d08_amp = 1.0 via
_setRawByLabel at engine init, but on the first inference tick
routeOutputs writes the untrained MLP's output (~0.5 normalised) to
every paramMeta index. For matrix cells the raw range is [-1, 1], so
normalised 0.5 denormalises to 0 — clobbering the default amp routing
and killing all audio.

Skip the setParam loop for modular when iml.exampleCount === 0, so the
worklet keeps running on the default patch we already pushed at init.
Once the user captures at least one training example the MLP has a
target to reproduce, and routing resumes normally.

Only affects modular mode; other engines are unchanged.
2026-04-11 08:20:24 +02:00
monkey-w1n5t0n
e6938ebb8b fix(playground): modular worklet base-class loading + live matrix display
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.
2026-04-11 08:08:29 +02:00
monkey-w1n5t0n
0de64463fd build(faust): auto re-exec under nix-shell when faust is missing
If faust isn't in PATH but nix-shell is, transparently re-exec the
script via `nix-shell -p faust --run` instead of erroring out with a
hint. NISPS_BUILD_IN_NIX_SHELL guards against infinite recursion if the
shell somehow still lacks faust.
2026-04-11 07:58:43 +02:00
monkey-w1n5t0n
2993328859 fix(a-app): bail out of showGroupDrawer for non-C15 engines
showGroupDrawer indexes into the hardcoded SYNTH_SECTIONS table and
groupOverrides, which only exist for the shaper-feedback engine. For
additive/fm/modular engines, paramToSection is rebuilt dynamically from
paramMeta groups, so region.index points into a larger dynamic section
list and SYNTH_SECTIONS[region.index] is undefined. Hovering a section
label in Modular mode threw "Cannot read properties of undefined
(reading 'name')".

These engines have their own param UIs; the C15 group drawer doesn't
apply. Guard and early-return.
2026-04-11 07:53:37 +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
w1n5t0n
445bc48c24 Improve solidjs migration plan 2026-04-07 03:44:36 +01:00
w1n5t0n
47b6b1466b Add SolidJS migration plan 2026-04-07 03:29:34 +01:00
w1n5t0n
b72914813c test(shapeseq): add test runner + smoke tests
Shell runner for the shapeseq unit test suite and a smoke test that
exercises the core import paths.
2026-04-07 00:03:29 +01:00
w1n5t0n
d10f80210c feat(playground): add ShapeSeq container to immersive layout
Add #shapeseq-container div with a canvas and chain mount point,
positioned above the bottom controls. Visible when ?shapeseq=1 is
set and synth mode is active.
2026-04-07 00:01:57 +01:00
w1n5t0n
1e9dda6e38 fix(a-app): create AudioCanvas before computing target output count
Move the lazy AudioCanvas initialization above the outputCountForMode()
call in setOutputMode() so that the real output count is available
instead of the 12-output fallback.
2026-04-07 00:01:37 +01:00
w1n5t0n
9ebfec8814 feat(shapeseq): session serialization + preset integration
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.
2026-04-07 00:01:12 +01:00
w1n5t0n
70da83d6dc feat(freeze-ui): freeze/live param toggle interaction
When frozen, param rows dim to indicate frozen state. Tapping a row
toggles it to "live" (highlighted, receives ML deltas). Unfreezing
clears all visual indicators.
2026-04-07 00:00:56 +01:00
w1n5t0n
8bb965740e feat(chain-ui): drawer layout + signal-based reactive updates
Replace the inline chain builder with a full-screen slide-up drawer
(overlay + glass backdrop + drag handle). Param sliders now subscribe
to a vanilla signal (createArraySignal) for O(changed params) updates
instead of querySelectorAll sweeps. Card list uses keyed reconciliation.
2026-04-06 23:59:25 +01:00
w1n5t0n
4240b03c1c feat(sequencer): mono/poly voice mode
Mono mode (default) kills the previous note before firing a new one —
classic monophonic synth behavior. Poly mode retriggers same-pitch
notes without killing others. All active notes are released on mode
switch.
2026-04-06 23:59:04 +01:00
w1n5t0n
4aedc92bf9 feat(shapeseq): unified/dual MLP mode switching
Unified mode: a single MLP drives both timbre and sequence — the timbre
MLP's outputs are partitioned and a configurable slice feeds the
sequencer pipeline via setSequenceOutputsFromTimbre().

Dual mode (default): separate MLPs for timbre and sequence with
independent training. The sequencer owns its own IML instance.
2026-04-06 23:57:41 +01:00