From 429da1d1e433b209fa45bb106ca6090ab22fa094 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Wed, 29 Apr 2026 16:26:49 +0300 Subject: [PATCH] feat(nisps/modes): all 8 concrete mode bindings (meml-beb) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One header per mode, each ~50-130 LOC built atop ModeBase: - `paf_synth.hpp` (4 inputs → 33 outputs, 7 voice spaces, note_on/off) - `channel_strip.hpp` (4 → 24, 6 voice spaces) - `xiasri.hpp` (4 → 24, 1 'Direct' voice space) - `verb_fx.hpp` (4 → 47, 12 voice spaces) - `memlcelium.hpp` (4 → 56, dual synth + 2-track sequencer; set_playing/update_bpm) - `breakor.hpp` (4 → 56, 8-track ratio sequencer; pumps engine NoteOn/Off/Clock events into ControlEvent ring buffer) - `elysiamorf.hpp` (4 → 40, 8-track FM-pair MIDI-CC sequencer; pumps CC events) - `sound_analysis_midi.hpp` (10 → 8; owns AnalysisEngine for input feature extraction PLUS NoOpEngine 'thru' for audio passthrough; ML outputs converted to MIDI CC events via on_post_inference; opts out of output→engine routing via ModeRoutesOutputsToEngine specialisation) Every mode satisfies `nisps::Mode` (verified via `static_assert` in each header). Schema `output_size` is verified against engine `param_count()` at compile time inside ModeBase. All hardware-specific glue (MEMLNaut::Instance, pico/util/queue, MIDIInOut, display widgets, button callbacks) is intentionally absent — that lives in firmware/glue and playground/src/modes per architecture.md §4.3. --- nisps/modes/breakor.hpp | 94 ++++++++++++++++++++ nisps/modes/channel_strip.hpp | 56 ++++++++++++ nisps/modes/elysiamorf.hpp | 87 +++++++++++++++++++ nisps/modes/memlcelium.hpp | 66 ++++++++++++++ nisps/modes/paf_synth.hpp | 71 +++++++++++++++ nisps/modes/sound_analysis_midi.hpp | 130 ++++++++++++++++++++++++++++ nisps/modes/verb_fx.hpp | 55 ++++++++++++ nisps/modes/xiasri.hpp | 56 ++++++++++++ 8 files changed, 615 insertions(+) create mode 100644 nisps/modes/breakor.hpp create mode 100644 nisps/modes/channel_strip.hpp create mode 100644 nisps/modes/elysiamorf.hpp create mode 100644 nisps/modes/memlcelium.hpp create mode 100644 nisps/modes/paf_synth.hpp create mode 100644 nisps/modes/sound_analysis_midi.hpp create mode 100644 nisps/modes/verb_fx.hpp create mode 100644 nisps/modes/xiasri.hpp diff --git a/nisps/modes/breakor.hpp b/nisps/modes/breakor.hpp new file mode 100644 index 0000000..aaf444a --- /dev/null +++ b/nisps/modes/breakor.hpp @@ -0,0 +1,94 @@ +// nisps/modes/breakor.hpp — Break|| 8-track ratio sequencer mode binding. +// +// 4 abstract input channels → MLP[4, 10, 14, 18, 56] → BreakOrEngine. +// process() returns silence; the engine queues NoteOn/NoteOff/Clock events +// each tick. The mode forwards them through its ControlEvent ring buffer +// for the platform glue (firmware MIDI/I2C, browser WebMIDI) to drain. + +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/perf.hpp" +#include "../core/types.hpp" +#include "../engines/breakor.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/breakor_schema.hpp" + +namespace nisps::modes { + +class BreakOrMode : public ModeBase< + BreakOrMode, + BreakOrEngine, + ml::MLP<4u, 10u, 14u, 18u, 56u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kBreakorModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + NISPS_FORCE_INLINE void set_playing(bool playing) noexcept { + engine_.set_playing(playing); + } + NISPS_FORCE_INLINE void update_bpm(float bpm) noexcept { + engine_.update_bpm(bpm); + } + NISPS_FORCE_INLINE void set_track_note(std::size_t track, std::uint8_t note) noexcept { + engine_.set_track_note(track, note); + } + + // Drains queued engine events, repackages them as ControlEvent and pushes + // them into the mode's event ring. Called by hardware glue from the + // audio thread (or right after process()) to forward MIDI/I2C output. + void pump_engine_events() noexcept { + std::array buf; + const std::size_t n = engine_.pop_events(std::span(buf)); + for (std::size_t i = 0u; i < n; ++i) { + const auto& e = buf[i]; + ControlEvent ce{}; + switch (e.kind) { + case BreakOrEngine::EventKind::NoteOn: + ce.kind = ControlEvent::Kind::NoteOn; break; + case BreakOrEngine::EventKind::NoteOff: + ce.kind = ControlEvent::Kind::NoteOff; break; + case BreakOrEngine::EventKind::Clock: + ce.kind = ControlEvent::Kind::Clock; break; + } + ce.channel = e.track; + ce.data1 = e.midi_note; + ce.data2 = e.velocity; + (void)push_control_event(ce); + } + } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kBreakorModeId, + generated::kBreakorEngineId, + std::span(generated::kBreakorInputChannels), + generated::kBreakorMLConfig.input_size, + std::span(generated::kBreakorHiddenLayers), + generated::kBreakorMLConfig.output_size, + generated::kBreakorMLConfig.default_spread, + generated::kBreakorMLConfig.default_learning_rate, + generated::kBreakorMLConfig.default_max_iterations, + std::span(generated::kBreakorParams), + std::span(generated::kBreakorVoiceSpaces), + generated::kBreakorUI, + }; +}; + +static_assert(Mode, "BreakOrMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/channel_strip.hpp b/nisps/modes/channel_strip.hpp new file mode 100644 index 0000000..c7ab6de --- /dev/null +++ b/nisps/modes/channel_strip.hpp @@ -0,0 +1,56 @@ +// nisps/modes/channel_strip.hpp — Channel-strip mode binding. +// +// 4 abstract input channels → MLP[4, 10, 10, 14, 24] → ChannelStripEngine +// (HPF/LPF + 2× peak EQ + low/high shelf + compressor + tanh saturation), +// dispatched through one of 6 console-emulation voice spaces. + +#pragma once + +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/types.hpp" +#include "../engines/channel_strip.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/channel_strip_schema.hpp" + +namespace nisps::modes { + +class ChannelStripMode : public ModeBase< + ChannelStripMode, + ChannelStripEngine, + ml::MLP<4u, 10u, 10u, 14u, 24u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kChannelStripModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kChannelStripModeId, + generated::kChannelStripEngineId, + std::span(generated::kChannelStripInputChannels), + generated::kChannelStripMLConfig.input_size, + std::span(generated::kChannelStripHiddenLayers), + generated::kChannelStripMLConfig.output_size, + generated::kChannelStripMLConfig.default_spread, + generated::kChannelStripMLConfig.default_learning_rate, + generated::kChannelStripMLConfig.default_max_iterations, + std::span(generated::kChannelStripParams), + std::span(generated::kChannelStripVoiceSpaces), + generated::kChannelStripUI, + }; +}; + +static_assert(Mode, "ChannelStripMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/elysiamorf.hpp b/nisps/modes/elysiamorf.hpp new file mode 100644 index 0000000..a46e1fc --- /dev/null +++ b/nisps/modes/elysiamorf.hpp @@ -0,0 +1,87 @@ +// nisps/modes/elysiamorf.hpp — Elysiamorf 8-track FM-pair MIDI-CC sequencer +// mode binding. +// +// 4 abstract input channels → MLP[4, 10, 14, 18, 40] → ElysiamorfEngine. +// process() returns silence and emits CC + Clock events each tick. The +// mode bridges them into its ControlEvent ring buffer. + +#pragma once + +#include +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/perf.hpp" +#include "../core/types.hpp" +#include "../engines/elysiamorf.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/elysiamorf_schema.hpp" + +namespace nisps::modes { + +class ElysiamorfMode : public ModeBase< + ElysiamorfMode, + ElysiamorfEngine, + ml::MLP<4u, 10u, 14u, 18u, 40u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kElysiamorfModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + NISPS_FORCE_INLINE void set_playing(bool playing) noexcept { + engine_.set_playing(playing); + } + NISPS_FORCE_INLINE void update_bpm(float bpm) noexcept { + engine_.update_bpm(bpm); + } + + void pump_engine_events() noexcept { + std::array buf; + const std::size_t n = engine_.pop_events(std::span(buf)); + for (std::size_t i = 0u; i < n; ++i) { + const auto& e = buf[i]; + ControlEvent ce{}; + switch (e.kind) { + case ElysiamorfEngine::EventKind::CC: + ce.kind = ControlEvent::Kind::ControlChange; + break; + case ElysiamorfEngine::EventKind::Clock: + ce.kind = ControlEvent::Kind::Clock; + break; + } + ce.channel = 0u; // single output channel for now + ce.data1 = e.cc_number; + ce.data2 = e.cc_value; + (void)push_control_event(ce); + } + } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kElysiamorfModeId, + generated::kElysiamorfEngineId, + std::span(generated::kElysiamorfInputChannels), + generated::kElysiamorfMLConfig.input_size, + std::span(generated::kElysiamorfHiddenLayers), + generated::kElysiamorfMLConfig.output_size, + generated::kElysiamorfMLConfig.default_spread, + generated::kElysiamorfMLConfig.default_learning_rate, + generated::kElysiamorfMLConfig.default_max_iterations, + std::span(generated::kElysiamorfParams), + std::span(generated::kElysiamorfVoiceSpaces), + generated::kElysiamorfUI, + }; +}; + +static_assert(Mode, "ElysiamorfMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/memlcelium.hpp b/nisps/modes/memlcelium.hpp new file mode 100644 index 0000000..94329ed --- /dev/null +++ b/nisps/modes/memlcelium.hpp @@ -0,0 +1,66 @@ +// nisps/modes/memlcelium.hpp — MEMLCelium dual-voice synth + 2-track ratio +// sequencer mode binding. +// +// 4 abstract input channels → MLP[4, 10, 14, 18, 56] → MEMLCeliumEngine. +// The engine handles sequencer ticks internally inside process(); the mode +// surface adds set_playing()/update_bpm() helpers for platform glue. + +#pragma once + +#include +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/perf.hpp" +#include "../core/types.hpp" +#include "../engines/memlcelium.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/memlcelium_schema.hpp" + +namespace nisps::modes { + +class MEMLCeliumMode : public ModeBase< + MEMLCeliumMode, + MEMLCeliumEngine, + ml::MLP<4u, 10u, 14u, 18u, 56u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kMemlceliumModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + NISPS_FORCE_INLINE void set_playing(bool playing) noexcept { + engine_.set_playing(playing); + } + NISPS_FORCE_INLINE void update_bpm(float bpm) noexcept { + engine_.update_bpm(bpm); + } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kMemlceliumModeId, + generated::kMemlceliumEngineId, + std::span(generated::kMemlceliumInputChannels), + generated::kMemlceliumMLConfig.input_size, + std::span(generated::kMemlceliumHiddenLayers), + generated::kMemlceliumMLConfig.output_size, + generated::kMemlceliumMLConfig.default_spread, + generated::kMemlceliumMLConfig.default_learning_rate, + generated::kMemlceliumMLConfig.default_max_iterations, + std::span(generated::kMemlceliumParams), + std::span(generated::kMemlceliumVoiceSpaces), + generated::kMemlceliumUI, + }; +}; + +static_assert(Mode, "MEMLCeliumMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/paf_synth.hpp b/nisps/modes/paf_synth.hpp new file mode 100644 index 0000000..f479e0b --- /dev/null +++ b/nisps/modes/paf_synth.hpp @@ -0,0 +1,71 @@ +// nisps/modes/paf_synth.hpp — PAF Synth mode binding. +// +// 4 abstract input channels (joy_x, joy_y, joy_z, joy_w) → MLP[4, 10, 10, 14, 33] +// → PAFSynthEngine.set_params() through the active voice space. +// +// Note triggering (note_on/note_off) lives in the engine; the mode exposes +// thin wrappers so platform glue can route incoming MIDI / touch events. +// No hardware-specific code; firmware glue and browser glue both call the +// same surface. + +#pragma once + +#include +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/perf.hpp" +#include "../core/types.hpp" +#include "../engines/paf_synth.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/paf_synth_schema.hpp" + +namespace nisps::modes { + +class PAFSynthMode : public ModeBase< + PAFSynthMode, + PAFSynthEngine, + ml::MLP<4u, 10u, 10u, 14u, 33u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + // ---- Concept-required statics ---- + static constexpr std::string_view mode_id() noexcept { + return generated::kPafSynthModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + // ---- Mode-specific control glue ---- + NISPS_FORCE_INLINE void note_on(std::uint8_t note, std::uint8_t velocity) noexcept { + engine_.note_on(note, velocity); + } + NISPS_FORCE_INLINE void note_off(std::uint8_t note) noexcept { + engine_.note_off(note); + } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kPafSynthModeId, + generated::kPafSynthEngineId, + std::span(generated::kPafSynthInputChannels), + generated::kPafSynthMLConfig.input_size, + std::span(generated::kPafSynthHiddenLayers), + generated::kPafSynthMLConfig.output_size, + generated::kPafSynthMLConfig.default_spread, + generated::kPafSynthMLConfig.default_learning_rate, + generated::kPafSynthMLConfig.default_max_iterations, + std::span(generated::kPafSynthParams), + std::span(generated::kPafSynthVoiceSpaces), + generated::kPafSynthUI, + }; +}; + +static_assert(Mode, "PAFSynthMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/sound_analysis_midi.hpp b/nisps/modes/sound_analysis_midi.hpp new file mode 100644 index 0000000..56485c7 --- /dev/null +++ b/nisps/modes/sound_analysis_midi.hpp @@ -0,0 +1,130 @@ +// nisps/modes/sound_analysis_midi.hpp — Sound-analysis-driven MIDI CC mode. +// +// This mode is structurally different from the synth modes: +// - Audio engine = NoOpEngine (engine_id "thru"). Audio output is silent +// by spec; the firmware version forwards audio through unchanged but +// for clean-slate parity we use NoOp. Platform glue can replace this +// with a real thru if it wants the input mirrored. +// - Analysis engine = AnalysisEngine. Runs feature extraction on the +// incoming audio in process() and exposes 6 features. +// - ML inputs (10) = 6 analysis features + 4 abstract joystick channels. +// - ML outputs (8) = MIDI CC values pushed into the mode's ControlEvent +// ring buffer for hardware glue to dispatch. +// +// Because outputs aren't routed to engine params we set +// `kRouteOutputsToEngine = false` so ModeBase skips the size-match assert. + +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/perf.hpp" +#include "../core/types.hpp" +#include "../engines/analysis.hpp" +#include "../engines/base.hpp" // NoOpEngine +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/sound_analysis_midi_schema.hpp" + +namespace nisps::modes { + +class SoundAnalysisMIDIMode; + +} // namespace nisps::modes + +namespace nisps { +// Specialise: this mode does not route ML outputs to engine params. +template <> +struct ModeRoutesOutputsToEngine : std::false_type {}; +} // namespace nisps + +namespace nisps::modes { + +class SoundAnalysisMIDIMode : public ModeBase< + SoundAnalysisMIDIMode, + NoOpEngine, + ml::MLP<10u, 10u, 10u, 14u, 8u>, + 10u> { + public: + using Base = ModeBase, 10u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kSoundAnalysisMidiModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + void on_setup(float sample_rate) noexcept { + analysis_.setup(sample_rate); + } + + // Audio path tap — modes call this from the audio thread (or the + // platform glue does after process()) to feed the analyser. Distinct + // from the audio engine's process() which is the silent passthrough. + NISPS_HOT NISPS_FORCE_INLINE void analyse(stereosample_t x) noexcept { + (void)analysis_.process(x); + } + + // Splice latest analysis features into the first 6 input channels just + // before the MLP runs. Joystick channels (idx 6..9) are untouched. + void on_pre_inference() noexcept { + std::array feats{}; + analysis_.copy_features(std::span(feats)); + auto chans = mutable_input_channels(); + for (std::size_t i = 0u; i < AnalysisEngine::kNFeatures; ++i) { + chans[i] = feats[i]; + } + } + + // After ML inference, scale the 8 sigmoid outputs to MIDI CC values + // (CC 0..7) and queue them as ControlEvents. + void on_post_inference() noexcept { + const auto outs = ml_.outputs(); + for (std::size_t i = 0u; i < kCCCount && i < outs.size(); ++i) { + float v = outs[i]; + if (v < 0.f) v = 0.f; + else if (v > 1.f) v = 1.f; + const std::uint8_t cc_value = static_cast(v * 127.f + 0.5f); + ControlEvent ce{}; + ce.kind = ControlEvent::Kind::ControlChange; + ce.channel = 0u; + ce.data1 = static_cast(i); // CC number + ce.data2 = cc_value; + (void)push_control_event(ce); + } + } + + AnalysisEngine& analysis() noexcept { return analysis_; } + const AnalysisEngine& analysis() const noexcept { return analysis_; } + + private: + static constexpr std::size_t kCCCount = 8u; + + AnalysisEngine analysis_{}; + + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kSoundAnalysisMidiModeId, + generated::kSoundAnalysisMidiEngineId, + std::span(generated::kSoundAnalysisMidiInputChannels), + generated::kSoundAnalysisMidiMLConfig.input_size, + std::span(generated::kSoundAnalysisMidiHiddenLayers), + generated::kSoundAnalysisMidiMLConfig.output_size, + generated::kSoundAnalysisMidiMLConfig.default_spread, + generated::kSoundAnalysisMidiMLConfig.default_learning_rate, + generated::kSoundAnalysisMidiMLConfig.default_max_iterations, + std::span(generated::kSoundAnalysisMidiParams), + std::span(generated::kSoundAnalysisMidiVoiceSpaces), + generated::kSoundAnalysisMidiUI, + }; +}; + +static_assert(Mode, + "SoundAnalysisMIDIMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/verb_fx.hpp b/nisps/modes/verb_fx.hpp new file mode 100644 index 0000000..fbc981d --- /dev/null +++ b/nisps/modes/verb_fx.hpp @@ -0,0 +1,55 @@ +// nisps/modes/verb_fx.hpp — VerbFX mode binding. +// +// 4 abstract input channels → MLP[4, 10, 14, 18, 47] → VerbFXEngine. +// 12 voice-space presets curated for various reverb characters. + +#pragma once + +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/types.hpp" +#include "../engines/verb_fx.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/verb_fx_schema.hpp" + +namespace nisps::modes { + +class VerbFXMode : public ModeBase< + VerbFXMode, + VerbFXEngine, + ml::MLP<4u, 10u, 14u, 18u, 47u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kVerbFxModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kVerbFxModeId, + generated::kVerbFxEngineId, + std::span(generated::kVerbFxInputChannels), + generated::kVerbFxMLConfig.input_size, + std::span(generated::kVerbFxHiddenLayers), + generated::kVerbFxMLConfig.output_size, + generated::kVerbFxMLConfig.default_spread, + generated::kVerbFxMLConfig.default_learning_rate, + generated::kVerbFxMLConfig.default_max_iterations, + std::span(generated::kVerbFxParams), + std::span(generated::kVerbFxVoiceSpaces), + generated::kVerbFxUI, + }; +}; + +static_assert(Mode, "VerbFXMode must satisfy nisps::Mode"); + +} // namespace nisps::modes diff --git a/nisps/modes/xiasri.hpp b/nisps/modes/xiasri.hpp new file mode 100644 index 0000000..fa3d85c --- /dev/null +++ b/nisps/modes/xiasri.hpp @@ -0,0 +1,56 @@ +// nisps/modes/xiasri.hpp — XIASRI FX mode binding. +// +// 4 abstract input channels → MLP[4, 10, 10, 14, 24] → XIASRIEngine +// (pitch-shift, allpass/comb reverb, 4-tap delay). Single voice space +// ("Direct") because the firmware engine ignores its voice-space slot. + +#pragma once + +#include +#include +#include + +#include "../core/concepts.hpp" +#include "../core/types.hpp" +#include "../engines/xiasri.hpp" +#include "../ml/mlp.hpp" +#include "base.hpp" +#include "generated/xiasri_schema.hpp" + +namespace nisps::modes { + +class XIASRIMode : public ModeBase< + XIASRIMode, + XIASRIEngine, + ml::MLP<4u, 10u, 10u, 14u, 24u>, + 4u> { + public: + using Base = ModeBase, 4u>; + using Base::Base; + + static constexpr std::string_view mode_id() noexcept { + return generated::kXiasriModeId; + } + static constexpr const ParamSchema& param_schema() noexcept { return kSchema; } + + private: + static inline constexpr ParamSchema kSchema = ParamSchema{ + generated::kXiasriModeId, + generated::kXiasriEngineId, + std::span(generated::kXiasriInputChannels), + generated::kXiasriMLConfig.input_size, + std::span(generated::kXiasriHiddenLayers), + generated::kXiasriMLConfig.output_size, + generated::kXiasriMLConfig.default_spread, + generated::kXiasriMLConfig.default_learning_rate, + generated::kXiasriMLConfig.default_max_iterations, + std::span(generated::kXiasriParams), + std::span(generated::kXiasriVoiceSpaces), + generated::kXiasriUI, + }; +}; + +static_assert(Mode, "XIASRIMode must satisfy nisps::Mode"); + +} // namespace nisps::modes