Map enter/exit-explore, reroll, nudge, begin-place, commit-place to the peripheral buttons via the shared FeedbackController. (Not compiled here — no arduino-cli; Placing-audition static_output hook is a flagged follow-up.)
176 lines
8 KiB
C++
176 lines
8 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) : jolt / move_weights
|
|
// TogB1 : add example (latched: when high, capture current
|
|
// inputs+outputs as a training pair)
|
|
// TogB2 : train (rising-edge → call ml.train())
|
|
//
|
|
// 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);
|
|
});
|
|
|
|
// ---- 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());
|
|
});
|
|
|
|
// 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
|