Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
Stream 3's NoOpEngine used engine_id()=='noop' but the schema for
sound_analysis_midi.json declares engine_id: 'thru' (matches firmware's
ThruAudioApp naming). The class name stays NoOpEngine — it accurately
describes what process() does — but the schema-facing identifier is now
'thru'. BreakOr/Elysiamorf compose NoOpEngine by type, not engine_id, so
they're unaffected.
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.
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.
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`.