memlnaut-nisps/docs/redesign/findings-engine-surface.md
monkey-w1n5t0n 24057e7b5f docs(redesign): Manifold research findings + design specs + build plan
Phase-1 audits (feedback-behaviour, engine-surface, design/Manifold), the
judge-panelled rl-feedback-design, dock/backends/inputs specs, the
upstream-firmware survey, the Manifold design mirror, and BUILD-PLAN.md
(resume anchor + locked decisions).
2026-06-28 04:14:12 +02:00

6.7 KiB
Raw Blame History

Findings — Engine Surface Audit (Phase-1)

Read-only audit, 2026-06-27. VERIFIED from source unless [INFER]. Engine A = deployed vanilla JS live at meml.lnfinitemonkeys.org (/home/w1n5t0n/deployments/meml-aimmersive/js/, a snapshot — repo source has diverged). Engine B = parity-tested TS playground in this repo (playground/src/).

Recommendation: wire the new React app to Engine B (the TS playground stack)

Confirmed by the operator's directive ("training must use the same core the firmware builds from"): Engine B's nisps.wasm is compiled from the nisps/ C++20 core and parity-locked to native within 1e-5 (scripts/parity-check.sh, tests/cpp/parity_*). Engine A's deployed WASM exports an older C ABI (nisps_mlp_*) the repo no longer builds (nisps_ml_* + nisps_engine_*). Lift Engine B, don't fork.

Executive summary

  1. Engine B is the current codebase; Engine A's WASM is a stale ABI snapshot.
  2. Engine B parity-tested (native==WASM ≤1e-5); A has no such guarantee.
  3. Engine B is fully typed TS as ES modules (WasmIML, EngineHost); A is clean ESM JS but untyped.
  4. Decisive: in Engine B, nisps.wasm produces the audio itself (_nisps_engine_process_block in the worklet) — true firmware parity. In Engine A, nisps.wasm is MLP-only; audio is separate JS/Faust/C15.
  5. So "reuse the MLP" via A still leaves audio to reimplement; via B you reuse MLP and the 8 audio engines from one WASM.
  6. Engine B's worklet is clean: no Emscripten glue, raw WebAssembly.compile, 128-sample blocks, per-thread instance, no SharedArrayBuffer for nisps audio.
  7. Engine B exposes the richer ML surface (feedback modes C API, layer stats, RL move/pin, batch infer) and a synchronous Playwright probe window.__nisps.
  8. Caveats (none blocking): fixed MLP arch (2→126), single-element lossHistory in sync train, mic input not wired, Solid coupling in WasmIML must be abstracted for React.

Engine A — deployed vanilla JS (LIVE)

WASM at meml-aimmersive/wasm/nisps.{js,wasm}. WasmIML (js/nisps/nisps-wasm.js, 612L): create(nInputs,nOutputs,hiddenLayers=[10,10,14],…), createWithWarmStart(snapshot,newOutputCount,…) (transfers hidden weights across output-count changes, used live), process() sync, inferBatch, dataset add/clear, train(options) sync (captures real per-iter loss history), trainAsync(onComplete) worker, evalLoss, randomiseWeights(spread), moveWeights(speed,spread,outputPinMask) (pin mask supported), weights get/set, getLayerStats()→{meanAbs,maxAbs,deadFrac,satFrac}. No feedback-mode API. Emscripten MODULARIZE glue + cwrap. Exported ABI: nisps_mlp_{create,destroy,draw_weights_spread,eval_loss,get_layer_stats,get_weights, infer_batch,inference,move_weights_ex,move_weights_spread,set_weights,train,train_ex,weight_count}MLP only, no audio. Audio is separate SynthEngine impls: C15 (c15-bridge.js, own c15_engine.wasm + worklet + SAB ring), Faust, JS modular-engine.js. Data path: input → WasmIML.process()outputs[]param-map.js (applyCurve,applyGroupOverride,SYNTH_PARAM_MAP) → engine.setParam → audio. Live arch bigger than expected: hidden [32,48,64]/[48,48,64], warm-start across output counts. COOP/COEP needed only by the C15 path; the nisps MLP path needs no SAB.

