memlnaut-nisps/firmware/MEMLNaut-NISPS/glue/audio_driver.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

112 lines
4.5 KiB
C++

// 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