Commit graph

21 commits

Author SHA1 Message Date
monkey-w1n5t0n
b16f26e6ab refactor(ml): one runtime-configurable training default (S26)
The operator's call: "there should be one default learning rate and one
default max iterations and they should both be configurable at runtime."

There were SIX copies, not the four the audit described, and they did not
agree:

  nisps/ml/mlp.hpp        no-arg train() hardcoding 1.f / 1000u / 0.001f —
                          and firmware's ONLY training path calls exactly
                          this, so firmware had no runtime knob at all
  wasm-iml.ts             train() and trainAsync() TS default params (x2)
  engine-api.ts           learningRate ?? 1.0, with no maxIterations knob
  vcv/src/iml.hpp         200 / 0.1 / 0.00001 — silently divergent
  external_synth_midi.hpp its own kDefaultLearningRate/kDefaultMaxIterations
  schemas/modes/*.json    x9, identical, read by nobody at runtime

Now: schemas/ml_defaults.json is the single declaration (validated against a
sibling meta-schema, matching the midi_device.schema.json convention), codegen
emits it to C++ and TS in the same run, and MLPCore carries a TrainConfig whose
default member initialisers read the generated constant.
set_train_config()/nisps_ml_set_train_config() make it runtime-overridable on
every target; the explicit-argument train() overload is untouched. min_error
joins the tuple — it was duplicated identically and belongs with the other two.

The per-mode ml block loses default_learning_rate/default_max_iterations.
default_spread stays (genuinely wired on both targets) and input_channels stays
(codegen-time validated, real information for sound_analysis_midi).

VCV BEHAVIOUR CHANGE, deliberate: MEMLNaut.cpp constructs IML positionally and
relies on those defaults, so the module moves to 1000/1.0/0.001 — 5x the max
iterations, 10x the learning rate, and a 100x looser early-stop threshold. The
old values were never justified anywhere; they arrived with fbc68eb alongside
an unrelated module rewrite and no tuning rationale. Firmware and WASM have
shipped 1.0/1000 all along. It is now runtime-settable if this turns out worse.

The generated header lands in nisps/ml/generated/, not nisps/modes/generated/
where the rest of codegen output lives: training hyperparameters are an ML
fact, and nisps/ml sits below nisps/modes, so emitting them there would make
mlp.hpp include upward. The agent that built this flagged the directory-crossing
rather than hiding it; this is the fix. CI's generated-freshness gate learns the
new directory.

Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS (max delta 2.38e-7),
lint clean, manifold typecheck + 17 unit + 33 e2e (which exercise train() and
trainAsync() through a real browser).
2026-07-21 17:20:10 +02:00
monkey-w1n5t0n
1a78ed9597 fix(vcv): close the audio-thread race and remove JSON from process()
Phase 2 (L34, L35). Both findings' line citations were accurate this time.

- L34 data race: process() (Rack's audio thread) called
  imlShadow.get_example_features()/get_example_labels() directly on the WORKER
  thread's private engine — which the file's own THREADING INVARIANT comment
  says only the worker may touch — while workerLoop() concurrently
  clear/refills those same std::vector<std::vector<float>> members via
  load_examples(), train_(), randomise_weights and clear_dataset. Unsynchronised
  reader/writer on a non-atomic vector: undefined behaviour.
  Fix extends the staged handoff the file ALREADY uses for pendingWeights
  rather than adding a second mutex: the worker deep-copies features/labels
  into pendingFeatures/pendingLabels at the same instant it copies
  pendingWeights, immediately before weightsPending.store(true), and the flag is
  now released only after the whole batch is consumed — closing an early-release
  window the old code had. process() no longer references imlShadow at all
  (verified: the only surviving mention is a comment).
- L35: process() ran full jansson serialize on every weight-swap OSC push and
  full json_loads + dataFromJson on incoming OSC state — heap-heavy tree work
  on the audio thread. The plan said to move it to the worker. It is DELETED
  instead: reading the actual transport shows both directions talk to nobody —
  osc-client.ts only ever sends {params|input|feedback}, and bridge.ts has no
  state/weights case and explicitly drops other addresses. Relocating
  heap-heavy work to serve a confirmed-zero consumer is complexity without a
  requirement; removing the cause is the smaller coherent design.
  dataToJson/dataFromJson are UNTOUCHED — they remain the live consumers for
  Rack patch save/load and the .nisps preset menu, both off the audio thread.

Neither is empirically reproduced: a real race needs a live Rack engine under
TSan, which is not available here. Justified by reading, and verified by
`cd vcv && make -j4` (clean) plus the host suite including
test_vcv_iml_parity.cpp, which pins iml.hpp bit-exactly against the core MLP —
iml.hpp was not modified, and parity holds.

Known remaining, pre-existing and out of scope: process() still takes a brief
lock_guard on feedbackMutex to copy a small staged struct, and several config
fields (slewMs, oscPort, output/input range flags) are written by the UI thread
without atomics.
2026-07-21 13:23:11 +02:00
monkey-w1n5t0n
53da84c425 chore(vcv): delete the dead test rig
Phase 1 group 7 (L33). vcv/test/smoke_test.cpp included a header that no longer
exists (a retired nisps-core path), asserted the pre-P6 2x12 module shape, and
ran in no gate; a compiled smoke_test binary was tracked alongside it. Removed
the directory plus Makefile.dist, updated BUILDING.md's two references, and
deleted the unreachable reply-to-sender branch in osc_server.hpp.

Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:49:25 +02:00
monkey-w1n5t0n
1b69254de2 feat(vcv): reunify module onto core MLP — thin iml.hpp adapter (P6)
Replace the vendored runtime MLP in vcv/src/iml.hpp (DetRng + 3D-weight-store
MLP + Dataset + IML) with a THIN, Rack-free adapter over the shared core:
nisps::ml::MLPCore<nisps::ml::DynamicStorage> (8->[16,24,16]->16, the P2 dynamic
case), nisps::Rng, and the core MLP's own FIFO dataset. Behaviour changes from
the vendored approximation to core-exact firmware/WASM semantics.

- MEMLNaut.cpp: staged/pending weight buffers and patch JSON now use the core's
  flat [weights..][biases..] vector (nisps::IML<float>::Weights); patch version
  bumped to 3. Double-buffer / single-writer threading discipline unchanged.
- New ctest tests/cpp/test_vcv_iml_parity.cpp: seeded train/infer/move_weights
  session through the adapter is memcmp-equal to a bare MLPCore<DynamicStorage>.
- Docs: vcv-module.md delta #5 marked CLOSED (2026-07-18); MAP.md vcv/ updated.

Closes vcv-module.md delta #5.
2026-07-18 13:01:28 +02:00
monkey-w1n5t0n
45f3ca5cae docs: restructure design docs into docs/specs (adr/plans/recon), update path references 2026-07-13 23:15:46 +03:00
monkey-w1n5t0n
6584423ccd build(vcv): macOS x64+arm64 cross-build here, resource-bounded (osxcross + system clang)
- vcv/build-mac.sh: bounded Docker osxcross build (system clang, NOT LLVM-from-
  source; cached toolchain image for fast reruns) → ad-hoc-signed mac-x64 +
  mac-arm64 .vcvplugin. MacOSX12.3 SDK from joseluisq/macosx-sdks.
- DISTRIBUTION.md: macOS is now a host bounded cross-build, not CI-only.
All four platforms (lin/win/mac-x64/mac-arm64) now published at /next/vcv.
2026-06-28 06:22:51 +02:00
monkey-w1n5t0n
c86c7da5df build(vcv): build Windows here in a resource-bounded container + link ws2_32
- vcv/Makefile: include arch.mk early, link -lws2_32 when ARCH_WIN (the OSC
  server uses Winsock; mingw ignores the MSVC #pragma comment lib).
- vcv/build-win.sh: bounded Docker mingw cross-build (hard CPU/mem caps, no
  extra swap, prebuilt mingw — no toolchain compile). Produces win-x64 .vcvplugin.
- DISTRIBUTION.md: reflect the no-dedicated-box reality — Linux + Windows built
  locally (supervised + bounded), macOS needs the Apple SDK (CI or a Mac).
2026-06-28 05:16:39 +02:00
monkey-w1n5t0n
7691b9ca8c build(vcv): cross-platform CI + Linux dist + /next/vcv publish flow
- add .github/workflows/vcv-plugin.yml: official rack-plugin-toolchain
  (pinned to commit 4fd1318, Rack SDK 2.6.6, image v19) cross-builds the
  MEMLNaut plugin for lin-x64/win-x64/mac-x64/mac-arm64 on CI, uploads
  .vcvplugin artifacts, and attaches them to GitHub Releases on v* tags.
- add vcv/DISTRIBUTION.md: build + publish flow, how to add CI release
  artifacts to meml.lnfinitemonkeys.org/next/vcv.
- add vcv/.gitignore for build products (build/, dist/, *.vcvplugin, binaries).
- BUILDING.md: point at DISTRIBUTION.md; note make dist supersedes Makefile.dist.

Linux x64 .vcvplugin built + verified locally via make dist and published to
/next/vcv (additive subdir). Windows/macOS deferred to CI per prod-host policy.
2026-06-28 04:49:40 +02:00
monkey-w1n5t0n
fbc68ebfab feat(vcv): evolve module to 8x16 + LED rings + token palette + WS-OSC bridge
8 inputs x 16 outputs; per-output LED ring widget (drawLayer+nvgArc); palette
from frontend tokens; OSC bridge verbs for bidirectional browser training;
vendored self-contained iml.hpp (retired nisps-core); compiles against Rack
SDK 2.6.4. See SPEC.md BUILD DELTAS.
2026-06-28 04:14:30 +02:00
w1n5t0n
2060f10b40 fix(vcv): resolve 6 critical thread safety issues from code review
Fixes from Opus 4.6 review (C1-C6, I1, I3, I4, I8):

C1: static lastOutputs → per-instance lastOutputsForDelta member
C2: add_example() now on audio thread only (owns iml); worker reads
    from mutex-protected staging area (stagedFeatures/stagedLabels)
C3: Worker reads stagedWeightsForWorker (not iml.get_weights()),
    eliminating concurrent read/write on iml's MLP
C4: Worker spins on weightsPending before writing pendingWeights,
    preventing double-write race
C5: RAND and CLEAR now enqueue Randomize/Clear jobs through worker
    instead of directly mutating iml on the audio thread
C6: OSC callbacks stage JSON into oscStagedJson + atomic flag;
    audio thread applies in process() (no recv-thread mutation)

Also fixed:
- I4: Separate pendingJob field (enqueueJob no longer overwrites currentJob)
- I8: Removed redundant swapReady atomic
- noiseLevel, cachedNovelty, cachedConfidence now std::atomic<float>
- Worker syncs examples back to iml after training via load_examples()
2026-03-28 02:02:11 +02:00
w1n5t0n
c7964fd454 fix(vcv): implement proper double-buffered threading
Replace direct-mutation threading with shadow IML:
- Background thread clones weights from main → shadow IML
- Training and perturbation operate only on shadow instance
- New weights staged in pendingWeights, swapped atomically by audio thread
- Thumbs-down now enqueues Perturb job instead of calling move_weights directly
- Audio thread applies new weights via iml.set_weights() at safe point
- Examples copied to shadow for training, results copied back as weights only

Threading invariant now fully enforced: background thread never writes to
the inference IML. Audio thread applies staged weights between inference calls.
2026-03-28 01:55:30 +02:00
w1n5t0n
ba0fcab2a2 feat(vcv,playground): complete Phases 8, 9, 10 — all phases done
Phase 8 — Companion webapp bridge:
- NISPS-FORMAT.md: full .nisps JSON schema with validation rules
- Webapp iml.js: exportState() / importState() with bias handling
- osc_server.hpp: minimal UDP OSC server (cross-platform, no deps)
- VCV module: OSC toggle + port selection in right-click menu
- osc-client.js: WebSocket client with auto-reconnect
- Bridge scripts updated for bidirectional VCV↔webapp relay

Phase 9 — Panel layout variants:
- MEMLNaut.svg: 30HP standard panel (matches widget positions)
- MEMLNaut-wide.svg: 44HP with expanded display and 8 input slots
- MEMLNaut-expander.svg: 8HP with 6 extra inputs and LINK LED

Phase 10 — Polish & distribution:
- README.md: 267-line user guide (install, quick start, RL workflow,
  presets, OSC, technical details)
- BUILDING.md: build prerequisites, SDK setup, local install
- Makefile.dist: platform-stamped zip packaging
- SPEC.md: performance characteristics (1060 MADs/pass, ~46KB/instance)
- SPEC.md: v1 compatibility assessment (v2-only recommended)
2026-03-28 01:48:44 +02:00
w1n5t0n
74c52fadc7 feat(nisps-core,vcv): complete Phases 6 + 7 — persistence, derived outputs
Phase 6 — State persistence:
- Full state serialization: version, weights (3D), examples (features+labels),
  mlpConfig, noiseLevel, slewMs, output/input ranges
- Validation on load: version check, graceful missing field handling
- .nisps preset save/load via right-click menu (osdialog file dialogs)
- Param values included in preset files

Phase 7 — Derived outputs:
- Mean, STD, delta computed on audio thread (trivial cost)
- Novelty/confidence: nearest_example_distance() computed on background
  thread after each training/perturbation job, cached for audio thread
- Defaults with 0 examples: novelty=10V, confidence=0V

nisps-core IML additions:
- get_weights() / set_weights() for MLP weight serialization
- get_example_features/labels() / load_examples() for dataset serialization
- nearest_example_distance() for novelty/confidence metric
- get_example_count() / get_max_examples() for UI display
2026-03-28 01:27:19 +02:00
w1n5t0n
6a76f15736 feat(vcv): complete Phases 3, 4, 5 — RL feedback, display, configurability
Phase 3 — RL feedback system:
- Background worker thread with job queue, condition variable, atomic flags
- Thumbs up/down buttons + CV trigger inputs (Schmitt triggers)
- Learn enable toggle + gate input (OR logic)
- Noise level tracking (decay on +, increase on −, spread-dependent cap)
- Post-change output crossfade (configurable slew, default 10ms)
- Rapid feedback queueing with coalescing (max depth 1)
- Graceful thread shutdown (shouldStop flag, joins in destructor)

Phase 4 — Visual feedback:
- NanoVG bar graph display (12 hue-coded bars, noise level, TRAIN indicator)
- 12 output level LEDs, LEARN LED (green), training LED (yellow)

Phase 5 — Configurability:
- RATE knob: exponential decimation from block-rate to audio-rate
- Per-output range: unipolar (0-10V) / bipolar (±5V) via context menu
- Per-input range: unipolar / bipolar via context menu
- 12 attenuverter trimpots (-1 to +1)
- SPREAD CV input for knob modulation
- CLEAR button with 1-second long-press guard
- Output slew configurable via context menu (0-100ms)
- State serialization (ranges, noise, slew) via dataToJson/dataFromJson

Note: double-buffering uses direct IML access (not shadow copy) pending
IML weight get/set API (filed as meml-ft7).
2026-03-28 01:04:07 +02:00
w1n5t0n
d2f6ffc65e test(vcv): add smoke test harness — 7/7 pass
Standalone test exercising IML inference pipeline without VCV runtime:
- Basic inference: all 12 outputs valid in [0,1]
- Input responsiveness: 12/12 outputs change between corners
- Randomize: weights change produces different output mapping
- Spread parameter: measurably different distributions at 0 vs 1
- Expressiveness: 6/6 corner pairs distinct, full range utilized
- Smoothness: 100% of sweep steps are smooth (no binary jumps)

Finding: spread sigmoid saturation effect is architecture-dependent
with small [16,24,16] network (filed as meml-l5a for investigation).
2026-03-28 00:50:36 +02:00
w1n5t0n
e52b800a92 feat(vcv): complete Phase 2 — core engine wired up
- IML<float> with [16,24,16] hidden layers as module member
- CV inputs read, clamped 0-10V, normalized to [0,1]
- MLP inference in process(), 12 outputs scaled to 0-10V
- SPREAD knob (0-1, default 0.6) controls weight initialization
- RAND button randomizes weights using current spread value
- Panel: knob + button + 2 inputs + 12 outputs in 2x6 grid
2026-03-28 00:38:58 +02:00
w1n5t0n
d0ba1faaea feat(nisps-core,vcv): complete Phase 0 + Phase 1
Phase 0 — spread-aware API ported to nisps-core C++:
- MLP::DrawWeightsSpread(T spread) — interpolate uniform↔Xavier per layer
- MLP::MoveWeightsSpread(T speed, T spread) — per-layer noise + weight decay
- IML::randomise_weights(Float spread) and IML::move_weights(speed, spread)
- 5 unit tests (10/10 total pass)

Phase 1 — VCV Rack 2 plugin skeleton:
- Makefile with C++20, nisps-core include path
- plugin.json manifest
- Empty MEMLNaut module: 2 inputs, 12 outputs, placeholder SVG panel
- static_assert verifies nisps-core headers resolve
- C++20 confirmed working in VCV SDK (8 existing plugins use it)
2026-03-28 00:36:46 +02:00
w1n5t0n
9040886e16 docs(vcv): third review — input ranges, CV modulation, lifecycle, smoke test
- Add per-input range configuration (unipolar/bipolar) for LFO vs envelope compat
- Add SPREAD CV input for automated exploration/precision control
- Add background thread graceful shutdown (shouldStop flag + join timeout)
- Document multi-instance behavior (per-instance threads, ~80KB each)
- Add integration smoke test milestone after Phase 2 (go/no-go gate)
- Note OSC library dependency for Phase 8
- Fix 30HP layout: acknowledge density, defer validation to Phase 9
2026-03-28 00:22:43 +02:00
w1n5t0n
b4340c7f34 docs(vcv): fix 7 issues from second fresh-eyes review
1. Rename derived output SPREAD → STD to avoid collision with SPREAD knob
2. Route thumbs-down through background thread (was mutating inference
   MLP directly — data race). Establish single-writer threading invariant.
3. Add rapid feedback queueing with coalescing (max queue depth 1)
4. Document slew-on-slew interaction (decimation + crossfade compose cleanly)
5. Cut compact (20HP) panel — physically impossible to fit 22 jacks
6. Define derived output defaults with 0 examples (NOVELTY=10V, CONFIDENCE=0V)
7. Initial random output is intentional (shows liveness, gives RL starting point)
2026-03-27 23:39:56 +02:00
w1n5t0n
9dfa1429d0 docs(vcv): address spec gaps from fresh-eyes review
- Fix licensing: nisps-core is MPL-2.0, VCV SDK is GPLv3
- Fix VCV Rack 2 description: Community Edition is free, Pro is paid
- Add Phase 0: port spread-aware drawWeights/moveWeights to nisps-core C++
- Detail threading model: two MLP instances, atomic swap flag, memory cost
- Add configurable output slew (default 10ms) for post-training crossfade
- Add input signal handling: mono channel 0, hard-clamp out-of-range CV
- Add .nisps file format version field for forward compatibility
- Document dataset capacity (100 max, FIFO forgetting)
- Specify novelty/confidence computation strategy (training thread, cached grid)
- Add open questions: novelty grid scaling, C++20 toolchain, expander protocol
2026-03-25 12:25:21 +02:00
w1n5t0n
412782ed0e docs(vcv): add VCV Rack module specification
Full spec for MEMLNaut VCV Rack module based on interview:
- 2-8 configurable CV inputs, 12 raw + 5 derived CV outputs
- RL feedback via panel buttons + CV triggers with learn gate guard
- nisps-core C++ engine with background thread training
- User-configurable inference rate (block to audio rate)
- Bidirectional state transfer with companion webapp (file + OSC)
- 10-phase development plan from skeleton to distribution
2026-03-25 12:17:24 +02:00