memlnaut-nisps/tests/cpp/test_mode_learning.cpp
monkey-w1n5t0n 4e60d0192a 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

104 lines
3.1 KiB
C++

// tests/cpp/test_mode_learning.cpp — ModeBase adaptive-learning wiring,
// exercised through SLPWorkshopMode.
//
// - inert parity: with Jolt inactive and OU disabled (the defaults),
// SLPWorkshopMode is bit-identical to MEMLCeliumMode (same engine, same
// MLP shape, same seed) — proving the new gestures don't perturb the
// baseline mapping.
// - Jolt: pressing it morphs the network weights over successive control
// ticks and gates the LR (0 while held, ramps back after release).
// - OU explore: intensity round-trips, toggles enabled state, and keeps
// audio finite.
#include <array>
#include <cmath>
#include <cstdint>
#include "test_helpers.hpp"
#include "../../nisps/core/types.hpp"
#include "../../nisps/modes/memlcelium.hpp"
#include "../../nisps/modes/slp_workshop.hpp"
using namespace nisps;
NISPS_TEST(slp_workshop_inert_equals_memlcelium) {
modes::MEMLCeliumMode a(123ull);
modes::SLPWorkshopMode b(123ull);
a.setup(48000.f);
b.setup(48000.f);
for (int t = 0; t < 8; ++t) {
const float x = 0.3f + 0.05f * static_cast<float>(t);
a.set_input(0, x);
a.set_input(1, 0.6f);
b.set_input(0, x);
b.set_input(1, 0.6f);
a.tick_control();
b.tick_control();
for (int s = 0; s < 32; ++s) {
const stereosample_t za{0.f, 0.f};
const stereosample_t zb{0.f, 0.f};
const auto oa = a.process(za);
const auto ob = b.process(zb);
NISPS_EXPECT(oa.L == ob.L);
NISPS_EXPECT(oa.R == ob.R);
}
}
}
NISPS_TEST(slp_workshop_jolt_morphs_weights_and_gates_lr) {
using ML = modes::SLPWorkshopMode::ML;
modes::SLPWorkshopMode m(5ull);
m.setup(48000.f);
std::array<float, ML::weight_count()> before{};
{
auto w = m.ml().get_weights();
for (std::size_t i = 0; i < w.size(); ++i) before[i] = w[i];
}
NISPS_EXPECT(!m.jolt_active());
NISPS_EXPECT(m.jolt_lr_scale() == 1.f);
m.jolt_press();
NISPS_EXPECT(m.jolt_active());
NISPS_EXPECT(m.jolt_lr_scale() == 0.f);
for (int t = 0; t < 100; ++t) m.tick_control();
int changed = 0;
{
auto w = m.ml().get_weights();
for (std::size_t i = 0; i < w.size(); ++i) {
if (before[i] != w[i]) ++changed;
}
}
NISPS_EXPECT(changed > 0);
m.jolt_release();
NISPS_EXPECT(!m.jolt_active());
for (int t = 0; t < 2000; ++t) m.tick_control(); // ramps LR back to full
NISPS_EXPECT_NEAR(m.jolt_lr_scale(), 1.f, 1e-6);
}
NISPS_TEST(slp_workshop_explore_intensity_roundtrip_finite) {
modes::SLPWorkshopMode m(9ull);
m.setup(48000.f);
NISPS_EXPECT(!m.ou_noise().enabled());
m.set_explore_intensity(0.5f);
NISPS_EXPECT(m.ou_noise().enabled());
NISPS_EXPECT_NEAR(m.explore_intensity(), 0.5f, 1e-6);
for (int t = 0; t < 8; ++t) {
m.tick_control();
for (int s = 0; s < 16; ++s) {
const auto o = m.process(stereosample_t{0.f, 0.f});
NISPS_EXPECT(std::isfinite(o.L) && std::isfinite(o.R));
}
}
m.set_explore_intensity(0.f);
NISPS_EXPECT(!m.ou_noise().enabled());
}