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.
104 lines
3.1 KiB
C++
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());
|
|
}
|