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).
6.7 KiB
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
- Engine B is the current codebase; Engine A's WASM is a stale ABI snapshot.
- Engine B parity-tested (native==WASM ≤1e-5); A has no such guarantee.
- Engine B is fully typed TS as ES modules (
WasmIML,EngineHost); A is clean ESM JS but untyped. - Decisive: in Engine B,
nisps.wasmproduces the audio itself (_nisps_engine_process_blockin the worklet) — true firmware parity. In Engine A,nisps.wasmis MLP-only; audio is separate JS/Faust/C15. - So "reuse the MLP" via A still leaves audio to reimplement; via B you reuse MLP and the 8 audio engines from one WASM.
- Engine B's worklet is clean: no Emscripten glue, raw
WebAssembly.compile, 128-sample blocks, per-thread instance, no SharedArrayBuffer for nisps audio. - 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. - Caveats (none blocking): fixed MLP arch (2→126), single-element
lossHistoryin sync train, mic input not wired, Solid coupling inWasmIMLmust 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.
lossHistorysingle-element on synctrain()(wasm-iml.ts:419);trainAsyncreturns a real array; C API lacksnisps_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).