memlnaut-nisps/nisps/modes/external_synth_midi.hpp

197 lines
8.7 KiB
C++
Raw Normal View History

// nisps/modes/external_synth_midi.hpp — Drive an external MIDI synth via CC.
//
// A platform-agnostic mode that maps the abstract joystick inputs through the
// MLP to MIDI Control Change messages for an EXTERNAL hardware synth, using a
// compile-time-selected device template from nisps/midi/generated/midi_devices.hpp
// (the same templates the Manifold browser picker uses).
//
// - Audio engine = NoOpEngine (engine_id "thru"); no audio is produced.
// - ML inputs (4) = the abstract joystick channels (joy_x/y/z/w).
// - ML outputs (NOut) = MIDI CC values pushed into the ControlEvent ring for
// the firmware glue to dispatch (output_router -> midi_io::queueCC).
// - Device is chosen at COMPILE TIME (one flashable variant per synth, e.g.
// MEMLNautModeExtSynthSub37). Which of the device's params occupy the NOut
// output slots is chosen by `pick_cc_slots` (prefers musical params over
// Bank Select / global housekeeping CCs). A runtime `enabled_` mask can
// mute individual slots (set_param_enabled); rich per-param selection is the
// browser's job — the hardware drives this curated subset.
//
// `kRouteOutputsToEngine = false` (specialised below) so ModeBase skips routing
// ML outputs into engine params, exactly like SoundAnalysisMIDIMode.
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
//
// NOT folded into the schemas/modes/*.json codegen pipeline (L11, one-core
// simplification 2026-07): that pipeline emits ONE static schema per JSON
// file, but this "mode" is a C++ template family over an externally supplied
// `Device` (one of the nisps/midi/generated device templates) and an output
// count `NOut` — there is no single (device, NOut) pair to author a JSON
// schema for, and its per-output "params" aren't independently named at all;
// they're whichever NOut slots `pick_cc_slots` selects from `Device.params`,
// which is ITSELF already schema-generated (codegen/generate-midi-devices.ts,
// schemas/midi_devices/). Folding would mean inventing a new
// device-templated schema kind for a single, already-degenerate consumer —
// not a behaviour-preserving consolidation. Instead: the net-shape + ML
// defaults this mode needs (identical to every other 4-joystick-input mode —
// see e.g. schemas/modes/memlcelium.json) are named ONCE below
// (`ext_synth_defaults`), so the `MLP<>` shape and the `ParamSchema` instance
// both read from the same place instead of re-typing the same literals.
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string_view>
#include "../core/concepts.hpp"
#include "../core/perf.hpp"
#include "../core/types.hpp"
#include "../engines/base.hpp" // NoOpEngine
#include "../midi/generated/midi_devices.hpp"
#include "../ml/mlp.hpp"
#include "base.hpp"
namespace nisps::modes {
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
// Shared net-shape + ML defaults for every ExternalSynthMIDIMode
// instantiation (see the file-header comment for why these are hand-named
// rather than codegen'd): 4 joystick inputs -> [10,14,18] hidden -> NOut
// (device-CC-count-driven) outputs, matching every other 4-joystick-input
refactor(ml): one runtime-configurable training default (S26) The operator's call: "there should be one default learning rate and one default max iterations and they should both be configurable at runtime." There were SIX copies, not the four the audit described, and they did not agree: nisps/ml/mlp.hpp no-arg train() hardcoding 1.f / 1000u / 0.001f — and firmware's ONLY training path calls exactly this, so firmware had no runtime knob at all wasm-iml.ts train() and trainAsync() TS default params (x2) engine-api.ts learningRate ?? 1.0, with no maxIterations knob vcv/src/iml.hpp 200 / 0.1 / 0.00001 — silently divergent external_synth_midi.hpp its own kDefaultLearningRate/kDefaultMaxIterations schemas/modes/*.json x9, identical, read by nobody at runtime Now: schemas/ml_defaults.json is the single declaration (validated against a sibling meta-schema, matching the midi_device.schema.json convention), codegen emits it to C++ and TS in the same run, and MLPCore carries a TrainConfig whose default member initialisers read the generated constant. set_train_config()/nisps_ml_set_train_config() make it runtime-overridable on every target; the explicit-argument train() overload is untouched. min_error joins the tuple — it was duplicated identically and belongs with the other two. The per-mode ml block loses default_learning_rate/default_max_iterations. default_spread stays (genuinely wired on both targets) and input_channels stays (codegen-time validated, real information for sound_analysis_midi). VCV BEHAVIOUR CHANGE, deliberate: MEMLNaut.cpp constructs IML positionally and relies on those defaults, so the module moves to 1000/1.0/0.001 — 5x the max iterations, 10x the learning rate, and a 100x looser early-stop threshold. The old values were never justified anywhere; they arrived with fbc68eb alongside an unrelated module rewrite and no tuning rationale. Firmware and WASM have shipped 1.0/1000 all along. It is now runtime-settable if this turns out worse. The generated header lands in nisps/ml/generated/, not nisps/modes/generated/ where the rest of codegen output lives: training hyperparameters are an ML fact, and nisps/ml sits below nisps/modes, so emitting them there would make mlp.hpp include upward. The agent that built this flagged the directory-crossing rather than hiding it; this is the fix. CI's generated-freshness gate learns the new directory. Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS (max delta 2.38e-7), lint clean, manifold typecheck + 17 unit + 33 e2e (which exercise train() and trainAsync() through a real browser).
2026-07-21 17:20:10 +02:00
// mode's schema default_spread (0.6). The training hyperparameter defaults
// (learning_rate/max_iterations/min_error) are no longer part of
// `ParamSchema` at all (S26) — they live once, globally, in
// `nisps::ml::generated::kMlTrainDefaults` (nisps/ml/generated/
// ml_defaults.hpp) and are wired via `nisps::ml::MLPCore::TrainConfig`'s
// default member initialisers instead.
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
namespace ext_synth_defaults {
inline constexpr std::size_t kInputSize = 4u;
inline constexpr std::array<std::string_view, 4> kInputChannels{
std::string_view{"joy_x"}, std::string_view{"joy_y"},
std::string_view{"joy_z"}, std::string_view{"joy_w"}};
inline constexpr std::array<std::size_t, 3> kHiddenLayers{10u, 14u, 18u};
inline constexpr float kDefaultSpread = 0.6f;
} // namespace ext_synth_defaults
// The net-shape alias every instantiation uses (mirrors S6/S25's per-mode
// `<Mode>MLP` codegen alias, generalised to a template parameter since NOut
// varies per device/output-count combination).
template <std::size_t NOut>
using ExtSynthMIDIMLP = ::nisps::ml::MLP<
ext_synth_defaults::kInputSize,
ext_synth_defaults::kHiddenLayers[0],
ext_synth_defaults::kHiddenLayers[1],
ext_synth_defaults::kHiddenLayers[2],
NOut>;
template <const ::nisps::midi::generated::MidiDevice& Device, std::size_t NOut>
class ExternalSynthMIDIMode;
// Compile-time slot picker: choose which NOut device params the MLP drives.
// Prefers "musical" params (any group except "global"); fills any remaining
// slots from the leftover (global) params. Deterministic, zero runtime cost.
template <const ::nisps::midi::generated::MidiDevice& Device, std::size_t NOut>
consteval std::array<std::uint8_t, NOut> pick_cc_slots() {
std::array<std::uint8_t, NOut> slots{};
std::size_t n = 0u;
for (std::size_t i = 0u; i < Device.params.size() && n < NOut; ++i) {
if (Device.params[i].group != std::string_view{"global"}) {
slots[n++] = static_cast<std::uint8_t>(i);
}
}
for (std::size_t i = 0u; i < Device.params.size() && n < NOut; ++i) {
if (Device.params[i].group == std::string_view{"global"}) {
slots[n++] = static_cast<std::uint8_t>(i);
}
}
return slots;
}
} // namespace nisps::modes
namespace nisps {
// Specialise: this mode does not route ML outputs to engine params.
template <const ::nisps::midi::generated::MidiDevice& D, std::size_t N>
struct ModeRoutesOutputsToEngine<modes::ExternalSynthMIDIMode<D, N>> : std::false_type {};
} // namespace nisps
namespace nisps::modes {
template <const ::nisps::midi::generated::MidiDevice& Device, std::size_t NOut = 8u>
class ExternalSynthMIDIMode : public ModeBase<
ExternalSynthMIDIMode<Device, NOut>,
NoOpEngine,
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
ExtSynthMIDIMLP<NOut>,
4u> {
static_assert(Device.params.size() >= NOut,
"device template has fewer params than the mode's output count");
public:
using Base = ModeBase<ExternalSynthMIDIMode<Device, NOut>, NoOpEngine,
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
ExtSynthMIDIMLP<NOut>, 4u>;
using Base::Base;
static constexpr std::string_view mode_id() noexcept { return Device.device_id; }
static constexpr const ParamSchema& param_schema() noexcept { return kSchema; }
// Map the NOut MLP outputs onto the selected device CCs and queue them.
void on_post_inference() noexcept {
const auto outs = this->ml_.outputs();
const std::uint8_t ch =
Device.default_channel > 0u ? static_cast<std::uint8_t>(Device.default_channel - 1u) : 0u;
for (std::size_t i = 0u; i < NOut && i < outs.size(); ++i) {
if (!enabled_[i]) continue;
const auto& p = Device.params[kSlots[i]];
float v = outs[i];
if (v < 0.f) v = 0.f;
else if (v > 1.f) v = 1.f;
const float lo = static_cast<float>(p.min);
const float hi = static_cast<float>(p.max);
ControlEvent ce{};
ce.kind = ControlEvent::Kind::ControlChange;
ce.channel = ch;
ce.data1 = p.cc;
ce.data2 = static_cast<std::uint8_t>(lo + v * (hi - lo) + 0.5f);
(void)this->push_control_event(ce);
}
}
// Runtime mute of an individual output slot (hardware has no rich selector;
// this is the hook a future TFT menu / glue would drive).
void set_param_enabled(std::size_t slot, bool on) noexcept {
if (slot < NOut) enabled_[slot] = on;
}
static constexpr std::size_t slot_count() noexcept { return NOut; }
// The device param backing output slot i (for glue / display by name).
static constexpr const ::nisps::midi::generated::MidiParam& slot_param(std::size_t i) noexcept {
return Device.params[kSlots[i]];
}
private:
static constexpr auto kSlots = pick_cc_slots<Device, NOut>();
std::array<bool, NOut> enabled_ = [] {
std::array<bool, NOut> a{};
for (auto& b : a) b = true;
return a;
}();
static constexpr std::array<generated::Param, 0> kNoParams{};
static constexpr std::array<std::string_view, 0> kNoVoiceSpaces{};
static constexpr generated::UIConfig kUI{generated::PrimaryInput::Joystick, false, false};
static inline constexpr ParamSchema kSchema = ParamSchema{
Device.device_id,
std::string_view{"thru"},
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
std::span<const std::string_view>(ext_synth_defaults::kInputChannels),
ext_synth_defaults::kInputSize,
std::span<const std::size_t>(ext_synth_defaults::kHiddenLayers),
NOut,
refactor(codegen): codegen owns mode identity, per-mode schemas and net dims Phase 3 (S1, S5, S6, S25, L11, L37, ST11, ST12). Behaviour-preserving by construction: the diff on the generated directories is PURELY ADDITIVE (217 insertions, 0 deletions), so no emitted constant changed value. This moves where truth lives; it does not change what truth says. - S5: nine mode headers each carried a mechanically identical 12-field positional ParamSchema aggregate. codegen now emits one `inline constexpr ParamSchema k<Mode>Schema` per mode, and the struct itself moved into generated/schema_types.hpp. Each param_schema() is a one-line return. - S6 + S25: every mode hand-typed its net shape a second time as MLP template args, duplicating the schema's own dims. codegen emits a `<Mode>MLP` alias built from the already-emitted constants (not re-literalled), and all nine modes use it. NMaxExamples still defaults from kDefaultMaxExamples (Phase 2). - S1: model.ts hand-imported all nine schemas by name and hand-paired each with its overlay — so the SET of modes was hand-maintained and could silently drift from codegen. codegen now emits ALL_MODE_SCHEMAS; SCHEMA_MODE_OVERLAYS is purely display truth (label/glyph/css/order), which stays hand-curated. - L37: deleted the hand-written modeEngineId switch, which duplicated schema.engine_id and silently defaulted unknown modes to 'thru'. Routes on MFMode.engineId with exactly one documented exception: sound_analysis_midi declares engine_id 'thru' because its ModeBase audio slot really is NoOpEngine, while it separately drives the real AnalysisEngine. - L11: ExternalSynthMIDIMode's shape was literal in two places; now named once in an ext_synth_defaults namespace with an ExtSynthMIDIMLP alias. Full folding into the JSON pipeline is NOT done and the reason is recorded in-file: it is a template family over an externally-supplied Device and variable NOut, with no single (device, NOut) schema to author. - ST12: extracted codegen/lib.ts for the helpers both generators duplicated, and corrected the comment that claimed they had to be separate. Proof the extraction was behaviour-free: regenerating the MIDI-device outputs produces a byte-identical tree. - ST11: deleted codegen/templates/ — dead "reference" files no generator reads, already drifted from the real emitters. Gates: run-all-tests.sh ALL GREEN; codegen idempotent (re-running both generators yields no further diff), which is what CI's dirty-diff gate checks.
2026-07-21 14:02:23 +02:00
ext_synth_defaults::kDefaultSpread,
std::span<const generated::Param>(kNoParams),
std::span<const std::string_view>(kNoVoiceSpaces),
kUI,
};
};
} // namespace nisps::modes