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

113 lines
4.5 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/audio_driver.hpp — Bridge memllib AudioDriver to nisps modes.
//
// memllib's AudioDriver delivers samples via a *block* callback shaped like:
// void cb(float in[][kBufferSize], float out[][kBufferSize], size_t nch, size_t nf)
// The nisps `Mode` concept exposes a per-sample `process(stereosample_t)`.
//
// We register a free function as the AudioDriver block callback and pump it
// frame-by-frame into the active mode. The mode pointer is held in a
// templated free function that captures it by reference at call-site (so we
// avoid std::function indirection in the audio path).
//
// The two `stereosample_t` types — the pre-existing firmware POD and the new
// nisps namespaced one — have identical memory layout (two floats L,R) so the
// bridge does an explicit field-wise copy. No reinterpret_cast.
#pragma once
#include <Arduino.h>
#include "../src/nisps/core/perf.hpp"
#include "../src/nisps/core/types.hpp"
#include "../src/memllib/audio/AudioDriver.hpp"
namespace nisps_firmware {
// Pointer to the active mode. The audio block callback reads through this.
// Set during setup1() before AudioDriver::Setup() is called. Marked
// `__not_in_flash("audio")` so the audio ISR path does not pay flash latency.
struct ActiveModeBridge {
void* mode_ptr = nullptr; // type-erased Mode*
void (*process_block)(void*,
float[][kBufferSize],
float[][kBufferSize],
size_t, size_t) = nullptr;
};
// Defined in the .ino (not inline-in-header) — `inline` and `__not_in_flash`
// section attributes don't combine cleanly: comdat groups want shared
// linkage, named sections want unique ownership. The .ino owns the
// definition.
extern volatile ActiveModeBridge AUDIO_MEM g_active_mode_bridge;
// Templated trampoline: instantiated once per concrete Mode type. It does the
// per-sample loop and the field-wise stereosample_t copy. Templated rather
// than virtual because the audio path forbids virtual dispatch (architecture
// §3.5).
//
// We can't use `__not_in_flash_func(name)` on a templated function because
// that macro stringifies the function name into the section attribute, and
// the template instantiations all share a single section name — collisions
// are benign at link time but a per-instantiation section would be cleaner.
// Use `__attribute__((hot))` instead for these templated trampolines and
// rely on the platform linker default for placement.
template <typename Mode>
__attribute__((hot)) static void process_block_typed(
void* mode_ptr,
float in[][kBufferSize],
float out[][kBufferSize],
size_t /*n_channels*/,
size_t n_frames) {
auto* mode = static_cast<Mode*>(mode_ptr);
for (size_t i = 0; i < n_frames; ++i) {
::stereosample_t fw_in{in[0][i], in[1][i]};
::nisps::stereosample_t in_ns{fw_in.L, fw_in.R};
const auto out_ns = mode->process(in_ns);
out[0][i] = out_ns.L;
out[1][i] = out_ns.R;
// Modes that override `analyse(stereosample_t)` (e.g. SoundAnalysisMIDI)
// see the input frame here, before the next sample.
if constexpr (requires { mode->analyse(in_ns); }) {
mode->analyse(in_ns);
}
}
}
// Inner forward — the .ino provides the actual block callback (with
// `__not_in_flash_func` placement) and forwards into this. Keeping the
// non-template body in a non-inline function avoids comdat / section
// conflicts.
inline void dispatch_audio_block(
float in[][kBufferSize],
float out[][kBufferSize],
size_t n_channels,
size_t n_frames) {
auto& bridge = const_cast<ActiveModeBridge&>(g_active_mode_bridge);
if (bridge.mode_ptr == nullptr || bridge.process_block == nullptr) {
for (size_t i = 0; i < n_frames; ++i) {
out[0][i] = 0.f;
out[1][i] = 0.f;
}
return;
}
bridge.process_block(bridge.mode_ptr, in, out, n_channels, n_frames);
}
// Public registration: call from setup1() with the active mode and the
// `__not_in_flash_func`-placed block callback that forwards into
// `dispatch_audio_block`.
template <typename Mode>
inline void register_audio_engine(Mode& mode, audiocallback_block_fptr_t block_cb) {
ActiveModeBridge b{};
b.mode_ptr = static_cast<void*>(&mode);
b.process_block = &process_block_typed<Mode>;
__sync_synchronize();
const_cast<ActiveModeBridge&>(g_active_mode_bridge) = b;
__sync_synchronize();
AudioDriver::SetBlockCallback(block_cb);
}
} // namespace nisps_firmware