addModule() was handed new URL('./nisps-processor.ts', import.meta.url), which
Vite copies as raw TypeScript (served as video/mp2t) instead of compiling it —
that pattern only bundles for new Worker(). Import the worklet with
?worker&url so Vite emits compiled, hashed .js. Fixes the synth engine failing
to load on meml.lnfinitemonkeys.org.
Builds the SolidJS-side feature parity with the legacy a-immersive
playground. Every Phase 1–4 feature called out in
.local/recon/04-playground.md now flows through the runtime → stores →
primitives pipeline.
Phase 1 (compound axes / zoom / drawer / trail):
- control-routing.ts: resolves Boldness/Memory/Precision into
input/output/exploration store writes via createEffect.
- SettingsDrawer (new): tabbed sections for Input / Training /
Exploration / Output / Per-param overrides / Advanced. Per-param
ParamEditor list wires through modeStore.setOverride.
- JoyMap navigator added to ModeShell; tap-to-return + long-press
region pin handlers wired into the runtime.
- Anchor mode toggle (auto/sticky/center) exposed via drawer.
Phase 2 (pinning / history / A/B):
- features/snapshots.ts: bridge sessionStore push/pop to actual
Float32Array weights. Auto-snapshot before train/randomize/thumbs-down.
- Undo button + long-press snapshot list popup wired in ModeShell.
- A/B capture/toggle/accept/revert in SettingsDrawer.
- features/overrides.ts buildPinMask() composes override `pinned`
flags + sessionStore param pins; passed to mlStore.moveWeights.
- Region pinning long-press → addRegionPin + auto-snapshot tag
"pinned baseline".
Phase 3 (refinement / exploration):
- features/heatmap-sampler.ts: 16x16 grid via mlStore.inferBatch,
three colour modes, throttled 5/sec, refreshed on ml.trained /
ml.delta_update bus events.
- exploration-store.ts (new): RL noise level, floor/cap/growth/decay,
spread, learning rate, weight decay, auto-explore, pressure.
- Auto-explore timer in mode-runtime drives moveWeights at user
interval, zoom-scaled intensity.
- Pressure feedback: window pointerdown/up timestamps mapped to
explorationStore.setPressure; feeds growNoise/decayNoise.
Phase 4 (output / persistence / polish):
- features/weight-health.ts: histogram, status, per-layer L2 deltas,
vanishing/exploding/converged classification.
- features/session-preset.ts: full state capture/restore +
base64url URL sharing; main.tsx applies on load.
- SettingsDrawer Advanced tab renders WeightHealth, GradientFlow,
LayerStats, Heatmap, session preset save/load/share.
Override application:
- features/overrides.ts applies per-param mute/freeze/curve/range
between MLP outputs and the engine. Mode runtime exposes
`paramOutputs` (length = schema.params) which all modes now bind to
OutputDisplay so ranges and mutes are visible.
- Freeze flags push into outputStore.freezeMask reactively.
Mic input:
- features/mic-input.ts: getUserMedia + AnalyserNode-derived
{energy, brightness, pitch, aperiodicity}. Mode runtime feeds
them into channels 2..(input_size-1) when active. ModeShell shows
a Mic toggle for audio_in modes.
Debug probe (probe.ts):
- Synchronous bypass for snapshot, A/B, pins, overrides, axes,
spread, output freeze, heatmap, weight health, session presets,
URL params, bus emit/on. Untracked reads/writes throughout to
avoid SolidJS reactivity surprises in tests.
Modes:
- All 8 firmware mode TSX files now use the default SettingsDrawer
(no per-mode SliderBank scaffolding). They bind OutputDisplay to
runtime.paramOutputs (post-override) instead of processedOutputs.
Build status:
- bun run typecheck: clean
- bun run build: clean
- dev server smoke: index, /modes, mode-runtime, SettingsDrawer,
features/* all serve.
Note: bd close meml-5wg failed because Dolt server unreachable from
this worktree. Issue should be closed manually by orchestrator.
Stream 9 (part 1/2) — shared infrastructure for the per-mode TSX layer.
- ModeShell.tsx: common chrome (header, voice-space PillToggle, audio
start/stop, control axes, training controls, drawer, status line).
Modes only have to author primary input + output JSX.
- mode-runtime.ts: useModeRuntime(schema) hook. Owns input pipeline,
WASM ML calls, output pipeline, and a 50ms-throttled engine-host
bridge. Exposes setInput/processedOutputs/training/audio surfaces
so mode TSX stays declarative.
- mode-helpers.ts: schema → SliderBank config + Float32Array →
slider-range value mapping. Pure functions.
- ModeSwitcher.tsx: top-of-page <select> wired to modeStore.
ModeShell and the runtime read controlStore (Boldness/Memory/Precision)
to derive learning rate + RL noise cap so axes affect training without
needing per-mode plumbing.
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.
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).
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).
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).
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).
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.