feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
// nisps/modes/base.hpp — Common scaffolding for every concrete mode.
|
|
|
|
|
//
|
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
|
|
|
// Provides `ModeBase<Derived, EngineT, MLPType, NInputs>` — a CRTP base
|
|
|
|
|
// that absorbs the per-mode boilerplate (input forwarding, ML inference
|
|
|
|
|
// driving engine params, voice space selection, control event ring
|
|
|
|
|
// buffer). Concrete modes derive from this and only specialise:
|
|
|
|
|
// - the schema reference (static; `nisps::ParamSchema` — the aggregate
|
|
|
|
|
// type the `Mode` concept's `param_schema()` returns a const-reference
|
|
|
|
|
// to — is now DEFINED in the codegen output, `generated/schema_types.hpp`,
|
|
|
|
|
// included below; codegen also emits one `inline constexpr ParamSchema
|
|
|
|
|
// k<Mode>Schema` per mode, so `param_schema()` is a one-line return),
|
|
|
|
|
// - the "extra" pre-mapping done before set_params (e.g. analysis
|
|
|
|
|
// features stitched into ML inputs in SoundAnalysisMIDI),
|
|
|
|
|
// - any engine-specific control glue (note_on/note_off, sequencer
|
|
|
|
|
// play/stop, BPM updates).
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
//
|
|
|
|
|
// Modes are platform-agnostic. Hardware/browser glue maps abstract input
|
|
|
|
|
// channels (float [0, 1]) into `set_input(idx, value)` and drains
|
|
|
|
|
// `pop_control_events()` for MIDI/I2C dispatch.
|
|
|
|
|
//
|
|
|
|
|
// No heap, no virtuals, no pico/Arduino headers.
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <cstddef>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <span>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
#include <type_traits>
|
|
|
|
|
|
|
|
|
|
#include "../core/concepts.hpp"
|
|
|
|
|
#include "../core/perf.hpp"
|
|
|
|
|
#include "../core/ring_buffer.hpp"
|
|
|
|
|
#include "../core/types.hpp"
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
#include "../ml/jolt.hpp"
|
|
|
|
|
#include "../ml/ou_noise.hpp"
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
#include "generated/schema_types.hpp"
|
|
|
|
|
|
|
|
|
|
namespace nisps {
|
|
|
|
|
|
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
|
|
|
// `nisps::ParamSchema` is defined in generated/schema_types.hpp (included
|
|
|
|
|
// above) — codegen owns it (S5, one-core simplification 2026-07) since its
|
|
|
|
|
// shape must match the `nisps::Mode` concept's forward declaration
|
|
|
|
|
// (nisps/core/concepts.hpp) exactly, and every mode's actual schema VALUE is
|
|
|
|
|
// itself codegen output (`generated::k<Mode>Schema`).
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Abstract control event — emitted by modes for the platform glue to drain.
|
|
|
|
|
// Sequencer modes (BreakOr, Elysiamorf) push real events; synth modes push
|
|
|
|
|
// none unless they want to relay MIDI thru.
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
struct ControlEvent {
|
|
|
|
|
enum class Kind : std::uint8_t {
|
|
|
|
|
None,
|
|
|
|
|
NoteOn,
|
|
|
|
|
NoteOff,
|
|
|
|
|
ControlChange,
|
|
|
|
|
Clock,
|
|
|
|
|
};
|
|
|
|
|
Kind kind = Kind::None;
|
|
|
|
|
std::uint8_t channel = 0u;
|
|
|
|
|
std::uint8_t data1 = 0u;
|
|
|
|
|
std::uint8_t data2 = 0u;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Sized at the larger of the engines' event buffers (BreakOr/Elysiamorf
|
|
|
|
|
// publish 64 entries; we mirror that for consistency).
|
|
|
|
|
inline constexpr std::size_t kModeEventBufferSize = 64u;
|
|
|
|
|
|
|
|
|
|
// Trait controlling whether ModeBase routes ML outputs into engine.set_params().
|
|
|
|
|
// Default: true (every synth/effect mode). Specialise to `false` for modes
|
|
|
|
|
// that don't (e.g. SoundAnalysisMIDIMode where outputs become MIDI CC).
|
|
|
|
|
template <typename Derived>
|
|
|
|
|
struct ModeRoutesOutputsToEngine : std::true_type {};
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ModeBase — CRTP scaffold.
|
|
|
|
|
//
|
|
|
|
|
// Derived classes provide:
|
|
|
|
|
// static constexpr const ParamSchema& schema() // their generated schema
|
|
|
|
|
// void on_setup(float sample_rate) noexcept // optional hook
|
|
|
|
|
// void on_pre_inference() noexcept // optional, before ml_.process()
|
|
|
|
|
// void on_post_inference() noexcept // optional, after engine.set_params()
|
feat: curve truth, DriverConfig, real telemetry, engine benchmark
Four items from one workflow, committed together because their build and CI
wiring genuinely interleaves — nisps/CMakeLists.txt, run-all-tests.sh and
ci.yml each carry hunks from two of them, and the stage renumbering (1/5 ->
1/6) touches every line. Splitting would produce commits that do not build,
which is worse than a commit that does four things and says so.
S26 part 2 — the curve declaration now matches reality. params[].curve stays
the mode-wide DEFAULT; a voice_spaces entry may now be {name, curve_overrides}
declaring only the slots where THAT voice space deviates. The 6 modes with one
voice space are byte-identical. The values were derived MECHANICALLY by a new
codegen/curve-audit.ts that models the four idioms a p[N]*p[N] regex misses
(alias form, memlcelium's implicit-counter sq() lambda, loop-generated indices,
smooth_params_), inlines helpers, and RAISES rather than guessing when it
cannot reduce an expression. A drift gate cross-checks 1179 (voice space x
param) slots against engine source on every run and was proved to fail loudly
on three drift classes. Application stays in the engine: nisps/engines,
nisps/pipeline and nisps/core are untouched, generated output is pure insertion
(755 insertions, 0 deletions), and the rebuilt nisps.wasm was byte-identical.
S4 / 7.2 — firmware reads the active mode's driver config at mode start, and
mic/line is real. My brief assumed the engine owns this; the code disagreed and
the code was right. sound_analysis_midi's EngineT is NoOpEngine — the mic lives
on a separately-composed AnalysisEngine member — so engine-level wiring would
have compiled, passed every gate, and left the one mic mode on line input.
Hence a mode-level seam defaulting to engine().driver_config(). Separately,
DriverConfig's defaults (line_level 0, output_volume 1.0) had drifted from
memllib's actual 3/0.8 because nothing had ever read them; wiring them as-is
would have made every silent mode louder and its line input maximally
insensitive — a behaviour change disguised as plumbing. Now pinned by a test.
Also: GetSysClockSpeed() panic()s on unsupported sample rates and runs on the
first line of setup(), so sample_rate needed a fallback ahead of clock setup.
CI's firmware env list gains soundanalysismidi — it is the only mic variant and
nothing else compiles that path.
Plan 5e — telemetry is real. A loss_history C-API entry across the full 5-layer
chain lets the browser read the per-iteration loss the core already records.
The audit named one fabrication site; there were two — wasm-iml.ts's
synchronous train() published lossHistory: [loss] as well. A third, ctx.loss,
was not merely dead but actively synthetic (fallbacks of prev * 0.82 and a
literal 0.5, rendered by nothing) and is deleted. The firmware buffer stays
untouched, per the L25 call. EngineApi.lossHistory() reads spine state rather
than the MLP handle, because trainAsync() fits on the worker's mirror net and
the handle would give a subtly-wrong second answer.
Plan 5f — engine throughput is measurable. One source compiled twice (CMake
natively, emcc for WASM) so the targets compare directly and no WASM export is
added. Sequencers are driven into a working state, and every row prints its own
working-state evidence so a number produced by an idle engine is visible rather
than plausible. Reports, never asserts: a wall-clock threshold on shared
hardware is meaningless or flaky, same call as the firmware size job.
ALIGNMENT: the telemetry defect is deleted (built, not deferred); the
performance defect is rewritten to what is actually left — these are HOST
numbers, and nothing measures the RP2350 at 150 MHz, which is the target the
mission's constraint is about. Q4 (memllib ownership) and Q5 (legacy feedback
modes) are closed.
Corrections to my own earlier claims, both found by agents contradicting the
brief: manifold/ONBOARDING.md was NOT "now accurate" — its primitives list
still named five deleted primitives and cited a seededGradient() that does not
exist. And the parity harness misses the sequencer engines because it runs 128
frames while their sequencers evaluate every 400-500 samples, NOT because
all-params-0.5 fails to trigger them (it does trigger: 0.5 maps to ratio 2,
firing three times per bar). The fix is a longer window, not different params.
Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS, lint clean, curve
drift 1179 slots ok, 39 e2e (was 33). Firmware: 5 envs built including the mic
variant.
2026-07-21 22:02:23 +02:00
|
|
|
// DriverConfig on_driver_config() const noexcept // optional, overrides the
|
|
|
|
|
// // engine's driver config
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
//
|
|
|
|
|
// Derived classes may choose the engine type (`EngineT`) and ML type
|
|
|
|
|
// (`MLPType`) freely; both must satisfy `MLEngine` and `AudioEngine`
|
|
|
|
|
// respectively, except for sequencer modes whose engine still satisfies
|
|
|
|
|
// `AudioEngine` (process() returns silence).
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
template <typename Derived,
|
|
|
|
|
typename EngineT,
|
|
|
|
|
typename MLPType,
|
|
|
|
|
std::size_t NInputs>
|
|
|
|
|
class ModeBase {
|
|
|
|
|
public:
|
|
|
|
|
using Engine = EngineT;
|
|
|
|
|
using ML = MLPType;
|
|
|
|
|
|
|
|
|
|
static_assert(AudioEngine<EngineT>,
|
|
|
|
|
"ModeBase: EngineT must satisfy nisps::AudioEngine concept");
|
|
|
|
|
static_assert(MLEngine<MLPType>,
|
|
|
|
|
"ModeBase: MLPType must satisfy nisps::MLEngine concept");
|
|
|
|
|
static_assert(MLPType::kInput == NInputs,
|
|
|
|
|
"ModeBase: NInputs must equal MLP::kInput");
|
|
|
|
|
// Most modes route ML outputs directly into engine params; require the
|
|
|
|
|
// sizes to match. SoundAnalysisMIDI opts out by specialising
|
|
|
|
|
// ModeRoutesOutputsToEngine<Derived> to std::false_type.
|
|
|
|
|
static constexpr bool kRouteOutputsToEngine =
|
|
|
|
|
ModeRoutesOutputsToEngine<Derived>::value;
|
|
|
|
|
static_assert(!kRouteOutputsToEngine ||
|
|
|
|
|
MLPType::kOutput == EngineT::param_count(),
|
|
|
|
|
"ModeBase: MLP output_size must equal engine param_count() "
|
|
|
|
|
"unless ModeRoutesOutputsToEngine<Derived> is false");
|
|
|
|
|
|
|
|
|
|
static constexpr std::size_t input_channel_count() noexcept { return NInputs; }
|
|
|
|
|
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
explicit ModeBase(std::uint64_t seed = 0xC0FFEEu) noexcept
|
|
|
|
|
: ml_(seed),
|
|
|
|
|
jolt_(seed ^ 0x91E10C5Eull),
|
|
|
|
|
ou_(seed ^ 0x0CEA0FF5ull) {}
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
|
|
|
|
|
// ---- Mode concept surface ----
|
|
|
|
|
void setup(float sample_rate) noexcept {
|
|
|
|
|
sample_rate_ = sample_rate;
|
|
|
|
|
engine_.setup(sample_rate);
|
|
|
|
|
for (auto& v : input_channels_) v = 0.5f;
|
|
|
|
|
// Run an inference at default inputs so engine has params on first
|
|
|
|
|
// process() call, even if no input has been touched.
|
|
|
|
|
for (std::size_t i = 0u; i < NInputs; ++i) {
|
2026-06-28 21:17:51 +02:00
|
|
|
ml_.set_input(i, effective_input(i));
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
}
|
|
|
|
|
ml_.process();
|
|
|
|
|
if constexpr (kRouteOutputsToEngine) {
|
|
|
|
|
engine_.set_params(ml_.outputs());
|
|
|
|
|
}
|
|
|
|
|
if constexpr (requires(Derived& d, float s) { d.on_setup(s); }) {
|
|
|
|
|
static_cast<Derived&>(*this).on_setup(sample_rate);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: curve truth, DriverConfig, real telemetry, engine benchmark
Four items from one workflow, committed together because their build and CI
wiring genuinely interleaves — nisps/CMakeLists.txt, run-all-tests.sh and
ci.yml each carry hunks from two of them, and the stage renumbering (1/5 ->
1/6) touches every line. Splitting would produce commits that do not build,
which is worse than a commit that does four things and says so.
S26 part 2 — the curve declaration now matches reality. params[].curve stays
the mode-wide DEFAULT; a voice_spaces entry may now be {name, curve_overrides}
declaring only the slots where THAT voice space deviates. The 6 modes with one
voice space are byte-identical. The values were derived MECHANICALLY by a new
codegen/curve-audit.ts that models the four idioms a p[N]*p[N] regex misses
(alias form, memlcelium's implicit-counter sq() lambda, loop-generated indices,
smooth_params_), inlines helpers, and RAISES rather than guessing when it
cannot reduce an expression. A drift gate cross-checks 1179 (voice space x
param) slots against engine source on every run and was proved to fail loudly
on three drift classes. Application stays in the engine: nisps/engines,
nisps/pipeline and nisps/core are untouched, generated output is pure insertion
(755 insertions, 0 deletions), and the rebuilt nisps.wasm was byte-identical.
S4 / 7.2 — firmware reads the active mode's driver config at mode start, and
mic/line is real. My brief assumed the engine owns this; the code disagreed and
the code was right. sound_analysis_midi's EngineT is NoOpEngine — the mic lives
on a separately-composed AnalysisEngine member — so engine-level wiring would
have compiled, passed every gate, and left the one mic mode on line input.
Hence a mode-level seam defaulting to engine().driver_config(). Separately,
DriverConfig's defaults (line_level 0, output_volume 1.0) had drifted from
memllib's actual 3/0.8 because nothing had ever read them; wiring them as-is
would have made every silent mode louder and its line input maximally
insensitive — a behaviour change disguised as plumbing. Now pinned by a test.
Also: GetSysClockSpeed() panic()s on unsupported sample rates and runs on the
first line of setup(), so sample_rate needed a fallback ahead of clock setup.
CI's firmware env list gains soundanalysismidi — it is the only mic variant and
nothing else compiles that path.
Plan 5e — telemetry is real. A loss_history C-API entry across the full 5-layer
chain lets the browser read the per-iteration loss the core already records.
The audit named one fabrication site; there were two — wasm-iml.ts's
synchronous train() published lossHistory: [loss] as well. A third, ctx.loss,
was not merely dead but actively synthetic (fallbacks of prev * 0.82 and a
literal 0.5, rendered by nothing) and is deleted. The firmware buffer stays
untouched, per the L25 call. EngineApi.lossHistory() reads spine state rather
than the MLP handle, because trainAsync() fits on the worker's mirror net and
the handle would give a subtly-wrong second answer.
Plan 5f — engine throughput is measurable. One source compiled twice (CMake
natively, emcc for WASM) so the targets compare directly and no WASM export is
added. Sequencers are driven into a working state, and every row prints its own
working-state evidence so a number produced by an idle engine is visible rather
than plausible. Reports, never asserts: a wall-clock threshold on shared
hardware is meaningless or flaky, same call as the firmware size job.
ALIGNMENT: the telemetry defect is deleted (built, not deferred); the
performance defect is rewritten to what is actually left — these are HOST
numbers, and nothing measures the RP2350 at 150 MHz, which is the target the
mission's constraint is about. Q4 (memllib ownership) and Q5 (legacy feedback
modes) are closed.
Corrections to my own earlier claims, both found by agents contradicting the
brief: manifold/ONBOARDING.md was NOT "now accurate" — its primitives list
still named five deleted primitives and cited a seededGradient() that does not
exist. And the parity harness misses the sequencer engines because it runs 128
frames while their sequencers evaluate every 400-500 samples, NOT because
all-params-0.5 fails to trigger them (it does trigger: 0.5 maps to ratio 2,
firing three times per bar). The fix is a longer window, not different params.
Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS, lint clean, curve
drift 1179 slots ok, 39 e2e (was 33). Firmware: 5 envs built including the mic
variant.
2026-07-21 22:02:23 +02:00
|
|
|
// ---- Audio-driver configuration ----
|
|
|
|
|
//
|
|
|
|
|
// What the platform's audio driver should be set up as for this mode:
|
|
|
|
|
// codec input source (mic vs line), gain staging, preferred sample rate.
|
|
|
|
|
// Firmware glue reads this at mode start; see
|
|
|
|
|
// firmware/MEMLNaut-NISPS/glue/audio_driver.hpp.
|
|
|
|
|
//
|
|
|
|
|
// Default = whatever the mode's audio engine advertises, so a mode that
|
|
|
|
|
// does not care says NOTHING and inherits the engine's (or, for
|
|
|
|
|
// NoOpEngine, `DriverConfig{}`'s) values. A mode whose audio INPUT is
|
|
|
|
|
// consumed by something other than `engine_` — e.g. SoundAnalysisMIDIMode,
|
|
|
|
|
// whose engine is a silent NoOp while a separately-composed AnalysisEngine
|
|
|
|
|
// owns the microphone — declares `on_driver_config()` and that wins.
|
|
|
|
|
DriverConfig driver_config() const noexcept {
|
|
|
|
|
if constexpr (requires(const Derived& d) { d.on_driver_config(); }) {
|
|
|
|
|
return static_cast<const Derived&>(*this).on_driver_config();
|
|
|
|
|
} else {
|
|
|
|
|
return engine_.driver_config();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
NISPS_FORCE_INLINE void set_input(std::size_t idx, float value) noexcept {
|
|
|
|
|
if (idx >= NInputs) return;
|
|
|
|
|
if (value < 0.f) value = 0.f;
|
|
|
|
|
else if (value > 1.f) value = 1.f;
|
|
|
|
|
input_channels_[idx] = value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-28 21:17:51 +02:00
|
|
|
// ---- Input neutralization (single/double controller toggle) ----
|
|
|
|
|
//
|
|
|
|
|
// A pinned channel feeds `pin_value_` (neutral, default 0.5) to the MLP
|
|
|
|
|
// instead of its live value, without rebuilding/resizing the network.
|
|
|
|
|
// Glue toggles which channels are pinned (e.g. single-joystick mode pins
|
|
|
|
|
// the second 2D controller's two channels). The stored live value is left
|
|
|
|
|
// untouched, so unpinning resumes from the controller's current position.
|
|
|
|
|
NISPS_FORCE_INLINE void set_input_pinned(std::size_t idx, bool pinned) noexcept {
|
|
|
|
|
if (idx >= NInputs) return;
|
|
|
|
|
input_pinned_[idx] = pinned;
|
|
|
|
|
}
|
|
|
|
|
NISPS_FORCE_INLINE bool is_input_pinned(std::size_t idx) const noexcept {
|
|
|
|
|
return idx < NInputs && input_pinned_[idx];
|
|
|
|
|
}
|
|
|
|
|
NISPS_FORCE_INLINE void set_pin_value(float v) noexcept {
|
|
|
|
|
if (v < 0.f) v = 0.f;
|
|
|
|
|
else if (v > 1.f) v = 1.f;
|
|
|
|
|
pin_value_ = v;
|
|
|
|
|
}
|
|
|
|
|
float pin_value() const noexcept { return pin_value_; }
|
|
|
|
|
|
|
|
|
|
// Effective value fed to the MLP for channel i (pin override applied).
|
|
|
|
|
NISPS_FORCE_INLINE float effective_input(std::size_t i) const noexcept {
|
|
|
|
|
return input_pinned_[i] ? pin_value_ : input_channels_[i];
|
|
|
|
|
}
|
|
|
|
|
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
NISPS_HOT void tick_control() noexcept {
|
|
|
|
|
if constexpr (requires(Derived& d) { d.on_pre_inference(); }) {
|
|
|
|
|
static_cast<Derived&>(*this).on_pre_inference();
|
|
|
|
|
}
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
// Jolt: continuous weight morph while a gesture is held. Inert (and
|
|
|
|
|
// free) when inactive — no copy, no RNG advance, weights untouched.
|
|
|
|
|
// Kept out-of-line so the heavy get/set-weights copy doesn't bloat
|
|
|
|
|
// the control path or perturb the optimizer's analysis of the
|
|
|
|
|
// control-event ring buffer below.
|
|
|
|
|
if (jolt_.active()) apply_jolt_();
|
|
|
|
|
jolt_.tick_lr_ramp();
|
|
|
|
|
|
2026-06-28 21:17:51 +02:00
|
|
|
// Forward (possibly Derived-mutated) channels into the MLP, applying
|
|
|
|
|
// the per-channel pin override.
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
for (std::size_t i = 0u; i < NInputs; ++i) {
|
2026-06-28 21:17:51 +02:00
|
|
|
ml_.set_input(i, effective_input(i));
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
}
|
|
|
|
|
ml_.process();
|
|
|
|
|
if constexpr (kRouteOutputsToEngine) {
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
// Exploration OU walk on the output vector (inert when intensity
|
|
|
|
|
// is 0 → falls through to the original direct-route path).
|
|
|
|
|
if (ou_.enabled()) {
|
|
|
|
|
const auto o = ml_.outputs();
|
|
|
|
|
const std::size_t no = o.size();
|
|
|
|
|
for (std::size_t i = 0u; i < no && i < out_buf_.size(); ++i) {
|
|
|
|
|
out_buf_[i] = o[i];
|
|
|
|
|
}
|
|
|
|
|
ou_.apply(std::span<float>(out_buf_.data(), no));
|
|
|
|
|
engine_.set_params(std::span<const float>(out_buf_.data(), no));
|
|
|
|
|
} else {
|
|
|
|
|
engine_.set_params(ml_.outputs());
|
|
|
|
|
}
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
}
|
|
|
|
|
if constexpr (requires(Derived& d) { d.on_post_inference(); }) {
|
|
|
|
|
static_cast<Derived&>(*this).on_post_inference();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
// ---- Adaptive-learning gestures (shared by every mode) ----
|
|
|
|
|
//
|
|
|
|
|
// Jolt — held gesture that continuously morphs a scatter of weights,
|
|
|
|
|
// then freezes them on release (see ml/jolt.hpp). Wire a momentary
|
|
|
|
|
// button / MIDI pedal: press on down-edge, release on up-edge.
|
|
|
|
|
void jolt_press() noexcept { jolt_.press(MLPType::weight_count()); }
|
|
|
|
|
void jolt_release() noexcept { jolt_.release(); }
|
|
|
|
|
bool jolt_active() const noexcept { return jolt_.active(); }
|
|
|
|
|
// Effective-LR multiplier for the caller's training step (0 while held,
|
|
|
|
|
// ramps to 1 after release). Multiply your training LR by this.
|
|
|
|
|
float jolt_lr_scale() const noexcept { return jolt_.lr_scale(); }
|
|
|
|
|
ml::Jolt& jolt() noexcept { return jolt_; }
|
|
|
|
|
const ml::Jolt& jolt() const noexcept { return jolt_; }
|
|
|
|
|
|
|
|
|
|
// Exploration — Ornstein-Uhlenbeck random walk added to the output
|
|
|
|
|
// vector (see ml/ou_noise.hpp). `level` in [0,1]; 0 disables.
|
|
|
|
|
void set_explore_intensity(float level) noexcept { ou_.set_intensity(level); }
|
|
|
|
|
float explore_intensity() const noexcept { return ou_.intensity(); }
|
|
|
|
|
ml::OUNoise<MLPType::kOutput>& ou_noise() noexcept { return ou_; }
|
|
|
|
|
const ml::OUNoise<MLPType::kOutput>& ou_noise() const noexcept { return ou_; }
|
|
|
|
|
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
NISPS_HOT NISPS_FORCE_INLINE stereosample_t process(stereosample_t x) noexcept {
|
|
|
|
|
return engine_.process(x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Engine& engine() noexcept { return engine_; }
|
|
|
|
|
const Engine& engine() const noexcept { return engine_; }
|
|
|
|
|
ML& ml() noexcept { return ml_; }
|
|
|
|
|
const ML& ml() const noexcept { return ml_; }
|
|
|
|
|
|
|
|
|
|
// ---- Common helpers ----
|
|
|
|
|
|
|
|
|
|
// Read-only view of latest input-channel values [0, 1].
|
|
|
|
|
std::span<const float> input_channels() const noexcept {
|
|
|
|
|
return std::span<const float>(input_channels_.data(), NInputs);
|
|
|
|
|
}
|
|
|
|
|
// Mutable accessor for derived classes (e.g. SoundAnalysisMIDI splices
|
|
|
|
|
// analysis features into the channel array before forwarding to ML).
|
|
|
|
|
std::span<float> mutable_input_channels() noexcept {
|
|
|
|
|
return std::span<float>(input_channels_.data(), NInputs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Voice space selection (engines that support it expose set_voice_space).
|
|
|
|
|
void set_voice_space(std::size_t idx) noexcept {
|
|
|
|
|
if constexpr (requires(EngineT& e) { e.set_voice_space(typename EngineT::VoiceSpace{}); }) {
|
|
|
|
|
using VS = typename EngineT::VoiceSpace;
|
|
|
|
|
if (idx >= EngineT::kVoiceSpaceCount) return;
|
|
|
|
|
engine_.set_voice_space(static_cast<VS>(idx));
|
|
|
|
|
voice_space_idx_ = idx;
|
|
|
|
|
// Re-apply current params under the new voice space mapping.
|
|
|
|
|
engine_.set_params(ml_.outputs());
|
|
|
|
|
} else {
|
|
|
|
|
(void)idx;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::size_t voice_space_index() const noexcept { return voice_space_idx_; }
|
|
|
|
|
|
|
|
|
|
// Control event ring — modes/derived classes push, hardware glue pops.
|
|
|
|
|
NISPS_FORCE_INLINE bool push_control_event(const ControlEvent& e) noexcept {
|
|
|
|
|
return events_.try_push(e);
|
|
|
|
|
}
|
|
|
|
|
std::size_t pop_control_events(std::span<ControlEvent> out) noexcept {
|
|
|
|
|
std::size_t n = 0u;
|
|
|
|
|
while (n < out.size()) {
|
|
|
|
|
ControlEvent e;
|
|
|
|
|
if (!events_.try_pop(e)) break;
|
|
|
|
|
out[n++] = e;
|
|
|
|
|
}
|
|
|
|
|
return n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float sample_rate() const noexcept { return sample_rate_; }
|
|
|
|
|
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
private:
|
|
|
|
|
// Copy the flat weights out of the MLP, morph the jolt-selected few, and
|
|
|
|
|
// write them back. Out-of-line on purpose (see tick_control).
|
|
|
|
|
NISPS_NOINLINE void apply_jolt_() noexcept {
|
|
|
|
|
const auto w = ml_.get_weights(); // span into ml_'s flat scratch
|
|
|
|
|
const std::size_t wc = w.size();
|
|
|
|
|
for (std::size_t i = 0u; i < wc && i < jolt_buf_.size(); ++i) {
|
|
|
|
|
jolt_buf_[i] = w[i];
|
|
|
|
|
}
|
|
|
|
|
jolt_.step(std::span<float>(jolt_buf_.data(), wc));
|
|
|
|
|
ml_.set_weights(std::span<const float>(jolt_buf_.data(), wc));
|
|
|
|
|
}
|
|
|
|
|
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
protected:
|
|
|
|
|
float sample_rate_ = 48000.f;
|
|
|
|
|
EngineT engine_{};
|
|
|
|
|
MLPType ml_;
|
|
|
|
|
std::array<float, NInputs> input_channels_{};
|
2026-06-28 21:17:51 +02:00
|
|
|
std::array<bool, NInputs> input_pinned_{}; // false => live
|
|
|
|
|
float pin_value_ = 0.5f; // neutral
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
std::size_t voice_space_idx_ = 0u;
|
|
|
|
|
RingBuffer<ControlEvent, kModeEventBufferSize> events_{};
|
feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:
- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).
Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.
Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).
Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00
|
|
|
// Adaptive-learning state (Jolt + OU). Declared AFTER events_ so the
|
|
|
|
|
// lock-free ring buffer keeps its original object offset — inserting the
|
|
|
|
|
// large jolt_buf_ before it provokes a spurious GCC -Wstringop-overflow
|
|
|
|
|
// on the ring's atomic index under -O3. Order matters only to that
|
|
|
|
|
// false positive; behaviour is identical either way.
|
|
|
|
|
ml::Jolt jolt_;
|
|
|
|
|
ml::OUNoise<MLPType::kOutput> ou_;
|
|
|
|
|
// Scratch for OU output blending and Jolt weight morphing. Sized to the
|
|
|
|
|
// single compiled mode; firmware only ever instantiates one mode.
|
|
|
|
|
std::array<float, MLPType::kOutput> out_buf_{};
|
|
|
|
|
std::array<float, MLPType::weight_count()> jolt_buf_{};
|
feat(nisps/modes): scaffolding for platform-agnostic mode layer (meml-beb)
Adds two foundational pieces shared by every concrete mode:
- `base.hpp`: defines `nisps::ParamSchema` (the aggregate type the
`Mode` concept's `param_schema()` returns by const-reference),
`nisps::ControlEvent`, and `ModeBase<Derived, EngineT, MLPType,
NInputs>` — a CRTP scaffold absorbing input forwarding, ML
inference + voice space mapping, engine.set_params() routing, and
a per-mode ControlEvent ring buffer for MIDI/I2C events.
`ModeRoutesOutputsToEngine<Derived>` lets a derived mode (e.g.
SoundAnalysisMIDI) opt out of routing ML outputs to engine
params when its outputs become MIDI CCs instead.
- `voice_space.hpp`: `VoiceSpaceEntry` + `make_voice_space_entries()`
+ `find_voice_space()` helpers. Voice-space *mapping code* lives
inside engines (mirroring firmware); mode layer just selects
which voice space the engine uses by index/name.
No heap, no virtuals, no Pico/Arduino dependencies; satisfies stream
4 of architecture.md.
2026-04-29 15:26:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace nisps
|