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.
This commit is contained in:
parent
9ad0b8b9a0
commit
4e60d0192a
22 changed files with 2016 additions and 9 deletions
|
|
@ -128,6 +128,7 @@ The WASM target is fixed at `MLP<2, 10, 14, 18, 126>`. Modes with smaller `outpu
|
||||||
- Engine MLP architecture is fixed at compile time — supporting per-mode hidden-layer shapes would need either multiple WASM modules or runtime variation.
|
- Engine MLP architecture is fixed at compile time — supporting per-mode hidden-layer shapes would need either multiple WASM modules or runtime variation.
|
||||||
- Mic input through the worklet for XIASRI / SoundAnalysisMIDI is not wired; UI scaffolds render but feature is TODO.
|
- Mic input through the worklet for XIASRI / SoundAnalysisMIDI is not wired; UI scaffolds render but feature is TODO.
|
||||||
- C15 voice space integration in C15Mode is a placeholder.
|
- C15 voice space integration in C15Mode is a placeholder.
|
||||||
|
- Jolt + OU-explore learning gestures are wired in firmware (`ModeBase`, surfaced on TogB1 / RVX1) and the SLP-Workshop mode runs in the browser, but the *interactive* browser controls for Jolt/OU are not yet wired into the playground UI (the WASM weight bindings exist; the runtime/output-stage hooks are the remaining work).
|
||||||
|
|
||||||
## URL parameters (playground)
|
## URL parameters (playground)
|
||||||
|
|
||||||
|
|
|
||||||
8
MAP.md
8
MAP.md
|
|
@ -6,10 +6,10 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
|
||||||
|
|
||||||
### `nisps/` — platform-agnostic C++20 library (the only ML/DSP/engine code)
|
### `nisps/` — platform-agnostic C++20 library (the only ML/DSP/engine code)
|
||||||
- `nisps/core/` — `perf.hpp` (memory section attrs), `types.hpp`, `concepts.hpp` (`MLEngine`, `AudioEngine`, `Mode`), `fixed_buffer.hpp`, `ring_buffer.hpp` (SPSC lock-free, replaces pico/util/queue), `rng.hpp` (xoshiro256+ deterministic), `math.hpp` (fast_sigmoid, `Curve` enum + `apply_curve`).
|
- `nisps/core/` — `perf.hpp` (memory section attrs), `types.hpp`, `concepts.hpp` (`MLEngine`, `AudioEngine`, `Mode`), `fixed_buffer.hpp`, `ring_buffer.hpp` (SPSC lock-free, replaces pico/util/queue), `rng.hpp` (xoshiro256+ deterministic), `math.hpp` (fast_sigmoid, `Curve` enum + `apply_curve`).
|
||||||
- `nisps/ml/` — MLP class template `MLP<NIn, NH1, NH2, NH3, NOut>`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (SGD + grad clipping), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `feedback.hpp` (`FeedbackController<MLP_T>` — the 3-mode "Down Action" negative-feedback state machine: Avoid / RandomiseOutputs / RandomiseMlp; header-only, zero-heap, own deterministic RNG, exposed via `nisps_ml_feedback_*` C API), `stats.hpp`.
|
- `nisps/ml/` — MLP class template `MLP<NIn, NH1, NH2, NH3, NOut>`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (SGD + grad clipping), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `jolt.hpp` (`Jolt` — held continuous weight-morph over the flat weight buffer + post-release LR ramp; ported from upstream InterfaceRL), `ou_noise.hpp` (`OUNoise<N>` — Ornstein-Uhlenbeck exploration walk on the output vector; ported from upstream InterfaceRL), `feedback.hpp` (`FeedbackController<MLP_T>` — the 3-mode "Down Action" negative-feedback state machine: Avoid / RandomiseOutputs / RandomiseMlp; header-only, zero-heap, own deterministic RNG, exposed via `nisps_ml_feedback_*` C API), `stats.hpp`. Jolt + OU are inert by default and wired into `ModeBase`, so every mode exposes `jolt_press/jolt_release`, `jolt_lr_scale`, and `set_explore_intensity`.
|
||||||
- `nisps/dsp/` — `biquad.hpp`, `delay.hpp`, `reverb.hpp`, `filter.hpp`, `env.hpp`, `osc.hpp`, `pitch_shift.hpp`, `dc_blocker.hpp`. Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.
|
- `nisps/dsp/` — `biquad.hpp`, `delay.hpp`, `reverb.hpp`, `filter.hpp`, `env.hpp`, `osc.hpp`, `pitch_shift.hpp`, `dc_blocker.hpp`. Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.
|
||||||
- `nisps/engines/` — eight audio engines, each satisfying `AudioEngine`: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp` (sequencer, NoOp audio), `elysiamorf.hpp` (sequencer, NoOp audio), `analysis.hpp` (input-side spectral features). Plus `base.hpp` (`NoOpEngine`, engine_id "thru").
|
- `nisps/engines/` — eight audio engines, each satisfying `AudioEngine`: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp` (sequencer, NoOp audio), `elysiamorf.hpp` (sequencer, NoOp audio), `analysis.hpp` (input-side spectral features). Plus `base.hpp` (`NoOpEngine`, engine_id "thru").
|
||||||
- `nisps/modes/` — platform-agnostic modes binding `{ML config, engine, voice space lambdas, abstract I/O channels}`. Files: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp`, `elysiamorf.hpp`, `sound_analysis_midi.hpp`, `external_synth_midi.hpp` (`ExternalSynthMIDIMode<const MidiDevice&, NOut>` — joystick→MLP→MIDI CC for an external synth; compile-time device from `nisps/midi`; `consteval pick_cc_slots` curates which params fill the NOut slots; NoOpEngine, `kRouteOutputsToEngine=false`). `base.hpp` provides a CRTP scaffold eliminating the duplication that previously plagued firmware modes. `voice_space.hpp` holds engine-side voice space dispatch helpers. `generated/` contains codegen output (do not edit by hand).
|
- `nisps/modes/` — platform-agnostic modes binding `{ML config, engine, voice space lambdas, abstract I/O channels}`. Files: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `slp_workshop.hpp` (`SLPWorkshopMode` — the Synth Library Portland workshop build; reuses the MEMLCelium engine + MLP shape, foregrounds the Jolt + OU explore gestures), `breakor.hpp`, `elysiamorf.hpp`, `sound_analysis_midi.hpp`, `external_synth_midi.hpp` (`ExternalSynthMIDIMode<const MidiDevice&, NOut>` — joystick→MLP→MIDI CC for an external synth; compile-time device from `nisps/midi`; `consteval pick_cc_slots` curates which params fill the NOut slots; NoOpEngine, `kRouteOutputsToEngine=false`). `base.hpp` provides a CRTP scaffold eliminating the duplication that previously plagued firmware modes. `voice_space.hpp` holds engine-side voice space dispatch helpers. `generated/` contains codegen output (do not edit by hand).
|
||||||
- `nisps/wasm/bindings.cpp` — flat C API exported to WASM (Emscripten target only).
|
- `nisps/wasm/bindings.cpp` — flat C API exported to WASM (Emscripten target only).
|
||||||
- `nisps/midi/generated/midi_devices.hpp` — codegen output: no-heap `constexpr` external-MIDI-synth templates (`nisps::midi::generated`; `MidiParam`/`MidiDevice` + `kMidiDevices` registry). Source = `schemas/midi_devices/`; do not edit by hand.
|
- `nisps/midi/generated/midi_devices.hpp` — codegen output: no-heap `constexpr` external-MIDI-synth templates (`nisps::midi::generated`; `MidiParam`/`MidiDevice` + `kMidiDevices` registry). Source = `schemas/midi_devices/`; do not edit by hand.
|
||||||
- `nisps/CMakeLists.txt` + `nisps/build/` — host-target builds + ctest.
|
- `nisps/CMakeLists.txt` + `nisps/build/` — host-target builds + ctest.
|
||||||
|
|
@ -18,7 +18,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
|
||||||
- `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` — entry point. Selects active mode at compile time via `#define MEMLNAUT_MODE_TYPE`. Forks on `NISPS_SELFTEST`: normal modes run the engine/ML path; the `SelfTest` variant delegates all four entry points to `glue/selftest.hpp`.
|
- `firmware/MEMLNaut-NISPS/MEMLNaut-NISPS.ino` — entry point. Selects active mode at compile time via `#define MEMLNAUT_MODE_TYPE`. Forks on `NISPS_SELFTEST`: normal modes run the engine/ML path; the `SelfTest` variant delegates all four entry points to `glue/selftest.hpp`.
|
||||||
- `firmware/MEMLNaut-NISPS/glue/` — hardware bindings:
|
- `firmware/MEMLNaut-NISPS/glue/` — hardware bindings:
|
||||||
- `audio_driver.hpp` — bridges memllib `AudioDriver` callback → `Mode::process(stereosample_t)`.
|
- `audio_driver.hpp` — bridges memllib `AudioDriver` callback → `Mode::process(stereosample_t)`.
|
||||||
- `peripherals.hpp` — joystick / pots / buttons → `Mode::set_input` and ML primitives.
|
- `peripherals.hpp` — joystick / pots / buttons → `Mode::set_input` and ML primitives. Also wires the shared adaptive-learning gestures: **TogB1** = Jolt (held weight morph), **RVX1** = exploration amount (OU output walk).
|
||||||
- `midi_io.hpp` — MIDI in → mode `note_on`/`update_bpm`/`set_playing`; drains `ControlEvent` ring → MIDI UART.
|
- `midi_io.hpp` — MIDI in → mode `note_on`/`update_bpm`/`set_playing`; drains `ControlEvent` ring → MIDI UART.
|
||||||
- `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line. Includes the six `MEMLNautModeExtSynth*` external-synth variants (one per device template in `nisps/midi`, e.g. `MEMLNautModeExtSynthSub37`). Also defines the `MEMLNautModeSelfTest` pseudo-variant (tag type) + the `NISPS_ST_*`/`NISPS_ST_CAT` token-paste macros the `.ino` uses to compute `NISPS_SELFTEST`. Note: `src/nisps/` exposes each referenced top-level nisps subdir as a symlink — `midi` was added alongside `core/dsp/engines/ml/modes`.
|
- `mode_select.hpp` — type aliases mapping firmware mode identifiers to `nisps::modes::*Mode` C++ types. Build script rewrites the active line. Includes the six `MEMLNautModeExtSynth*` external-synth variants (one per device template in `nisps/midi`, e.g. `MEMLNautModeExtSynthSub37`). Also defines the `MEMLNautModeSelfTest` pseudo-variant (tag type) + the `NISPS_ST_*`/`NISPS_ST_CAT` token-paste macros the `.ino` uses to compute `NISPS_SELFTEST`. Note: `src/nisps/` exposes each referenced top-level nisps subdir as a symlink — `midi` was added alongside `core/dsp/engines/ml/modes`.
|
||||||
- `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`.
|
- `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`.
|
||||||
|
|
@ -90,7 +90,7 @@ the "BUILD DELTAS" block at the top of `vcv/SPEC.md`). `src/MEMLNaut.cpp` (modul
|
||||||
|
|
||||||
### `schemas/` — JSON parameter contracts (firmware/browser source of truth)
|
### `schemas/` — JSON parameter contracts (firmware/browser source of truth)
|
||||||
- `schemas/schema.json` — Draft 2020-12 meta-schema validating mode files.
|
- `schemas/schema.json` — Draft 2020-12 meta-schema validating mode files.
|
||||||
- `schemas/modes/<mode>.json` (×8) — each mode's params, ranges, defaults, curves, voice spaces, ML config.
|
- `schemas/modes/<mode>.json` (×9) — each mode's params, ranges, defaults, curves, voice spaces, ML config. (`slp_workshop.json` reuses `engine_id: memlcelium`.)
|
||||||
- `schemas/modes/params_notes.md` — provenance notes and judgement calls per mode.
|
- `schemas/modes/params_notes.md` — provenance notes and judgement calls per mode.
|
||||||
- `schemas/midi_device.schema.json` — Draft 2020-12 meta-schema for external-MIDI-synth templates.
|
- `schemas/midi_device.schema.json` — Draft 2020-12 meta-schema for external-MIDI-synth templates.
|
||||||
- `schemas/midi_devices/<device>.json` (×6) — CC-controllable external synths (Moog Sub 37 / Sub Phatty, Creamware Pro-12 ASB, Elektron Analog Keys, ASM Hydrasynth, Roland JD-800). Each param: `{id, cc, label, min, max, default, group}`. Canonical source for both firmware + browser device pickers. Verified-CC provenance + sources live in `synth-midi-cc.json` (repo root).
|
- `schemas/midi_devices/<device>.json` (×6) — CC-controllable external synths (Moog Sub 37 / Sub Phatty, Creamware Pro-12 ASB, Elektron Analog Keys, ASM Hydrasynth, Roland JD-800). Each param: `{id, cc, label, min, max, default, group}`. Canonical source for both firmware + browser device pickers. Verified-CC provenance + sources live in `synth-midi-cc.json` (repo root).
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
// #define MEMLNAUT_MODE_TYPE MEMLNautModeChannelStrip
|
// #define MEMLNAUT_MODE_TYPE MEMLNautModeChannelStrip
|
||||||
#define MEMLNAUT_MODE_TYPE MEMLNautModePAFSynth
|
#define MEMLNAUT_MODE_TYPE MEMLNautModePAFSynth
|
||||||
// #define MEMLNAUT_MODE_TYPE MEMLNautModeMEMLCelium
|
// #define MEMLNAUT_MODE_TYPE MEMLNautModeMEMLCelium
|
||||||
|
// #define MEMLNAUT_MODE_TYPE MEMLNautModeSLPWorkshop
|
||||||
// ---- External-synth MIDI-CC variants (control an external hardware synth) ----
|
// ---- External-synth MIDI-CC variants (control an external hardware synth) ----
|
||||||
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthSub37
|
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthSub37
|
||||||
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthSubPhatty
|
// #define MEMLNAUT_MODE_TYPE MEMLNautModeExtSynthSubPhatty
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
#include "../src/nisps/modes/external_synth_midi.hpp"
|
#include "../src/nisps/modes/external_synth_midi.hpp"
|
||||||
#include "../src/nisps/modes/memlcelium.hpp"
|
#include "../src/nisps/modes/memlcelium.hpp"
|
||||||
#include "../src/nisps/modes/paf_synth.hpp"
|
#include "../src/nisps/modes/paf_synth.hpp"
|
||||||
|
#include "../src/nisps/modes/slp_workshop.hpp"
|
||||||
#include "../src/nisps/modes/sound_analysis_midi.hpp"
|
#include "../src/nisps/modes/sound_analysis_midi.hpp"
|
||||||
#include "../src/nisps/modes/verb_fx.hpp"
|
#include "../src/nisps/modes/verb_fx.hpp"
|
||||||
#include "../src/nisps/modes/xiasri.hpp"
|
#include "../src/nisps/modes/xiasri.hpp"
|
||||||
|
|
@ -58,6 +59,10 @@ using MEMLNautModeBreakOr = ::nisps::modes::BreakOrMode;
|
||||||
using MEMLNautModeVerbFX = ::nisps::modes::VerbFXMode;
|
using MEMLNautModeVerbFX = ::nisps::modes::VerbFXMode;
|
||||||
using MEMLNautModeElysiamorfs = ::nisps::modes::ElysiamorfMode;
|
using MEMLNautModeElysiamorfs = ::nisps::modes::ElysiamorfMode;
|
||||||
using MEMLNautModeMEMLCelium = ::nisps::modes::MEMLCeliumMode;
|
using MEMLNautModeMEMLCelium = ::nisps::modes::MEMLCeliumMode;
|
||||||
|
// SLP-Workshop: the Synth Library Portland workshop build. Reuses the
|
||||||
|
// MEMLCelium engine; foregrounds the Jolt (TogB1) + OU-explore (RVX1)
|
||||||
|
// adaptive-learning gestures wired in peripherals.hpp.
|
||||||
|
using MEMLNautModeSLPWorkshop = ::nisps::modes::SLPWorkshopMode;
|
||||||
|
|
||||||
// ---- External-synth MIDI-CC variants (one flashable variant per device) ----
|
// ---- External-synth MIDI-CC variants (one flashable variant per device) ----
|
||||||
// Joystick -> MLP -> MIDI CC for the named external hardware synth. Device
|
// Joystick -> MLP -> MIDI CC for the named external hardware synth. Device
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@
|
||||||
// MomA1 (TA up) : randomise / draw weights
|
// MomA1 (TA up) : randomise / draw weights
|
||||||
// MomA2 (TA down) : clear examples (reset dataset)
|
// MomA2 (TA down) : clear examples (reset dataset)
|
||||||
// MomB1 (MA up) : randomise (synonym, deliberate)
|
// MomB1 (MA up) : randomise (synonym, deliberate)
|
||||||
// MomB2 (MA down) : jolt / move_weights
|
// MomB2 (MA down) : nudge (small bounded perturbation)
|
||||||
// TogB1 : add example (latched: when high, capture current
|
// TogB1 : Jolt — held continuous weight morph (up=morph, down=freeze)
|
||||||
// inputs+outputs as a training pair)
|
|
||||||
// TogB2 : train (rising-edge → call ml.train())
|
// TogB2 : train (rising-edge → call ml.train())
|
||||||
|
// RVX1 : exploration amount (Ornstein-Uhlenbeck output walk)
|
||||||
//
|
//
|
||||||
// The button block below wires the SHARED ExploreAndPlace lifecycle
|
// The button block below wires the SHARED ExploreAndPlace lifecycle
|
||||||
// (nisps/ml/feedback.hpp, the same core the browser drives via WASM) to the
|
// (nisps/ml/feedback.hpp, the same core the browser drives via WASM) to the
|
||||||
|
|
@ -97,6 +97,15 @@ inline void bind_peripherals(Mode& mode) {
|
||||||
AudioDriver::SetMasterVolume(v);
|
AudioDriver::SetMasterVolume(v);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// RVX1 → exploration amount. Drives the Ornstein-Uhlenbeck random walk
|
||||||
|
// added to the mode's output vector (nisps/ml/ou_noise.hpp, ported from
|
||||||
|
// upstream InterfaceRL). 0 = off (inert); turning it up makes the sound
|
||||||
|
// slowly roam so likes/dislikes can steer the net. Available on every
|
||||||
|
// mode via ModeBase::set_explore_intensity.
|
||||||
|
meml->setRVX1Callback([&mode](float v) {
|
||||||
|
mode.set_explore_intensity(v);
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Buttons / toggles → ExploreAndPlace lifecycle (SHARED C++ core) ----
|
// ---- Buttons / toggles → ExploreAndPlace lifecycle (SHARED C++ core) ----
|
||||||
//
|
//
|
||||||
// The same nisps::ml::FeedbackController that runs in the browser (via
|
// The same nisps::ml::FeedbackController that runs in the browser (via
|
||||||
|
|
@ -147,6 +156,17 @@ inline void bind_peripherals(Mode& mode) {
|
||||||
feedback.begin_place(mode.ml());
|
feedback.begin_place(mode.ml());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TogB1: Jolt — held continuous weight morph (nisps/ml/jolt.hpp, ported
|
||||||
|
// from upstream InterfaceRL). Flip up to start morphing a scatter of
|
||||||
|
// weights live; flip down to freeze them (the change is permanent) and
|
||||||
|
// let the learning rate ramp gently back in. A toggle (not a momentary)
|
||||||
|
// because Jolt is a HELD gesture and momentary buttons only fire on
|
||||||
|
// press. Available on every mode via ModeBase::jolt_press/jolt_release.
|
||||||
|
meml->setTogB1Callback([&mode](bool state) {
|
||||||
|
if (state) mode.jolt_press();
|
||||||
|
else mode.jolt_release();
|
||||||
|
});
|
||||||
|
|
||||||
// TogB2 (rising): up → commit place at the current joystick input, then
|
// TogB2 (rising): up → commit place at the current joystick input, then
|
||||||
// store the +1 example (input → placed output) and train. Outside Placing,
|
// store the +1 example (input → placed output) and train. Outside Placing,
|
||||||
// just train (legacy behaviour).
|
// just train (legacy behaviour).
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ if(NOT EMSCRIPTEN)
|
||||||
${NISPS_TEST_DIR}/test_mlp_training.cpp
|
${NISPS_TEST_DIR}/test_mlp_training.cpp
|
||||||
${NISPS_TEST_DIR}/test_mlp_loss.cpp
|
${NISPS_TEST_DIR}/test_mlp_loss.cpp
|
||||||
${NISPS_TEST_DIR}/test_mlp_rl.cpp
|
${NISPS_TEST_DIR}/test_mlp_rl.cpp
|
||||||
|
${NISPS_TEST_DIR}/test_mlp_jolt.cpp
|
||||||
|
${NISPS_TEST_DIR}/test_mlp_ou_noise.cpp
|
||||||
${NISPS_TEST_DIR}/test_mlp_feedback.cpp
|
${NISPS_TEST_DIR}/test_mlp_feedback.cpp
|
||||||
${NISPS_TEST_DIR}/test_mlp_serialize.cpp
|
${NISPS_TEST_DIR}/test_mlp_serialize.cpp
|
||||||
)
|
)
|
||||||
|
|
@ -115,6 +117,7 @@ if(NOT EMSCRIPTEN)
|
||||||
${NISPS_TEST_DIR}/test_mode_paf_synth.cpp
|
${NISPS_TEST_DIR}/test_mode_paf_synth.cpp
|
||||||
${NISPS_TEST_DIR}/test_mode_voice_space.cpp
|
${NISPS_TEST_DIR}/test_mode_voice_space.cpp
|
||||||
${NISPS_TEST_DIR}/test_mode_breakor_events.cpp
|
${NISPS_TEST_DIR}/test_mode_breakor_events.cpp
|
||||||
|
${NISPS_TEST_DIR}/test_mode_learning.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(nisps_modes_tests PRIVATE nisps_core)
|
target_link_libraries(nisps_modes_tests PRIVATE nisps_core)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,16 @@
|
||||||
#define NISPS_APP_SRAM __not_in_flash("app")
|
#define NISPS_APP_SRAM __not_in_flash("app")
|
||||||
#define NISPS_FORCE_INLINE __attribute__((always_inline)) inline
|
#define NISPS_FORCE_INLINE __attribute__((always_inline)) inline
|
||||||
#define NISPS_HOT __attribute__((hot))
|
#define NISPS_HOT __attribute__((hot))
|
||||||
|
#define NISPS_NOINLINE __attribute__((noinline))
|
||||||
#else
|
#else
|
||||||
#define NISPS_AUDIO_MEM
|
#define NISPS_AUDIO_MEM
|
||||||
#define NISPS_AUDIO_FUNC(decl) decl
|
#define NISPS_AUDIO_FUNC(decl) decl
|
||||||
#define NISPS_APP_SRAM
|
#define NISPS_APP_SRAM
|
||||||
#define NISPS_FORCE_INLINE inline
|
#define NISPS_FORCE_INLINE inline
|
||||||
#define NISPS_HOT
|
#define NISPS_HOT
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
#define NISPS_NOINLINE __attribute__((noinline))
|
||||||
|
#else
|
||||||
|
#define NISPS_NOINLINE
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
131
nisps/ml/jolt.hpp
Normal file
131
nisps/ml/jolt.hpp
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
// nisps/ml/jolt.hpp — "Jolt": held-button continuous weight morph.
|
||||||
|
//
|
||||||
|
// Ported from upstream memllib InterfaceRL (commit 9fcd459 "jolts",
|
||||||
|
// e291192). While a button/pedal is held, pick a handful of random weights
|
||||||
|
// scattered across the whole network and EMA-glide each toward a bounded
|
||||||
|
// random target; on arrival, re-roll the target so the motion never stops.
|
||||||
|
// Releasing freezes the weights where they landed (the change is permanent),
|
||||||
|
// then ramps the effective learning rate back from 0 → 1 over ~5 s so any
|
||||||
|
// subsequent training eases in instead of yanking the net off the jolted
|
||||||
|
// sound.
|
||||||
|
//
|
||||||
|
// This operates on the MLP's FLAT weight buffer (mlp.hpp `get_weights()` /
|
||||||
|
// `set_weights()` / `weight_count()`), so it is architecture-agnostic — it
|
||||||
|
// does not care how many layers or how wide. It owns a per-instance
|
||||||
|
// deterministic `Rng` (seeded by the caller) so firmware ↔ browser parity
|
||||||
|
// holds.
|
||||||
|
//
|
||||||
|
// DEFAULT STATE IS INERT: a freshly constructed Jolt is inactive, `step()`
|
||||||
|
// is a no-op, and `lr_scale()` returns 1.0 (full LR). Nothing perturbs the
|
||||||
|
// network until `press()` is called.
|
||||||
|
//
|
||||||
|
// Upstream constants (InterfaceRL.hpp kJolt*) are reproduced verbatim in
|
||||||
|
// JoltParams defaults.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#include "../core/perf.hpp"
|
||||||
|
#include "../core/rng.hpp"
|
||||||
|
|
||||||
|
namespace nisps::ml {
|
||||||
|
|
||||||
|
// Upper bound on simultaneously-morphed weights; sizes the index/target
|
||||||
|
// arrays so the class stays heap-free. Upstream uses 40.
|
||||||
|
inline constexpr std::size_t kJoltMaxWeights = 64u;
|
||||||
|
|
||||||
|
struct JoltParams {
|
||||||
|
std::size_t num_weights = 40u; // kJoltNumWeights
|
||||||
|
float morph_rate = 0.017f; // kJoltMorphRate (EMA per tick, ~1s @200Hz)
|
||||||
|
float target_min = -1.2f; // kJoltWeightMin (== weight-init range)
|
||||||
|
float target_max = 0.9f; // kJoltWeightMax
|
||||||
|
float target_epsilon = 0.05f; // kJoltTargetEpsilon (re-roll within this)
|
||||||
|
float lr_ramp_step = 0.001f; // kJoltLRRampStep (1/(5s*200Hz))
|
||||||
|
};
|
||||||
|
|
||||||
|
class Jolt {
|
||||||
|
public:
|
||||||
|
explicit Jolt(std::uint64_t seed) noexcept : rng_(seed) {}
|
||||||
|
|
||||||
|
void set_params(const JoltParams& p) noexcept { params_ = p; }
|
||||||
|
const JoltParams& params() const noexcept { return params_; }
|
||||||
|
|
||||||
|
bool active() const noexcept { return active_; }
|
||||||
|
|
||||||
|
// Effective-learning-rate multiplier for the caller's training step:
|
||||||
|
// 0 while the jolt is held, then ramps 0 → 1 after release. Callers that
|
||||||
|
// train (firmware optimise / playground continuous trainer) should
|
||||||
|
// multiply their LR by this. Pure-example modes that only train on an
|
||||||
|
// explicit gesture may ignore it.
|
||||||
|
float lr_scale() const noexcept { return active_ ? 0.f : lr_ramp_; }
|
||||||
|
|
||||||
|
// Begin a jolt over a flat weight buffer of `weight_count` entries: pick
|
||||||
|
// `num_weights` random global indices and a bounded random target each.
|
||||||
|
void press(std::size_t weight_count) noexcept {
|
||||||
|
active_ = true;
|
||||||
|
lr_ramp_ = 0.f;
|
||||||
|
n_ = params_.num_weights;
|
||||||
|
if (n_ > kJoltMaxWeights) n_ = kJoltMaxWeights;
|
||||||
|
if (weight_count == 0u) { n_ = 0u; return; }
|
||||||
|
for (std::size_t i = 0u; i < n_; ++i) {
|
||||||
|
idx_[i] = static_cast<std::size_t>(rng_.next_u64() % weight_count);
|
||||||
|
target_[i] = roll_target_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per control tick while held: EMA-glide each selected weight toward its
|
||||||
|
// target, re-rolling targets that have been reached. No-op when inactive.
|
||||||
|
NISPS_HOT void step(std::span<float> weights) noexcept {
|
||||||
|
if (!active_) return;
|
||||||
|
const std::size_t wc = weights.size();
|
||||||
|
for (std::size_t i = 0u; i < n_; ++i) {
|
||||||
|
const std::size_t k = idx_[i];
|
||||||
|
if (k >= wc) continue;
|
||||||
|
float w = weights[k];
|
||||||
|
w += params_.morph_rate * (target_[i] - w);
|
||||||
|
weights[k] = w;
|
||||||
|
if (std::fabs(target_[i] - w) < params_.target_epsilon) {
|
||||||
|
target_[i] = roll_target_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release: freeze weights where they are (permanent) and re-arm the LR
|
||||||
|
// ramp from 0.
|
||||||
|
void release() noexcept {
|
||||||
|
active_ = false;
|
||||||
|
lr_ramp_ = 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the post-release LR ramp toward full. Call once per control
|
||||||
|
// tick; a no-op while active or already ramped.
|
||||||
|
void tick_lr_ramp() noexcept {
|
||||||
|
if (active_ || lr_ramp_ >= 1.f) return;
|
||||||
|
lr_ramp_ += params_.lr_ramp_step;
|
||||||
|
if (lr_ramp_ > 1.f) lr_ramp_ = 1.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-seed the internal RNG (parity / reset).
|
||||||
|
void seed(std::uint64_t s) noexcept { rng_.seed(s); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
NISPS_FORCE_INLINE float roll_target_() noexcept {
|
||||||
|
return params_.target_min +
|
||||||
|
rng_.next_float_uniform() * (params_.target_max - params_.target_min);
|
||||||
|
}
|
||||||
|
|
||||||
|
JoltParams params_{};
|
||||||
|
Rng rng_;
|
||||||
|
bool active_ = false;
|
||||||
|
float lr_ramp_ = 1.f;
|
||||||
|
std::size_t n_ = 0u;
|
||||||
|
std::array<std::size_t, kJoltMaxWeights> idx_{};
|
||||||
|
std::array<float, kJoltMaxWeights> target_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nisps::ml
|
||||||
101
nisps/ml/ou_noise.hpp
Normal file
101
nisps/ml/ou_noise.hpp
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// nisps/ml/ou_noise.hpp — Ornstein-Uhlenbeck exploration noise.
|
||||||
|
//
|
||||||
|
// Ported from upstream memllib InterfaceRL (commit d0d8a72 "noise",
|
||||||
|
// e291192), which adds a per-output Ornstein-Uhlenbeck random walk to the
|
||||||
|
// network's action vector. Unlike i.i.d. per-frame noise (which jitters),
|
||||||
|
// an OU process is temporally correlated: each output drifts in long, smooth
|
||||||
|
// sweeps and is gently pulled back toward the mapping output (mean reversion).
|
||||||
|
// Because learning stays active while the noise roams, "likes" registered
|
||||||
|
// during the wander steer the network toward sounds the player wants.
|
||||||
|
//
|
||||||
|
// Discrete Euler-Maruyama update, per output channel x:
|
||||||
|
// x += theta * (mu - x) * dt + noise_scale * N(0,1)
|
||||||
|
// out = clamp(out + x, 0, 1)
|
||||||
|
// where, to make the process's stationary standard deviation equal a
|
||||||
|
// requested `std`:
|
||||||
|
// sigma = std * sqrt(2 * theta) (continuous OU relation)
|
||||||
|
// noise_scale = sigma * sqrt(dt) = std * sqrt(2 * theta * dt)
|
||||||
|
//
|
||||||
|
// The exploration knob [0,1] maps to the stationary std via
|
||||||
|
// `set_intensity(level)` → std = level * kMaxAmplitude (0.65, upstream).
|
||||||
|
//
|
||||||
|
// DEFAULT STATE IS INERT: intensity defaults to 0, `enabled()` is false, and
|
||||||
|
// `apply()` neither advances the RNG nor touches the output — so a mode that
|
||||||
|
// never sets an intensity behaves bit-identically to one without OU at all
|
||||||
|
// (parity-safe). Owns a per-instance deterministic `Rng`.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#include "../core/perf.hpp"
|
||||||
|
#include "../core/rng.hpp"
|
||||||
|
|
||||||
|
namespace nisps::ml {
|
||||||
|
|
||||||
|
// Upstream kMaxAmplitude: the exploration knob's full-scale stationary std in
|
||||||
|
// parameter space.
|
||||||
|
inline constexpr float kOUMaxAmplitude = 0.65f;
|
||||||
|
|
||||||
|
template <std::size_t N>
|
||||||
|
class OUNoise {
|
||||||
|
public:
|
||||||
|
explicit OUNoise(std::uint64_t seed) noexcept : rng_(seed) {
|
||||||
|
recompute_scale_();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exploration amount in [0,1]; 0 disables (inert). Maps to the OU
|
||||||
|
// stationary std = level * kMaxAmplitude.
|
||||||
|
void set_intensity(float level) noexcept {
|
||||||
|
if (level < 0.f) level = 0.f;
|
||||||
|
else if (level > 1.f) level = 1.f;
|
||||||
|
stationary_std_ = level * kOUMaxAmplitude;
|
||||||
|
recompute_scale_();
|
||||||
|
}
|
||||||
|
float intensity() const noexcept { return stationary_std_ / kOUMaxAmplitude; }
|
||||||
|
bool enabled() const noexcept { return stationary_std_ > 0.f; }
|
||||||
|
|
||||||
|
// OU smoothness controls (upstream defaults theta=0.02, dt=0.001).
|
||||||
|
void set_theta(float theta) noexcept { theta_ = theta; recompute_scale_(); }
|
||||||
|
void set_dt(float dt) noexcept { dt_ = dt; recompute_scale_(); }
|
||||||
|
|
||||||
|
// Advance the per-channel OU state and add it (clamped) to `out`. No-op
|
||||||
|
// when disabled. `out` is the post-inference parameter vector.
|
||||||
|
NISPS_HOT void apply(std::span<float> out) noexcept {
|
||||||
|
if (!enabled()) return;
|
||||||
|
const std::size_t n = out.size() < N ? out.size() : N;
|
||||||
|
for (std::size_t i = 0u; i < n; ++i) {
|
||||||
|
// mu = 0: the walk is an offset that mean-reverts to zero, so the
|
||||||
|
// network's own mapping output stays the anchor.
|
||||||
|
state_[i] += theta_ * (-state_[i]) * dt_ +
|
||||||
|
noise_scale_ * rng_.next_float_gaussian(1.f);
|
||||||
|
float v = out[i] + state_[i];
|
||||||
|
if (v < 0.f) v = 0.f;
|
||||||
|
else if (v > 1.f) v = 1.f;
|
||||||
|
out[i] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() noexcept { state_.fill(0.f); }
|
||||||
|
void seed(std::uint64_t s) noexcept { rng_.seed(s); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
NISPS_FORCE_INLINE void recompute_scale_() noexcept {
|
||||||
|
float k = 2.f * theta_ * dt_;
|
||||||
|
if (k < 0.f) k = 0.f;
|
||||||
|
noise_scale_ = stationary_std_ * std::sqrt(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rng rng_;
|
||||||
|
std::array<float, N> state_{};
|
||||||
|
float theta_ = 0.02f;
|
||||||
|
float dt_ = 0.001f;
|
||||||
|
float stationary_std_ = 0.f;
|
||||||
|
float noise_scale_ = 0.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nisps::ml
|
||||||
|
|
@ -37,6 +37,8 @@
|
||||||
#include "../core/perf.hpp"
|
#include "../core/perf.hpp"
|
||||||
#include "../core/ring_buffer.hpp"
|
#include "../core/ring_buffer.hpp"
|
||||||
#include "../core/types.hpp"
|
#include "../core/types.hpp"
|
||||||
|
#include "../ml/jolt.hpp"
|
||||||
|
#include "../ml/ou_noise.hpp"
|
||||||
#include "generated/schema_types.hpp"
|
#include "generated/schema_types.hpp"
|
||||||
|
|
||||||
namespace nisps {
|
namespace nisps {
|
||||||
|
|
@ -131,7 +133,10 @@ class ModeBase {
|
||||||
|
|
||||||
static constexpr std::size_t input_channel_count() noexcept { return NInputs; }
|
static constexpr std::size_t input_channel_count() noexcept { return NInputs; }
|
||||||
|
|
||||||
explicit ModeBase(std::uint64_t seed = 0xC0FFEEu) noexcept : ml_(seed) {}
|
explicit ModeBase(std::uint64_t seed = 0xC0FFEEu) noexcept
|
||||||
|
: ml_(seed),
|
||||||
|
jolt_(seed ^ 0x91E10C5Eull),
|
||||||
|
ou_(seed ^ 0x0CEA0FF5ull) {}
|
||||||
|
|
||||||
// ---- Mode concept surface ----
|
// ---- Mode concept surface ----
|
||||||
void setup(float sample_rate) noexcept {
|
void setup(float sample_rate) noexcept {
|
||||||
|
|
@ -164,13 +169,33 @@ class ModeBase {
|
||||||
if constexpr (requires(Derived& d) { d.on_pre_inference(); }) {
|
if constexpr (requires(Derived& d) { d.on_pre_inference(); }) {
|
||||||
static_cast<Derived&>(*this).on_pre_inference();
|
static_cast<Derived&>(*this).on_pre_inference();
|
||||||
}
|
}
|
||||||
|
// 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();
|
||||||
|
|
||||||
// Forward (possibly Derived-mutated) channels into the MLP.
|
// Forward (possibly Derived-mutated) channels into the MLP.
|
||||||
for (std::size_t i = 0u; i < NInputs; ++i) {
|
for (std::size_t i = 0u; i < NInputs; ++i) {
|
||||||
ml_.set_input(i, input_channels_[i]);
|
ml_.set_input(i, input_channels_[i]);
|
||||||
}
|
}
|
||||||
ml_.process();
|
ml_.process();
|
||||||
if constexpr (kRouteOutputsToEngine) {
|
if constexpr (kRouteOutputsToEngine) {
|
||||||
engine_.set_params(ml_.outputs());
|
// 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
input_dirty_ = false;
|
input_dirty_ = false;
|
||||||
if constexpr (requires(Derived& d) { d.on_post_inference(); }) {
|
if constexpr (requires(Derived& d) { d.on_post_inference(); }) {
|
||||||
|
|
@ -178,6 +203,27 @@ class ModeBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 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_; }
|
||||||
|
|
||||||
NISPS_HOT NISPS_FORCE_INLINE stereosample_t process(stereosample_t x) noexcept {
|
NISPS_HOT NISPS_FORCE_INLINE stereosample_t process(stereosample_t x) noexcept {
|
||||||
return engine_.process(x);
|
return engine_.process(x);
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +276,19 @@ class ModeBase {
|
||||||
|
|
||||||
float sample_rate() const noexcept { return sample_rate_; }
|
float sample_rate() const noexcept { return sample_rate_; }
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
float sample_rate_ = 48000.f;
|
float sample_rate_ = 48000.f;
|
||||||
EngineT engine_{};
|
EngineT engine_{};
|
||||||
|
|
@ -238,6 +297,17 @@ class ModeBase {
|
||||||
bool input_dirty_ = false;
|
bool input_dirty_ = false;
|
||||||
std::size_t voice_space_idx_ = 0u;
|
std::size_t voice_space_idx_ = 0u;
|
||||||
RingBuffer<ControlEvent, kModeEventBufferSize> events_{};
|
RingBuffer<ControlEvent, kModeEventBufferSize> events_{};
|
||||||
|
// 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_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace nisps
|
} // namespace nisps
|
||||||
|
|
|
||||||
554
nisps/modes/generated/slp_workshop_schema.hpp
Normal file
554
nisps/modes/generated/slp_workshop_schema.hpp
Normal file
|
|
@ -0,0 +1,554 @@
|
||||||
|
// AUTOGENERATED — do not edit. Source: schemas/modes/slp_workshop.json. Run `bun run codegen/generate.ts` to regenerate.
|
||||||
|
#ifndef NISPS_GENERATED_SLP_WORKSHOP_SCHEMA_HPP
|
||||||
|
#define NISPS_GENERATED_SLP_WORKSHOP_SCHEMA_HPP
|
||||||
|
|
||||||
|
#include "schema_types.hpp"
|
||||||
|
|
||||||
|
namespace nisps::modes::generated {
|
||||||
|
|
||||||
|
inline constexpr std::string_view kSlpWorkshopModeId = "slp_workshop";
|
||||||
|
inline constexpr std::string_view kSlpWorkshopEngineId = "memlcelium";
|
||||||
|
|
||||||
|
inline constexpr std::array<std::string_view, 4> kSlpWorkshopInputChannels = {{
|
||||||
|
"joy_x",
|
||||||
|
"joy_y",
|
||||||
|
"joy_z",
|
||||||
|
"joy_w",
|
||||||
|
}};
|
||||||
|
|
||||||
|
inline constexpr std::array<std::size_t, 3> kSlpWorkshopHiddenLayers = {{
|
||||||
|
10u,
|
||||||
|
14u,
|
||||||
|
18u,
|
||||||
|
}};
|
||||||
|
|
||||||
|
inline constexpr MLConfig kSlpWorkshopMLConfig = {
|
||||||
|
4u,
|
||||||
|
56u,
|
||||||
|
0.6f,
|
||||||
|
1.0f,
|
||||||
|
1000u,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr std::size_t kSlpWorkshopParamCount = 56u;
|
||||||
|
inline constexpr std::array<Param, kSlpWorkshopParamCount> kSlpWorkshopParams = {{
|
||||||
|
Param{
|
||||||
|
"seq0_ratio0",
|
||||||
|
"Seq0 Ratio 0",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq0_ratio1",
|
||||||
|
"Seq0 Ratio 1",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq0_ratio2",
|
||||||
|
"Seq0 Ratio 2",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq0_phasor_mul",
|
||||||
|
"Seq0 Phasor Mul",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq0_phase_off",
|
||||||
|
"Seq0 Phase Off",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq0_amp0",
|
||||||
|
"Seq0 Amp 0",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq0_amp1",
|
||||||
|
"Seq0 Amp 1",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_ratio0",
|
||||||
|
"Seq1 Ratio 0",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_ratio1",
|
||||||
|
"Seq1 Ratio 1",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_ratio2",
|
||||||
|
"Seq1 Ratio 2",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_phasor_mul",
|
||||||
|
"Seq1 Phasor Mul",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_phase_off",
|
||||||
|
"Seq1 Phase Off",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_amp0",
|
||||||
|
"Seq1 Amp 0",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"seq1_amp1",
|
||||||
|
"Seq1 Amp 1",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"sequencer",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_base_freq",
|
||||||
|
"V0 Base Freq",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf0_cf",
|
||||||
|
"V0 PAF0 CF",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf1_cf",
|
||||||
|
"V0 PAF1 CF",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf2_cf",
|
||||||
|
"V0 PAF2 CF",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf0_bw",
|
||||||
|
"V0 PAF0 BW",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf1_bw",
|
||||||
|
"V0 PAF1 BW",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf2_bw",
|
||||||
|
"V0 PAF2 BW",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf_vib",
|
||||||
|
"V0 PAF Vib",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::square,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf_vfr",
|
||||||
|
"V0 PAF Vib Rate",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::square,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf0_shift",
|
||||||
|
"V0 PAF0 Shift",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf1_shift",
|
||||||
|
"V0 PAF1 Shift",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_paf2_shift",
|
||||||
|
"V0 PAF2 Shift",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_amp_attack",
|
||||||
|
"V0 Amp Attack",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_amp_decay",
|
||||||
|
"V0 Amp Decay",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.3f,
|
||||||
|
Curve::square,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_amp_sustain",
|
||||||
|
"V0 Amp Sustain",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_amp_release",
|
||||||
|
"V0 Amp Release",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.3f,
|
||||||
|
Curve::square,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_pitch_attack",
|
||||||
|
"V0 Pitch Attack",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_pitch_decay",
|
||||||
|
"V0 Pitch Decay",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.3f,
|
||||||
|
Curve::square,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_pitch_emph",
|
||||||
|
"V0 Pitch Emph",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_shape_gain",
|
||||||
|
"V0 Shape Gain",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_shape_asym",
|
||||||
|
"V0 Shape Asym",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_shape_mix",
|
||||||
|
"V0 Shape Mix",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v0_rm_gain",
|
||||||
|
"V0 RingMod Gain",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v0_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_base_freq",
|
||||||
|
"V1 Base Freq",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_detune1",
|
||||||
|
"V1 Detune 1",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_detune2",
|
||||||
|
"V1 Detune 2",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf0_cf",
|
||||||
|
"V1 PAF0 CF",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf1_cf",
|
||||||
|
"V1 PAF1 CF",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf2_cf",
|
||||||
|
"V1 PAF2 CF",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf0_bw",
|
||||||
|
"V1 PAF0 BW",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf1_bw",
|
||||||
|
"V1 PAF1 BW",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf2_bw",
|
||||||
|
"V1 PAF2 BW",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf0_shift",
|
||||||
|
"V1 PAF0 Shift",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf1_shift",
|
||||||
|
"V1 PAF1 Shift",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_paf2_shift",
|
||||||
|
"V1 PAF2 Shift",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_synth",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_amp_attack",
|
||||||
|
"V1 Amp Attack",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_amp_decay",
|
||||||
|
"V1 Amp Decay",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.3f,
|
||||||
|
Curve::square,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_amp_sustain",
|
||||||
|
"V1 Amp Sustain",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.5f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_amp_release",
|
||||||
|
"V1 Amp Release",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.3f,
|
||||||
|
Curve::square,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_pitch_attack",
|
||||||
|
"V1 Pitch Attack",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_pitch_decay",
|
||||||
|
"V1 Pitch Decay",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.3f,
|
||||||
|
Curve::square,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
Param{
|
||||||
|
"v1_pitch_emph",
|
||||||
|
"V1 Pitch Emph",
|
||||||
|
0.0f,
|
||||||
|
1.0f,
|
||||||
|
0.0f,
|
||||||
|
Curve::linear,
|
||||||
|
"v1_env",
|
||||||
|
},
|
||||||
|
}};
|
||||||
|
|
||||||
|
inline constexpr std::size_t kSlpWorkshopVoiceSpaceCount = 1u;
|
||||||
|
inline constexpr std::array<std::string_view, kSlpWorkshopVoiceSpaceCount> kSlpWorkshopVoiceSpaces = {{
|
||||||
|
"Direct",
|
||||||
|
}};
|
||||||
|
|
||||||
|
inline constexpr UIConfig kSlpWorkshopUI = {
|
||||||
|
PrimaryInput::XYPad,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nisps::modes::generated
|
||||||
|
|
||||||
|
#endif // NISPS_GENERATED_SLP_WORKSHOP_SCHEMA_HPP
|
||||||
78
nisps/modes/slp_workshop.hpp
Normal file
78
nisps/modes/slp_workshop.hpp
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// nisps/modes/slp_workshop.hpp — SLP-Workshop mode binding.
|
||||||
|
//
|
||||||
|
// The Synth Library Portland workshop instrument. It reuses the MEMLCelium
|
||||||
|
// engine and MLP shape verbatim (4 input channels → MLP[4,10,14,18,56] →
|
||||||
|
// MEMLCeliumEngine, sequencer ticked internally in process()), but exists as
|
||||||
|
// a distinct mode so the workshop firmware carries its own identity (preset
|
||||||
|
// dir, display name) and foregrounds the adaptive-learning gestures that now
|
||||||
|
// live in ModeBase for every mode:
|
||||||
|
//
|
||||||
|
// - Jolt — held gesture, continuously morphs a scatter of weights and
|
||||||
|
// freezes them on release (Base::jolt_press/jolt_release).
|
||||||
|
// - OU explore — Ornstein-Uhlenbeck random walk on the output vector
|
||||||
|
// (Base::set_explore_intensity).
|
||||||
|
//
|
||||||
|
// Both were ported from upstream memllib InterfaceRL (see ml/jolt.hpp,
|
||||||
|
// ml/ou_noise.hpp). Because they sit in the shared base, the synthesis
|
||||||
|
// mapping here is identical to MEMLCeliumMode — only the surfaced controls
|
||||||
|
// and identity differ.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#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/slp_workshop_schema.hpp"
|
||||||
|
|
||||||
|
namespace nisps::modes {
|
||||||
|
|
||||||
|
class SLPWorkshopMode : public ModeBase<
|
||||||
|
SLPWorkshopMode,
|
||||||
|
MEMLCeliumEngine,
|
||||||
|
ml::MLP<4u, 10u, 14u, 18u, 56u>,
|
||||||
|
4u> {
|
||||||
|
public:
|
||||||
|
using Base = ModeBase<SLPWorkshopMode, MEMLCeliumEngine,
|
||||||
|
ml::MLP<4u, 10u, 14u, 18u, 56u>, 4u>;
|
||||||
|
using Base::Base;
|
||||||
|
|
||||||
|
static constexpr std::string_view mode_id() noexcept {
|
||||||
|
return generated::kSlpWorkshopModeId;
|
||||||
|
}
|
||||||
|
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::kSlpWorkshopModeId,
|
||||||
|
generated::kSlpWorkshopEngineId,
|
||||||
|
std::span<const std::string_view>(generated::kSlpWorkshopInputChannels),
|
||||||
|
generated::kSlpWorkshopMLConfig.input_size,
|
||||||
|
std::span<const std::size_t>(generated::kSlpWorkshopHiddenLayers),
|
||||||
|
generated::kSlpWorkshopMLConfig.output_size,
|
||||||
|
generated::kSlpWorkshopMLConfig.default_spread,
|
||||||
|
generated::kSlpWorkshopMLConfig.default_learning_rate,
|
||||||
|
generated::kSlpWorkshopMLConfig.default_max_iterations,
|
||||||
|
std::span<const generated::Param>(generated::kSlpWorkshopParams),
|
||||||
|
std::span<const std::string_view>(generated::kSlpWorkshopVoiceSpaces),
|
||||||
|
generated::kSlpWorkshopUI,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(Mode<SLPWorkshopMode>, "SLPWorkshopMode must satisfy nisps::Mode");
|
||||||
|
|
||||||
|
} // namespace nisps::modes
|
||||||
62
playground/src/modes/SLPWorkshopMode.tsx
Normal file
62
playground/src/modes/SLPWorkshopMode.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* SLPWorkshopMode — the Synth Library Portland workshop instrument.
|
||||||
|
*
|
||||||
|
* Reuses the MEMLCelium engine and MLP shape verbatim (so the synth voice is
|
||||||
|
* identical), but foregrounds the adaptive-learning gestures ported from
|
||||||
|
* upstream InterfaceRL and now living in the shared core:
|
||||||
|
* - Jolt: hold to continuously morph the network's weights live, release to
|
||||||
|
* freeze (nisps/ml/jolt.hpp).
|
||||||
|
* - Explore: an Ornstein-Uhlenbeck random walk on the output that makes the
|
||||||
|
* sound slowly roam so likes/dislikes can steer it (nisps/ml/ou_noise.hpp).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, createSignal } from 'solid-js';
|
||||||
|
import { ModeShell } from './ModeShell';
|
||||||
|
import { useModeRuntime } from './mode-runtime';
|
||||||
|
import { XYPad } from '../primitives/XYPad';
|
||||||
|
import { OutputDisplay } from '../primitives/OutputDisplay';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { SlpWorkshopSchema } from './generated/slp_workshop_schema';
|
||||||
|
|
||||||
|
export const SLPWorkshopMode: Component = () => {
|
||||||
|
const schema = SlpWorkshopSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
activeVoiceSpace={voiceSpace}
|
||||||
|
onVoiceSpaceChange={setVoiceSpace}
|
||||||
|
drawerTitle="SLP-Workshop settings"
|
||||||
|
primaryInput={() => (
|
||||||
|
<>
|
||||||
|
<XYPad
|
||||||
|
size={280}
|
||||||
|
ariaLabel="SLP-Workshop pad"
|
||||||
|
onMove={(x, y) => runtime.setInput(x, y)}
|
||||||
|
position={runtime.pipedInput}
|
||||||
|
/>
|
||||||
|
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
||||||
|
Synth Library Portland workshop instrument — MEMLCelium voice with
|
||||||
|
live Jolt + Explore learning controls.
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
outputArea={() => (
|
||||||
|
<>
|
||||||
|
<OutputDisplay
|
||||||
|
values={runtime.paramOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="#ef9e5b"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SLPWorkshopMode;
|
||||||
|
|
@ -7,6 +7,7 @@ export * from './channel_strip_schema';
|
||||||
export * from './elysiamorf_schema';
|
export * from './elysiamorf_schema';
|
||||||
export * from './memlcelium_schema';
|
export * from './memlcelium_schema';
|
||||||
export * from './paf_synth_schema';
|
export * from './paf_synth_schema';
|
||||||
|
export * from './slp_workshop_schema';
|
||||||
export * from './sound_analysis_midi_schema';
|
export * from './sound_analysis_midi_schema';
|
||||||
export * from './verb_fx_schema';
|
export * from './verb_fx_schema';
|
||||||
export * from './xiasri_schema';
|
export * from './xiasri_schema';
|
||||||
|
|
|
||||||
598
playground/src/modes/generated/slp_workshop_schema.ts
Normal file
598
playground/src/modes/generated/slp_workshop_schema.ts
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
// AUTOGENERATED — do not edit. Source: schemas/modes/slp_workshop.json. Run `bun run codegen/generate.ts` to regenerate.
|
||||||
|
import type { ModeSchema } from './types';
|
||||||
|
|
||||||
|
export interface SlpWorkshopParams {
|
||||||
|
readonly seq0_ratio0: number;
|
||||||
|
readonly seq0_ratio1: number;
|
||||||
|
readonly seq0_ratio2: number;
|
||||||
|
readonly seq0_phasor_mul: number;
|
||||||
|
readonly seq0_phase_off: number;
|
||||||
|
readonly seq0_amp0: number;
|
||||||
|
readonly seq0_amp1: number;
|
||||||
|
readonly seq1_ratio0: number;
|
||||||
|
readonly seq1_ratio1: number;
|
||||||
|
readonly seq1_ratio2: number;
|
||||||
|
readonly seq1_phasor_mul: number;
|
||||||
|
readonly seq1_phase_off: number;
|
||||||
|
readonly seq1_amp0: number;
|
||||||
|
readonly seq1_amp1: number;
|
||||||
|
readonly v0_base_freq: number;
|
||||||
|
readonly v0_paf0_cf: number;
|
||||||
|
readonly v0_paf1_cf: number;
|
||||||
|
readonly v0_paf2_cf: number;
|
||||||
|
readonly v0_paf0_bw: number;
|
||||||
|
readonly v0_paf1_bw: number;
|
||||||
|
readonly v0_paf2_bw: number;
|
||||||
|
readonly v0_paf_vib: number;
|
||||||
|
readonly v0_paf_vfr: number;
|
||||||
|
readonly v0_paf0_shift: number;
|
||||||
|
readonly v0_paf1_shift: number;
|
||||||
|
readonly v0_paf2_shift: number;
|
||||||
|
readonly v0_amp_attack: number;
|
||||||
|
readonly v0_amp_decay: number;
|
||||||
|
readonly v0_amp_sustain: number;
|
||||||
|
readonly v0_amp_release: number;
|
||||||
|
readonly v0_pitch_attack: number;
|
||||||
|
readonly v0_pitch_decay: number;
|
||||||
|
readonly v0_pitch_emph: number;
|
||||||
|
readonly v0_shape_gain: number;
|
||||||
|
readonly v0_shape_asym: number;
|
||||||
|
readonly v0_shape_mix: number;
|
||||||
|
readonly v0_rm_gain: number;
|
||||||
|
readonly v1_base_freq: number;
|
||||||
|
readonly v1_detune1: number;
|
||||||
|
readonly v1_detune2: number;
|
||||||
|
readonly v1_paf0_cf: number;
|
||||||
|
readonly v1_paf1_cf: number;
|
||||||
|
readonly v1_paf2_cf: number;
|
||||||
|
readonly v1_paf0_bw: number;
|
||||||
|
readonly v1_paf1_bw: number;
|
||||||
|
readonly v1_paf2_bw: number;
|
||||||
|
readonly v1_paf0_shift: number;
|
||||||
|
readonly v1_paf1_shift: number;
|
||||||
|
readonly v1_paf2_shift: number;
|
||||||
|
readonly v1_amp_attack: number;
|
||||||
|
readonly v1_amp_decay: number;
|
||||||
|
readonly v1_amp_sustain: number;
|
||||||
|
readonly v1_amp_release: number;
|
||||||
|
readonly v1_pitch_attack: number;
|
||||||
|
readonly v1_pitch_decay: number;
|
||||||
|
readonly v1_pitch_emph: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlpWorkshopSchema: ModeSchema = {
|
||||||
|
mode_id: 'slp_workshop',
|
||||||
|
engine_id: 'memlcelium',
|
||||||
|
ml: {
|
||||||
|
input_channels: [
|
||||||
|
'joy_x',
|
||||||
|
'joy_y',
|
||||||
|
'joy_z',
|
||||||
|
'joy_w',
|
||||||
|
],
|
||||||
|
input_size: 4,
|
||||||
|
hidden_layers: [
|
||||||
|
10,
|
||||||
|
14,
|
||||||
|
18,
|
||||||
|
],
|
||||||
|
output_size: 56,
|
||||||
|
default_spread: 0.6,
|
||||||
|
default_learning_rate: 1,
|
||||||
|
default_max_iterations: 1000,
|
||||||
|
},
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'seq0_ratio0',
|
||||||
|
label: 'Seq0 Ratio 0',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq0_ratio1',
|
||||||
|
label: 'Seq0 Ratio 1',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq0_ratio2',
|
||||||
|
label: 'Seq0 Ratio 2',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq0_phasor_mul',
|
||||||
|
label: 'Seq0 Phasor Mul',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq0_phase_off',
|
||||||
|
label: 'Seq0 Phase Off',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq0_amp0',
|
||||||
|
label: 'Seq0 Amp 0',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq0_amp1',
|
||||||
|
label: 'Seq0 Amp 1',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_ratio0',
|
||||||
|
label: 'Seq1 Ratio 0',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_ratio1',
|
||||||
|
label: 'Seq1 Ratio 1',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_ratio2',
|
||||||
|
label: 'Seq1 Ratio 2',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_phasor_mul',
|
||||||
|
label: 'Seq1 Phasor Mul',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_phase_off',
|
||||||
|
label: 'Seq1 Phase Off',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_amp0',
|
||||||
|
label: 'Seq1 Amp 0',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seq1_amp1',
|
||||||
|
label: 'Seq1 Amp 1',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'sequencer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_base_freq',
|
||||||
|
label: 'V0 Base Freq',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf0_cf',
|
||||||
|
label: 'V0 PAF0 CF',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf1_cf',
|
||||||
|
label: 'V0 PAF1 CF',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf2_cf',
|
||||||
|
label: 'V0 PAF2 CF',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf0_bw',
|
||||||
|
label: 'V0 PAF0 BW',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf1_bw',
|
||||||
|
label: 'V0 PAF1 BW',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf2_bw',
|
||||||
|
label: 'V0 PAF2 BW',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf_vib',
|
||||||
|
label: 'V0 PAF Vib',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf_vfr',
|
||||||
|
label: 'V0 PAF Vib Rate',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf0_shift',
|
||||||
|
label: 'V0 PAF0 Shift',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf1_shift',
|
||||||
|
label: 'V0 PAF1 Shift',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_paf2_shift',
|
||||||
|
label: 'V0 PAF2 Shift',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_amp_attack',
|
||||||
|
label: 'V0 Amp Attack',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_amp_decay',
|
||||||
|
label: 'V0 Amp Decay',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.3,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_amp_sustain',
|
||||||
|
label: 'V0 Amp Sustain',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_amp_release',
|
||||||
|
label: 'V0 Amp Release',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.3,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_pitch_attack',
|
||||||
|
label: 'V0 Pitch Attack',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_pitch_decay',
|
||||||
|
label: 'V0 Pitch Decay',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.3,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_pitch_emph',
|
||||||
|
label: 'V0 Pitch Emph',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_shape_gain',
|
||||||
|
label: 'V0 Shape Gain',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_shape_asym',
|
||||||
|
label: 'V0 Shape Asym',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_shape_mix',
|
||||||
|
label: 'V0 Shape Mix',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v0_rm_gain',
|
||||||
|
label: 'V0 RingMod Gain',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v0_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_base_freq',
|
||||||
|
label: 'V1 Base Freq',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_detune1',
|
||||||
|
label: 'V1 Detune 1',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_detune2',
|
||||||
|
label: 'V1 Detune 2',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf0_cf',
|
||||||
|
label: 'V1 PAF0 CF',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf1_cf',
|
||||||
|
label: 'V1 PAF1 CF',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf2_cf',
|
||||||
|
label: 'V1 PAF2 CF',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf0_bw',
|
||||||
|
label: 'V1 PAF0 BW',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf1_bw',
|
||||||
|
label: 'V1 PAF1 BW',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf2_bw',
|
||||||
|
label: 'V1 PAF2 BW',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf0_shift',
|
||||||
|
label: 'V1 PAF0 Shift',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf1_shift',
|
||||||
|
label: 'V1 PAF1 Shift',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_paf2_shift',
|
||||||
|
label: 'V1 PAF2 Shift',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_synth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_amp_attack',
|
||||||
|
label: 'V1 Amp Attack',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_amp_decay',
|
||||||
|
label: 'V1 Amp Decay',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.3,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_amp_sustain',
|
||||||
|
label: 'V1 Amp Sustain',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.5,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_amp_release',
|
||||||
|
label: 'V1 Amp Release',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.3,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_pitch_attack',
|
||||||
|
label: 'V1 Pitch Attack',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_pitch_decay',
|
||||||
|
label: 'V1 Pitch Decay',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0.3,
|
||||||
|
curve: 'square',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v1_pitch_emph',
|
||||||
|
label: 'V1 Pitch Emph',
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
default: 0,
|
||||||
|
curve: 'linear',
|
||||||
|
group: 'v1_env',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
voice_spaces: [
|
||||||
|
'Direct',
|
||||||
|
],
|
||||||
|
ui: {
|
||||||
|
primary_input: 'xy_pad',
|
||||||
|
show_voice_space_selector: false,
|
||||||
|
show_synth_visualizer: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -12,6 +12,7 @@ import { ChannelStripMode } from './ChannelStripMode';
|
||||||
import { XIASRIMode } from './XIASRIMode';
|
import { XIASRIMode } from './XIASRIMode';
|
||||||
import { VerbFXMode } from './VerbFXMode';
|
import { VerbFXMode } from './VerbFXMode';
|
||||||
import { MEMLCeliumMode } from './MEMLCeliumMode';
|
import { MEMLCeliumMode } from './MEMLCeliumMode';
|
||||||
|
import { SLPWorkshopMode } from './SLPWorkshopMode';
|
||||||
import { BreakOrMode } from './BreakOrMode';
|
import { BreakOrMode } from './BreakOrMode';
|
||||||
import { ElysiamorfMode } from './ElysiamorfMode';
|
import { ElysiamorfMode } from './ElysiamorfMode';
|
||||||
import { SoundAnalysisMIDIMode } from './SoundAnalysisMIDIMode';
|
import { SoundAnalysisMIDIMode } from './SoundAnalysisMIDIMode';
|
||||||
|
|
@ -61,6 +62,12 @@ export const MODE_REGISTRY: ReadonlyArray<ModeRegistration> = [
|
||||||
description: 'Voice + dual-MLP CV/gate via uSEQ.',
|
description: 'Voice + dual-MLP CV/gate via uSEQ.',
|
||||||
Component: MEMLCeliumMode,
|
Component: MEMLCeliumMode,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'slp_workshop',
|
||||||
|
label: 'SLP-Workshop',
|
||||||
|
description: 'MEMLCelium voice + live Jolt / Explore learning (Synth Library Portland).',
|
||||||
|
Component: SLPWorkshopMode,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'breakor',
|
id: 'breakor',
|
||||||
label: 'Breakor',
|
label: 'Breakor',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const MODE_IDS = [
|
||||||
'xiasri',
|
'xiasri',
|
||||||
'verb_fx',
|
'verb_fx',
|
||||||
'memlcelium',
|
'memlcelium',
|
||||||
|
'slp_workshop',
|
||||||
'breakor',
|
'breakor',
|
||||||
'elysiamorf',
|
'elysiamorf',
|
||||||
'sound_analysis_midi',
|
'sound_analysis_midi',
|
||||||
|
|
|
||||||
81
schemas/modes/slp_workshop.json
Normal file
81
schemas/modes/slp_workshop.json
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"$schema": "../schema.json",
|
||||||
|
"_note": "SLP-Workshop — the Synth Library Portland workshop instrument. Reuses the MEMLCelium engine (engine_id 'memlcelium', 56-param dual-voice PAF synth + 2-track ratio sequencer) and MLP shape verbatim; it exists as a distinct mode so the workshop build carries its own identity (preset dir, display name) and surfaces the adaptive-learning gestures ported from upstream InterfaceRL: Jolt (held continuous weight morph) and OU exploration noise. Both live in the shared ModeBase, so the mapping/params are identical to MEMLCelium — only the surfaced controls differ.",
|
||||||
|
"mode_id": "slp_workshop",
|
||||||
|
"engine_id": "memlcelium",
|
||||||
|
"ml": {
|
||||||
|
"input_channels": ["joy_x", "joy_y", "joy_z", "joy_w"],
|
||||||
|
"input_size": 4,
|
||||||
|
"hidden_layers": [10, 14, 18],
|
||||||
|
"output_size": 56,
|
||||||
|
"default_spread": 0.6,
|
||||||
|
"default_learning_rate": 1.0,
|
||||||
|
"default_max_iterations": 1000
|
||||||
|
},
|
||||||
|
"params": [
|
||||||
|
{ "name": "seq0_ratio0", "label": "Seq0 Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq0_ratio1", "label": "Seq0 Ratio 1", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq0_ratio2", "label": "Seq0 Ratio 2", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq0_phasor_mul","label": "Seq0 Phasor Mul", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq0_phase_off", "label": "Seq0 Phase Off", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq0_amp0", "label": "Seq0 Amp 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq0_amp1", "label": "Seq0 Amp 1", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_ratio0", "label": "Seq1 Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_ratio1", "label": "Seq1 Ratio 1", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_ratio2", "label": "Seq1 Ratio 2", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_phasor_mul","label": "Seq1 Phasor Mul", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_phase_off", "label": "Seq1 Phase Off", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_amp0", "label": "Seq1 Amp 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "seq1_amp1", "label": "Seq1 Amp 1", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
{ "name": "v0_base_freq", "label": "V0 Base Freq", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf0_cf", "label": "V0 PAF0 CF", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf1_cf", "label": "V0 PAF1 CF", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf2_cf", "label": "V0 PAF2 CF", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf0_bw", "label": "V0 PAF0 BW", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf1_bw", "label": "V0 PAF1 BW", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf2_bw", "label": "V0 PAF2 BW", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf_vib", "label": "V0 PAF Vib", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "square", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf_vfr", "label": "V0 PAF Vib Rate", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "square", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf0_shift", "label": "V0 PAF0 Shift", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf1_shift", "label": "V0 PAF1 Shift", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_paf2_shift", "label": "V0 PAF2 Shift", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_amp_attack", "label": "V0 Amp Attack", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_env" },
|
||||||
|
{ "name": "v0_amp_decay", "label": "V0 Amp Decay", "min": 0.0, "max": 1.0, "default": 0.3, "curve": "square", "group": "v0_env" },
|
||||||
|
{ "name": "v0_amp_sustain", "label": "V0 Amp Sustain", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v0_env" },
|
||||||
|
{ "name": "v0_amp_release", "label": "V0 Amp Release", "min": 0.0, "max": 1.0, "default": 0.3, "curve": "square", "group": "v0_env" },
|
||||||
|
{ "name": "v0_pitch_attack","label": "V0 Pitch Attack", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_env" },
|
||||||
|
{ "name": "v0_pitch_decay", "label": "V0 Pitch Decay", "min": 0.0, "max": 1.0, "default": 0.3, "curve": "square", "group": "v0_env" },
|
||||||
|
{ "name": "v0_pitch_emph", "label": "V0 Pitch Emph", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_env" },
|
||||||
|
{ "name": "v0_shape_gain", "label": "V0 Shape Gain", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_shape_asym", "label": "V0 Shape Asym", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_shape_mix", "label": "V0 Shape Mix", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v0_rm_gain", "label": "V0 RingMod Gain", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v0_synth" },
|
||||||
|
{ "name": "v1_base_freq", "label": "V1 Base Freq", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_detune1", "label": "V1 Detune 1", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_detune2", "label": "V1 Detune 2", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf0_cf", "label": "V1 PAF0 CF", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf1_cf", "label": "V1 PAF1 CF", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf2_cf", "label": "V1 PAF2 CF", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf0_bw", "label": "V1 PAF0 BW", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf1_bw", "label": "V1 PAF1 BW", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf2_bw", "label": "V1 PAF2 BW", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf0_shift", "label": "V1 PAF0 Shift", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf1_shift", "label": "V1 PAF1 Shift", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_paf2_shift", "label": "V1 PAF2 Shift", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_synth" },
|
||||||
|
{ "name": "v1_amp_attack", "label": "V1 Amp Attack", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v1_env" },
|
||||||
|
{ "name": "v1_amp_decay", "label": "V1 Amp Decay", "min": 0.0, "max": 1.0, "default": 0.3, "curve": "square", "group": "v1_env" },
|
||||||
|
{ "name": "v1_amp_sustain", "label": "V1 Amp Sustain", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "v1_env" },
|
||||||
|
{ "name": "v1_amp_release", "label": "V1 Amp Release", "min": 0.0, "max": 1.0, "default": 0.3, "curve": "square", "group": "v1_env" },
|
||||||
|
{ "name": "v1_pitch_attack","label": "V1 Pitch Attack", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v1_env" },
|
||||||
|
{ "name": "v1_pitch_decay", "label": "V1 Pitch Decay", "min": 0.0, "max": 1.0, "default": 0.3, "curve": "square", "group": "v1_env" },
|
||||||
|
{ "name": "v1_pitch_emph", "label": "V1 Pitch Emph", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "v1_env" }
|
||||||
|
],
|
||||||
|
"voice_spaces": [
|
||||||
|
"Direct"
|
||||||
|
],
|
||||||
|
"ui": {
|
||||||
|
"primary_input": "xy_pad",
|
||||||
|
"show_voice_space_selector": false,
|
||||||
|
"show_synth_visualizer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
94
tests/cpp/test_mlp_jolt.cpp
Normal file
94
tests/cpp/test_mlp_jolt.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
// tests/cpp/test_mlp_jolt.cpp — Jolt (held continuous weight morph) behavior.
|
||||||
|
//
|
||||||
|
// - default state is inert: inactive, step() is a no-op, lr_scale()==1.
|
||||||
|
// - press() activates and pauses learning (lr_scale()==0).
|
||||||
|
// - step() morphs selected weights toward bounded targets, staying within
|
||||||
|
// the [target_min, target_max] envelope.
|
||||||
|
// - release() freezes and re-arms the LR ramp (0 → 1 via tick_lr_ramp).
|
||||||
|
// - same seed ⇒ identical morph (determinism / parity).
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#include "test_helpers.hpp"
|
||||||
|
|
||||||
|
#include "../../nisps/ml/jolt.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using nisps::ml::Jolt;
|
||||||
|
using nisps::ml::JoltParams;
|
||||||
|
|
||||||
|
NISPS_TEST(jolt_inert_by_default) {
|
||||||
|
Jolt j(0ull);
|
||||||
|
NISPS_EXPECT(!j.active());
|
||||||
|
NISPS_EXPECT(j.lr_scale() == 1.f);
|
||||||
|
std::array<float, 32> w{};
|
||||||
|
w.fill(0.5f);
|
||||||
|
j.step(std::span<float>(w)); // no-op while inactive
|
||||||
|
for (float v : w) NISPS_EXPECT(v == 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(jolt_press_activates_and_pauses_learning) {
|
||||||
|
Jolt j(1ull);
|
||||||
|
j.press(100u);
|
||||||
|
NISPS_EXPECT(j.active());
|
||||||
|
NISPS_EXPECT(j.lr_scale() == 0.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(jolt_step_morphs_weights_within_envelope) {
|
||||||
|
Jolt j(7ull);
|
||||||
|
JoltParams p;
|
||||||
|
p.num_weights = 16u;
|
||||||
|
j.set_params(p);
|
||||||
|
|
||||||
|
std::array<float, 64> w{};
|
||||||
|
w.fill(0.f);
|
||||||
|
j.press(w.size());
|
||||||
|
for (int i = 0; i < 300; ++i) j.step(std::span<float>(w));
|
||||||
|
|
||||||
|
int changed = 0;
|
||||||
|
for (float v : w) {
|
||||||
|
if (std::fabs(v) > 1e-4f) ++changed;
|
||||||
|
// Targets are bounded to [target_min, target_max] = [-1.2, 0.9];
|
||||||
|
// EMA interpolation keeps weights inside that envelope.
|
||||||
|
NISPS_EXPECT(v >= p.target_min - 0.01f);
|
||||||
|
NISPS_EXPECT(v <= p.target_max + 0.01f);
|
||||||
|
}
|
||||||
|
NISPS_EXPECT(changed > 0);
|
||||||
|
NISPS_EXPECT(changed <= 16); // at most num_weights distinct indices
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(jolt_release_ramps_lr_back_to_full) {
|
||||||
|
Jolt j(3ull);
|
||||||
|
j.press(50u);
|
||||||
|
NISPS_EXPECT(j.lr_scale() == 0.f);
|
||||||
|
|
||||||
|
j.release();
|
||||||
|
NISPS_EXPECT(!j.active());
|
||||||
|
NISPS_EXPECT(j.lr_scale() == 0.f); // ramp re-armed at 0
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) j.tick_lr_ramp();
|
||||||
|
NISPS_EXPECT(j.lr_scale() > 0.f && j.lr_scale() <= 1.f);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2000; ++i) j.tick_lr_ramp(); // default step 0.001
|
||||||
|
NISPS_EXPECT_NEAR(j.lr_scale(), 1.f, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(jolt_deterministic_same_seed) {
|
||||||
|
Jolt a(99ull), b(99ull);
|
||||||
|
std::array<float, 48> wa{}, wb{};
|
||||||
|
wa.fill(0.25f);
|
||||||
|
wb.fill(0.25f);
|
||||||
|
a.press(wa.size());
|
||||||
|
b.press(wb.size());
|
||||||
|
for (int i = 0; i < 150; ++i) {
|
||||||
|
a.step(std::span<float>(wa));
|
||||||
|
b.step(std::span<float>(wb));
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < wa.size(); ++i) NISPS_EXPECT(wa[i] == wb[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
77
tests/cpp/test_mlp_ou_noise.cpp
Normal file
77
tests/cpp/test_mlp_ou_noise.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// tests/cpp/test_mlp_ou_noise.cpp — Ornstein-Uhlenbeck exploration noise.
|
||||||
|
//
|
||||||
|
// - default state is inert: intensity 0, disabled, apply() leaves the
|
||||||
|
// output untouched (parity-safe).
|
||||||
|
// - set_intensity enables it; apply() drifts the output but keeps it in
|
||||||
|
// [0, 1].
|
||||||
|
// - same seed ⇒ identical walk (determinism / parity).
|
||||||
|
// - intensity back to 0 disables it again (inert).
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#include "test_helpers.hpp"
|
||||||
|
|
||||||
|
#include "../../nisps/ml/ou_noise.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using nisps::ml::OUNoise;
|
||||||
|
|
||||||
|
NISPS_TEST(ou_inert_by_default) {
|
||||||
|
OUNoise<8> ou(0ull);
|
||||||
|
NISPS_EXPECT(!ou.enabled());
|
||||||
|
NISPS_EXPECT(ou.intensity() == 0.f);
|
||||||
|
std::array<float, 8> o{};
|
||||||
|
o.fill(0.5f);
|
||||||
|
ou.apply(std::span<float>(o)); // no-op while disabled
|
||||||
|
for (float v : o) NISPS_EXPECT(v == 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(ou_intensity_enables_and_perturbs_bounded) {
|
||||||
|
OUNoise<8> ou(5ull);
|
||||||
|
ou.set_intensity(0.6f);
|
||||||
|
NISPS_EXPECT(ou.enabled());
|
||||||
|
|
||||||
|
bool moved = false;
|
||||||
|
for (int t = 0; t < 500; ++t) {
|
||||||
|
std::array<float, 8> o{};
|
||||||
|
o.fill(0.5f);
|
||||||
|
ou.apply(std::span<float>(o));
|
||||||
|
for (float v : o) {
|
||||||
|
NISPS_EXPECT(v >= 0.f && v <= 1.f); // clamped to param space
|
||||||
|
if (std::fabs(v - 0.5f) > 1e-4f) moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NISPS_EXPECT(moved);
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(ou_deterministic_same_seed) {
|
||||||
|
OUNoise<4> a(42ull), b(42ull);
|
||||||
|
a.set_intensity(0.5f);
|
||||||
|
b.set_intensity(0.5f);
|
||||||
|
for (int t = 0; t < 100; ++t) {
|
||||||
|
std::array<float, 4> oa{}, ob{};
|
||||||
|
oa.fill(0.3f);
|
||||||
|
ob.fill(0.3f);
|
||||||
|
a.apply(std::span<float>(oa));
|
||||||
|
b.apply(std::span<float>(ob));
|
||||||
|
for (int i = 0; i < 4; ++i) NISPS_EXPECT(oa[i] == ob[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(ou_intensity_zero_disables_again) {
|
||||||
|
OUNoise<4> ou(1ull);
|
||||||
|
ou.set_intensity(0.5f);
|
||||||
|
NISPS_EXPECT(ou.enabled());
|
||||||
|
ou.set_intensity(0.f);
|
||||||
|
NISPS_EXPECT(!ou.enabled());
|
||||||
|
std::array<float, 4> o{};
|
||||||
|
o.fill(0.2f);
|
||||||
|
ou.apply(std::span<float>(o));
|
||||||
|
for (float v : o) NISPS_EXPECT(v == 0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
#include "../../nisps/modes/elysiamorf.hpp"
|
#include "../../nisps/modes/elysiamorf.hpp"
|
||||||
#include "../../nisps/modes/memlcelium.hpp"
|
#include "../../nisps/modes/memlcelium.hpp"
|
||||||
#include "../../nisps/modes/paf_synth.hpp"
|
#include "../../nisps/modes/paf_synth.hpp"
|
||||||
|
#include "../../nisps/modes/slp_workshop.hpp"
|
||||||
#include "../../nisps/modes/sound_analysis_midi.hpp"
|
#include "../../nisps/modes/sound_analysis_midi.hpp"
|
||||||
#include "../../nisps/modes/verb_fx.hpp"
|
#include "../../nisps/modes/verb_fx.hpp"
|
||||||
#include "../../nisps/modes/xiasri.hpp"
|
#include "../../nisps/modes/xiasri.hpp"
|
||||||
|
|
@ -25,6 +26,7 @@ static_assert(Mode<modes::ChannelStripMode>);
|
||||||
static_assert(Mode<modes::XIASRIMode>);
|
static_assert(Mode<modes::XIASRIMode>);
|
||||||
static_assert(Mode<modes::VerbFXMode>);
|
static_assert(Mode<modes::VerbFXMode>);
|
||||||
static_assert(Mode<modes::MEMLCeliumMode>);
|
static_assert(Mode<modes::MEMLCeliumMode>);
|
||||||
|
static_assert(Mode<modes::SLPWorkshopMode>);
|
||||||
static_assert(Mode<modes::BreakOrMode>);
|
static_assert(Mode<modes::BreakOrMode>);
|
||||||
static_assert(Mode<modes::ElysiamorfMode>);
|
static_assert(Mode<modes::ElysiamorfMode>);
|
||||||
static_assert(Mode<modes::SoundAnalysisMIDIMode>);
|
static_assert(Mode<modes::SoundAnalysisMIDIMode>);
|
||||||
|
|
@ -70,6 +72,16 @@ NISPS_TEST(mode_memlcelium_metadata) {
|
||||||
NISPS_EXPECT(s.engine_id == "memlcelium");
|
NISPS_EXPECT(s.engine_id == "memlcelium");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(mode_slp_workshop_metadata) {
|
||||||
|
const auto& s = modes::SLPWorkshopMode::param_schema();
|
||||||
|
NISPS_EXPECT(modes::SLPWorkshopMode::mode_id() == "slp_workshop");
|
||||||
|
NISPS_EXPECT(modes::SLPWorkshopMode::input_channel_count() == 4u);
|
||||||
|
NISPS_EXPECT(s.output_size == 56u);
|
||||||
|
// Reuses the MEMLCelium engine verbatim.
|
||||||
|
NISPS_EXPECT(s.engine_id == "memlcelium");
|
||||||
|
NISPS_EXPECT(s.voice_spaces.size() == 1u);
|
||||||
|
}
|
||||||
|
|
||||||
NISPS_TEST(mode_breakor_metadata) {
|
NISPS_TEST(mode_breakor_metadata) {
|
||||||
const auto& s = modes::BreakOrMode::param_schema();
|
const auto& s = modes::BreakOrMode::param_schema();
|
||||||
NISPS_EXPECT(modes::BreakOrMode::mode_id() == "breakor");
|
NISPS_EXPECT(modes::BreakOrMode::mode_id() == "breakor");
|
||||||
|
|
|
||||||
104
tests/cpp/test_mode_learning.cpp
Normal file
104
tests/cpp/test_mode_learning.cpp
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// tests/cpp/test_mode_learning.cpp — ModeBase adaptive-learning wiring,
|
||||||
|
// exercised through SLPWorkshopMode.
|
||||||
|
//
|
||||||
|
// - inert parity: with Jolt inactive and OU disabled (the defaults),
|
||||||
|
// SLPWorkshopMode is bit-identical to MEMLCeliumMode (same engine, same
|
||||||
|
// MLP shape, same seed) — proving the new gestures don't perturb the
|
||||||
|
// baseline mapping.
|
||||||
|
// - Jolt: pressing it morphs the network weights over successive control
|
||||||
|
// ticks and gates the LR (0 while held, ramps back after release).
|
||||||
|
// - OU explore: intensity round-trips, toggles enabled state, and keeps
|
||||||
|
// audio finite.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "test_helpers.hpp"
|
||||||
|
|
||||||
|
#include "../../nisps/core/types.hpp"
|
||||||
|
#include "../../nisps/modes/memlcelium.hpp"
|
||||||
|
#include "../../nisps/modes/slp_workshop.hpp"
|
||||||
|
|
||||||
|
using namespace nisps;
|
||||||
|
|
||||||
|
NISPS_TEST(slp_workshop_inert_equals_memlcelium) {
|
||||||
|
modes::MEMLCeliumMode a(123ull);
|
||||||
|
modes::SLPWorkshopMode b(123ull);
|
||||||
|
a.setup(48000.f);
|
||||||
|
b.setup(48000.f);
|
||||||
|
|
||||||
|
for (int t = 0; t < 8; ++t) {
|
||||||
|
const float x = 0.3f + 0.05f * static_cast<float>(t);
|
||||||
|
a.set_input(0, x);
|
||||||
|
a.set_input(1, 0.6f);
|
||||||
|
b.set_input(0, x);
|
||||||
|
b.set_input(1, 0.6f);
|
||||||
|
a.tick_control();
|
||||||
|
b.tick_control();
|
||||||
|
for (int s = 0; s < 32; ++s) {
|
||||||
|
const stereosample_t za{0.f, 0.f};
|
||||||
|
const stereosample_t zb{0.f, 0.f};
|
||||||
|
const auto oa = a.process(za);
|
||||||
|
const auto ob = b.process(zb);
|
||||||
|
NISPS_EXPECT(oa.L == ob.L);
|
||||||
|
NISPS_EXPECT(oa.R == ob.R);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(slp_workshop_jolt_morphs_weights_and_gates_lr) {
|
||||||
|
using ML = modes::SLPWorkshopMode::ML;
|
||||||
|
modes::SLPWorkshopMode m(5ull);
|
||||||
|
m.setup(48000.f);
|
||||||
|
|
||||||
|
std::array<float, ML::weight_count()> before{};
|
||||||
|
{
|
||||||
|
auto w = m.ml().get_weights();
|
||||||
|
for (std::size_t i = 0; i < w.size(); ++i) before[i] = w[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_EXPECT(!m.jolt_active());
|
||||||
|
NISPS_EXPECT(m.jolt_lr_scale() == 1.f);
|
||||||
|
|
||||||
|
m.jolt_press();
|
||||||
|
NISPS_EXPECT(m.jolt_active());
|
||||||
|
NISPS_EXPECT(m.jolt_lr_scale() == 0.f);
|
||||||
|
|
||||||
|
for (int t = 0; t < 100; ++t) m.tick_control();
|
||||||
|
|
||||||
|
int changed = 0;
|
||||||
|
{
|
||||||
|
auto w = m.ml().get_weights();
|
||||||
|
for (std::size_t i = 0; i < w.size(); ++i) {
|
||||||
|
if (before[i] != w[i]) ++changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NISPS_EXPECT(changed > 0);
|
||||||
|
|
||||||
|
m.jolt_release();
|
||||||
|
NISPS_EXPECT(!m.jolt_active());
|
||||||
|
for (int t = 0; t < 2000; ++t) m.tick_control(); // ramps LR back to full
|
||||||
|
NISPS_EXPECT_NEAR(m.jolt_lr_scale(), 1.f, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
NISPS_TEST(slp_workshop_explore_intensity_roundtrip_finite) {
|
||||||
|
modes::SLPWorkshopMode m(9ull);
|
||||||
|
m.setup(48000.f);
|
||||||
|
|
||||||
|
NISPS_EXPECT(!m.ou_noise().enabled());
|
||||||
|
m.set_explore_intensity(0.5f);
|
||||||
|
NISPS_EXPECT(m.ou_noise().enabled());
|
||||||
|
NISPS_EXPECT_NEAR(m.explore_intensity(), 0.5f, 1e-6);
|
||||||
|
|
||||||
|
for (int t = 0; t < 8; ++t) {
|
||||||
|
m.tick_control();
|
||||||
|
for (int s = 0; s < 16; ++s) {
|
||||||
|
const auto o = m.process(stereosample_t{0.f, 0.f});
|
||||||
|
NISPS_EXPECT(std::isfinite(o.L) && std::isfinite(o.R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.set_explore_intensity(0.f);
|
||||||
|
NISPS_EXPECT(!m.ou_noise().enabled());
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue