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.
196 lines
9 KiB
C++
196 lines
9 KiB
C++
// firmware/glue/peripherals.hpp — Joystick, buttons, toggles → mode input
|
|
// channels.
|
|
//
|
|
// The MEMLNaut hardware exposes:
|
|
// - 3 analog joystick axes (X, Y, Z) via `setJoyXCallback` / `Y` / `Z`,
|
|
// each delivering a float in [0, 1].
|
|
// - Up to 5 rotary pots (RVGain1, RVZ1, RVY1, RVX1, ADC3) — same callback
|
|
// shape.
|
|
// - 4 momentary buttons (MomA1/2, MomB1/2) and 5 toggles (TogA1/2, TogB1/2,
|
|
// JoySW). Buttons fire `void()` on press; toggles fire `void(bool)` on
|
|
// edge.
|
|
//
|
|
// The `Mode` concept's input channels are abstract floats in [0, 1]. Each
|
|
// concrete mode's schema names its channels (e.g. paf_synth: joy_x, joy_y,
|
|
// joy_z, joy_w). The glue maps the *first N* analog inputs onto channels
|
|
// `[0, N)` — that's the trivial mapping that matches every concrete mode's
|
|
// schema today (joystick first, optional 4D extra pot for joy_w).
|
|
//
|
|
// Buttons drive the InteractiveML primitives directly:
|
|
// MomA1 (TA up) : randomise / draw weights
|
|
// MomA2 (TA down) : clear examples (reset dataset)
|
|
// MomB1 (MA up) : randomise (synonym, deliberate)
|
|
// MomB2 (MA down) : nudge (small bounded perturbation)
|
|
// TogB1 : Jolt — held continuous weight morph (up=morph, down=freeze)
|
|
// TogB2 : train (rising-edge → call ml.train())
|
|
// RVX1 : exploration amount (Ornstein-Uhlenbeck output walk)
|
|
//
|
|
// The button block below wires the SHARED ExploreAndPlace lifecycle
|
|
// (nisps/ml/feedback.hpp, the same core the browser drives via WASM) to the
|
|
// hardware buttons. See the per-button mapping at the binding site.
|
|
//
|
|
// FOLLOW-UP (needs a firmware build to verify — no arduino-cli here): while the
|
|
// controller is PLACING, the audition should hold feedback.static_output()
|
|
// (the frozen vector) instead of running fresh inference. That requires a hook
|
|
// in the control path (tick_control / audio_driver) to consult
|
|
// feedback.static_output() before ml().process() — not yet wired here because
|
|
// the controller lives in this translation unit. Wiring the buttons is the
|
|
// deliverable; the audition-hold is a small follow-up.
|
|
|
|
#pragma once
|
|
|
|
#include <Arduino.h>
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
|
|
#include "../src/nisps/core/perf.hpp"
|
|
#include "../src/nisps/ml/feedback.hpp"
|
|
#include "../src/memllib/hardware/memlnaut/MEMLNaut.hpp"
|
|
|
|
namespace nisps_firmware {
|
|
|
|
// Salt matching nisps/wasm/bindings.cpp so the firmware FeedbackController's
|
|
// per-instance Rng stream is independent of (and reproducible against) the
|
|
// MLP's inference/move RNG — same discipline as the browser.
|
|
inline constexpr std::uint64_t kFeedbackSalt = 0xFEEDBACC0DEull;
|
|
|
|
// Firmware undo-ring depth for ExploreAndPlace (rl-feedback-design §2.2: WASM
|
|
// D=4, firmware D=2 — SRAM budget).
|
|
inline constexpr std::size_t kFirmwareUndoDepth = 2u;
|
|
|
|
// Number of analog input channels we forward to the mode. Modes with fewer
|
|
// inputs ignore the surplus (set_input(idx, ...) silently rejects out-of-
|
|
// range idx in ModeBase).
|
|
inline constexpr std::size_t kAnalogInputCount = 4u; // X, Y, Z, plus one pot
|
|
|
|
// A small POD that the .ino owns. Captured by lambda below.
|
|
template <typename Mode>
|
|
struct PeripheralBindings {
|
|
Mode* mode = nullptr;
|
|
};
|
|
|
|
// Wire all hardware → mode bindings. Must be called *after*
|
|
// MEMLNaut::Initialize() (so MEMLNaut::Instance() is valid). The Mode
|
|
// reference must outlive the program (it's a static in the .ino).
|
|
template <typename Mode>
|
|
inline void bind_peripherals(Mode& mode) {
|
|
auto* meml = MEMLNaut::Instance();
|
|
if (!meml) return;
|
|
|
|
// ---- Analog inputs → mode input channels ----
|
|
meml->setJoyXCallback([&mode](float v) { mode.set_input(0u, v); });
|
|
meml->setJoyYCallback([&mode](float v) { mode.set_input(1u, v); });
|
|
meml->setJoyZCallback([&mode](float v) { mode.set_input(2u, v); });
|
|
// Fourth analog input: use ADC3 (the spare). Modes that don't expose a
|
|
// joy_w channel just don't see it (set_input drops out-of-range idx).
|
|
meml->setADC3Callback([&mode](float v) { mode.set_input(3u, v); });
|
|
|
|
// ---- Rotary pots ----
|
|
// Reuse RVGain1 as a "tempo" knob for sequencer modes. The mode is free
|
|
// to ignore via the same channel-bounds drop. RVGain1 is also used by
|
|
// InterfaceRL legacy as an audio output volume knob — for now we hand
|
|
// it to the ML interface for non-sequencer modes by setting the output
|
|
// master volume directly via AudioDriver.
|
|
meml->setRVGain1Callback([](float v) {
|
|
AudioDriver::SetMasterVolume(v);
|
|
});
|
|
|
|
// RVX1 → exploration amount. Drives the Ornstein-Uhlenbeck random walk
|
|
// added to the mode's output vector (nisps/ml/ou_noise.hpp, ported from
|
|
// upstream InterfaceRL). 0 = off (inert); turning it up makes the sound
|
|
// slowly roam so likes/dislikes can steer the net. Available on every
|
|
// mode via ModeBase::set_explore_intensity.
|
|
meml->setRVX1Callback([&mode](float v) {
|
|
mode.set_explore_intensity(v);
|
|
});
|
|
|
|
// ---- Buttons / toggles → ExploreAndPlace lifecycle (SHARED C++ core) ----
|
|
//
|
|
// The same nisps::ml::FeedbackController that runs in the browser (via
|
|
// WASM) drives the Idle → Exploring → Placing → Idle state machine here.
|
|
// The HARDWARE button mapping differs from the browser's on_down/on_up
|
|
// default policy — firmware maps its buttons to the GRANULAR core methods:
|
|
//
|
|
// MomA1 (TA up) : down — enter explore (Idle→Exploring) /
|
|
// exit explore (Exploring→Idle) TOGGLE
|
|
// MomB1 (MA up) : randomise — reroll the scratchpad (Exploring)
|
|
// MomB2 (MA down) : nudge — small bounded perturbation (Exploring)
|
|
// MomA2 (TA down) : like — begin place (Exploring→Placing, freeze output)
|
|
// TogB2 (rising) : up — commit place (Placing→Idle) at the CURRENT
|
|
// joystick input, then store the +1 example and
|
|
// train (caller owns training). Outside Placing,
|
|
// a plain train().
|
|
//
|
|
// The controller is a function-local static so it lives for the whole
|
|
// program (like the mode). It is seeded off the same salt as the browser.
|
|
using FB = nisps::ml::FeedbackController<typename Mode::ML, kFirmwareUndoDepth>;
|
|
static FB feedback(static_cast<std::uint64_t>(0xC0FFEEu) ^ kFeedbackSalt);
|
|
feedback.set_mode(nisps::ml::FeedbackMode::ExploreAndPlace, mode.ml());
|
|
|
|
const float spread = mode.param_schema().default_spread;
|
|
|
|
// MomA1: enter/exit explore toggle.
|
|
meml->setMomA1Callback([&mode]() {
|
|
if (feedback.explore_state() == nisps::ml::ExploreState::Idle) {
|
|
feedback.enter_explore(mode.ml(), mode.param_schema().default_spread);
|
|
} else {
|
|
feedback.exit_explore(mode.ml());
|
|
}
|
|
});
|
|
|
|
// MomB1: reroll the scratchpad (only in Exploring; no-op otherwise).
|
|
meml->setMomB1Callback([&mode]() {
|
|
feedback.reroll(mode.ml(), mode.param_schema().default_spread);
|
|
});
|
|
|
|
// MomB2: nudge the scratchpad (small bounded perturbation; undoable).
|
|
meml->setMomB2Callback([&mode]() {
|
|
feedback.nudge(mode.ml(), 0.05f);
|
|
});
|
|
|
|
// MomA2: like → begin place. Freezes the current scratchpad output so the
|
|
// user can move the joystick to choose WHERE to place it.
|
|
meml->setMomA2Callback([&mode]() {
|
|
feedback.begin_place(mode.ml());
|
|
});
|
|
|
|
// TogB1: Jolt — held continuous weight morph (nisps/ml/jolt.hpp, ported
|
|
// from upstream InterfaceRL). Flip up to start morphing a scatter of
|
|
// weights live; flip down to freeze them (the change is permanent) and
|
|
// let the learning rate ramp gently back in. A toggle (not a momentary)
|
|
// because Jolt is a HELD gesture and momentary buttons only fire on
|
|
// press. Available on every mode via ModeBase::jolt_press/jolt_release.
|
|
meml->setTogB1Callback([&mode](bool state) {
|
|
if (state) mode.jolt_press();
|
|
else mode.jolt_release();
|
|
});
|
|
|
|
// TogB2 (rising): up → commit place at the current joystick input, then
|
|
// store the +1 example (input → placed output) and train. Outside Placing,
|
|
// just train (legacy behaviour).
|
|
meml->setTogB2Callback([&mode](bool state) {
|
|
if (!state) return;
|
|
if (feedback.placing()) {
|
|
// Capture the current joystick input BEFORE the restore.
|
|
const auto in = mode.input_channels();
|
|
std::array<float, Mode::ML::kInput> features{};
|
|
for (std::size_t i = 0; i < Mode::ML::kInput; ++i) {
|
|
features[i] = (i < in.size()) ? in[i] : 0.f;
|
|
}
|
|
feedback.commit_place(mode.ml()); // restores the real net
|
|
// CALLER owns training: add the +1 example then warm-start train.
|
|
const auto label = feedback.committed_output(); // valid post-commit
|
|
if (!label.empty()) {
|
|
mode.ml().add_example(
|
|
std::span<const float>(features.data(), Mode::ML::kInput), label);
|
|
(void)mode.ml().train();
|
|
}
|
|
} else {
|
|
(void)mode.ml().train();
|
|
}
|
|
});
|
|
}
|
|
|
|
} // namespace nisps_firmware
|