memlnaut-nisps/firmware/MEMLNaut-NISPS/glue/midi_io.hpp
monkey-w1n5t0n e37f16739e 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

99 lines
3.4 KiB
C++

// 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>
inline void drain_mode_events(std::shared_ptr<MIDIInOut> midi, Mode& mode) {
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