memlnaut-nisps/firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino
monkey-w1n5t0n 3952c65d75 feat(firmware): on-device Joystick Dual/Single settings menu
Add glue/settings_view.hpp: wire_settings(mode) registers a "Joystick"
SingleSelectView on the MEMLNaut display carousel (TFT + rotary encoder, same
DisplayDriver as SystemView/SelfTest). For the 4-input two-joystick modes,
"Single" pins ML input channels 2,3 (the second joystick) to neutral via
ModeBase::set_input_pinned — the network is never rebuilt and trained state
survives toggling. Default is Dual. Only registered when input arity == 4.

Called from MEMLNaut-NISPS.ino setup() after addSystemInfoView(). MAP.md +
CLAUDE.md glue listings updated.

NOTE: compile-unverified — no arduino-cli/RP2350 toolchain on this host and no
hardware; needs scripts/build-firmware.sh + a flash test (chokepoint A).
2026-06-28 21:17:51 +02:00

226 lines
7.9 KiB
C++

// firmware/MEMLNaut-NISPS.ino — Thin entry point.
//
// The heavy lifting now lives under:
// - nisps/... — platform-agnostic ML, DSP, engines, modes
// - firmware/glue/... — hardware bindings (audio driver, MIDI,
// peripherals, input/output routers)
//
// This file does only:
// 1. Pick a mode at compile time (`MEMLNAUT_MODE_TYPE`).
// 2. Instantiate it (in `AUDIO_MEM` so it lives in SRAM).
// 3. setup() / loop() on core 0:
// - boot board
// - bind peripherals → mode.set_input
// - bind MIDI in → mode.note_on/update_bpm/...
// - run mode.tick_control() at ML cadence (5ms)
// 4. setup1() / loop1() on core 1:
// - register the audio bridge so AudioDriver streams into mode.process
// - pump engine events / drain MIDI out at sub-ms cadence
// ---- Hardware ----
#include "src/memllib/PicoDefs.hpp"
#include "src/memllib/audio/AudioDriver.hpp"
#include "src/memllib/hardware/memlnaut/MEMLNaut.hpp"
#include "src/memllib/interface/MIDIInOut.hpp"
#include "src/memllib/utils/perf.hpp"
#include "hardware/structs/bus_ctrl.h"
// ---- Glue ----
#include "glue/audio_driver.hpp"
#include "glue/input_router.hpp"
#include "glue/midi_io.hpp"
#include "glue/mode_select.hpp"
#include "glue/output_router.hpp"
#include "glue/settings_view.hpp"
// ---- Mode selection ----
// Build script rewrites which line is uncommented.
// #define MEMLNAUT_MODE_TYPE MEMLNautModeSoundAnalysisMIDI
// #define MEMLNAUT_MODE_TYPE MEMLNautModeXIASRI
// #define MEMLNAUT_MODE_TYPE MEMLNautModeVerbFX
// #define MEMLNAUT_MODE_TYPE MEMLNautModeBreakOr
// #define MEMLNAUT_MODE_TYPE MEMLNautModeElysiamorfs
// #define MEMLNAUT_MODE_TYPE MEMLNautModeChannelStrip
#define MEMLNAUT_MODE_TYPE MEMLNautModePAFSynth
// #define MEMLNAUT_MODE_TYPE MEMLNautModeMEMLCelium
// ---- External-synth MIDI-CC variants (control an external hardware synth) ----
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthSub37
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthSubPhatty
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthPro12
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthAnalogKeys
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthHydrasynth
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthJD800
// #define MEMLNAUT_MODE_TYPE MEMLNautModeSelfTest
// NISPS_SELFTEST == 1 iff the selected variant is the guided hardware self-test
// (see glue/mode_select.hpp). Must be computed AFTER the mode #define above so
// MEMLNAUT_MODE_TYPE is in scope for the token paste.
#define NISPS_SELFTEST NISPS_ST_CAT(MEMLNAUT_MODE_TYPE)
#include <memory>
// Inter-core handshake flags + stack flag — shared by BOTH the normal-mode and
// self-test build paths, so they live outside the fork below.
volatile bool APP_SRAM g_core0_ready = false;
volatile bool APP_SRAM g_core1_ready = false;
volatile bool APP_SRAM g_serial_ready = false;
volatile bool APP_SRAM g_iface_ready = false;
bool core1_separate_stack = true;
#if !NISPS_SELFTEST
// =====================================================================
// Normal-mode build path (an engine + ML mode runs the device).
// =====================================================================
using ActiveMode = MEMLNAUT_MODE_TYPE;
ActiveMode AUDIO_MEM g_mode;
// Definition of the audio bridge declared in glue/audio_driver.hpp.
// Lives in the audio SRAM section so the per-block callback dereferences it
// without paying flash latency.
volatile nisps_firmware::ActiveModeBridge AUDIO_MEM nisps_firmware::g_active_mode_bridge{};
// Global MIDI handle (shared across cores like the legacy entry point did).
std::shared_ptr<MIDIInOut> APP_SRAM g_midi;
// Audio block callback — placed in SRAM via __not_in_flash_func so the audio
// ISR avoids XIP latency. Forwards into the (header-inline) dispatch helper.
void AUDIO_FUNC(audio_block_callback)(
float in[][kBufferSize],
float out[][kBufferSize],
size_t n_channels,
size_t n_frames) {
nisps_firmware::dispatch_audio_block(in, out, n_channels, n_frames);
}
static uint32_t get_rosc_entropy_seed(int bits) {
uint32_t seed = 0;
for (int i = 0; i < bits; ++i) {
busy_wait_us_32(5);
seed <<= 1;
seed |= (rosc_hw->randombit & 1);
}
return seed;
}
// =====================================================================
// Core 0 — UI / hardware polling / ML inference
// =====================================================================
void setup() {
set_sys_clock_khz(AudioDriver::GetSysClockSpeed(), true);
bus_ctrl_hw->priority = BUSCTRL_BUS_PRIORITY_DMA_W_BITS
| BUSCTRL_BUS_PRIORITY_DMA_R_BITS
| BUSCTRL_BUS_PRIORITY_PROC1_BITS;
const uint32_t seed = get_rosc_entropy_seed(32);
srand(seed);
g_mode.ml().seed(static_cast<uint64_t>(seed));
g_mode.ml().draw_weights(g_mode.param_schema().default_spread);
g_midi = std::make_shared<MIDIInOut>();
Serial.begin(115200);
Serial.println("Serial initialised.");
WRITE_VOLATILE(g_serial_ready, true);
MEMLNaut::Initialize();
pinMode(33, OUTPUT);
// Wire hardware → mode I/O channels.
nisps_firmware::wire_inputs(g_mode);
WRITE_VOLATILE(g_iface_ready, true);
Serial.println("Bound peripherals to mode.");
WRITE_VOLATILE(g_core0_ready, true);
while (!READ_VOLATILE(g_core1_ready)) {
MEMORY_BARRIER();
delay(1);
}
MEMLNaut::Instance()->addSystemInfoView();
// Settings menu (e.g. Joystick: Dual/Single for the 4-input modes).
nisps_firmware::wire_settings(g_mode);
Serial.println("Finished initialising core 0.");
}
PERF_DECLARE(MLSTATS);
#define ML_INFERENCE_PERIOD_US 5000
void loop() {
PERIODIC_RUN_US({
PERF_BEGIN(MLSTATS);
g_mode.tick_control();
MEMLNaut::Instance()->loop();
PERF_END(MLSTATS);
}, ML_INFERENCE_PERIOD_US)
PERIODIC_RUN_US({
static size_t blip_counter = 0;
if (blip_counter++ > 10) {
blip_counter = 0;
Serial.println(".");
digitalWrite(33, HIGH);
constexpr float audioHeadroomMul = 1.0f / (1000000.f * 48.0f / kSampleRate);
Serial.printf("ml: %d, aud: %d, q: %f\n",
PERF_GET_MEAN(MLSTATS),
AUDIOLOOP_MEAN,
AUDIOLOOP_MEAN * audioHeadroomMul);
} else {
digitalWrite(33, LOW);
}
}, 100000)
}
// =====================================================================
// Core 1 — real-time audio + MIDI I/O drain
// =====================================================================
void setup1() {
while (!READ_VOLATILE(g_serial_ready)) { MEMORY_BARRIER(); delay(1); }
while (!READ_VOLATILE(g_iface_ready)) { MEMORY_BARRIER(); delay(1); }
if (g_midi) {
g_midi->Setup(/*n_outputs=*/16);
g_midi->SetMIDISendChannel(1);
nisps_firmware::bind_midi_input(g_midi, g_mode);
}
g_mode.setup(static_cast<float>(AudioDriver::GetSampleRate()));
nisps_firmware::register_audio_engine(g_mode, &audio_block_callback);
AudioDriver::Setup();
WRITE_VOLATILE(g_core1_ready, true);
while (!READ_VOLATILE(g_core0_ready)) { MEMORY_BARRIER(); delay(1); }
Serial.println("Finished initialising core 1.");
}
void loop1() {
PERIODIC_RUN_US({
nisps_firmware::drain_outputs(g_midi, g_mode);
}, 1000)
PERIODIC_RUN_US({
if (g_midi) g_midi->Poll();
}, 1000)
}
#else
// =====================================================================
// SelfTest build path — guided hardware self-test rig (no engine / no ML).
// All four entry points delegate into glue/selftest.hpp.
// =====================================================================
#include "glue/selftest.hpp"
void setup() { nisps_firmware::selftest::setup(); }
void loop() { nisps_firmware::selftest::loop(); }
void setup1() { nisps_firmware::selftest::setup1(); }
void loop1() { nisps_firmware::selftest::loop1(); }
#endif // NISPS_SELFTEST