2026-04-29 14:22:01 +02:00
|
|
|
cmake_minimum_required(VERSION 3.20)
|
|
|
|
|
project(nisps_core CXX)
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Toolchain / standard
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
|
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
|
|
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
|
|
|
|
|
|
|
|
# Default to a Release build when invoked without -DCMAKE_BUILD_TYPE so host
|
|
|
|
|
# tests get optimized math; flip with `-DCMAKE_BUILD_TYPE=Debug` for stepping.
|
|
|
|
|
if(NOT CMAKE_BUILD_TYPE)
|
|
|
|
|
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Core interface library — header-only. Other parts of the build (ml/, dsp/,
|
|
|
|
|
# engines/, modes/) will link against this once they exist.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
add_library(nisps_core INTERFACE)
|
|
|
|
|
target_include_directories(nisps_core
|
|
|
|
|
INTERFACE
|
|
|
|
|
${CMAKE_CURRENT_SOURCE_DIR}
|
|
|
|
|
)
|
|
|
|
|
target_compile_features(nisps_core INTERFACE cxx_std_20)
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Emscripten / WASM target detection
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# When building for WASM via emcmake, ${EMSCRIPTEN} is set automatically. We
|
|
|
|
|
# don't actually emit a WASM binary from this CMakeLists yet — the WASM build
|
|
|
|
|
# script lives in stream 7 (playground/build) and assembles its own
|
|
|
|
|
# Emscripten link command. Here we just gate the host-only test executable so
|
|
|
|
|
# `emcmake cmake -S nisps -B build-wasm` configures cleanly.
|
|
|
|
|
if(EMSCRIPTEN)
|
|
|
|
|
message(STATUS "nisps_core: configuring for Emscripten/WASM target")
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Test scaffold — only built on the host. We use a hand-rolled assertion-
|
|
|
|
|
# based harness (see tests/cpp/test_helpers.hpp) rather than Catch2/doctest;
|
|
|
|
|
# the rationale is in test_helpers.hpp.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if(NOT EMSCRIPTEN)
|
|
|
|
|
set(NISPS_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../tests/cpp)
|
|
|
|
|
|
|
|
|
|
add_executable(nisps_core_tests
|
|
|
|
|
${NISPS_TEST_DIR}/test_main.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_fixed_buffer.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_ring_buffer.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_rng.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_math.cpp
|
feat(nisps/ml): MLP library with fixed-architecture template + spread-aware RL (meml-wmh)
Stream 2 of the clean-slate rewrite: nisps/ml/ replaces src/memlp/ with a
header-only, heap-free MLP that satisfies nisps::core::MLEngine.
Files (nisps/ml/):
- activations.hpp — ReLU (leaky 0.01 for parity), sigmoid, tanh
- loss.hpp — MSE per-sample (fixes meml-ues double-scaling: returns the
sample's MSE without an extra 1/N multiplication; the training loop
averages explicitly)
- init.hpp — uniform/Xavier/spread-aware weight init
- training.hpp — gradient clip helper (±10.0 matches legacy)
- rl.hpp — move_weights with per-layer Xavier scaling, weight decay
(10% * spread), gaussian noise via the deterministic Rng (matches the
legacy JS sum-of-three-uniforms shape); draw_weights also spread-aware
- stats.hpp — per-layer mean/max/dead/saturating diagnostics
- mlp.hpp — 4-layer (3 hidden + sigmoid output) MLP class with
std::array-backed weights, biases, gradient accumulators, dataset
ring buffer (default 128 examples), loss history (default 4096 iters).
Bias is a separate per-layer parameter — no input-vector mutation.
Flat get_weights/set_weights layout: weights all layers (row-major,
layer order), then biases all layers.
Tests (tests/cpp/, all 50 passing under -Wall -Wextra -Werror -Wpedantic):
- test_mlp_init.cpp — deterministic seeding, spread regimes,
static_assert MLEngine concept satisfied
- test_mlp_inference.cpp — golden hand-computed forward pass match,
sigmoid output range, set_input bounds
- test_mlp_training.cpp — XOR convergence (loss < 0.01 in <2k iters),
ring-buffer eviction
- test_mlp_loss.cpp — meml-ues regression test: reported loss equals
hand-computed average MSE without extra 1/N scaling; sample weights
honoured
- test_mlp_rl.cpp — move_weights respects output_pin_mask (final-layer
rows + biases preserved); spread regimes; grad clear after draw_weights
- test_mlp_serialize.cpp — get_weights/set_weights round-trip preserves
inference exactly; eval_loss is non-mutating; infer_batch matches
individual inference
Verification:
- Clean build, no warnings
- 50 tests pass (22 prior + 28 new)
- No std::vector / new / malloc in nisps/ml/
- All float literals .f-suffixed in code (comments excepted)
2026-04-29 14:55:43 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mlp_init.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mlp_inference.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mlp_training.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mlp_loss.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mlp_rl.cpp
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mlp_storage_parity.cpp
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mlp_jolt.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mlp_ou_noise.cpp
|
2026-06-28 04:14:12 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mlp_feedback.cpp
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mlp_geo_dislike.cpp
|
feat(nisps/ml): MLP library with fixed-architecture template + spread-aware RL (meml-wmh)
Stream 2 of the clean-slate rewrite: nisps/ml/ replaces src/memlp/ with a
header-only, heap-free MLP that satisfies nisps::core::MLEngine.
Files (nisps/ml/):
- activations.hpp — ReLU (leaky 0.01 for parity), sigmoid, tanh
- loss.hpp — MSE per-sample (fixes meml-ues double-scaling: returns the
sample's MSE without an extra 1/N multiplication; the training loop
averages explicitly)
- init.hpp — uniform/Xavier/spread-aware weight init
- training.hpp — gradient clip helper (±10.0 matches legacy)
- rl.hpp — move_weights with per-layer Xavier scaling, weight decay
(10% * spread), gaussian noise via the deterministic Rng (matches the
legacy JS sum-of-three-uniforms shape); draw_weights also spread-aware
- stats.hpp — per-layer mean/max/dead/saturating diagnostics
- mlp.hpp — 4-layer (3 hidden + sigmoid output) MLP class with
std::array-backed weights, biases, gradient accumulators, dataset
ring buffer (default 128 examples), loss history (default 4096 iters).
Bias is a separate per-layer parameter — no input-vector mutation.
Flat get_weights/set_weights layout: weights all layers (row-major,
layer order), then biases all layers.
Tests (tests/cpp/, all 50 passing under -Wall -Wextra -Werror -Wpedantic):
- test_mlp_init.cpp — deterministic seeding, spread regimes,
static_assert MLEngine concept satisfied
- test_mlp_inference.cpp — golden hand-computed forward pass match,
sigmoid output range, set_input bounds
- test_mlp_training.cpp — XOR convergence (loss < 0.01 in <2k iters),
ring-buffer eviction
- test_mlp_loss.cpp — meml-ues regression test: reported loss equals
hand-computed average MSE without extra 1/N scaling; sample weights
honoured
- test_mlp_rl.cpp — move_weights respects output_pin_mask (final-layer
rows + biases preserved); spread regimes; grad clear after draw_weights
- test_mlp_serialize.cpp — get_weights/set_weights round-trip preserves
inference exactly; eval_loss is non-mutating; infer_batch matches
individual inference
Verification:
- Clean build, no warnings
- 50 tests pass (22 prior + 28 new)
- No std::vector / new / malloc in nisps/ml/
- All float literals .f-suffixed in code (comments excepted)
2026-04-29 14:55:43 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mlp_serialize.cpp
|
feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
power→EMA→momentum update). Caller-supplied dt, internal accumulated
clock (no wall clock — deterministic; matches the P1 fixtures' clock
contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
process/reset, pipeline state save/load, nisps_curve_apply(+batch)
(ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
chains (2 configs each) + full curve catalog — 1273 floats PASS, the
pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
state round-trip, slew, freeze gate/mask, reseed-on-length-change,
centred-power endpoints.
Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
|
|
|
${NISPS_TEST_DIR}/test_pipeline.cpp
|
2026-07-18 13:01:28 +02:00
|
|
|
${NISPS_TEST_DIR}/test_vcv_iml_parity.cpp
|
2026-04-29 14:22:01 +02:00
|
|
|
)
|
|
|
|
|
target_link_libraries(nisps_core_tests PRIVATE nisps_core)
|
|
|
|
|
|
|
|
|
|
# Chris's rule: the core compiles cleanly under -Wall -Wextra -Werror.
|
|
|
|
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
|
|
|
|
|
target_compile_options(nisps_core_tests PRIVATE
|
|
|
|
|
-Wall -Wextra -Werror -Wpedantic
|
|
|
|
|
)
|
|
|
|
|
elseif(MSVC)
|
|
|
|
|
target_compile_options(nisps_core_tests PRIVATE /W4 /WX)
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
enable_testing()
|
|
|
|
|
add_test(NAME nisps_core_tests COMMAND nisps_core_tests)
|
feat(nisps/engines): port firmware audio engines to AudioEngine concept (meml-1v6)
Concept-based, no virtual dispatch, per-engine voice spaces as inline
methods. Each engine satisfies nisps::AudioEngine via static_assert.
- NoOpEngine: silent passthrough; used for sequencer-only modes and
for the SoundAnalysisMIDI mode's audio path.
- PAFSynthEngine (33 params, 7 voice spaces): 4-voice PAF synth with
detune cascade, ring-mod, sine-shaper, ADSR, feedback delay. note_on/
note_off interface for MIDI keyboard.
- ChannelStripEngine (24 params, 6 voice spaces): stereo console strip
(pre-gain/HPF/LPF/2x peak/low-shelf/high-shelf/comp/post-gain). Voice
spaces: WannabeNeve66, SSL4K, SSL9K, MaleVox, FemaleVox, Neve80
(stepped-frequency).
- XIASRIEngine (24 params, "Direct" voice space): pitch-shift + 6 allpass
+ 2 comb + 4 delays. Direct NN→param mapping per firmware semantics.
- VerbFXEngine (47 params, 12 voice spaces): 8-band SVF filterbank +
3-lane dynamic delay + 8-lpcomb/4-allpass Freeverb-style tail with
cross-fades. All 12 voice spaces ported from voicespaces/VerbFX/*.hpp.
- MEMLCeliumEngine (56 params): 2-track ratio sequencer + dual-voice
PAF synth (7+7+22+20 layout). Sequencer triggers V0/V1 ADSR.
- BreakOrEngine (56 params): 8-track ratio sequencer; emits NoteOn/
NoteOff/Clock events via pop_events(span). process() returns silence.
- ElysiamorfEngine (40 params): 8-track FM-pair sequencer; emits CC
events on CCs {1,2,3,4,5,9,11,12}. Silent audio path.
- AnalysisEngine (0 params, 6 features): port of XiasriAnalysis (pitch
via zero-crossing, aperiodicity via MAD, log-domain energy + attack
derivative + brightness ratio). Inputs to ML on SoundAnalysisMIDI mode.
All param_count() values match schemas/modes/*.json output_size.
4074 LOC total. CMake adds nisps_dsp_engine_tests target with 38
passing tests under -Wall -Wextra -Werror -Wpedantic.
2026-04-29 15:09:12 +02:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
# DSP + engines tests (stream 3). Built as a separate executable so
|
|
|
|
|
# failures here don't take core/ML tests with them.
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
add_executable(nisps_dsp_engine_tests
|
|
|
|
|
${NISPS_TEST_DIR}/test_main.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_dsp_biquad.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_dsp_delay.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_dsp_reverb.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_dsp_pitch_shift.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_no_op.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_paf_synth.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_channel_strip.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_xiasri.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_verb_fx.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_memlcelium.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_breakor.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_elysiamorf.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_engine_analysis.cpp
|
|
|
|
|
)
|
|
|
|
|
target_link_libraries(nisps_dsp_engine_tests PRIVATE nisps_core)
|
|
|
|
|
|
|
|
|
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
|
|
|
|
|
target_compile_options(nisps_dsp_engine_tests PRIVATE
|
|
|
|
|
-Wall -Wextra -Werror -Wpedantic
|
|
|
|
|
)
|
|
|
|
|
elseif(MSVC)
|
|
|
|
|
target_compile_options(nisps_dsp_engine_tests PRIVATE /W4 /WX)
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
add_test(NAME nisps_dsp_engine_tests COMMAND nisps_dsp_engine_tests)
|
test(nisps/modes): host C++ tests for stream 4 mode layer (meml-beb)
Adds `nisps_modes_tests` executable to nisps/CMakeLists.txt with four TUs:
- `test_mode_concepts.cpp`: 8 `static_assert(Mode<...>)` (concept
satisfaction), plus runtime metadata sanity for each mode (mode_id,
input_channel_count, schema sizes match engine param_count).
- `test_mode_paf_synth.cpp`: end-to-end exercise — setup, set_input,
tick_control, process audio. Verifies idle process is finite, output
bounds [0,1] hold, note_on triggers nonzero audio, input clamping,
and engine/ml accessors round-trip.
- `test_mode_voice_space.cpp`: voice-space round trip for PAFSynth,
ChannelStrip and VerbFX (all dispatched modes). Confirms
out-of-range index is silently ignored.
- `test_mode_breakor_events.cpp`: sequencer event pumping. BreakOr
emits Clock + NoteOn/Off, Elysiamorf emits CC, SoundAnalysisMIDI
converts 8 ML outputs → 8 ControlEvents (CC 0..7) per tick, ring
buffer overflow drops cleanly.
Total: 22 new tests (110 across nisps/), all passing under -Werror
-Wpedantic. Build remains clean.
2026-04-29 15:27:03 +02:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
# Mode tests (stream 4). Cover concept satisfaction, voice space
|
|
|
|
|
# dispatch, control-event pumping, and end-to-end inference→audio.
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
add_executable(nisps_modes_tests
|
|
|
|
|
${NISPS_TEST_DIR}/test_main.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mode_concepts.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mode_paf_synth.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mode_voice_space.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/test_mode_breakor_events.cpp
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
${NISPS_TEST_DIR}/test_mode_learning.cpp
|
test(nisps/modes): host C++ tests for stream 4 mode layer (meml-beb)
Adds `nisps_modes_tests` executable to nisps/CMakeLists.txt with four TUs:
- `test_mode_concepts.cpp`: 8 `static_assert(Mode<...>)` (concept
satisfaction), plus runtime metadata sanity for each mode (mode_id,
input_channel_count, schema sizes match engine param_count).
- `test_mode_paf_synth.cpp`: end-to-end exercise — setup, set_input,
tick_control, process audio. Verifies idle process is finite, output
bounds [0,1] hold, note_on triggers nonzero audio, input clamping,
and engine/ml accessors round-trip.
- `test_mode_voice_space.cpp`: voice-space round trip for PAFSynth,
ChannelStrip and VerbFX (all dispatched modes). Confirms
out-of-range index is silently ignored.
- `test_mode_breakor_events.cpp`: sequencer event pumping. BreakOr
emits Clock + NoteOn/Off, Elysiamorf emits CC, SoundAnalysisMIDI
converts 8 ML outputs → 8 ControlEvents (CC 0..7) per tick, ring
buffer overflow drops cleanly.
Total: 22 new tests (110 across nisps/), all passing under -Werror
-Wpedantic. Build remains clean.
2026-04-29 15:27:03 +02:00
|
|
|
)
|
|
|
|
|
target_link_libraries(nisps_modes_tests PRIVATE nisps_core)
|
|
|
|
|
|
|
|
|
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
|
|
|
|
|
target_compile_options(nisps_modes_tests PRIVATE
|
|
|
|
|
-Wall -Wextra -Werror -Wpedantic
|
|
|
|
|
)
|
|
|
|
|
elseif(MSVC)
|
|
|
|
|
target_compile_options(nisps_modes_tests PRIVATE /W4 /WX)
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
add_test(NAME nisps_modes_tests COMMAND nisps_modes_tests)
|
2026-04-29 18:50:55 +02:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
# Stream 11 verification suite (golden vectors + engine impulse).
|
|
|
|
|
# Lives in tests/cpp/, registered separately so a regression here can
|
|
|
|
|
# be diagnosed without rebuilding the world.
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
add_executable(nisps_golden_tests
|
|
|
|
|
${NISPS_TEST_DIR}/test_main.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/ml_golden_vectors.cpp
|
|
|
|
|
${NISPS_TEST_DIR}/engine_impulse.cpp
|
|
|
|
|
)
|
|
|
|
|
target_link_libraries(nisps_golden_tests PRIVATE nisps_core)
|
|
|
|
|
|
|
|
|
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
|
|
|
|
|
target_compile_options(nisps_golden_tests PRIVATE
|
|
|
|
|
-Wall -Wextra -Werror -Wpedantic
|
|
|
|
|
)
|
|
|
|
|
elseif(MSVC)
|
|
|
|
|
target_compile_options(nisps_golden_tests PRIVATE /W4 /WX)
|
|
|
|
|
endif()
|
|
|
|
|
|
|
|
|
|
add_test(NAME nisps_golden_tests COMMAND nisps_golden_tests)
|
|
|
|
|
# Run the impulse test from the repo root so the relative baseline path
|
|
|
|
|
# in `engine_impulse.cpp` resolves to tests/cpp/engine_impulse_baseline.bin.
|
|
|
|
|
set_tests_properties(nisps_golden_tests PROPERTIES
|
|
|
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Standalone parity-check runner. NOT registered with ctest — it's
|
|
|
|
|
# invoked from scripts/parity-check.sh which orchestrates native+WASM
|
|
|
|
|
# together.
|
|
|
|
|
add_executable(nisps_parity_check
|
|
|
|
|
${NISPS_TEST_DIR}/parity_check.cpp
|
|
|
|
|
)
|
|
|
|
|
target_link_libraries(nisps_parity_check PRIVATE nisps_core)
|
|
|
|
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
|
|
|
|
|
target_compile_options(nisps_parity_check PRIVATE
|
|
|
|
|
-Wall -Wextra -Werror -Wpedantic
|
feat(manifold): MIDI + game controller inputs; widen ML net to N-D
Wire the modular input layer into the Console and reshape the browser
engine so input axes are genuine independent dimensions.
Inputs (manifold/src/inputs/):
- gamepad-source: emit press+release edges with standard-mapping labels
(enables hold-and-move); single/double-stick already present.
- midi-input-source: single-device selection + batch "MIDI Learn"
(every CC swept while armed becomes an axis); notes stay discrete.
- input-layer: compose() forwards each axis 1:1 (no mean-blend);
add onReducedInput so the manifold tracks gamepad/MIDI position.
- types: InputAction.phase, InputMode.
Console (manifold/src/console/):
- ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down /
X randomise / Y nudge / B undo / A-hold reposition); mirror composed
position onto the manifold.
- Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI
device picker + batch-learn flow, learned-control meters).
Engine (nisps/wasm, manifold/src/engine):
- DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each
active axis gets a dedicated slot, unused slots held at 0 (inert).
Rebuilt nisps.wasm (playground + manifold).
- spine/engine-api: setInputs writes the full N-D vector (was dropping
arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the
whole vector via spine.reprocess().
Tests:
- parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs.
- CMakeLists: build parity binary with -ffp-contract=off so native
matches FMA-free WASM (training amplified the gap past 1e-5).
Inputs dock is still an exclusive picker; mixing toggles, reshape modal,
and the >2-D slider view (inputs-spec.md) are groundwork-laid but not
yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
|
|
|
# Disable FP multiply-add contraction so native matches the WASM
|
|
|
|
|
# build, which has no FMA instruction. Without this, native clang/gcc
|
|
|
|
|
# fuses MACs in the training backprop and the (chaotic) loop amplifies
|
|
|
|
|
# the rounding difference past the 1e-5 parity tolerance — pronounced
|
|
|
|
|
# since the input layer widened to 32 for mix-and-match inputs.
|
|
|
|
|
-ffp-contract=off
|
2026-04-29 18:50:55 +02:00
|
|
|
)
|
|
|
|
|
elseif(MSVC)
|
feat(manifold): MIDI + game controller inputs; widen ML net to N-D
Wire the modular input layer into the Console and reshape the browser
engine so input axes are genuine independent dimensions.
Inputs (manifold/src/inputs/):
- gamepad-source: emit press+release edges with standard-mapping labels
(enables hold-and-move); single/double-stick already present.
- midi-input-source: single-device selection + batch "MIDI Learn"
(every CC swept while armed becomes an axis); notes stay discrete.
- input-layer: compose() forwards each axis 1:1 (no mean-blend);
add onReducedInput so the manifold tracks gamepad/MIDI position.
- types: InputAction.phase, InputMode.
Console (manifold/src/console/):
- ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down /
X randomise / Y nudge / B undo / A-hold reposition); mirror composed
position onto the manifold.
- Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI
device picker + batch-learn flow, learned-control meters).
Engine (nisps/wasm, manifold/src/engine):
- DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each
active axis gets a dedicated slot, unused slots held at 0 (inert).
Rebuilt nisps.wasm (playground + manifold).
- spine/engine-api: setInputs writes the full N-D vector (was dropping
arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the
whole vector via spine.reprocess().
Tests:
- parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs.
- CMakeLists: build parity binary with -ffp-contract=off so native
matches FMA-free WASM (training amplified the gap past 1e-5).
Inputs dock is still an exclusive picker; mixing toggles, reshape modal,
and the >2-D slider view (inputs-spec.md) are groundwork-laid but not
yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
|
|
|
target_compile_options(nisps_parity_check PRIVATE /W4 /WX /fp:precise)
|
2026-04-29 18:50:55 +02:00
|
|
|
endif()
|
2026-04-29 14:22:01 +02:00
|
|
|
endif()
|