Engine B — parity-tested TS playground (in repo)

Files: ml/wasm-iml.ts (650), audio/engine-host.ts (232), audio/worklet/nisps-processor.ts (309), input/pipeline.ts (307), output/pipeline.ts (153), output/curves.ts (128), debug/probe.ts. WasmIML: create(opts) — arch resolved from the WASM build via _nisps_ml_describe (caller sizes warned+ignored); fixed 2→126, hidden [10,14,18], 4 layers. setInput, process():Float32Array (copies out + pushes to mlStore), inferXY, inferBatch (chunked at 4096), dataset add/clear, train sync, trainAsync worker, evalLoss, randomiseWeights, moveWeights(speed,spread,pinMask), weights get/set, getLayerStats()→{meanAbs,maxAbs,deadFrac,saturatingFrac}, getLayerStatsFlat, dispose, reset, debounced saveNow(), tryLoadFromStorage_. Richer ABI (nisps/wasm/bindings.cpp): all of A's MLP ops PLUS nisps_ml_feedback_{up,down,drag,exploring,set_focus,set_mode,get_mode,static_output,learning_paused}, nisps_ml_describe, AND audio nisps_engine_{create,destroy,set_params,process_block}.

Audio VIA nisps.wasm (key difference): EngineHost lazy-creates AudioContext, registers worklet via Vite ?worker&url, fetches nisps.wasm on main thread, transfers bytes to worklet. NispsProcessor holds a 2nd nisps.wasm instance compiled without Emscripten glue (raw WebAssembly.compile + import-shape discovery); per 128-sample block calls _nisps_engine_process_block(handle,inL,inR,outL,outR,128). Engine switch = port message → _nisps_engine_create with new ASCII id. Data path: input pipeline (invert→deadzone→circular clamp→zoom→curve→EMA→momentum, pure fn) → WasmIML.inferXY → 126-vec → output pipeline (global curve→EMA→slew→freeze) → EngineHost.setParams(Float32Array) → worklet. Same C++ engines as firmware. COOP/COEP set in vite.config.ts (same-origin/require-corp); nisps audio needs no SAB (separate per-thread instances); SAB only for the browser-only C15 path. Three isolated nisps.wasm instances (main infer/sync-train, worker async-train, worklet audio). Debug probe window.__nisps (debug/probe.ts): synchronous, bypasses Solid via untrack/batch; never throws.

Gaps to budget for (none blocking)

  • Fixed MLP arch (2→126, hidden [10,14,18]) baked into WASM; the modular N×M requirement (workstream F) needs either multiple WASM modules or runtime-shaped MLP (deferred behind a passing parity check). Engine A's configurable layers + warm-start is the reference for how to add this.
  • lossHistory single-element on sync train() (wasm-iml.ts:419); trainAsync returns a real array; C API lacks nisps_ml_loss_history. Port A's per-iter capture if loss curves matter.
  • Mic input not wired through the worklet (XIASRI / SoundAnalysisMIDI) — UI scaffolds only.
  • C15 is browser-only, own WASM + SAB; voice space a placeholder. (UI label: "Powerful Synth Engine".)
  • Solid coupling in WasmIML/probe (mlStore.__set*, coreBus.emit) must become an injected callback/emitter to make it framework-neutral for React.

Reuse plan for the React app

Import WasmIML for the MLP, EngineHost + the worklet for audio, lift input/pipeline.ts, output/pipeline.ts, output/curves.ts as pure functions. Replace the Solid mlStore/coreBus side-effects with an injected emitter so the engine is framework-neutral, then mount it under React via context (see findings-design-and-manifold.md §4 reactive spine).