Add a durable, committed source of truth for external MIDI synth control:
- synth-midi-cc.json: verified CC maps + provenance/sources (8 devices researched)
- schemas/midi_device.schema.json + schemas/midi_devices/*.json: 6 CC-controllable
device templates (Moog Sub 37/Sub Phatty, Creamware Pro-12 ASB, Elektron Analog
Keys, ASM Hydrasynth, Roland JD-800), params keyed {id, cc, label, min, max,
default, group}.
- codegen/generate-midi-devices.ts (isolated from the mode golden test) emits both
nisps/midi/generated/midi_devices.hpp (no-heap constexpr, firmware+WASM) and
manifold/src/midi-devices/generated/ (typed catalogue for the browser).
- codegen/seed-midi-devices.ts: reproducible seed from the research artifact.
Lets a performer pick a device and address its parameters by name (not CC number)
on both the firmware and the Manifold browser engine.
17 KiB
MAP
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 codebase (nisps/) compiles to two targets: (1) Arduino/RP2350 firmware for the MEMLNaut hardware, (2) WASM in a SolidJS browser playground that runs the same engines + ML through an AudioWorklet. Browser audio engines are a superset of firmware engines (C15 is browser-only). See CLAUDE.md for the long-form architecture narrative and ALIGNMENT.md for current strategic gaps.
Layout
nisps/ — platform-agnostic C++20 library (the only ML/DSP/engine code)
nisps/core/—perf.hpp(memory section attrs),types.hpp,concepts.hpp(MLEngine,AudioEngine,Mode),fixed_buffer.hpp,ring_buffer.hpp(SPSC lock-free, replaces pico/util/queue),rng.hpp(xoshiro256+ deterministic),math.hpp(fast_sigmoid,Curveenum +apply_curve).nisps/ml/— MLP class templateMLP<NIn, NH1, NH2, NH3, NOut>. Files:mlp.hpp,activations.hpp,loss.hpp(MSE, no double-scaling),training.hpp(SGD + grad clipping),init.hpp(spread-aware uniform↔Xavier),rl.hpp(move_weightswith output pin mask + per-layer scaling + weight decay),feedback.hpp(FeedbackController<MLP_T>— the 3-mode "Down Action" negative-feedback state machine: Avoid / RandomiseOutputs / RandomiseMlp; header-only, zero-heap, own deterministic RNG, exposed vianisps_ml_feedback_*C API),stats.hpp.nisps/dsp/—biquad.hpp,delay.hpp,reverb.hpp,filter.hpp,env.hpp,osc.hpp,pitch_shift.hpp,dc_blocker.hpp. Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.nisps/engines/— eight audio engines, each satisfyingAudioEngine:paf_synth.hpp,channel_strip.hpp,xiasri.hpp,verb_fx.hpp,memlcelium.hpp,breakor.hpp(sequencer, NoOp audio),elysiamorf.hpp(sequencer, NoOp audio),analysis.hpp(input-side spectral features). Plusbase.hpp(NoOpEngine, engine_id "thru").nisps/modes/— eight platform-agnostic modes binding{ML config, engine, voice space lambdas, abstract I/O channels}. Files:paf_synth.hpp,channel_strip.hpp,xiasri.hpp,verb_fx.hpp,memlcelium.hpp,breakor.hpp,elysiamorf.hpp,sound_analysis_midi.hpp.base.hppprovides a CRTP scaffold eliminating the duplication that previously plagued firmware modes.voice_space.hppholds engine-side voice space dispatch helpers.generated/contains codegen output (do not edit by hand).nisps/wasm/bindings.cpp— flat C API exported to WASM (Emscripten target only).nisps/midi/generated/midi_devices.hpp— codegen output: no-heapconstexprexternal-MIDI-synth templates (nisps::midi::generated;MidiParam/MidiDevice+kMidiDevicesregistry). Source =schemas/midi_devices/; do not edit by hand.nisps/CMakeLists.txt+nisps/build/— host-target builds + ctest.
firmware/ — Arduino sketch + hardware glue
firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino— entry point. Selects active mode at compile time via#define MEMLNAUT_MODE_TYPE. Forks onNISPS_SELFTEST: normal modes run the engine/ML path; theSelfTestvariant delegates all four entry points toglue/selftest.hpp.firmware/MEMLNaut-NISPS/glue/— hardware bindings:audio_driver.hpp— bridges memllibAudioDrivercallback →Mode::process(stereosample_t).peripherals.hpp— joystick / pots / buttons →Mode::set_inputand ML primitives.midi_io.hpp— MIDI in → modenote_on/update_bpm/set_playing; drainsControlEventring → MIDI UART.mode_select.hpp— type aliases mapping firmware mode identifiers tonisps::modes::*ModeC++ types. Build script rewrites the active line. Also defines theMEMLNautModeSelfTestpseudo-variant (tag type) + theNISPS_ST_*/NISPS_ST_CATtoken-paste macros the.inouses to computeNISPS_SELFTEST.selftest.hpp— standalone guided hardware self-test rig (SelfTestvariant; no engine/ML). Step-driven state machine on aSelfTestView: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnosticnisps/.input_router.hpp,output_router.hpp— top-levelwire_inputs()/drain_outputs()entry points.
firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}— symlinks (Arduino-CLI requires sketch-tree includes; preprocessor refuses..in headers).firmware/README.md— structure + build instructions.
playground/ — SolidJS + Vite + TypeScript app
playground/index.html,vite.config.ts,tsconfig.json,package.json— scaffold. COOP/COEP headers configured.playground/src/main.tsx,App.tsx— entry + router (/,/dev/primitives,/modes).playground/src/primitives/— 16 UI building blocks:Slider,SliderBank,VirtualJoystick,XYPad,Heatmap,OutputDisplay,TrainingControls,Drawer,ControlAxis,ProgressRing,PillToggle,ParamEditor,JoyMap,WeightHealth,GradientFlow,LossPlot. Each has a.demo.tsxshowcased on/dev/primitives.playground/src/modes/— one TSX per firmware mode (+C15Modebrowser-only).ModeShell.tsxis the shared scaffold;ModeSwitcher.tsxpicks the active mode;mode-runtime.tsis the schema → ML → audio wiring hook;mode-helpers.tsfor SliderBank configs.generated/holds codegen-produced TS schemas (do not edit).playground/src/stores/— Solid stores:ml-store,input-store,output-store,mode-store,control-store(compound axes Boldness/Memory/Precision),session-store(snapshots, A/B, presets),exploration-store,bus(typed signal bus).persistence.tsdebounces localStorage writes.playground/src/audio/engine-host.ts,worklet/nisps-processor.ts— main-thread engine host + AudioWorklet processor. WASM loaded twice (main thread for ML, worklet for engines).playground/src/ml/wasm-iml.ts,wasm-worker.ts,dataset.ts,types.ts— main-thread WasmIML class + disposable async-training worker + FIFO dataset.playground/src/input/pipeline.ts,playground/src/output/pipeline.ts,playground/src/output/curves.ts— pure-fn pipelines (deadzone→zoom→curve→smoothing→momentum, then global curve→smoothing→slew→freeze).playground/src/features/— additional feature modules (heatmap sampling, snapshot stack, A/B compare, region pin, param pin, trail, weight health, etc.).playground/src/debug/probe.ts— synchronouswindow.__nispsdebug probe for Playwright.playground/public/nisps.{wasm,js},c15.wasm,c15-glue.js— compiled WASM artifacts (built byscripts/build-wasm.sh).playground/tests/e2e/— Playwright specs (ml-engine,modes,persistence,ui-interactions) +helpers.ts.playground/playwright.config.ts— Vite preview server setup.
manifold/ — Vite + React + TS convertible-mode app (the NEW front-end, WIP)
The Manifold "convertible" Console on the real engine, deployed at meml.lnfinitemonkeys.org/next (staging,
alongside the live vanilla a-immersive at /). Built 2026-06-27/28; see docs/redesign/BUILD-PLAN.md (resume
anchor + locked decisions) and the docs/redesign/*-spec.md set.
manifold/src/engine/— the parity-tested TS engine LIFTED fromplayground/src(samenisps.wasm), made framework-neutral:wasm-iml.ts(rewired off Solid stores onto an injectedEngineSink),engine-host.ts+worklet/nisps-processor.ts(audio),input-pipeline.ts/output-pipeline.ts/curves.ts,wasm-worker.ts,spine.ts(the reactive spine BELOW React —setInputderives processed→ml→routed eagerly off-render),engine-api.ts(EngineApifaçade incl.feedback.*wrappers over thenisps_ml_feedback_*C ABI),EngineProvider.tsx/useEngine.ts(React binding viauseSyncExternalStoreversion counter). nisps.js is loaded via fetch+indirect-eval (Emscripten MODULARIZE glue has no ES exports), base-aware viadocument.baseURIfor the/nextsub-path.manifold/src/primitives/— the 12 design primitives as typed React.manifold/src/console/— the convertible Console:ConsoleApp,CompositeStage(single-divider convertible with snap/magnetism/minimap-demotion),SplitStage/OutputStage/InputMini/Manifold(canvas, rect↔circular- feedback markers),
Dock(top Mode selector + 5 vertically-centred drawers),Drawers(Learning/Inputs/ Outputs/Settings/Help),VerdictCluster(mode-aware),ReadoutStrip,OutputEditor/CurvePad,icons.tsx(monochrome currentColor SVG),model.ts,output-mode.ts.
- feedback markers),
manifold/src/dock/—OutputControlRow(off/fixed/live + mute + solo/arm + min/max/curve),output-state.ts,OutputsBackendConfig.tsx(per-backend specialised Outputs panel),BackendAdvanced.tsx.manifold/src/backends/—OutputBackendadapter +BackendManager(spine consumer);midi-backend.ts(WebMIDI),osc-backend.ts+osc-client.ts(OSC-over-WS),presets.ts(named presets),manager.ts.manifold/src/midi-devices/— external-synth device templates.generated/is codegen output fromschemas/midi_devices/(MIDI_DEVICEScatalogue +MIDI_DEVICES_BY_ID, params by name+CC). The MIDI Outputs config (dock/OutputsBackendConfig.tsx) reads it for the device picker + param-select that fills the CC table.manifold/src/feedback/—controller.ts(Explore-and-place scratchpad + geometric-dislike + solo, TS prototype),rng.ts(seeded).manifold/src/settings/—settings-store.ts(monochrome icons, input-map shape, corner radius).manifold/src/serial/—memlnaut-serial.tsWeb Serial scaffold +EditorPanel.tsx(MEMLNaut Editor mode).manifold/src/debug/probe.ts—window.__nisps(?debug=1).manifold/tests/e2e/smoke.spec.ts. E2E on the VPS runs via non-snap node (see BUILD-PLAN).manifold/osc-bridge/— Deno WS↔UDP-OSC bridge.
vcv/ — VCV Rack 2 plugin (MEMLNaut module, WIP)
Native C++ Rack module: ML CV-mapper with RL feedback + a browser bridge. Currently 2→12 (being evolved to
8 inputs × 16 outputs + per-output LED rings, palette from the frontend tokens, WS↔OSC browser bridge — see
the "BUILD DELTAS" block at the top of vcv/SPEC.md). src/MEMLNaut.cpp (module), src/osc_server.hpp (bridge),
src/plugin.{hpp,cpp}, res/*.svg (panels), Makefile (needs RACK_DIR). Was built against the retired
nisps-core; the core include path is being repointed.
schemas/ — JSON parameter contracts (firmware/browser source of truth)
schemas/schema.json— Draft 2020-12 meta-schema validating mode files.schemas/modes/<mode>.json(×8) — each mode's params, ranges, defaults, curves, voice spaces, ML config.schemas/modes/params_notes.md— provenance notes and judgement calls per mode.schemas/midi_device.schema.json— Draft 2020-12 meta-schema for external-MIDI-synth templates.schemas/midi_devices/<device>.json(×6) — CC-controllable external synths (Moog Sub 37 / Sub Phatty, Creamware Pro-12 ASB, Elektron Analog Keys, ASM Hydrasynth, Roland JD-800). Each param:{id, cc, label, min, max, default, group}. Canonical source for both firmware + browser device pickers. Verified-CC provenance + sources live insynth-midi-cc.json(repo root).
codegen/ — schema → C++/TS code
codegen/generate.ts— Bun script: validates schemas via ajv, emits per-modenisps/modes/generated/<mode>_schema.hpp(constexpr,nisps::modes::generated) andplayground/src/modes/generated/<mode>_schema.ts. Idempotent.codegen/generate-midi-devices.ts— separate Bun script (isolated from the mode golden test): validatesschemas/midi_devices/via ajv, emitsnisps/midi/generated/midi_devices.hpp(no-heapconstexpr) andmanifold/src/midi-devices/generated/{types,devices,index}.ts. Idempotent.codegen/seed-midi-devices.ts— one-time/idempotent seed derivingschemas/midi_devices/*.jsonfrom thesynth-midi-cc.jsonresearch artifact (slugifies labels → ids, heuristic groups).codegen/templates/,codegen/tests/golden/— reference templates + golden snapshot for paf_synth.
tests/cpp/ — host C++ tests
- Per-component tests:
test_dsp_*.cpp,test_engine_*.cpp,test_mlp_*.cpp,test_mode_*.cpp,test_fixed_buffer.cpp,test_ring_buffer.cpp,test_rng.cpp,test_math.cpp. Helpers intest_helpers.hpp. - Verification:
ml_golden_vectors.cpp,engine_impulse.cpp(+engine_impulse_baseline.bin),parity_check.cpp+parity_wasm.mjs+parity_diff.mjs— native-vs-WASM bit-equivalence within 1e-5.
scripts/ — build + verify entry points
build-firmware.sh,flash-firmware.sh,build-and-flash-firmware.sh,firmware-common.sh— Arduino-CLI wrapper for RP2350 target with C++20 flag.build-wasm.sh— Emscripten compile producingplayground/public/nisps.{wasm,js}.build-cpp-tests.sh— CMake configure + build + ctest (Ninja).parity-check.sh— runs native + WASM and diffs binary outputs.lint-cpp.sh—.fliteral warn + heap/Arduino.hviolation fail.run-all-tests.sh— master verification script.
.github/workflows/
ci.yml— GitHub Actions: cmake build + ctest + WASM build + parity check + lint + Playwright (cpp-tests + playground-tests jobs). Firmware compile is documented as manual.
Submodules (in src/)
src/memllib/— hardware abstraction (audio driver, peripherals, MIDI). Not auto-initialized — fresh clones needgit submodule update --init --recursive.src/daisysp/— vendored DSP library. Used by some firmware glue; nisps replaced its PitchShifter with a custom granular impl.
Top-level docs
CLAUDE.md— long-form architecture narrative.MAP.md— this file.ALIGNMENT.md— strategic gaps + open mission questions, dated, opinionated.README.md— short quickstart.AGENTS.md— beads/bd conventions.
Entry points
- Firmware:
scripts/build-firmware.sh [VARIANT](interactive prompt if omitted),scripts/flash-firmware.sh,scripts/build-and-flash-firmware.sh. Target:rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3,-std=gnu++20. - Playground dev:
cd playground && bun install && bun run dev(Vite, port 5173, COOP/COEP headers). - Playground build:
cd playground && bun run build. - WASM rebuild:
bash scripts/build-wasm.sh(needsemcc). - Host C++ tests:
bash scripts/build-cpp-tests.sh. - Parity check:
bash scripts/parity-check.sh. - All tests:
bash scripts/run-all-tests.sh. - Playwright:
cd playground && bunx playwright test. - Codegen:
cd codegen && bun run generate.ts(regeneratesnisps/modes/generated/andplayground/src/modes/generated/).
Conventions
- Firmware mode selection is compile-time only —
#define MEMLNAUT_MODE_TYPEin the.ino. nisps/follows Chris's RP2350 perf rules globally: no heap,static const floatfor non-trivial constants, strict.fsuffix, memory section attrs (NISPS_AUDIO_MEM,NISPS_AUDIO_FUNC,NISPS_APP_SRAM,NISPS_HOT,NISPS_FORCE_INLINE).- C++ identifiers:
PascalCasetypes,snake_casefunctions/variables,kPascalCaseconstexpr. JSON keyssnake_case. TS typesPascalCase, componentsPascalCase.tsx, moduleskebab-case.ts. Curveenum lives innisps/core/math.hpp(lowercase:linear/exp/log/square/sqrt/sigmoid/cubic); generated mode headers re-export viausing Curve = ::nisps::Curve;. TS mirror atplayground/src/output/curves.tswith same names.- Modes are TSX components composed of primitives; mode parameter contracts are JSON schemas with codegen → C++/TS types. No declarative JSON UI.
- WASM and firmware share the same C++; WASM is fixed at
MLP<2, 10, 14, 18, 126>and modes use a slice of outputs based on schema'soutput_size. - Cross-platform parity:
scripts/parity-check.shenforces native vs WASM agreement within 1e-5.
Gotchas
src/memllibsubmodule is not auto-checked-out.- Firmware sketch path is
firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino(Arduino-CLI requires sketch dir name == sketch file name);firmware/MEMLNaut-NISPS/src/{memllib,daisysp,nisps}are symlinks because Arduino's preprocessor refuses..in includes from sketch headers. firmware/MEMLNaut-NISPS/glue/mode_select.hpp#undefs Arduino macros (sq,min,max,abs,round) before pulling nisps headers — engines use those identifiers as method names.nisps_firmware::g_active_mode_bridgeisexterninglue/audio_driver.hppand defined in the.ino; combininginlinewith__not_in_flashproduces a comdat conflict at link time.- The host fallback of
NISPS_AUDIO_FUNCinnisps/core/perf.hppis misshapen for use as a function-name decorator (firmware path expands to__not_in_flash_funcwhich takes only a name); firmware glue avoids the macro to dodge the inconsistency. SeeALIGNMENT.md. nisps_modes_testsbuilds against generated schemas undernisps/modes/generated/; if you add a new mode, regenerate viabun run codegen/generate.tsbefore building.
Smells / strategic concerns
See ALIGNMENT.md.