memlnaut-nisps/firmware/MEMLNaut-NISPS/glue/midi_io.hpp
monkey-w1n5t0n 68d4cc4017 build(firmware): migrate to PlatformIO and vendor memllib (plan §5)
One cut, no dual path. Closes ALIGNMENT defect 3 ("Arduino-CLI build
machinery is actively hostile") and vision bullet 4.

platformio.ini carries 16 [env:], one per variant, each passing
-DMEMLNAUT_MODE_TYPE; selftest passes -DNISPS_SELFTEST=1 instead. The env list
IS the registry now — the .ino comment-registry and the NISPS_ST_* token-paste
table are deleted rather than migrated. L12 noted that table was already
silently missing the currently-shipped SLPWorkshop variant, which is the whole
argument against having a second list.

Also deleted: the Python/sed machinery that rewrote the COMMITTED .ino on every
build, the sketch symlink forest, the global TFT_eSPI User_Setup.h mutation
(now -D flags — TFT_eSPI's own documented PlatformIO recipe), the UF2
boot-mount detection stack (upload_protocol=picotool talks to the bootloader
directly), and build-firmware-arch.sh entirely. Scripts 683 -> 435 lines.

memllib is vendored at lib/memllib/ from upstream e291192; no submodules
remain. VENDORED.md records provenance and the re-sync procedure.

S9: a firmware-build CI job compiles three representative envs against a cached
toolchain and reports per-variant flash/RAM. Firmware is in an automated gate
for the FIRST time. The old ci.yml comment justified excluding it as "low
verification value" — an assessment that did not survive contact, since the
SelfTest variant sat broken for an unknown period calling a DisplayDriver
method that did not exist at the pinned memllib commit, and nothing noticed
because nothing built it.

Verified: all 16 envs build from an empty cache, each within ~520 bytes of the
arduino-cli binary it replaces, flash and RAM. Measured as .text+.rodata /
.data+.bss+vector+uninitialized — NOT PlatformIO's console line, which
double-counts .data on this board. This does not prove the hardware boots; no
flash+smoke test was possible and that stays an operator chokepoint.

  slpworkshop 248232/145028   pafsynth 256880/149716   selftest 216228/17960
  (all 16 in the CI log format; none exceeds 2% of a 16 MB flash)

Two traps recorded so nobody rediscovers them: vendoring memllib's subdirs
without a src/ wrapper makes PlatformIO's library builder silently compile
NOTHING while still linking; and project build_flags land BEFORE the
framework's own -std=gnu++17 -Os, so build_unflags is required.

CORRECTION carried in this commit: the firmware sizes in c19d846's message and
the first version of the memllib recon doc were wrong — SLPWorkshop 145348,
PAFSynth 145300, SelfTest 141840. They came from building variants in sequence
through a SHARED incremental arduino-cli build directory, which reused stale
objects and under-reported by ~75 KB. Clean-cache rebuilds of the identical
commit give 216736/18492 for SelfTest. The real cost of the memllib upstream
bump is +216 bytes flash, not +316. Never measure firmware size through a
reused build dir.

HISTORY NOTE: this commit and the docs commit before it were rebuilt (force-push,
2026-07-21) so that each contains only what its message describes. The first
versions had the firmware deletions stranded in the docs commit by a shared-index
race between concurrent agents; content is byte-identical to the originals.

Gates: run-all-tests.sh ALL GREEN (nisps/ untouched by this change beyond
include paths); 16/16 pio envs build.
2026-07-21 20:17:58 +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 "nisps/modes/base.hpp"
#include "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