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.
101 lines
3.5 KiB
C++
101 lines
3.5 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/core/perf.hpp"
|
|
#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>
|
|
NISPS_AUDIO_FUNC(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
|