memlnaut-nisps/firmware/MEMLNaut-NISPS/glue/midi_io.hpp

100 lines
3.4 KiB
C++
Raw Normal View History

Stream 6: extract firmware glue under firmware/ Move the Arduino sketch into firmware/MEMLNaut-NISPS/ and bridge the hardware (memllib) to the platform-agnostic nisps/ library through a slim glue layer. Delete the legacy root-level *AudioApp.hpp, modes/MEMLNautMode*.hpp, voicespaces/, IMLInterface.hpp, XiasriAnalysis, and the src/memlp submodule. Glue layout (firmware/MEMLNaut-NISPS/glue/): audio_driver.hpp - bridge memllib block callback to Mode::process via per-Mode templated trampoline (no virtual dispatch) peripherals.hpp - joystick/pots/buttons -> Mode::set_input + ML primitives midi_io.hpp - MIDI in -> mode.note_on/update_bpm/set_playing, drains mode ControlEvent ring -> MIDI UART mode_select.hpp - using-aliases mapping MEMLNautMode<Name> to nisps::modes::*Mode (build script rewrites the #define MEMLNAUT_MODE_TYPE line) input_router.hpp / output_router.hpp - top-level wire/drain entry points The sketch tree uses src/{memllib,daisysp,nisps} symlinks because Arduino-CLI rejects ".." in include paths from sketch-tree headers. mode_select.hpp #undefs Arduino's sq/min/max/abs/round macros before including nisps headers (some nisps engines use those identifiers as method names). The audio bridge struct is extern in the header and defined in the .ino because inline + __not_in_flash section attribute collide at link time. Verification: arduino-cli compile succeeds for PAFSynth, ChannelStrip, and BreakOr (rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3, -std=gnu++20). Host C++ tests under nisps/build still pass (3 binaries, 110+ tests). Build script (scripts/build-firmware.sh) updated to point at the new sketch path; mode-rewrite logic unchanged. Closes meml-gkm.
2026-04-29 16:05:38 +02:00
// firmware/glue/midi_io.hpp — Bridge MIDIInOut <-> Mode.
//
// Two directions:
//
// IN (MIDI bytes from UART → Mode):
// - Note on/off: dispatched to mode.note_on/note_off if the mode
// provides them (PAFSynth, MEMLCelium). Otherwise dropped.
// - BPM (tempo) update: dispatched to mode.update_bpm if available
// (BreakOr, Elysiamorf).
// - Transport (start/stop): dispatched to mode.set_playing if available.
//
// OUT (Mode ControlEvent ring → MIDI bytes):
// - Drained on the firmware loop1() at audio-rate-adjacent cadence.
// - NoteOn/NoteOff/CC/Clock are translated to MIDIInOut queue calls
// and flushed once per drain cycle.
//
// All cross-thread comms uses memllib's existing pico queue (in-bound MIDI
// callbacks fire on core 1; mode events are pushed on the audio thread).
// The mode's RingBuffer is SPSC-safe; we don't need an extra queue here.
#pragma once
#include <Arduino.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include "../src/nisps/modes/base.hpp"
#include "../src/memllib/interface/MIDIInOut.hpp"
namespace nisps_firmware {
// Bind incoming MIDI to the mode. Sets the note + bpm + transport callbacks
// on the MIDIInOut interface. Type-trait dispatch ensures we only attach
// callbacks the mode supports.
template <typename Mode>
inline void bind_midi_input(std::shared_ptr<MIDIInOut> midi, Mode& mode) {
if (!midi) return;
// Note callback — modes that handle notes expose `note_on(byte, byte)`
// and `note_off(byte)`.
midi->SetNoteCallback([&mode](bool note_on, uint8_t note, uint8_t vel) {
if constexpr (requires { mode.note_on(note, vel); }) {
if (note_on) mode.note_on(note, vel);
}
if constexpr (requires { mode.note_off(note); }) {
if (!note_on) mode.note_off(note);
}
});
// BPM updates from MIDI clock.
if constexpr (requires { mode.update_bpm(120.f); }) {
midi->SetBPMCallback([&mode](float bpm) {
mode.update_bpm(bpm);
});
}
// Transport (start/stop).
if constexpr (requires { mode.set_playing(true); }) {
midi->SetTransportCallback([&mode](bool playing) {
mode.set_playing(playing);
});
}
}
// Drain the mode's ControlEvent ring and dispatch each event to MIDI out.
// Called at ~1 kHz from loop1(). Non-blocking; no allocations.
template <typename Mode>
refactor(nisps): delete dead core/ML mass; keep the legacy feedback modes Phase 1 group 2 (L27, L26, L28, S21, L13, ST6, S20). - L27: fixed_buffer.hpp + its test + the CMake entry — no consumers. - L26: dislike_multiplier_ and its doubling/halving bookkeeping — upstream InterfaceRL residue that drove nothing. The audit pointed at the wrong test file for the surviving reference; the actual assert was in test_mlp_geo_dislike.cpp:211, removed here. - L28: added copy_weights_to(std::span<float>) to FixedStorage and DynamicStorage and switched feedback.hpp's take_snapshot/push_undo/nudge to it. Drops the permanent whole-net flat_ scratch buffer from FixedStorage and the per-gesture double copy. Behaviour-identical: same source values, same write order, same RNG draw order in nudge(). - S21 + L13: deleted NISPS_AUDIO_MEM / NISPS_APP_SRAM / NISPS_AUDIO_FUNC — zero use sites outside perf.hpp and comments — and rewrote midi_io.hpp's one misshapen NISPS_AUDIO_FUNC use as a plain `inline void`. perf.hpp now documents only the inlining/hotness macros that actually exist, and audio_driver.hpp no longer claims an SRAM discipline the code never had. - ST6: feedback.hpp's header now describes the four current modes and the Geometric default, dropping the retracted "geometric push NOT ported" claim. S20 — OPERATOR DECISION (§7.1): the four legacy feedback behaviours (RandomiseOutputs, RandomiseMlp, AvoidStyle::Diffuse, the RandomiseMlp branch of on_drag) are KEPT, not deleted. They are wanted as building blocks for experimenting with how different instruments feel under different behaviours. Each is now marked at its definition as deliberately-retained research reserve so future audits stop flagging it as dead code. L25 (the 16 KB firmware loss-history buffer) is NOT done here — see the phase report; it turned out to be coupled into the shared mlp.hpp, and its fate belongs with the browser telemetry build (§7.3 / plan §6.5e). Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:48:27 +02:00
inline void drain_mode_events(std::shared_ptr<MIDIInOut> midi, Mode& mode) {
Stream 6: extract firmware glue under firmware/ Move the Arduino sketch into firmware/MEMLNaut-NISPS/ and bridge the hardware (memllib) to the platform-agnostic nisps/ library through a slim glue layer. Delete the legacy root-level *AudioApp.hpp, modes/MEMLNautMode*.hpp, voicespaces/, IMLInterface.hpp, XiasriAnalysis, and the src/memlp submodule. Glue layout (firmware/MEMLNaut-NISPS/glue/): audio_driver.hpp - bridge memllib block callback to Mode::process via per-Mode templated trampoline (no virtual dispatch) peripherals.hpp - joystick/pots/buttons -> Mode::set_input + ML primitives midi_io.hpp - MIDI in -> mode.note_on/update_bpm/set_playing, drains mode ControlEvent ring -> MIDI UART mode_select.hpp - using-aliases mapping MEMLNautMode<Name> to nisps::modes::*Mode (build script rewrites the #define MEMLNAUT_MODE_TYPE line) input_router.hpp / output_router.hpp - top-level wire/drain entry points The sketch tree uses src/{memllib,daisysp,nisps} symlinks because Arduino-CLI rejects ".." in include paths from sketch-tree headers. mode_select.hpp #undefs Arduino's sq/min/max/abs/round macros before including nisps headers (some nisps engines use those identifiers as method names). The audio bridge struct is extern in the header and defined in the .ino because inline + __not_in_flash section attribute collide at link time. Verification: arduino-cli compile succeeds for PAFSynth, ChannelStrip, and BreakOr (rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3, -std=gnu++20). Host C++ tests under nisps/build still pass (3 binaries, 110+ tests). Build script (scripts/build-firmware.sh) updated to point at the new sketch path; mode-rewrite logic unchanged. Closes meml-gkm.
2026-04-29 16:05:38 +02:00
if (!midi) return;
std::array<::nisps::ControlEvent, 32u> buf{};
const std::size_t n = mode.pop_control_events(std::span<::nisps::ControlEvent>(buf));
for (std::size_t i = 0u; i < n; ++i) {
const auto& e = buf[i];
switch (e.kind) {
case ::nisps::ControlEvent::Kind::NoteOn:
midi->queueNoteOn(e.data1, e.data2);
break;
case ::nisps::ControlEvent::Kind::NoteOff:
midi->queueNoteOff(e.data1, e.data2);
break;
case ::nisps::ControlEvent::Kind::ControlChange:
midi->queueCC(e.data1, e.data2);
break;
case ::nisps::ControlEvent::Kind::Clock:
midi->queueClock();
break;
case ::nisps::ControlEvent::Kind::None:
default:
break;
}
}
if (n > 0u) {
(void)midi->flushQueue();
}
}
} // namespace nisps_firmware