feat(manifold)!: P1 — retire playground/, manifold is the sole browser app
Playground archived at branch archive/playground-solidjs (tag playground-solidjs-final) and deleted from main. Retargets: - run-all-tests.sh stage 5 → manifold (typecheck + bun test + build + playwright via non-snap node runner, BUILD-PLAN gotcha) - ci.yml playground-tests → manifold-tests; osc-bridge.yml → manifold/osc-bridge (was already broken: playground/osc-bridge no longer existed) - codegen: TS emission target removed (returns at P5 → manifold); golden test now C++-only; TS emitters retained dormant - .gitignore: manifold/osc-bridge paths; drop dead playground faust exception - docs: AGENTS.md gates, README quickstart, MAP.md, ALIGNMENT.md (C15 now archive-only — defect #1 updated), AGENT-REFERENCE.md, specs/MAIN.md Part of docs/specs/plans/one-core-engine-refactor.md P1.
This commit is contained in:
parent
29dc88be3a
commit
0d1a2102eb
159 changed files with 141 additions and 19347 deletions
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
|
|
@ -3,10 +3,11 @@ name: CI
|
||||||
# Stream 11 verification pipeline.
|
# Stream 11 verification pipeline.
|
||||||
#
|
#
|
||||||
# Two parallel jobs:
|
# Two parallel jobs:
|
||||||
# * cpp-tests — builds nisps host C++ tests, builds nisps.wasm, runs
|
# * cpp-tests — builds nisps host C++ tests, builds nisps.wasm, runs
|
||||||
# the parity check, runs the lint script.
|
# the parity check, runs the lint script.
|
||||||
# * playground-tests — typechecks the SolidJS playground, builds the
|
# * manifold-tests — typechecks the React manifold app, runs bun unit
|
||||||
# production bundle, runs Playwright e2e tests.
|
# tests, builds the production bundle, runs Playwright
|
||||||
|
# e2e tests.
|
||||||
#
|
#
|
||||||
# Firmware compilation is NOT included in this workflow. Arduino-cli +
|
# Firmware compilation is NOT included in this workflow. Arduino-cli +
|
||||||
# rp2040 board package add ~2 minutes per run, and the verification value
|
# rp2040 board package add ~2 minutes per run, and the verification value
|
||||||
|
|
@ -84,8 +85,8 @@ jobs:
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
playground-tests:
|
manifold-tests:
|
||||||
name: Playground typecheck + e2e
|
name: Manifold typecheck + unit + e2e
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -96,24 +97,28 @@ jobs:
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
|
|
||||||
- name: Install playground deps
|
- name: Install manifold deps
|
||||||
working-directory: playground
|
working-directory: manifold
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
working-directory: playground
|
working-directory: manifold
|
||||||
run: bun run typecheck
|
run: bun run typecheck
|
||||||
|
|
||||||
- name: Build playground bundle
|
- name: Unit tests
|
||||||
working-directory: playground
|
working-directory: manifold
|
||||||
|
run: bun run test
|
||||||
|
|
||||||
|
- name: Build manifold bundle
|
||||||
|
working-directory: manifold
|
||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
working-directory: playground
|
working-directory: manifold
|
||||||
run: bunx playwright install --with-deps chromium
|
run: bunx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
working-directory: playground
|
working-directory: manifold
|
||||||
run: bunx playwright test
|
run: bunx playwright test
|
||||||
|
|
||||||
- name: Upload Playwright report on failure
|
- name: Upload Playwright report on failure
|
||||||
|
|
@ -121,5 +126,5 @@ jobs:
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: playwright-report
|
name: playwright-report
|
||||||
path: playground/playwright-report/
|
path: manifold/playwright-report/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
|
||||||
2
.github/workflows/osc-bridge.yml
vendored
2
.github/workflows/osc-bridge.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
|
|
||||||
- name: Compile
|
- name: Compile
|
||||||
run: |
|
run: |
|
||||||
cd playground/osc-bridge
|
cd manifold/osc-bridge
|
||||||
deno compile --allow-net --unstable-net --target ${{ matrix.target }} --output ../../dist/nisps-osc-bridge-${{ matrix.suffix }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} bridge.ts
|
deno compile --allow-net --unstable-net --target ${{ matrix.target }} --output ../../dist/nisps-osc-bridge-${{ matrix.suffix }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} bridge.ts
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -25,10 +25,6 @@
|
||||||
*.la
|
*.la
|
||||||
*.a
|
*.a
|
||||||
*.lib
|
*.lib
|
||||||
# Exception: Faust shared library files (text source, .lib extension by
|
|
||||||
# Faust convention) should be tracked.
|
|
||||||
!playground/faust/*.lib
|
|
||||||
|
|
||||||
# Executables
|
# Executables
|
||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
|
|
@ -38,8 +34,8 @@
|
||||||
build
|
build
|
||||||
|
|
||||||
# OSC bridge compiled binaries (built via compile.sh or CI)
|
# OSC bridge compiled binaries (built via compile.sh or CI)
|
||||||
playground/osc-bridge/dist/
|
manifold/osc-bridge/dist/
|
||||||
playground/osc-bridge/node_modules/
|
manifold/osc-bridge/node_modules/
|
||||||
|
|
||||||
# Dolt database files (added by bd init)
|
# Dolt database files (added by bd init)
|
||||||
.dolt/
|
.dolt/
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# Agent Instructions — MEMLNaut-NISPS
|
# Agent Instructions — MEMLNaut-NISPS
|
||||||
|
|
||||||
One C++20 NISPS core targets RP2350 firmware and a SolidJS/WASM playground. Read
|
One C++20 NISPS core targets RP2350 firmware and the Manifold React/WASM browser app. Read
|
||||||
`MAP.md`, `ALIGNMENT.md`, and `docs/AGENT-REFERENCE.md`; for Manifold UI work read
|
`MAP.md`, `ALIGNMENT.md`, and `docs/AGENT-REFERENCE.md`; for Manifold UI work read
|
||||||
`manifold/ONBOARDING.md` first. Parameter JSON schemas generate both C++ and TypeScript
|
`manifold/ONBOARDING.md` first. Parameter JSON schemas generate the C++ contracts
|
||||||
contracts.
|
(TS codegen returns at P5 of `docs/specs/plans/one-core-engine-refactor.md`).
|
||||||
|
|
||||||
## Gates
|
## Gates
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ bash scripts/build-cpp-tests.sh
|
||||||
bash scripts/parity-check.sh
|
bash scripts/parity-check.sh
|
||||||
bash scripts/lint-cpp.sh
|
bash scripts/lint-cpp.sh
|
||||||
scripts/build-firmware.sh [VARIANT]
|
scripts/build-firmware.sh [VARIANT]
|
||||||
cd playground && bun run typecheck && bun run build
|
cd manifold && bun run typecheck && bun run test && bun run build
|
||||||
```
|
```
|
||||||
|
|
||||||
- `nisps/` must remain platform-neutral and allocation-free in hot paths: no heap, no
|
- `nisps/` must remain platform-neutral and allocation-free in hot paths: no heap, no
|
||||||
|
|
|
||||||
14
ALIGNMENT.md
14
ALIGNMENT.md
|
|
@ -6,17 +6,17 @@
|
||||||
|
|
||||||
A research platform for interactive ML control of audio. We're building it to figure out what works and what doesn't — different ergonomics and ergodynamics of parameter sets, modes, ML architectures, audio engines, UI, and UX. Therefore: keep most/all parameters tweakable, ML/engine/UI/UX should each be configurable on their own axis, and the codebase has to enable/assist agentic AI coding patterns (confident changes, verifiable without hardware).
|
A research platform for interactive ML control of audio. We're building it to figure out what works and what doesn't — different ergonomics and ergodynamics of parameter sets, modes, ML architectures, audio engines, UI, and UX. Therefore: keep most/all parameters tweakable, ML/engine/UI/UX should each be configurable on their own axis, and the codebase has to enable/assist agentic AI coding patterns (confident changes, verifiable without hardware).
|
||||||
|
|
||||||
The clean-slate rewrite (2026-04-29) consolidates everything into one C++20 codebase compiling to firmware AND WASM, with SolidJS playground primitives composed into TSX mode components, and JSON schemas as the firmware↔browser parameter contract.
|
The clean-slate rewrite (2026-04-29) consolidated everything into one C++20 codebase compiling to firmware AND WASM. Since 2026-07-13 (P1 of `docs/specs/plans/one-core-engine-refactor.md`) the sole browser app is the React Manifold; the SolidJS playground is archived (`archive/playground-solidjs`). JSON schemas remain the firmware↔browser parameter contract.
|
||||||
|
|
||||||
## Top defects (ranked by mission impact)
|
## Top defects (ranked by mission impact)
|
||||||
|
|
||||||
### 1. Browser-only audio engines incomplete (2026-04-29)
|
### 1. Browser-only audio engines incomplete (2026-04-29; updated 2026-07-13)
|
||||||
|
|
||||||
**What.** Stream 9 stubbed `C15Mode` as a placeholder ("C15 mode TODO"). The C15 worklet/bridge from the legacy playground is in `.local/playground-archive/js/synth/` but not wired in. Mic input for XIASRI / SoundAnalysisMIDI is also TODO — the UI renders but doesn't capture audio.
|
**What.** C15 never got past a stubbed placeholder mode, and with the playground retired at P1 (2026-07-13) it has NO home on main — the stub UI, `c15.wasm`, and `c15-glue.js` live only on branch `archive/playground-solidjs`. Mic input for XIASRI / SoundAnalysisMIDI is likewise not wired in manifold.
|
||||||
|
|
||||||
**Why it blocks the mission.** "Browser engines ⊇ firmware engines" was a non-negotiable. Without C15 + mic input, the playground can't fully demonstrate the modes; users can't audition XIASRI or SoundAnalysisMIDI in the browser.
|
**Why it blocks the mission.** "Browser engines ⊇ firmware engines" was a non-negotiable. Without C15 + mic input, manifold can't fully demonstrate the modes; users can't audition XIASRI or SoundAnalysisMIDI in the browser.
|
||||||
|
|
||||||
**Rough cost.** ~1–2 days. C15 wiring is mostly straightforward porting from the archived bridge. Mic input requires the engine-host to expose an input stream to the worklet (small worklet refactor).
|
**Rough cost.** ~2 days. Reviving C15 now means porting the archived bridge into manifold's engine host. Mic input requires the engine-host to expose an input stream to the worklet (small worklet refactor).
|
||||||
|
|
||||||
### 2. Per-iteration loss curve not plumbed through WASM (2026-04-29)
|
### 2. Per-iteration loss curve not plumbed through WASM (2026-04-29)
|
||||||
|
|
||||||
|
|
@ -61,13 +61,13 @@ Schemas currently declare per-mode `hidden_layers` (some `[10, 10, 14]`, some `[
|
||||||
|
|
||||||
### Q2: How to express "advanced" features (gradient flow, weight health) without cluttering modes? (2026-04-29)
|
### Q2: How to express "advanced" features (gradient flow, weight health) without cluttering modes? (2026-04-29)
|
||||||
|
|
||||||
Current playground reproduces the a-immersive "Advanced" toggle. Power features hide behind it. Is this the right model, or should the mode UI itself decide what's exposed (some modes are "expert-only", some are simpler)?
|
The retired playground reproduced the a-immersive "Advanced" toggle; manifold hides power features in drawers instead. Is this the right model, or should the mode UI itself decide what's exposed (some modes are "expert-only", some are simpler)?
|
||||||
|
|
||||||
### Q3: Engine event taxonomy (2026-04-29)
|
### Q3: Engine event taxonomy (2026-04-29)
|
||||||
|
|
||||||
`nisps/modes/base.hpp` exposes a `ControlEvent` ring buffer pop_events interface for sequencer modes (BreakOr, Elysiamorf). Currently events are a flat enum. As we add more event-emitting modes (custom MIDI mappings, lighting, networked control), how should the event vocabulary grow? Open question; revisit when we add the third event-emitting mode.
|
`nisps/modes/base.hpp` exposes a `ControlEvent` ring buffer pop_events interface for sequencer modes (BreakOr, Elysiamorf). Currently events are a flat enum. As we add more event-emitting modes (custom MIDI mappings, lighting, networked control), how should the event vocabulary grow? Open question; revisit when we add the third event-emitting mode.
|
||||||
|
|
||||||
### Q4: Should the playground stay desktop-first? (2026-04-29)
|
### Q4: Should the browser app (manifold) stay desktop-first? (2026-04-29)
|
||||||
|
|
||||||
The original a-immersive was mobile-first ("designed for touch / foldable phone use"). The SolidJS rewrite is desktop-first by default. If the research story is "the user holds a phone and pinches to zoom while a synth runs in their pocket", we'll need a responsive pass. Defer until we have user data.
|
The original a-immersive was mobile-first ("designed for touch / foldable phone use"). The SolidJS rewrite is desktop-first by default. If the research story is "the user holds a phone and pinches to zoom while a synth runs in their pocket", we'll need a responsive pass. Defer until we have user data.
|
||||||
|
|
||||||
|
|
|
||||||
37
MAP.md
37
MAP.md
|
|
@ -1,6 +1,6 @@
|
||||||
# MAP
|
# MAP
|
||||||
|
|
||||||
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 codebase (`nisps/`) compiles to two targets: (1) Arduino/RP2350 firmware for the MEMLNaut hardware, (2) WASM in a SolidJS browser playground that runs the same engines + ML through an AudioWorklet. Browser audio engines are a superset of firmware engines (C15 is browser-only). See `CLAUDE.md` for the long-form architecture narrative and `ALIGNMENT.md` for current strategic gaps.
|
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 codebase (`nisps/`) compiles to two targets: (1) Arduino/RP2350 firmware for the MEMLNaut hardware, (2) WASM in the Manifold React browser app that runs the same engines + ML through an AudioWorklet. (The former SolidJS playground was retired 2026-07-13 — branch `archive/playground-solidjs`, tag `playground-solidjs-final`; the browser-only C15 engine lives only there for now.) See `CLAUDE.md` for the long-form architecture narrative and `ALIGNMENT.md` for current strategic gaps.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
|
@ -29,22 +29,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
|
||||||
- `firmware/MEMLCelium-upstream/` — **vendored** verbatim snapshot of the upstream `MusicallyEmbodiedML/MEMLNaut-NISPS` @ `main` Arduino sketch (pre-refactor monorepo, does NOT use `nisps/`), preset to `MODE_MEMLCELIUM`. Self-contained: upstream `memllib`@`e291192d` + `memlp`@`ea777502` vendored as plain files; `src/daisysp` in-tree. Built directly with `arduino-cli` (not the repo build scripts) — see its `README.md` for provenance + the compile command.
|
- `firmware/MEMLCelium-upstream/` — **vendored** verbatim snapshot of the upstream `MusicallyEmbodiedML/MEMLNaut-NISPS` @ `main` Arduino sketch (pre-refactor monorepo, does NOT use `nisps/`), preset to `MODE_MEMLCELIUM`. Self-contained: upstream `memllib`@`e291192d` + `memlp`@`ea777502` vendored as plain files; `src/daisysp` in-tree. Built directly with `arduino-cli` (not the repo build scripts) — see its `README.md` for provenance + the compile command.
|
||||||
- `firmware/useq-celium/` — standalone RP2040 firmware (PlatformIO, Arduino-Pico core) that turns a uSEQ module + CV expander into a USB→CV/gate converter driven by the manifold `cvgate` backend. `shared/protocol.h` is the v2 wire-protocol single source of truth (mirrored by `manifold/src/backends/useq-protocol.ts`); `main/` (USB serial → CV1–3 + GATE1–3, I2C → expander) and `expander/` (I2C slave → CV4–11). Wire spec: `docs/specs/useq-cv-protocol.md`. Restored from the April-2026 "uSEQ-Celium" mode.
|
- `firmware/useq-celium/` — standalone RP2040 firmware (PlatformIO, Arduino-Pico core) that turns a uSEQ module + CV expander into a USB→CV/gate converter driven by the manifold `cvgate` backend. `shared/protocol.h` is the v2 wire-protocol single source of truth (mirrored by `manifold/src/backends/useq-protocol.ts`); `main/` (USB serial → CV1–3 + GATE1–3, I2C → expander) and `expander/` (I2C slave → CV4–11). Wire spec: `docs/specs/useq-cv-protocol.md`. Restored from the April-2026 "uSEQ-Celium" mode.
|
||||||
|
|
||||||
### `playground/` — SolidJS + Vite + TypeScript app
|
### `manifold/` — Vite + React + TS convertible-mode app (the sole browser app)
|
||||||
- `playground/index.html`, `vite.config.ts`, `tsconfig.json`, `package.json` — scaffold. COOP/COEP headers configured.
|
|
||||||
- `playground/src/main.tsx`, `App.tsx` — entry + router (`/`, `/dev/primitives`, `/modes`).
|
|
||||||
- `playground/src/primitives/` — 16 UI building blocks: `Slider`, `SliderBank`, `VirtualJoystick`, `XYPad`, `Heatmap`, `OutputDisplay`, `TrainingControls`, `Drawer`, `ControlAxis`, `ProgressRing`, `PillToggle`, `ParamEditor`, `JoyMap`, `WeightHealth`, `GradientFlow`, `LossPlot`. Each has a `.demo.tsx` showcased on `/dev/primitives`.
|
|
||||||
- `playground/src/modes/` — one TSX per firmware mode (+ `C15Mode` browser-only). `ModeShell.tsx` is the shared scaffold; `ModeSwitcher.tsx` picks the active mode; `mode-runtime.ts` is the schema → ML → audio wiring hook; `mode-helpers.ts` for SliderBank configs. `generated/` holds codegen-produced TS schemas (do not edit).
|
|
||||||
- `playground/src/stores/` — Solid stores: `ml-store`, `input-store`, `output-store`, `mode-store`, `control-store` (compound axes Boldness/Memory/Precision), `session-store` (snapshots, A/B, presets), `exploration-store`, `bus` (typed signal bus). `persistence.ts` debounces localStorage writes.
|
|
||||||
- `playground/src/audio/engine-host.ts`, `worklet/nisps-processor.ts` — main-thread engine host + AudioWorklet processor. WASM loaded twice (main thread for ML, worklet for engines).
|
|
||||||
- `playground/src/ml/wasm-iml.ts`, `wasm-worker.ts`, `dataset.ts`, `types.ts` — main-thread WasmIML class + disposable async-training worker + FIFO dataset.
|
|
||||||
- `playground/src/input/pipeline.ts`, `playground/src/output/pipeline.ts`, `playground/src/output/curves.ts` — pure-fn pipelines (deadzone→zoom→curve→smoothing→momentum, then global curve→smoothing→slew→freeze).
|
|
||||||
- `playground/src/features/` — additional feature modules (heatmap sampling, snapshot stack, A/B compare, region pin, param pin, trail, weight health, etc.).
|
|
||||||
- `playground/src/debug/probe.ts` — synchronous `window.__nisps` debug probe for Playwright.
|
|
||||||
- `playground/public/nisps.{wasm,js}`, `c15.wasm`, `c15-glue.js` — WASM artifacts. `nisps.*` are a transitional copy from `scripts/build-wasm.sh` (canonical output is `manifold/public/`; copy dies with playground at P1).
|
|
||||||
- `playground/tests/e2e/` — Playwright specs (`ml-engine`, `modes`, `persistence`, `ui-interactions`) + `helpers.ts`.
|
|
||||||
- `playground/playwright.config.ts` — Vite preview server setup.
|
|
||||||
|
|
||||||
### `manifold/` — Vite + React + TS convertible-mode app (the NEW front-end, WIP)
|
|
||||||
The Manifold "convertible" Console on the real engine, deployed at `meml.lnfinitemonkeys.org/next` (staging,
|
The Manifold "convertible" Console on the real engine, deployed at `meml.lnfinitemonkeys.org/next` (staging,
|
||||||
alongside the live vanilla a-immersive at `/`). Built 2026-06-27/28; see `docs/specs/plans/BUILD-PLAN.md` (resume
|
alongside the live vanilla a-immersive at `/`). Built 2026-06-27/28; see `docs/specs/plans/BUILD-PLAN.md` (resume
|
||||||
anchor + locked decisions) and the `docs/specs/*-spec.md` set.
|
anchor + locked decisions) and the `docs/specs/*-spec.md` set.
|
||||||
|
|
@ -102,7 +87,7 @@ the "BUILD DELTAS" block at the top of `docs/specs/vcv-module.md`). `src/MEMLNau
|
||||||
- `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).
|
||||||
|
|
||||||
### `codegen/` — schema → C++/TS code
|
### `codegen/` — schema → C++/TS code
|
||||||
- `codegen/generate.ts` — Bun script: validates schemas via ajv, emits per-mode `nisps/modes/generated/<mode>_schema.hpp` (`constexpr`, `nisps::modes::generated`) and `playground/src/modes/generated/<mode>_schema.ts`. Idempotent.
|
- `codegen/generate.ts` — Bun script: validates schemas via ajv, emits per-mode `nisps/modes/generated/<mode>_schema.hpp` (`constexpr`, `nisps::modes::generated`). Idempotent. (TS emitters retained but dormant; re-targeted at `manifold/src/modes/generated/` in P5.)
|
||||||
- `codegen/generate-midi-devices.ts` — separate Bun script (isolated from the mode golden test): validates `schemas/midi_devices/` via ajv, emits `nisps/midi/generated/midi_devices.hpp` (no-heap `constexpr`) and `manifold/src/midi-devices/generated/{types,devices,index}.ts`. Idempotent.
|
- `codegen/generate-midi-devices.ts` — separate Bun script (isolated from the mode golden test): validates `schemas/midi_devices/` via ajv, emits `nisps/midi/generated/midi_devices.hpp` (no-heap `constexpr`) and `manifold/src/midi-devices/generated/{types,devices,index}.ts`. Idempotent.
|
||||||
- `codegen/seed-midi-devices.ts` — one-time/idempotent seed deriving `schemas/midi_devices/*.json` from the `synth-midi-cc.json` research artifact (slugifies labels → ids, heuristic groups).
|
- `codegen/seed-midi-devices.ts` — one-time/idempotent seed deriving `schemas/midi_devices/*.json` from the `synth-midi-cc.json` research artifact (slugifies labels → ids, heuristic groups).
|
||||||
- `codegen/templates/`, `codegen/tests/golden/` — reference templates + golden snapshot for paf_synth.
|
- `codegen/templates/`, `codegen/tests/golden/` — reference templates + golden snapshot for paf_synth.
|
||||||
|
|
@ -113,14 +98,14 @@ the "BUILD DELTAS" block at the top of `docs/specs/vcv-module.md`). `src/MEMLNau
|
||||||
|
|
||||||
### `scripts/` — build + verify entry points
|
### `scripts/` — build + verify entry points
|
||||||
- `build-firmware.sh`, `flash-firmware.sh`, `build-and-flash-firmware.sh`, `firmware-common.sh` — Arduino-CLI wrapper for RP2350 target with C++20 flag.
|
- `build-firmware.sh`, `flash-firmware.sh`, `build-and-flash-firmware.sh`, `firmware-common.sh` — Arduino-CLI wrapper for RP2350 target with C++20 flag.
|
||||||
- `build-wasm.sh` — Emscripten compile producing `manifold/public/nisps.{wasm,js}` (+ transitional copy to `playground/public/` until P1).
|
- `build-wasm.sh` — Emscripten compile producing `manifold/public/nisps.{wasm,js}`.
|
||||||
- `build-cpp-tests.sh` — CMake configure + build + ctest (Ninja).
|
- `build-cpp-tests.sh` — CMake configure + build + ctest (Ninja).
|
||||||
- `parity-check.sh` — runs native + WASM and diffs binary outputs.
|
- `parity-check.sh` — runs native + WASM and diffs binary outputs.
|
||||||
- `lint-cpp.sh` — `.f` literal warn + heap/`Arduino.h` violation fail.
|
- `lint-cpp.sh` — `.f` literal warn + heap/`Arduino.h` violation fail.
|
||||||
- `run-all-tests.sh` — master verification script.
|
- `run-all-tests.sh` — master verification script.
|
||||||
|
|
||||||
### `.github/workflows/`
|
### `.github/workflows/`
|
||||||
- `ci.yml` — GitHub Actions: cmake build + ctest + WASM build + parity check + lint + Playwright (cpp-tests + playground-tests jobs). Firmware compile is documented as manual.
|
- `ci.yml` — GitHub Actions: cmake build + ctest + WASM build + parity check + lint + Playwright (cpp-tests + manifold-tests jobs). Firmware compile is documented as manual.
|
||||||
|
|
||||||
### Submodules (in `src/`)
|
### Submodules (in `src/`)
|
||||||
- `src/memllib/` — hardware abstraction (audio driver, peripherals, MIDI). **Not auto-initialized** — fresh clones need `git submodule update --init --recursive`.
|
- `src/memllib/` — hardware abstraction (audio driver, peripherals, MIDI). **Not auto-initialized** — fresh clones need `git submodule update --init --recursive`.
|
||||||
|
|
@ -136,22 +121,22 @@ the "BUILD DELTAS" block at the top of `docs/specs/vcv-module.md`). `src/MEMLNau
|
||||||
## Entry points
|
## Entry points
|
||||||
|
|
||||||
- **Firmware**: `scripts/build-firmware.sh [VARIANT]` (interactive prompt if omitted), `scripts/flash-firmware.sh`, `scripts/build-and-flash-firmware.sh`. Target: `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3`, `-std=gnu++20`.
|
- **Firmware**: `scripts/build-firmware.sh [VARIANT]` (interactive prompt if omitted), `scripts/flash-firmware.sh`, `scripts/build-and-flash-firmware.sh`. Target: `rp2040:rp2040:solderparty_rp2350_stamp_xl:opt=Optimize3`, `-std=gnu++20`.
|
||||||
- **Playground dev**: `cd playground && bun install && bun run dev` (Vite, port 5173, COOP/COEP headers).
|
- **Manifold dev**: `cd manifold && bun install && bun run dev` (Vite, COOP/COEP headers).
|
||||||
- **Playground build**: `cd playground && bun run build`.
|
- **Manifold build**: `cd manifold && bun run build`.
|
||||||
- **WASM rebuild**: `bash scripts/build-wasm.sh` (needs `emcc`).
|
- **WASM rebuild**: `bash scripts/build-wasm.sh` (needs `emcc`).
|
||||||
- **Host C++ tests**: `bash scripts/build-cpp-tests.sh`.
|
- **Host C++ tests**: `bash scripts/build-cpp-tests.sh`.
|
||||||
- **Parity check**: `bash scripts/parity-check.sh`.
|
- **Parity check**: `bash scripts/parity-check.sh`.
|
||||||
- **All tests**: `bash scripts/run-all-tests.sh`.
|
- **All tests**: `bash scripts/run-all-tests.sh`.
|
||||||
- **Playwright**: `cd playground && bunx playwright test`.
|
- **Playwright**: `cd manifold && node node_modules/.bin/playwright test` (non-snap node runner on the VPS — BUILD-PLAN gotcha; `bunx playwright test` works elsewhere).
|
||||||
- **Codegen**: `cd codegen && bun run generate.ts` (regenerates `nisps/modes/generated/` and `playground/src/modes/generated/`).
|
- **Codegen**: `cd codegen && bun run generate.ts` (regenerates `nisps/modes/generated/`; TS target returns at P5 for manifold).
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Firmware mode selection is compile-time only — `#define MEMLNAUT_MODE_TYPE` in the `.ino`.
|
- Firmware mode selection is compile-time only — `#define MEMLNAUT_MODE_TYPE` in the `.ino`.
|
||||||
- `nisps/` follows Chris's RP2350 perf rules globally: no heap, `static const float` for non-trivial constants, strict `.f` suffix, memory section attrs (`NISPS_AUDIO_MEM`, `NISPS_AUDIO_FUNC`, `NISPS_APP_SRAM`, `NISPS_HOT`, `NISPS_FORCE_INLINE`).
|
- `nisps/` follows Chris's RP2350 perf rules globally: no heap, `static const float` for non-trivial constants, strict `.f` suffix, memory section attrs (`NISPS_AUDIO_MEM`, `NISPS_AUDIO_FUNC`, `NISPS_APP_SRAM`, `NISPS_HOT`, `NISPS_FORCE_INLINE`).
|
||||||
- C++ identifiers: `PascalCase` types, `snake_case` functions/variables, `kPascalCase` constexpr. JSON keys `snake_case`. TS types `PascalCase`, components `PascalCase.tsx`, modules `kebab-case.ts`.
|
- C++ identifiers: `PascalCase` types, `snake_case` functions/variables, `kPascalCase` constexpr. JSON keys `snake_case`. TS types `PascalCase`, components `PascalCase.tsx`, modules `kebab-case.ts`.
|
||||||
- `Curve` enum lives in `nisps/core/math.hpp` (lowercase: `linear/exp/log/square/sqrt/sigmoid/cubic`); generated mode headers re-export via `using Curve = ::nisps::Curve;`. TS mirror at `playground/src/output/curves.ts` with same names.
|
- `Curve` enum lives in `nisps/core/math.hpp` (lowercase: `linear/exp/log/square/sqrt/sigmoid/cubic`); generated mode headers re-export via `using Curve = ::nisps::Curve;`. TS mirror at `manifold/src/engine/curves.ts` with same names (moves into core at P4).
|
||||||
- Modes are TSX components composed of primitives; mode parameter contracts are JSON schemas with codegen → C++/TS types. **No declarative JSON UI.**
|
- Modes are TSX components composed of primitives; mode parameter contracts are JSON schemas with codegen → C++ types (TS codegen returns at P5). **No declarative JSON UI.**
|
||||||
- WASM and firmware share the same C++; WASM is fixed at `MLP<32, 10, 14, 18, 126>` (`nisps_ml_create` ignores requested dims — see `plans/one-core-engine-refactor.md` P2) and modes use a slice of outputs based on schema's `output_size`.
|
- WASM and firmware share the same C++; WASM is fixed at `MLP<32, 10, 14, 18, 126>` (`nisps_ml_create` ignores requested dims — see `plans/one-core-engine-refactor.md` P2) and modes use a slice of outputs based on schema's `output_size`.
|
||||||
- Cross-platform parity: `scripts/parity-check.sh` enforces native vs WASM agreement within 1e-5.
|
- Cross-platform parity: `scripts/parity-check.sh` enforces native vs WASM agreement within 1e-5.
|
||||||
|
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -21,16 +21,22 @@ Notes:
|
||||||
- `build-firmware.sh` accepts an optional variant name such as `MEMLCelium` or `BreakOr`. Matching remains case-insensitive, so `memlcelium` still works. If you omit it in an interactive shell, the script parses `MEMLNaut-NISPS.ino`, prompts for a variant, and rewrites the active `MEMLNAUT_MODE_TYPE` before building.
|
- `build-firmware.sh` accepts an optional variant name such as `MEMLCelium` or `BreakOr`. Matching remains case-insensitive, so `memlcelium` still works. If you omit it in an interactive shell, the script parses `MEMLNaut-NISPS.ino`, prompts for a variant, and rewrites the active `MEMLNAUT_MODE_TYPE` before building.
|
||||||
- `flash-firmware.sh` accepts an optional mountpoint argument, or auto-detects common UF2 bootloader mounts such as `/run/media/$USER/RP2350` and `/run/media/$USER/RPI-RP2`.
|
- `flash-firmware.sh` accepts an optional mountpoint argument, or auto-detects common UF2 bootloader mounts such as `/run/media/$USER/RP2350` and `/run/media/$USER/RPI-RP2`.
|
||||||
|
|
||||||
## Web Playground
|
## Manifold (browser app)
|
||||||
|
|
||||||
Try NISPS in your browser — no hardware required:
|
Try NISPS in your browser — no hardware required. Manifold is the React front-end
|
||||||
|
running the same C++ engines + ML as the firmware, compiled to WASM:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd playground
|
cd manifold
|
||||||
python3 -m http.server
|
bun install
|
||||||
# Open http://localhost:8000
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Train a neural network to map joystick positions to generative visuals through interactive machine learning. Two learning modes: direct example mapping and reinforcement learning with thumbs up/down feedback.
|
Staging deployment: https://meml.lnfinitemonkeys.org/next/
|
||||||
|
|
||||||
The playground UI includes an **Expand** toggle on the visual surface so you can make the canvas nearly full-screen while compressing parameter/control panels into a minimal strip beneath it.
|
Train a neural network to map input gestures to synth parameters through interactive
|
||||||
|
machine learning: place examples, or use verdict-based feedback (explore-and-place,
|
||||||
|
geometric dislike).
|
||||||
|
|
||||||
|
(The former SolidJS playground was retired in July 2026 — archived on branch
|
||||||
|
`archive/playground-solidjs`, tag `playground-solidjs-final`.)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@
|
||||||
*
|
*
|
||||||
* Writes: nisps/modes/generated/<mode_id>_schema.hpp
|
* Writes: nisps/modes/generated/<mode_id>_schema.hpp
|
||||||
* nisps/modes/generated/schema_types.hpp
|
* nisps/modes/generated/schema_types.hpp
|
||||||
* playground/src/modes/generated/<mode_id>_schema.ts
|
*
|
||||||
* playground/src/modes/generated/types.ts
|
* The TS emission target (formerly playground/src/modes/generated/) was
|
||||||
* playground/src/modes/generated/index.ts
|
* removed with the playground at P1 of
|
||||||
|
* docs/specs/plans/one-core-engine-refactor.md; the TS emitters below are
|
||||||
|
* retained and re-targeted at manifold/src/modes/generated/ in P5.
|
||||||
*
|
*
|
||||||
* Idempotent: regenerating the same schemas yields byte-identical output.
|
* Idempotent: regenerating the same schemas yields byte-identical output.
|
||||||
* Exits non-zero on validation failure.
|
* Exits non-zero on validation failure.
|
||||||
|
|
@ -64,7 +66,6 @@ const SCHEMAS_DIR = join(REPO_ROOT, "schemas");
|
||||||
const MODES_DIR = join(SCHEMAS_DIR, "modes");
|
const MODES_DIR = join(SCHEMAS_DIR, "modes");
|
||||||
const META_SCHEMA_PATH = join(SCHEMAS_DIR, "schema.json");
|
const META_SCHEMA_PATH = join(SCHEMAS_DIR, "schema.json");
|
||||||
const CPP_OUT_DIR = join(REPO_ROOT, "nisps", "modes", "generated");
|
const CPP_OUT_DIR = join(REPO_ROOT, "nisps", "modes", "generated");
|
||||||
const TS_OUT_DIR = join(REPO_ROOT, "playground", "src", "modes", "generated");
|
|
||||||
|
|
||||||
// ----- Helpers --------------------------------------------------------------
|
// ----- Helpers --------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -374,6 +375,8 @@ function emitModeHpp(schema: ModeSchema, sourceFile: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Per-mode TS emission -------------------------------------------------
|
// ----- Per-mode TS emission -------------------------------------------------
|
||||||
|
// Currently unused: the playground TS target was removed at P1; these emitters
|
||||||
|
// return in P5 targeting manifold/src/modes/generated/ (one-core-engine plan).
|
||||||
|
|
||||||
function emitModeTs(schema: ModeSchema, sourceFile: string): string {
|
function emitModeTs(schema: ModeSchema, sourceFile: string): string {
|
||||||
const constName = `${toPascalCase(schema.mode_id)}Schema`;
|
const constName = `${toPascalCase(schema.mode_id)}Schema`;
|
||||||
|
|
@ -545,22 +548,9 @@ function main(): number {
|
||||||
writeFileSync(out, emitModeHpp(schema, source));
|
writeFileSync(out, emitModeHpp(schema, source));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Emit TS outputs
|
// 5. Report
|
||||||
ensureDir(TS_OUT_DIR);
|
|
||||||
writeFileSync(join(TS_OUT_DIR, "types.ts"), emitSharedTsTypes());
|
|
||||||
for (const { source, schema } of schemas) {
|
|
||||||
const out = join(TS_OUT_DIR, `${schema.mode_id}_schema.ts`);
|
|
||||||
writeFileSync(out, emitModeTs(schema, source));
|
|
||||||
}
|
|
||||||
writeFileSync(
|
|
||||||
join(TS_OUT_DIR, "index.ts"),
|
|
||||||
emitTsIndex(schemas.map(s => s.schema.mode_id).sort())
|
|
||||||
);
|
|
||||||
|
|
||||||
// 6. Report
|
|
||||||
console.log(`OK ${schemas.length} mode schema(s) processed.`);
|
console.log(`OK ${schemas.length} mode schema(s) processed.`);
|
||||||
console.log(` C++ -> ${CPP_OUT_DIR}`);
|
console.log(` C++ -> ${CPP_OUT_DIR}`);
|
||||||
console.log(` TS -> ${TS_OUT_DIR}`);
|
|
||||||
for (const { schema } of schemas) {
|
for (const { schema } of schemas) {
|
||||||
console.log(
|
console.log(
|
||||||
` - ${schema.mode_id}: ${schema.params.length} params, ` +
|
` - ${schema.mode_id}: ${schema.params.length} params, ` +
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
/**
|
/**
|
||||||
* Golden test: re-runs codegen and asserts that the freshly-generated
|
* Golden test: re-runs codegen and asserts that the freshly-generated
|
||||||
* paf_synth_schema.{hpp,ts} files are byte-identical to the snapshots in
|
* paf_synth_schema.hpp is byte-identical to the snapshot in
|
||||||
* codegen/tests/golden/.
|
* codegen/tests/golden/.
|
||||||
*
|
*
|
||||||
|
* (The TS golden case was removed with the playground target at P1 of
|
||||||
|
* docs/specs/plans/one-core-engine-refactor.md; it returns in P5 against
|
||||||
|
* manifold/src/modes/generated/. The golden/paf_synth_schema.ts snapshot is
|
||||||
|
* kept as the P5 template reference.)
|
||||||
|
*
|
||||||
* If you change the codegen template intentionally, regenerate the goldens:
|
* If you change the codegen template intentionally, regenerate the goldens:
|
||||||
* bun run generate.ts
|
* bun run generate.ts
|
||||||
* cp ../nisps/modes/generated/paf_synth_schema.hpp tests/golden/
|
* cp ../nisps/modes/generated/paf_synth_schema.hpp tests/golden/
|
||||||
* cp ../playground/src/modes/generated/paf_synth_schema.ts tests/golden/
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync } from "node:fs";
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
|
|
@ -31,11 +35,6 @@ const CASES: Case[] = [
|
||||||
generatedPath: join(REPO_ROOT, "nisps", "modes", "generated", "paf_synth_schema.hpp"),
|
generatedPath: join(REPO_ROOT, "nisps", "modes", "generated", "paf_synth_schema.hpp"),
|
||||||
goldenPath: join(GOLDEN_DIR, "paf_synth_schema.hpp"),
|
goldenPath: join(GOLDEN_DIR, "paf_synth_schema.hpp"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "paf_synth_schema.ts",
|
|
||||||
generatedPath: join(REPO_ROOT, "playground", "src", "modes", "generated", "paf_synth_schema.ts"),
|
|
||||||
goldenPath: join(GOLDEN_DIR, "paf_synth_schema.ts"),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function diff(name: string, expected: string, actual: string): string {
|
function diff(name: string, expected: string, actual: string): string {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ This file provides guidance to coding agents working with this repository.
|
||||||
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. A research platform for interactive ML control of audio. **One C++20 codebase** (`nisps/`) compiles to two targets:
|
MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. A research platform for interactive ML control of audio. **One C++20 codebase** (`nisps/`) compiles to two targets:
|
||||||
|
|
||||||
1. **RP2350 firmware** for the MEMLNaut hardware platform (`firmware/`).
|
1. **RP2350 firmware** for the MEMLNaut hardware platform (`firmware/`).
|
||||||
2. **WASM** in a SolidJS browser playground (`playground/`) — same engines + ML, run through an AudioWorklet.
|
2. **WASM** in the Manifold React browser app (`manifold/`) — same engines + ML, run through an AudioWorklet.
|
||||||
|
|
||||||
Browser audio engines are a superset of firmware engines (C15 is browser-only). Parameter contracts are JSON schemas (`schemas/`) with codegen producing both C++ headers and TypeScript types.
|
(The former SolidJS playground was retired 2026-07-13 at P1 of `docs/specs/plans/one-core-engine-refactor.md`; archived on branch `archive/playground-solidjs`, tag `playground-solidjs-final`. The browser-only C15 engine currently lives only there.) Parameter contracts are JSON schemas (`schemas/`) with codegen producing the C++ headers (TS emission returns at P5).
|
||||||
|
|
||||||
Project documentation: https://musicallyembodiedml.github.io/memlnaut/approaches/nisps
|
Project documentation: https://musicallyembodiedml.github.io/memlnaut/approaches/nisps
|
||||||
|
|
||||||
|
|
@ -76,36 +76,24 @@ Build: `scripts/build-firmware.sh [VARIANT]`. Verified compiling for PAFSynth, C
|
||||||
- **Core 1**: Real-time audio processing (`Mode::process`), MIDI polling.
|
- **Core 1**: Real-time audio processing (`Mode::process`), MIDI polling.
|
||||||
- **Sync**: `nisps::core::ring_buffer` (templated SPSC lock-free, replaces pico/util/queue) + memory barriers (`nisps::core::memory_barrier`, `write_volatile`/`read_volatile`).
|
- **Sync**: `nisps::core::ring_buffer` (templated SPSC lock-free, replaces pico/util/queue) + memory barriers (`nisps::core::memory_barrier`, `write_volatile`/`read_volatile`).
|
||||||
|
|
||||||
## The `playground/` target
|
## The `manifold/` target
|
||||||
|
|
||||||
```
|
```
|
||||||
playground/ # Vite + SolidJS + TypeScript
|
manifold/ # Vite + React + TypeScript (the sole browser app)
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── primitives/ # 16 UI building blocks (Slider, JoyMap, Heatmap, …) + .demo.tsx for /dev/primitives
|
│ ├── engine/ # framework-neutral TS engine spine: wasm-iml, engine-host + worklet,
|
||||||
│ ├── modes/ # one TSX per firmware mode + C15Mode (browser-only); ModeShell + ModeSwitcher + mode-runtime
|
│ │ # input/output pipelines + curves (move into core at P4), spine.ts, EngineProvider
|
||||||
│ ├── stores/ # Solid stores (ml, input, output, mode, control, session, exploration, bus + persistence)
|
│ ├── primitives/ # 12 design primitives as typed React
|
||||||
│ ├── audio/ # engine-host + AudioWorklet processor (loads nisps.wasm separately on each thread)
|
│ ├── console/ # convertible Console (CompositeStage, Dock, Drawers, Manifold canvas, …)
|
||||||
│ ├── ml/ # WasmIML class + disposable async-training Worker + dataset
|
│ ├── dock/, backends/, inputs/, feedback/, settings/, serial/, midi-devices/
|
||||||
│ ├── input/, output/ # pure-fn pipelines (deadzone→zoom→curve→smoothing→momentum, then global curve→smoothing→slew→freeze)
|
│ └── debug/probe.ts # window.__nisps behind ?debug=1 for Playwright
|
||||||
│ ├── features/ # heatmap, snapshots, A/B compare, region/param pin, trail, weight health, gradient flow
|
├── public/ # nisps.{wasm,js} — canonical build-wasm.sh output
|
||||||
│ └── debug/probe.ts # synchronous window.__nisps for Playwright
|
└── tests/ # e2e Playwright specs + fixtures/ (P4 golden parity fixtures)
|
||||||
├── public/ # nisps.{wasm,js}, c15.{wasm,glue}
|
|
||||||
└── tests/e2e/ # Playwright specs + helpers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dev: `cd playground && bun install && bun run dev`. Build: `bun run build`. Typecheck: `bun run typecheck`. E2E: `bunx playwright test`.
|
Dev: `cd manifold && bun install && bun run dev`. Build: `bun run build`. Typecheck: `bun run typecheck`. Unit: `bun run test`. E2E: `bunx playwright test` (on the VPS run the runner via non-snap node — BUILD-PLAN gotcha).
|
||||||
|
|
||||||
### Stores + reactivity
|
Read `manifold/ONBOARDING.md` before touching manifold UI — layering, Stages, Dock, gotchas.
|
||||||
|
|
||||||
All stores use SolidJS `createStore` for objects, `createSignal` for primitives. ML outputs are stored in a separate Float32Array signal (per the migration plan's perf guidance). Persistence (debounced 200ms localStorage round-trip) wired in `playground/src/stores/persistence.ts`. The signal bus (`bus.ts`) handles cross-store events (`ml.*`, `mode.*`, `pin.*`, `ui.*`).
|
|
||||||
|
|
||||||
### Control surface
|
|
||||||
|
|
||||||
Three compound axes (Boldness / Memory / Precision) interpolate per-axis tables to drive ~6 underlying parameters each, with offset overrides ("trim-pot" model). State is in `control-store`. Six built-in control presets (Default, First Touch, Jazz Hands, Sculptor, Improviser, Microscope) available via the ModeShell control bar.
|
|
||||||
|
|
||||||
### Debug probe (Playwright)
|
|
||||||
|
|
||||||
`window.__nisps` is exposed synchronously and bypasses Solid reactivity (uses `untrack`/`batch`). API matches the `.local/recon/04-playground.md` spec — `setInputs`, `getOutputs`, `getLoss`, `train`, `thumbsUp`/`thumbsDown`, `randomise`, `clearExamples`, `inferBatch`, `getLayerStats`, `saveState`, etc.
|
|
||||||
|
|
||||||
## The `schemas/` + `codegen/` contract
|
## The `schemas/` + `codegen/` contract
|
||||||
|
|
||||||
|
|
@ -113,7 +101,7 @@ Each mode has a `schemas/modes/<mode>.json` describing its parameters (name, lab
|
||||||
|
|
||||||
Codegen (`bun run codegen/generate.ts`) emits:
|
Codegen (`bun run codegen/generate.ts`) emits:
|
||||||
- `nisps/modes/generated/<mode>_schema.hpp` — `constexpr` C++ data, namespace `nisps::modes::generated`, re-exports `nisps::Curve` from `nisps/core/math.hpp`.
|
- `nisps/modes/generated/<mode>_schema.hpp` — `constexpr` C++ data, namespace `nisps::modes::generated`, re-exports `nisps::Curve` from `nisps/core/math.hpp`.
|
||||||
- `playground/src/modes/generated/<mode>_schema.ts` — typed const objects + per-mode params interface.
|
- (TS emission is dormant since P1; it returns at P5 targeting `manifold/src/modes/generated/`.)
|
||||||
|
|
||||||
Codegen is idempotent. Golden test ensures regenerating produces byte-identical output.
|
Codegen is idempotent. Golden test ensures regenerating produces byte-identical output.
|
||||||
|
|
||||||
|
|
@ -121,29 +109,28 @@ Codegen is idempotent. Golden test ensures regenerating produces byte-identical
|
||||||
|
|
||||||
Two WASM instances at runtime:
|
Two WASM instances at runtime:
|
||||||
|
|
||||||
1. **Main thread** (`playground/src/ml/wasm-iml.ts`): ML inference + sync training + RL primitives. Update store after each call. Async training via disposable Web Worker (`wasm-worker.ts`).
|
1. **Main thread** (`manifold/src/engine/wasm-iml.ts`): ML inference + sync training + RL primitives, reporting into an injected `EngineSink`. Async training via disposable Web Worker (`wasm-worker.ts`).
|
||||||
2. **AudioWorklet** (`playground/src/audio/worklet/nisps-processor.ts`): runs engine `process_block` per audio block. Loads `nisps.wasm` directly via `WebAssembly.compile` (no Emscripten glue in worklet). Bytes posted from main thread.
|
2. **AudioWorklet** (`manifold/src/engine/worklet/nisps-processor.ts`): runs engine `process_block` per audio block. Loads `nisps.wasm` directly via `WebAssembly.compile` (no Emscripten glue in worklet). Bytes posted from main thread.
|
||||||
|
|
||||||
C API is in `nisps/wasm/bindings.cpp`. Build: `bash scripts/build-wasm.sh` (~94KB output to `manifold/public/`, transitional copy to `playground/public/` until P1).
|
C API is in `nisps/wasm/bindings.cpp`. Build: `bash scripts/build-wasm.sh` (~94KB output to `manifold/public/`).
|
||||||
|
|
||||||
The WASM target is fixed at `MLP<32u, 10u, 14u, 18u, 126u>` (`nisps_ml_create` ignores requested dims — see `docs/specs/plans/one-core-engine-refactor.md` P2). Modes with smaller `input_size`/`output_size` use a slice.
|
The WASM target is fixed at `MLP<32u, 10u, 14u, 18u, 126u>` (`nisps_ml_create` ignores requested dims — see `docs/specs/plans/one-core-engine-refactor.md` P2). Modes with smaller `input_size`/`output_size` use a slice.
|
||||||
|
|
||||||
### Known limitations
|
### Known limitations
|
||||||
|
|
||||||
- Loss history not yet plumbed through C API; `lossHistory` in the store is a single-element array per training run.
|
- Loss history not yet plumbed through C API; only the final loss of a training run reaches TS.
|
||||||
- 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 — runtime-shaped browser MLP arrives at P2 of `docs/specs/plans/one-core-engine-refactor.md`.
|
||||||
- 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 in manifold.
|
||||||
- C15 voice space integration in C15Mode is a placeholder.
|
- C15 has no home on main (see `ALIGNMENT.md` defect #1).
|
||||||
- The browser Jolt/OU controls (`playground/src/ml/jolt.ts`, `playground/src/output/ou-explore.ts`) reimplement the gesture math in TS rather than calling the C++ `ml::Jolt`/`ml::OUNoise` through WASM. They drive weights via the existing `nisps_ml_get/set_weights` bindings and use `Math.random()` (not the deterministic `Rng`) — fine for stochastic exploration aids, but firmware↔browser bit-parity of the noise itself is intentionally not guaranteed.
|
- The browser Jolt/OU controls in manifold reimplement the gesture math in TS (interim, ported from the retired playground) rather than calling the C++ `ml::Jolt`/`ml::OUNoise` through WASM. They drive weights via the existing `nisps_ml_get/set_weights` bindings — the P3 phase of the one-core-engine plan replaces them with `nisps_ml_jolt_press/release` + `nisps_ml_explore_intensity` bindings.
|
||||||
|
|
||||||
## URL parameters (playground)
|
## URL parameters (manifold)
|
||||||
|
|
||||||
| Param | Range | Default | Effect |
|
| Param | Range | Default | Effect |
|
||||||
|-------|-------|---------|--------|
|
|-------|-------|---------|--------|
|
||||||
| `tame` | 0–1 | 1 | Constrains synth output ranges toward safe limits. |
|
| `debug` | 1 | _(off)_ | Exposes the `window.__nisps` debug probe. |
|
||||||
| `spread` | 0–1 | 0.6 | Master noise regime (init scale, RL noise cap, per-layer Xavier scaling, weight decay). |
|
|
||||||
| `preset` | preset id | _(none)_ | Auto-loads a synth preset on first visit. |
|
(The playground-era `tame`/`spread`/`preset` URL params died with the playground; `spread` survives as an engine concept — see below.)
|
||||||
| `debug` | 1 | _(off)_ | Exposes `window.__nisps` debug probe. |
|
|
||||||
|
|
||||||
### `spread` — sigmoid saturation control
|
### `spread` — sigmoid saturation control
|
||||||
|
|
||||||
|
|
@ -187,12 +174,13 @@ scripts/build-firmware.sh PAFSynth # or any other variant
|
||||||
scripts/flash-firmware.sh
|
scripts/flash-firmware.sh
|
||||||
scripts/build-and-flash-firmware.sh
|
scripts/build-and-flash-firmware.sh
|
||||||
|
|
||||||
# Playground
|
# Manifold
|
||||||
cd playground && bun install
|
cd manifold && bun install
|
||||||
bun run dev # Vite dev (COOP/COEP enabled)
|
bun run dev # Vite dev (COOP/COEP enabled)
|
||||||
bun run typecheck
|
bun run typecheck
|
||||||
|
bun run test # bun unit tests
|
||||||
bun run build
|
bun run build
|
||||||
bunx playwright test
|
bunx playwright test # on the VPS: node node_modules/.bin/playwright test
|
||||||
|
|
||||||
# All tests
|
# All tests
|
||||||
bash scripts/run-all-tests.sh
|
bash scripts/run-all-tests.sh
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ layer: cross-cutting
|
||||||
|
|
||||||
# MEMLNaut-NISPS Spec Corpus
|
# MEMLNaut-NISPS Spec Corpus
|
||||||
|
|
||||||
**Neural Interactive Shaping of Parameter Spaces** — a research platform for interactive ML control of audio. One C++20 codebase compiles to two targets: RP2350 firmware for the MEMLNaut hardware, and WASM in a SolidJS browser playground running the same engines + ML through an AudioWorklet.
|
**Neural Interactive Shaping of Parameter Spaces** — a research platform for interactive ML control of audio. One C++20 codebase compiles to two targets: RP2350 firmware for the MEMLNaut hardware, and WASM in the Manifold React browser app running the same engines + ML through an AudioWorklet. (The SolidJS playground was retired 2026-07-13, P1 of plans/one-core-engine-refactor.md; archived on `archive/playground-solidjs`.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -30,8 +30,7 @@ The codebase's most architecturally significant areas referenced by this corpus:
|
||||||
| **Shared C++20** | `nisps/core/`, `nisps/ml/`, `nisps/dsp/`, `nisps/engines/`, `nisps/modes/` | One audio+ML library compiling to both firmware and WASM |
|
| **Shared C++20** | `nisps/core/`, `nisps/ml/`, `nisps/dsp/`, `nisps/engines/`, `nisps/modes/` | One audio+ML library compiling to both firmware and WASM |
|
||||||
| **Schema/Codegen** | `schemas/`, `codegen/` | Parameter contracts + code generation (C++ headers, TS types) |
|
| **Schema/Codegen** | `schemas/`, `codegen/` | Parameter contracts + code generation (C++ headers, TS types) |
|
||||||
| **Firmware** | `firmware/MEMLNaut-NISPS/glue/` | Hardware bindings (audio, peripherals, MIDI, settings) |
|
| **Firmware** | `firmware/MEMLNaut-NISPS/glue/` | Hardware bindings (audio, peripherals, MIDI, settings) |
|
||||||
| **Browser** | `playground/src/engine/`, `playground/src/audio/` | Headless TS engine + AudioWorklet, wired to Solid stores |
|
| **Browser (Manifold)** | `manifold/src/engine/`, `manifold/src/console/` | Headless TS engine + AudioWorklet + the convertible React Console |
|
||||||
| **Manifold** | `manifold/src/engine/`, `manifold/src/console/` | Same TS engine + React wrapper (the new convertible front-end) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -40,7 +39,7 @@ The codebase's most architecturally significant areas referenced by this corpus:
|
||||||
**What it is.** MEMLNaut-NISPS is a single C++20 codebase that compiles once to two distinct targets:
|
**What it is.** MEMLNaut-NISPS is a single C++20 codebase that compiles once to two distinct targets:
|
||||||
|
|
||||||
1. **Firmware** — runs on RP2350 hardware (MEMLNaut instrument). Real-time audio engines (8 variants) + interactive ML (4-layer MLP, SGD + RL feedback) with deterministic RNG + dual-core orchestration (audio on core 1, control on core 0).
|
1. **Firmware** — runs on RP2350 hardware (MEMLNaut instrument). Real-time audio engines (8 variants) + interactive ML (4-layer MLP, SGD + RL feedback) with deterministic RNG + dual-core orchestration (audio on core 1, control on core 0).
|
||||||
2. **Browser (WASM)** — runs in a SolidJS playground (or React Manifold front-end) via AudioWorklet. Same C++20 engines + ML, compiled to WASM; audio engines are a superset of firmware (C15 synth browser-only).
|
2. **Browser (WASM)** — runs in the React Manifold front-end via AudioWorklet. Same C++20 engines + ML, compiled to WASM; audio engines are a superset of firmware (C15 synth browser-only).
|
||||||
|
|
||||||
**The unifying constraint**: one source tree, one ML architecture, cross-platform parity (native ↔ WASM within 1e-5 numerical tolerance). Parameter shapes are JSON schemas with codegen producing both C++ headers and TypeScript types.
|
**The unifying constraint**: one source tree, one ML architecture, cross-platform parity (native ↔ WASM within 1e-5 numerical tolerance). Parameter shapes are JSON schemas with codegen producing both C++ headers and TypeScript types.
|
||||||
|
|
||||||
|
|
@ -65,7 +64,7 @@ The codebase's most architecturally significant areas referenced by this corpus:
|
||||||
Each firmware mode has a `schemas/modes/<mode>.json` describing parameters (name, label, range, default, curve, group), ML config (input/output sizes, hidden layers), voice spaces (names; bodies are inline lambdas in the engine), and UI config. The meta-schema at `schemas/schema.json` validates these. Codegen (`bun run codegen/generate.ts`) is idempotent and produces:
|
Each firmware mode has a `schemas/modes/<mode>.json` describing parameters (name, label, range, default, curve, group), ML config (input/output sizes, hidden layers), voice spaces (names; bodies are inline lambdas in the engine), and UI config. The meta-schema at `schemas/schema.json` validates these. Codegen (`bun run codegen/generate.ts`) is idempotent and produces:
|
||||||
|
|
||||||
- `nisps/modes/generated/<mode>_schema.hpp` — `constexpr` C++ data under `nisps::modes::generated`.
|
- `nisps/modes/generated/<mode>_schema.hpp` — `constexpr` C++ data under `nisps::modes::generated`.
|
||||||
- `playground/src/modes/generated/<mode>_schema.ts` — typed const objects + per-mode params interface.
|
- TS schema emission is dormant since P1; it returns at P5 targeting `manifold/src/modes/generated/`.
|
||||||
|
|
||||||
Regenerate after editing any `schemas/modes/*.json`. Golden test ensures output is byte-identical.
|
Regenerate after editing any `schemas/modes/*.json`. Golden test ensures output is byte-identical.
|
||||||
|
|
||||||
|
|
|
||||||
5
playground/.gitignore
vendored
5
playground/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
.vite
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
1
playground/dist/assets/PrimitivesShowcase-B55FOguW.css
vendored
Normal file
1
playground/dist/assets/PrimitivesShowcase-B55FOguW.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
._root_1attb_1{max-width:1200px;margin:0 auto;display:flex;flex-direction:column;gap:var(--sp-5)}._header_1attb_9{display:flex;flex-direction:column;gap:var(--sp-2)}._header_1attb_9 h1{margin:0;font-size:var(--fs-xl);color:var(--accent)}._lede_1attb_21{color:var(--fg-mute);margin:0;max-width:720px}._grid_1attb_27{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:var(--sp-4)}._card_1attb_33{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--r-3);padding:var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-3);min-height:280px}._cardHead_1attb_44{display:flex;flex-direction:column;gap:var(--sp-1);border-bottom:1px solid var(--line);padding-bottom:var(--sp-2)}._cardTitle_1attb_52{margin:0;font-size:var(--fs-md);color:var(--fg)}._cardDesc_1attb_58{margin:0;font-size:var(--fs-xs);color:var(--fg-mute)}._cardBody_1attb_64{display:flex;flex-direction:column;align-items:stretch;flex:1;justify-content:flex-start}._bank_3njyn_1{display:flex;flex-direction:column;gap:var(--sp-3);background:var(--bg-1);border:1px solid var(--line);border-radius:var(--r-2);padding:var(--sp-3)}._title_3njyn_11{font-size:var(--fs-md);margin:0 0 var(--sp-2) 0;color:var(--fg)}._section_3njyn_17{display:flex;flex-direction:column;gap:var(--sp-2)}._sectionHeader_3njyn_23{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-1) var(--sp-2);background:var(--bg-2);border:1px solid var(--line);text-align:left;width:100%;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.08em;color:var(--fg-mute)}._caret_3njyn_38{width:1ch;display:inline-block}._sectionName_3njyn_43{flex:1}._sectionCount_3njyn_47{color:var(--fg-dim)}._list_3njyn_51{display:flex;flex-direction:column;gap:var(--sp-2);padding:var(--sp-1) 0}
|
||||||
2
playground/dist/assets/PrimitivesShowcase-D2lYbVrQ.js
vendored
Normal file
2
playground/dist/assets/PrimitivesShowcase-D2lYbVrQ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
playground/dist/assets/PrimitivesShowcase-D2lYbVrQ.js.map
vendored
Normal file
1
playground/dist/assets/PrimitivesShowcase-D2lYbVrQ.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
playground/dist/assets/index-C1-a2ZPT.js
vendored
Normal file
3
playground/dist/assets/index-C1-a2ZPT.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
playground/dist/assets/index-C1-a2ZPT.js.map
vendored
Normal file
1
playground/dist/assets/index-C1-a2ZPT.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
playground/dist/assets/index-D19lnkK_.css
vendored
Normal file
1
playground/dist/assets/index-D19lnkK_.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
playground/dist/assets/nisps-processor-BuKuNOY1.js
vendored
Normal file
2
playground/dist/assets/nisps-processor-BuKuNOY1.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
(function(){"use strict";class f extends AudioWorkletProcessor{instance=null;engineHandle=0;engineId="thru";muted=!0;inLPtr=0;inRPtr=0;outLPtr=0;outRPtr=0;idPtr=0;paramsPtr=0;inLView=null;inRView=null;outLView=null;outRView=null;paramsView=null;idView=null;mem=null;pendingParams=null;constructor(){super(),this.port.onmessage=i=>this.onMessage_(i.data)}async onMessage_(i){if(i.kind==="init")try{await this.init_(i.wasmBinary,i.sampleRate),this.post_({kind:"ready"})}catch(r){this.post_({kind:"error",message:r instanceof Error?r.message:String(r)})}else i.kind==="engine"?this.switchEngine_(i.engineId):i.kind==="params"?this.applyParams_(i.params):i.kind==="mute"&&(this.muted=i.muted)}post_(i){this.port.postMessage(i)}async init_(i,r){const t=new WebAssembly.Memory({initial:128,maximum:4096,shared:!1}),a=await WebAssembly.compile(i),h=WebAssembly.Module.imports(a),n={};for(const e of h){n[e.module]||(n[e.module]={});const s=n[e.module];e.kind==="function"?(e.name,s[e.name]=(...o)=>0):e.kind==="memory"?s[e.name]=t:e.kind==="table"?s[e.name]=new WebAssembly.Table({element:"anyfunc",initial:0}):e.kind==="global"&&(s[e.name]=new WebAssembly.Global({value:"i32",mutable:!0},0))}for(const e of h){const s=n[e.module];e.name==="a"&&e.kind==="function"&&(s[e.name]=()=>{throw new Error("wasm aborted")}),e.name==="b"&&e.kind==="function"&&(s[e.name]=o=>0)}const w=await WebAssembly.instantiate(a,n),_=WebAssembly.Module.exports(a),P=new Map;for(const e of _)P.set(e.name,e.name);const p=w.exports;function u(...e){for(const s of e){const o=p[s];if(typeof o=="function")return o}throw new Error(`worklet: missing wasm export, tried: ${e.join(", ")}`)}function m(...e){return u(...e)}const l=u("_malloc","malloc"),g=m("_free","free"),y=u("_nisps_engine_create"),R=m("_nisps_engine_destroy"),b=m("_nisps_engine_set_params"),L=m("_nisps_engine_process_block");let d=null;for(const e of _)if(e.kind==="memory"){const s=p[e.name];if(s instanceof WebAssembly.Memory){d=s;break}}this.mem=d??t,this.instance={exports:{memory:this.mem,malloc:l,free:g,_nisps_engine_create:(e,s)=>y(e,s),_nisps_engine_destroy:e=>R(e),_nisps_engine_set_params:(e,s,o)=>b(e,s,o),_nisps_engine_process_block:(e,s,o,V,A,C)=>L(e,s,o,V,A,C)}},this.inLPtr=l(128*4),this.inRPtr=l(128*4),this.outLPtr=l(128*4),this.outRPtr=l(128*4),this.paramsPtr=l(256*4),this.idPtr=l(32);const c=this.mem.buffer;this.inLView=new Float32Array(c,this.inLPtr,128),this.inRView=new Float32Array(c,this.inRPtr,128),this.outLView=new Float32Array(c,this.outLPtr,128),this.outRView=new Float32Array(c,this.outRPtr,128),this.paramsView=new Float32Array(c,this.paramsPtr,256),this.idView=new Uint8Array(c,this.idPtr,32),this.spawnEngine_("thru",r),this.pendingParams&&(this.applyParams_(this.pendingParams),this.pendingParams=null),this.muted=!1}spawnEngine_(i,r){if(!this.instance||!this.idView)return;this.engineHandle&&(this.instance.exports._nisps_engine_destroy(this.engineHandle),this.engineHandle=0);const a=new TextEncoder().encode(i);this.idView.fill(0),this.idView.set(a.subarray(0,Math.min(a.length,31))),this.engineHandle=this.instance.exports._nisps_engine_create(this.idPtr,r),this.engineId=i}switchEngine_(i){this.spawnEngine_(i,sampleRate)}applyParams_(i){if(!this.instance||!this.paramsView){this.pendingParams=i;return}const r=Math.min(i.length,256);for(let t=0;t<r;++t)this.paramsView[t]=i[t];this.engineHandle&&this.instance.exports._nisps_engine_set_params(this.engineHandle,this.paramsPtr,r)}process(i,r){const t=r[0];if(!t||t.length===0)return!0;const a=t[0],h=t.length>1?t[1]:t[0];if(this.muted||!this.instance||!this.engineHandle||!this.inLView||!this.outLView||!this.outRView||!this.inRView)return a.fill(0),t.length>1&&h.fill(0),!0;const n=i[0];return n&&n[0]?this.inLView.set(n[0].subarray(0,128)):this.inLView.fill(0),n&&n[1]?this.inRView.set(n[1].subarray(0,128)):n&&n[0]?this.inRView.set(n[0].subarray(0,128)):this.inRView.fill(0),this.instance.exports._nisps_engine_process_block(this.engineHandle,this.inLPtr,this.inRPtr,this.outLPtr,this.outRPtr,128),a.set(this.outLView.subarray(0,a.length)),t.length>1&&h.set(this.outRView.subarray(0,h.length)),!0}}registerProcessor("nisps-processor",f)})();
|
||||||
|
//# sourceMappingURL=nisps-processor-BuKuNOY1.js.map
|
||||||
1
playground/dist/assets/nisps-processor-BuKuNOY1.js.map
vendored
Normal file
1
playground/dist/assets/nisps-processor-BuKuNOY1.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
playground/dist/assets/wasm-iml-FZSgVMmn.js
vendored
Normal file
2
playground/dist/assets/wasm-iml-FZSgVMmn.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
playground/dist/assets/wasm-iml-FZSgVMmn.js.map
vendored
Normal file
1
playground/dist/assets/wasm-iml-FZSgVMmn.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
playground/dist/assets/wasm-worker-OATBBHaT.js
vendored
Normal file
2
playground/dist/assets/wasm-worker-OATBBHaT.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
(function(){"use strict";if(typeof window>"u"&&typeof self<"u"&&typeof self.importScripts<"u"){let h=function(t,s,n,u){if(!e)throw new Error("worker module not loaded");c!==a&&(i&&e._free(i),i=e._malloc(a*4),c=a),t.length!==g&&(l&&e._free(l),l=e._malloc(t.length*4),g=t.length),s.length!==_&&(f&&e._free(f),f=e._malloc(s.length*4),_=s.length),n.length!==w&&(o&&e._free(o),o=n.length>0?e._malloc(n.length*4):0,w=n.length),new Float32Array(e.HEAPF32.buffer,i,a).set(u),new Float32Array(e.HEAPF32.buffer,l,t.length).set(t),new Float32Array(e.HEAPF32.buffer,f,s.length).set(s),o&&new Float32Array(e.HEAPF32.buffer,o,n.length).set(n)},p=function(t){if(!e)return{kind:"error",requestId:t.requestId,message:"worker not initialised"};try{h(t.features,t.labels,t.sampleWeights,t.weights),e._nisps_ml_set_weights(r,i),e._nisps_ml_clear_examples(r);const s=t.inputSize,n=t.outputSize,u=t.features.length/s;for(let d=0;d<u;++d){const S=l+d*s*4,H=f+d*n*4;e._nisps_ml_add_example(r,S,H)}const F=t.sampleWeights.length>0?o:0,m=e._nisps_ml_train(r,t.lr,t.maxIter,t.minErr,F);e._nisps_ml_get_weights(r,i);const A=new Float32Array(e.HEAPF32.buffer,i,a),P=new Float32Array(A),E=new Float32Array([m]);return{kind:"result",requestId:t.requestId,loss:m,weights:P,lossHistory:E}}catch(s){return{kind:"error",requestId:t.requestId,message:s instanceof Error?s.message:String(s)}}},y=function(){e&&(r&&(e._nisps_ml_destroy(r),r=0),i&&(e._free(i),i=0),l&&(e._free(l),l=0),f&&(e._free(f),f=0),o&&(e._free(o),o=0),e=null)},e=null,r=0,a=0,i=0,c=0,l=0,g=0,f=0,_=0,o=0,w=0;async function k(t){const s=await import(new URL("/nisps.js",self.location.origin).toString());e=await(s.default??s.createNispsModule)({locateFile:u=>u.endsWith(".wasm")?new URL("/nisps.wasm",self.location.origin).toString():u}),r=e._nisps_ml_create(0,0,0,0,t>>>0),a=e._nisps_ml_weight_count(r)}self.addEventListener("message",async t=>{const s=t.data;if(s.kind==="init")try{await k(s.seed),self.postMessage({kind:"ready"})}catch(n){self.postMessage({kind:"error",requestId:0,message:n instanceof Error?n.message:String(n)})}else if(s.kind==="train"){const n=p(s);n.kind==="result"?self.postMessage(n,[n.weights.buffer,n.lossHistory.buffer]):self.postMessage(n)}else s.kind==="dispose"&&y()})}})();
|
||||||
|
//# sourceMappingURL=wasm-worker-OATBBHaT.js.map
|
||||||
1
playground/dist/assets/wasm-worker-OATBBHaT.js.map
vendored
Normal file
1
playground/dist/assets/wasm-worker-OATBBHaT.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -5,9 +5,10 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<meta name="theme-color" content="#0d0d0d" />
|
<meta name="theme-color" content="#0d0d0d" />
|
||||||
<title>MEMLNaut Playground</title>
|
<title>MEMLNaut Playground</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-C1-a2ZPT.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-D19lnkK_.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
2
playground/dist/nisps.js
vendored
Normal file
2
playground/dist/nisps.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
playground/dist/nisps.wasm
vendored
Executable file
BIN
playground/dist/nisps.wasm
vendored
Executable file
Binary file not shown.
1886
playground/package-lock.json
generated
1886
playground/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "memlnaut-playground",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc --noEmit && vite build",
|
|
||||||
"preview": "vite preview --port 4173",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"test:e2e": "playwright test",
|
|
||||||
"test:e2e:headed": "playwright test --headed",
|
|
||||||
"test:e2e:ui": "playwright test --ui"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"solid-js": "^1.8.22"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@playwright/test": "^1.48.0",
|
|
||||||
"@types/node": "^22.7.0",
|
|
||||||
"typescript": "^5.5.4",
|
|
||||||
"vite": "^5.4.10",
|
|
||||||
"vite-plugin-solid": "^2.10.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { defineConfig } from '@playwright/test';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Playwright config for the SolidJS playground.
|
|
||||||
*
|
|
||||||
* The webServer block runs `vite preview` against the build output, so the
|
|
||||||
* tests exercise the real production bundle. Vite's preview command honors
|
|
||||||
* the COOP/COEP headers configured in vite.config.ts, which the AudioWorklet
|
|
||||||
* + WASM bridge needs.
|
|
||||||
*
|
|
||||||
* To run a fresh build before tests, run `bun run build` separately — the
|
|
||||||
* preview server expects `dist/` to already exist. CI does this in the
|
|
||||||
* workflow before invoking Playwright.
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './tests/e2e',
|
|
||||||
timeout: 30_000,
|
|
||||||
expect: { timeout: 10_000 },
|
|
||||||
use: {
|
|
||||||
baseURL: 'http://localhost:4173',
|
|
||||||
headless: true,
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
},
|
|
||||||
webServer: {
|
|
||||||
// `bun run preview` is `vite preview --port 4173` (see playground/package.json).
|
|
||||||
// Reuses an already-running server so `npx playwright test` after `bun
|
|
||||||
// dev` Just Works.
|
|
||||||
command: 'bun run preview',
|
|
||||||
cwd: '.',
|
|
||||||
url: 'http://localhost:4173',
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
timeout: 30_000,
|
|
||||||
},
|
|
||||||
projects: [{ name: 'chromium', use: { browserName: 'chromium' } }],
|
|
||||||
reporter: process.env.CI ? [['list'], ['github']] : [['list'], ['html', { open: 'never' }]],
|
|
||||||
});
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -1,84 +0,0 @@
|
||||||
.app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-3) var(--sp-4);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
background: var(--bg-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dim {
|
|
||||||
color: var(--fg-mute);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav button {
|
|
||||||
padding: var(--sp-1) var(--sp-3);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
border-color: var(--accent) !important;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--sp-5);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home {
|
|
||||||
max-width: 720px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--fs-xl);
|
|
||||||
margin: 0;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagline {
|
|
||||||
color: var(--fg-mute);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkList {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
color: var(--fg-dim);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
margin-top: var(--sp-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notFound {
|
|
||||||
max-width: 540px;
|
|
||||||
margin: var(--sp-6) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notFound h1 {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
import { Component, createMemo, createSignal, lazy, onCleanup, Show } from 'solid-js';
|
|
||||||
import { Dynamic } from 'solid-js/web';
|
|
||||||
import { modeStore } from './stores/mode-store';
|
|
||||||
import { MODE_REGISTRY, getModeById } from './modes';
|
|
||||||
import { ModeSwitcher } from './modes/ModeSwitcher';
|
|
||||||
import styles from './App.module.css';
|
|
||||||
|
|
||||||
type Route = 'home' | 'primitives' | 'modes' | 'unknown';
|
|
||||||
|
|
||||||
function parseRoute(path: string): Route {
|
|
||||||
if (path === '' || path === '/' || path === '/index.html') return 'home';
|
|
||||||
if (path === '/dev/primitives' || path === '/dev/primitives/') return 'primitives';
|
|
||||||
if (path === '/modes' || path === '/modes/' || path.startsWith('/modes/')) return 'modes';
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lazy-loaded so the home route doesn't pay the cost of loading every
|
|
||||||
// primitive demo. Stream 9/10 can grow the showcase freely.
|
|
||||||
const PrimitivesShowcase = lazy(() => import('./dev/PrimitivesShowcase'));
|
|
||||||
|
|
||||||
const App: Component = () => {
|
|
||||||
const [route, setRoute] = createSignal<Route>(parseRoute(window.location.pathname));
|
|
||||||
|
|
||||||
const onPop = () => setRoute(parseRoute(window.location.pathname));
|
|
||||||
window.addEventListener('popstate', onPop);
|
|
||||||
onCleanup(() => window.removeEventListener('popstate', onPop));
|
|
||||||
|
|
||||||
const navigate = (path: string) => {
|
|
||||||
if (window.location.pathname === path) return;
|
|
||||||
window.history.pushState({}, '', path);
|
|
||||||
setRoute(parseRoute(path));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={styles.app}>
|
|
||||||
<header class={styles.header}>
|
|
||||||
<strong>MEMLNaut</strong>
|
|
||||||
<span class={styles.dim}>playground</span>
|
|
||||||
<nav class={styles.nav}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={route() === 'home' ? styles.active : ''}
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
>
|
|
||||||
home
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={route() === 'modes' ? styles.active : ''}
|
|
||||||
onClick={() => navigate('/modes')}
|
|
||||||
>
|
|
||||||
/modes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={route() === 'primitives' ? styles.active : ''}
|
|
||||||
onClick={() => navigate('/dev/primitives')}
|
|
||||||
>
|
|
||||||
/dev/primitives
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main class={styles.main}>
|
|
||||||
<Show when={route() === 'home'}>
|
|
||||||
<Home navigate={navigate} />
|
|
||||||
</Show>
|
|
||||||
<Show when={route() === 'modes'}>
|
|
||||||
<ModesPage />
|
|
||||||
</Show>
|
|
||||||
<Show when={route() === 'primitives'}>
|
|
||||||
<PrimitivesShowcase />
|
|
||||||
</Show>
|
|
||||||
<Show when={route() === 'unknown'}>
|
|
||||||
<div class={styles.notFound}>
|
|
||||||
<h1>404</h1>
|
|
||||||
<p>No route at <code>{window.location.pathname}</code>.</p>
|
|
||||||
<p>
|
|
||||||
<a href="/" onClick={(e) => { e.preventDefault(); navigate('/'); }}>back to home</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ModesPage: Component = () => {
|
|
||||||
const activeId = () => modeStore.state.activeModeId ?? MODE_REGISTRY[0]!.id;
|
|
||||||
const ActiveComponent = createMemo(() => getModeById(activeId()).Component);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ModeSwitcher />
|
|
||||||
<Dynamic component={ActiveComponent()} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HomeProps {
|
|
||||||
navigate: (path: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Home: Component<HomeProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<div class={styles.home}>
|
|
||||||
<h1 class={styles.title}>MEMLNaut Playground</h1>
|
|
||||||
<p class={styles.tagline}>
|
|
||||||
Interactive ML control of audio. Stream 9: nine modes, one shell.
|
|
||||||
</p>
|
|
||||||
<ul class={styles.linkList}>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="/modes"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.navigate('/modes');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Modes
|
|
||||||
</a>{' '}
|
|
||||||
— pick a mode and start playing
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="/dev/primitives"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.navigate('/dev/primitives');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Primitives showcase
|
|
||||||
</a>{' '}
|
|
||||||
— UI building blocks
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class={styles.note}>
|
|
||||||
ML inference runs on the main thread via WASM. Audio synthesis runs
|
|
||||||
in an AudioWorklet — start it from inside any mode using the "Start
|
|
||||||
audio" button. Audio cannot autoplay (browser policy).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
/**
|
|
||||||
* EngineHost — main-thread side of the WASM AudioWorklet pipeline.
|
|
||||||
*
|
|
||||||
* Responsibilities:
|
|
||||||
* - Lazy-create AudioContext (browsers gate this on user gesture).
|
|
||||||
* - Register the AudioWorklet processor module.
|
|
||||||
* - Hand the worklet a copy of `nisps.wasm` bytes (we fetch on the main
|
|
||||||
* thread because AudioWorklet has no `fetch`/`importScripts`).
|
|
||||||
* - Send engine selection + parameter updates over the worklet `port`.
|
|
||||||
* - Tear everything down on `dispose()`.
|
|
||||||
*
|
|
||||||
* The actual DSP runs in `playground/src/audio/worklet/nisps-processor.ts`,
|
|
||||||
* which calls into a SECOND WASM instance owned by the worklet thread.
|
|
||||||
*
|
|
||||||
* IMPORTANT: this class never auto-starts audio. The caller must drive
|
|
||||||
* `start()` from a user gesture (button click, etc.) so browsers don't
|
|
||||||
* block AudioContext creation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { EngineId } from '../ml/types';
|
|
||||||
|
|
||||||
// `?worker&url` makes Vite COMPILE the worklet TS→JS, bundle its imports, and
|
|
||||||
// hand back a hashed .js URL. Plain `new URL('./x.ts', import.meta.url)` does
|
|
||||||
// NOT work for audioWorklet.addModule (Vite only treats that as a worker entry
|
|
||||||
// for `new Worker(...)`) — it copies raw .ts, which the browser rejects.
|
|
||||||
import workletUrl from './worklet/nisps-processor.ts?worker&url';
|
|
||||||
|
|
||||||
const NISPS_WASM_URL = '/nisps.wasm';
|
|
||||||
const PROCESSOR_NAME = 'nisps-processor';
|
|
||||||
|
|
||||||
/** Message protocol: main → worklet. */
|
|
||||||
export type HostToWorkletMessage =
|
|
||||||
| {
|
|
||||||
kind: 'init';
|
|
||||||
// ArrayBuffer transferred so the worklet can `WebAssembly.compile` it.
|
|
||||||
wasmBinary: ArrayBuffer;
|
|
||||||
sampleRate: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'engine';
|
|
||||||
engineId: EngineId;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'params';
|
|
||||||
// Float32Array transferred to avoid per-tick copy.
|
|
||||||
params: Float32Array;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'mute';
|
|
||||||
muted: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Message protocol: worklet → main. */
|
|
||||||
export type WorkletToHostMessage =
|
|
||||||
| { kind: 'ready' }
|
|
||||||
| { kind: 'error'; message: string };
|
|
||||||
|
|
||||||
export interface EngineHostOptions {
|
|
||||||
/** Override sample rate (default: AudioContext.sampleRate). */
|
|
||||||
sampleRate?: number;
|
|
||||||
/** Override worklet processor URL (testing). */
|
|
||||||
processorUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EngineHost {
|
|
||||||
private ctx: AudioContext | null = null;
|
|
||||||
private node: AudioWorkletNode | null = null;
|
|
||||||
private workletReady = false;
|
|
||||||
private currentEngine: EngineId = 'thru';
|
|
||||||
private disposed = false;
|
|
||||||
private options: EngineHostOptions;
|
|
||||||
|
|
||||||
// Cached bytes of nisps.wasm (we fetch once per host instance).
|
|
||||||
private wasmBytes: ArrayBuffer | null = null;
|
|
||||||
|
|
||||||
constructor(options: EngineHostOptions = {}) {
|
|
||||||
this.options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isStarted(): boolean {
|
|
||||||
return !!this.ctx && this.workletReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
get sampleRate(): number {
|
|
||||||
return this.ctx?.sampleRate ?? this.options.sampleRate ?? 48000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start audio. Must be called from a user gesture for AudioContext to
|
|
||||||
* resume. After this resolves, `setEngine()` and `setParams()` can be
|
|
||||||
* called.
|
|
||||||
*/
|
|
||||||
async start(engineId: EngineId = 'thru'): Promise<void> {
|
|
||||||
if (this.ctx) {
|
|
||||||
// Already started; just switch engine.
|
|
||||||
this.setEngine(engineId);
|
|
||||||
await this.ctx.resume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.ctx = new AudioContext({
|
|
||||||
sampleRate: this.options.sampleRate,
|
|
||||||
latencyHint: 'interactive',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch the WASM bytes on the main thread (the worklet doesn't have
|
|
||||||
// fetch). We pass the buffer to the worklet as a transferable.
|
|
||||||
if (!this.wasmBytes) {
|
|
||||||
this.wasmBytes = await this.fetchWasm_();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the processor module. `workletUrl` is the Vite-compiled,
|
|
||||||
// bundled .js (see import note at top); addModule needs real JS, not raw TS.
|
|
||||||
const procUrl = this.options.processorUrl ?? workletUrl;
|
|
||||||
await this.ctx.audioWorklet.addModule(procUrl);
|
|
||||||
|
|
||||||
this.node = new AudioWorkletNode(this.ctx, PROCESSOR_NAME, {
|
|
||||||
numberOfInputs: 1,
|
|
||||||
numberOfOutputs: 1,
|
|
||||||
outputChannelCount: [2],
|
|
||||||
});
|
|
||||||
this.node.connect(this.ctx.destination);
|
|
||||||
|
|
||||||
// Wire up message handler before sending init.
|
|
||||||
this.workletReady = false;
|
|
||||||
const ready = new Promise<void>((resolve, reject) => {
|
|
||||||
const onMsg = (ev: MessageEvent<WorkletToHostMessage>) => {
|
|
||||||
if (ev.data.kind === 'ready') {
|
|
||||||
this.workletReady = true;
|
|
||||||
this.node?.port.removeEventListener('message', onMsg);
|
|
||||||
resolve();
|
|
||||||
} else if (ev.data.kind === 'error') {
|
|
||||||
this.node?.port.removeEventListener('message', onMsg);
|
|
||||||
reject(new Error(ev.data.message));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.node!.port.addEventListener('message', onMsg);
|
|
||||||
this.node!.port.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send the WASM binary + sample rate. ArrayBuffer is transferable —
|
|
||||||
// we keep a copy on the main thread for re-init.
|
|
||||||
const copy = this.wasmBytes.slice(0);
|
|
||||||
this.node.port.postMessage(
|
|
||||||
{ kind: 'init', wasmBinary: copy, sampleRate: this.ctx.sampleRate } satisfies HostToWorkletMessage,
|
|
||||||
[copy],
|
|
||||||
);
|
|
||||||
|
|
||||||
await ready;
|
|
||||||
this.currentEngine = engineId;
|
|
||||||
if (engineId !== 'thru') {
|
|
||||||
this.setEngine(engineId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch which engine the worklet is processing. Cheap — just a message.
|
|
||||||
* The worklet handles engine destruction/creation internally.
|
|
||||||
*/
|
|
||||||
setEngine(engineId: EngineId): void {
|
|
||||||
if (!this.node || !this.workletReady) return;
|
|
||||||
this.currentEngine = engineId;
|
|
||||||
this.node.port.postMessage({ kind: 'engine', engineId } satisfies HostToWorkletMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push a fresh parameter vector. Caller should NOT reuse the buffer
|
|
||||||
* after this call — we transfer it. If you need to keep yours, pass a
|
|
||||||
* copy: `host.setParams(new Float32Array(myBuf))`.
|
|
||||||
*/
|
|
||||||
setParams(params: Float32Array): void {
|
|
||||||
if (!this.node || !this.workletReady) return;
|
|
||||||
this.node.port.postMessage(
|
|
||||||
{ kind: 'params', params } satisfies HostToWorkletMessage,
|
|
||||||
[params.buffer],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setMuted(muted: boolean): void {
|
|
||||||
if (!this.node || !this.workletReady) return;
|
|
||||||
this.node.port.postMessage({ kind: 'mute', muted } satisfies HostToWorkletMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
|
||||||
if (!this.ctx) return;
|
|
||||||
if (this.node) {
|
|
||||||
try {
|
|
||||||
this.node.disconnect();
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
this.node = null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.ctx.close();
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
this.ctx = null;
|
|
||||||
this.workletReady = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
if (this.disposed) return;
|
|
||||||
this.disposed = true;
|
|
||||||
void this.stop();
|
|
||||||
this.wasmBytes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchWasm_(): Promise<ArrayBuffer> {
|
|
||||||
const url = new URL(NISPS_WASM_URL, window.location.origin).toString();
|
|
||||||
const resp = await fetch(url);
|
|
||||||
if (!resp.ok) throw new Error(`fetch nisps.wasm: ${resp.status} ${resp.statusText}`);
|
|
||||||
return await resp.arrayBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Smoke-test helper: returns true iff the WASM module exposes the C
|
|
||||||
* functions we need. Useful as a build-time check inside `engine-host.ts`
|
|
||||||
* tests; not used in production.
|
|
||||||
*/
|
|
||||||
export async function smokeCheckWasm(): Promise<boolean> {
|
|
||||||
const url = new URL('/nisps.js', window.location.origin).toString();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const mod: any = await import(/* @vite-ignore */ url);
|
|
||||||
const factory = mod.default ?? mod.createNispsModule;
|
|
||||||
if (!factory) return false;
|
|
||||||
const m = await factory({
|
|
||||||
locateFile: (p: string) => p.endsWith('.wasm')
|
|
||||||
? new URL(NISPS_WASM_URL, window.location.origin).toString()
|
|
||||||
: p,
|
|
||||||
});
|
|
||||||
return typeof m._nisps_ml_create === 'function'
|
|
||||||
&& typeof m._nisps_engine_create === 'function'
|
|
||||||
&& typeof m._nisps_engine_process_block === 'function';
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# Why two WASM instances?
|
|
||||||
|
|
||||||
The playground loads `nisps.wasm` twice:
|
|
||||||
|
|
||||||
1. **Main thread**, via `playground/src/ml/wasm-iml.ts`. Used for ML
|
|
||||||
inference + sync training + RL operations + UI feedback.
|
|
||||||
2. **AudioWorklet thread**, via `nisps-processor.ts`. Used for engine
|
|
||||||
processing (per-128-sample-block).
|
|
||||||
|
|
||||||
Why not share?
|
|
||||||
|
|
||||||
- AudioWorklet runs on its own thread. Sharing memory across threads
|
|
||||||
requires SharedArrayBuffer + locking on every heap access; far more
|
|
||||||
expensive than two heaps.
|
|
||||||
- AudioWorklet has neither `fetch` nor ESM `import`, so it can't load
|
|
||||||
the Emscripten glue (`nisps.js`). The main thread fetches the WASM
|
|
||||||
bytes once and posts them here as a transferable `ArrayBuffer`; the
|
|
||||||
worklet then `WebAssembly.instantiate`s directly.
|
|
||||||
- Engines and ML never interact in the audio path. The main thread
|
|
||||||
computes the parameter vector each frame and pushes it into the
|
|
||||||
worklet via `port.postMessage`. The worklet pushes nothing back per
|
|
||||||
block (analysis features, if needed, are batched and sent at low
|
|
||||||
rate).
|
|
||||||
|
|
||||||
Per-frame data flow:
|
|
||||||
|
|
||||||
```
|
|
||||||
Joystick → input pipeline → mlStore.outputs (Float32Array, length=126)
|
|
||||||
↓ EngineHost.setParams()
|
|
||||||
↓ port.postMessage (transferable)
|
|
||||||
AudioWorklet ← WASM engine.process_block ← WASM engine.set_params
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom WASM loader
|
|
||||||
|
|
||||||
We do **not** use the Emscripten JS glue inside the worklet — the glue
|
|
||||||
contains `URL`, `Worker`, and `fetch` references that don't exist in
|
|
||||||
AudioWorkletGlobalScope. Instead `nisps-processor.ts` calls
|
|
||||||
`WebAssembly.instantiate` directly with hand-rolled imports and
|
|
||||||
discovers exports by walking the export descriptors. This makes the
|
|
||||||
worklet bundle small (just the processor TS) and avoids touching the
|
|
||||||
Emscripten init path.
|
|
||||||
|
|
||||||
The trade-off: only the engine API is callable here, not the ML API.
|
|
||||||
If you ever need ML inference inside the worklet (we don't), use the
|
|
||||||
main thread copy and post params over.
|
|
||||||
|
|
||||||
## Block size
|
|
||||||
|
|
||||||
AudioWorklet always calls `process()` with 128-sample blocks. Our
|
|
||||||
heap buffers in `nisps-processor.ts` are sized to match. Don't change
|
|
||||||
the block size without changing the buffer allocations.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* Type declarations for AudioWorkletGlobalScope. The default `lib.dom`
|
|
||||||
* and `lib.dom.iterable` files don't include these because they only
|
|
||||||
* exist inside an AudioWorklet thread.
|
|
||||||
*
|
|
||||||
* Keep this file minimal — only what `nisps-processor.ts` actually uses.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare const sampleRate: number;
|
|
||||||
declare const currentFrame: number;
|
|
||||||
declare const currentTime: number;
|
|
||||||
|
|
||||||
declare class AudioWorkletProcessor {
|
|
||||||
constructor(options?: { numberOfInputs?: number; numberOfOutputs?: number; processorOptions?: unknown });
|
|
||||||
readonly port: MessagePort;
|
|
||||||
process(
|
|
||||||
inputs: Float32Array[][],
|
|
||||||
outputs: Float32Array[][],
|
|
||||||
parameters: Record<string, Float32Array>,
|
|
||||||
): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare function registerProcessor(
|
|
||||||
name: string,
|
|
||||||
processorCtor: new (options?: unknown) => AudioWorkletProcessor,
|
|
||||||
): void;
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
/**
|
|
||||||
* AudioWorkletProcessor that runs `nisps.wasm` engines.
|
|
||||||
*
|
|
||||||
* Why a separate WASM instance from the main thread? AudioWorklet runs in
|
|
||||||
* its own thread + global scope; reusing a single instance would require
|
|
||||||
* SharedArrayBuffer + locking on the heap. Architecture.md §6.4 specifies
|
|
||||||
* separate instances connected by `port` messages instead.
|
|
||||||
*
|
|
||||||
* Wasm load path: AudioWorklet has NO `fetch` and NO ESM `import`. The
|
|
||||||
* main thread fetches `nisps.wasm` once and posts the bytes here as an
|
|
||||||
* ArrayBuffer; we then `WebAssembly.compile` and `instantiate` directly,
|
|
||||||
* skipping the Emscripten glue entirely. This is fine because the
|
|
||||||
* exported functions don't need any of the JS-side runtime.
|
|
||||||
*
|
|
||||||
* Block size: AudioWorklet ALWAYS calls process() with 128-sample blocks.
|
|
||||||
* We allocate 128-sample input and output buffers in the WASM linear
|
|
||||||
* memory and shuttle samples in/out per call.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// <reference path="./audioworklet-globals.d.ts" />
|
|
||||||
|
|
||||||
import type { EngineId } from '../../ml/types';
|
|
||||||
import type { HostToWorkletMessage, WorkletToHostMessage } from '../engine-host';
|
|
||||||
|
|
||||||
const PROC_BLOCK = 128;
|
|
||||||
const MAX_PARAMS = 256; // upper bound across all engines
|
|
||||||
|
|
||||||
interface WasmInstance {
|
|
||||||
exports: {
|
|
||||||
memory: WebAssembly.Memory;
|
|
||||||
malloc: (n: number) => number;
|
|
||||||
free: (p: number) => void;
|
|
||||||
_nisps_engine_create: (id_ptr: number, sample_rate: number) => number;
|
|
||||||
_nisps_engine_destroy: (engine: number) => void;
|
|
||||||
_nisps_engine_set_params: (engine: number, params_ptr: number, n: number) => void;
|
|
||||||
_nisps_engine_process_block: (
|
|
||||||
engine: number,
|
|
||||||
in_l: number, in_r: number,
|
|
||||||
out_l: number, out_r: number,
|
|
||||||
n_samples: number,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class NispsProcessor extends AudioWorkletProcessor {
|
|
||||||
private instance: WasmInstance | null = null;
|
|
||||||
private engineHandle = 0;
|
|
||||||
private engineId: EngineId = 'thru';
|
|
||||||
private muted = true;
|
|
||||||
|
|
||||||
// Pointers + buffer views (allocated once instance is up).
|
|
||||||
private inLPtr = 0;
|
|
||||||
private inRPtr = 0;
|
|
||||||
private outLPtr = 0;
|
|
||||||
private outRPtr = 0;
|
|
||||||
private idPtr = 0;
|
|
||||||
private paramsPtr = 0;
|
|
||||||
private inLView: Float32Array | null = null;
|
|
||||||
private inRView: Float32Array | null = null;
|
|
||||||
private outLView: Float32Array | null = null;
|
|
||||||
private outRView: Float32Array | null = null;
|
|
||||||
private paramsView: Float32Array | null = null;
|
|
||||||
private idView: Uint8Array | null = null;
|
|
||||||
private mem: WebAssembly.Memory | null = null;
|
|
||||||
|
|
||||||
// Pending params posted before the engine was ready.
|
|
||||||
private pendingParams: Float32Array | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.port.onmessage = (ev) => this.onMessage_(ev.data as HostToWorkletMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onMessage_(msg: HostToWorkletMessage): Promise<void> {
|
|
||||||
if (msg.kind === 'init') {
|
|
||||||
try {
|
|
||||||
await this.init_(msg.wasmBinary, msg.sampleRate);
|
|
||||||
this.post_({ kind: 'ready' });
|
|
||||||
} catch (err) {
|
|
||||||
this.post_({
|
|
||||||
kind: 'error',
|
|
||||||
message: err instanceof Error ? err.message : String(err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (msg.kind === 'engine') {
|
|
||||||
this.switchEngine_(msg.engineId);
|
|
||||||
} else if (msg.kind === 'params') {
|
|
||||||
this.applyParams_(msg.params);
|
|
||||||
} else if (msg.kind === 'mute') {
|
|
||||||
this.muted = msg.muted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private post_(msg: WorkletToHostMessage): void {
|
|
||||||
this.port.postMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile + instantiate the wasm module. We provide minimal imports —
|
|
||||||
* the Emscripten module needs `__abort_js` and `_emscripten_resize_heap`
|
|
||||||
* (we keep memory non-resizing so the latter is a stub).
|
|
||||||
*/
|
|
||||||
private async init_(bytes: ArrayBuffer, sampleRate: number): Promise<void> {
|
|
||||||
const memory = new WebAssembly.Memory({ initial: 128, maximum: 4096, shared: false });
|
|
||||||
const imports: WebAssembly.Imports = {
|
|
||||||
// Emscripten import "a" group; field names match the generated JS.
|
|
||||||
a: {
|
|
||||||
a: () => { throw new Error('wasm aborted'); },
|
|
||||||
b: () => false, // _emscripten_resize_heap returning 0 disables growth
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const compiled = await WebAssembly.compile(bytes);
|
|
||||||
// Discover the actual import shape from the module — names like "a",
|
|
||||||
// "b" depend on emcc's mangling; we accept whatever it produces.
|
|
||||||
const importDesc = WebAssembly.Module.imports(compiled);
|
|
||||||
const reshaped: WebAssembly.Imports = {};
|
|
||||||
for (const desc of importDesc) {
|
|
||||||
if (!reshaped[desc.module]) reshaped[desc.module] = {} as WebAssembly.ModuleImports;
|
|
||||||
const mod = reshaped[desc.module] as WebAssembly.ModuleImports;
|
|
||||||
if (desc.kind === 'function') {
|
|
||||||
if (desc.name === 'c') {
|
|
||||||
// unused
|
|
||||||
}
|
|
||||||
mod[desc.name] = (() => {
|
|
||||||
// Generic stub: log + return 0.
|
|
||||||
return (..._args: unknown[]) => 0;
|
|
||||||
})();
|
|
||||||
} else if (desc.kind === 'memory') {
|
|
||||||
mod[desc.name] = memory;
|
|
||||||
} else if (desc.kind === 'table') {
|
|
||||||
mod[desc.name] = new WebAssembly.Table({ element: 'anyfunc', initial: 0 });
|
|
||||||
} else if (desc.kind === 'global') {
|
|
||||||
mod[desc.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For known-needed Emscripten imports, supply real implementations.
|
|
||||||
for (const desc of importDesc) {
|
|
||||||
const mod = reshaped[desc.module] as WebAssembly.ModuleImports;
|
|
||||||
// __abort_js
|
|
||||||
if (desc.name === 'a' && desc.kind === 'function') {
|
|
||||||
mod[desc.name] = () => { throw new Error('wasm aborted'); };
|
|
||||||
}
|
|
||||||
// _emscripten_resize_heap
|
|
||||||
if (desc.name === 'b' && desc.kind === 'function') {
|
|
||||||
mod[desc.name] = (_size: number) => 0; // refuse growth in worklet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void imports; // silence unused
|
|
||||||
const wasmInst = await WebAssembly.instantiate(compiled, reshaped);
|
|
||||||
|
|
||||||
// Many Emscripten exports use single-letter mangled names. Discover
|
|
||||||
// by reading the export descriptors.
|
|
||||||
const exDesc = WebAssembly.Module.exports(compiled);
|
|
||||||
const exMap = new Map<string, string>(); // logical name → mangled
|
|
||||||
for (const e of exDesc) {
|
|
||||||
// The exports list includes both the original (with leading
|
|
||||||
// underscore for C funcs) and the mangled single-letter alias used
|
|
||||||
// in the import section. We only see the export side here, but
|
|
||||||
// Emscripten in modern versions also re-exports the C names with
|
|
||||||
// their leading-underscore form. Walk both.
|
|
||||||
exMap.set(e.name, e.name);
|
|
||||||
}
|
|
||||||
const exports = wasmInst.exports as Record<string, WebAssembly.ExportValue>;
|
|
||||||
|
|
||||||
function pickFn(...names: string[]): (...args: number[]) => number {
|
|
||||||
for (const n of names) {
|
|
||||||
const v = exports[n];
|
|
||||||
if (typeof v === 'function') return v as unknown as (...a: number[]) => number;
|
|
||||||
}
|
|
||||||
throw new Error(`worklet: missing wasm export, tried: ${names.join(', ')}`);
|
|
||||||
}
|
|
||||||
function pickFnVoid(...names: string[]): (...args: number[]) => void {
|
|
||||||
return pickFn(...names) as unknown as (...args: number[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The exports we need.
|
|
||||||
const malloc = pickFn('_malloc', 'malloc');
|
|
||||||
const free = pickFnVoid('_free', 'free');
|
|
||||||
const ec = pickFn('_nisps_engine_create');
|
|
||||||
const ed = pickFnVoid('_nisps_engine_destroy');
|
|
||||||
const esp = pickFnVoid('_nisps_engine_set_params');
|
|
||||||
const epb = pickFnVoid('_nisps_engine_process_block');
|
|
||||||
|
|
||||||
// The wasm-exported memory might be named `memory` or another mangled
|
|
||||||
// alias. Find it.
|
|
||||||
let wasmMemory: WebAssembly.Memory | null = null;
|
|
||||||
for (const e of exDesc) {
|
|
||||||
if (e.kind === 'memory') {
|
|
||||||
const v = exports[e.name];
|
|
||||||
if (v instanceof WebAssembly.Memory) { wasmMemory = v; break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the module imports memory (which our build does — we passed it),
|
|
||||||
// there will be no exported memory; use the imported one.
|
|
||||||
this.mem = wasmMemory ?? memory;
|
|
||||||
|
|
||||||
this.instance = {
|
|
||||||
exports: {
|
|
||||||
memory: this.mem,
|
|
||||||
malloc,
|
|
||||||
free,
|
|
||||||
_nisps_engine_create: (id, sr) => ec(id, sr),
|
|
||||||
_nisps_engine_destroy: (h) => ed(h),
|
|
||||||
_nisps_engine_set_params: (h, p, n) => esp(h, p, n),
|
|
||||||
_nisps_engine_process_block: (h, il, ir, ol, or_, n) => epb(h, il, ir, ol, or_, n),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Allocate buffers.
|
|
||||||
this.inLPtr = malloc(PROC_BLOCK * 4);
|
|
||||||
this.inRPtr = malloc(PROC_BLOCK * 4);
|
|
||||||
this.outLPtr = malloc(PROC_BLOCK * 4);
|
|
||||||
this.outRPtr = malloc(PROC_BLOCK * 4);
|
|
||||||
this.paramsPtr = malloc(MAX_PARAMS * 4);
|
|
||||||
// Engine ids are short ASCII; 32 bytes covers everything we have.
|
|
||||||
this.idPtr = malloc(32);
|
|
||||||
|
|
||||||
const buf = this.mem.buffer;
|
|
||||||
this.inLView = new Float32Array(buf, this.inLPtr, PROC_BLOCK);
|
|
||||||
this.inRView = new Float32Array(buf, this.inRPtr, PROC_BLOCK);
|
|
||||||
this.outLView = new Float32Array(buf, this.outLPtr, PROC_BLOCK);
|
|
||||||
this.outRView = new Float32Array(buf, this.outRPtr, PROC_BLOCK);
|
|
||||||
this.paramsView = new Float32Array(buf, this.paramsPtr, MAX_PARAMS);
|
|
||||||
this.idView = new Uint8Array(buf, this.idPtr, 32);
|
|
||||||
|
|
||||||
// Default engine: thru.
|
|
||||||
this.spawnEngine_('thru', sampleRate);
|
|
||||||
|
|
||||||
// Apply pending params if any arrived before init completed.
|
|
||||||
if (this.pendingParams) {
|
|
||||||
this.applyParams_(this.pendingParams);
|
|
||||||
this.pendingParams = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.muted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private spawnEngine_(id: EngineId, sampleRate: number): void {
|
|
||||||
if (!this.instance || !this.idView) return;
|
|
||||||
if (this.engineHandle) {
|
|
||||||
this.instance.exports._nisps_engine_destroy(this.engineHandle);
|
|
||||||
this.engineHandle = 0;
|
|
||||||
}
|
|
||||||
// Write engine_id as ASCII into idView, NUL-terminated.
|
|
||||||
const enc = new TextEncoder();
|
|
||||||
const bytes = enc.encode(id);
|
|
||||||
this.idView.fill(0);
|
|
||||||
this.idView.set(bytes.subarray(0, Math.min(bytes.length, 31)));
|
|
||||||
this.engineHandle = this.instance.exports._nisps_engine_create(this.idPtr, sampleRate);
|
|
||||||
this.engineId = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private switchEngine_(id: EngineId): void {
|
|
||||||
// sampleRate global from AudioWorkletGlobalScope.
|
|
||||||
this.spawnEngine_(id, sampleRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyParams_(params: Float32Array): void {
|
|
||||||
if (!this.instance || !this.paramsView) {
|
|
||||||
this.pendingParams = params;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const n = Math.min(params.length, MAX_PARAMS);
|
|
||||||
for (let i = 0; i < n; ++i) this.paramsView[i] = params[i];
|
|
||||||
if (this.engineHandle) {
|
|
||||||
this.instance.exports._nisps_engine_set_params(this.engineHandle, this.paramsPtr, n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
|
|
||||||
const out = outputs[0];
|
|
||||||
if (!out || out.length === 0) return true;
|
|
||||||
|
|
||||||
const outL = out[0];
|
|
||||||
const outR = out.length > 1 ? out[1] : out[0];
|
|
||||||
|
|
||||||
if (this.muted || !this.instance || !this.engineHandle ||
|
|
||||||
!this.inLView || !this.outLView || !this.outRView || !this.inRView) {
|
|
||||||
// Silence.
|
|
||||||
outL.fill(0);
|
|
||||||
if (out.length > 1) outR.fill(0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy inputs into wasm buffers (zero-fill if missing).
|
|
||||||
const inp = inputs[0];
|
|
||||||
if (inp && inp[0]) this.inLView.set(inp[0].subarray(0, PROC_BLOCK));
|
|
||||||
else this.inLView.fill(0);
|
|
||||||
if (inp && inp[1]) this.inRView.set(inp[1].subarray(0, PROC_BLOCK));
|
|
||||||
else if (inp && inp[0]) this.inRView.set(inp[0].subarray(0, PROC_BLOCK));
|
|
||||||
else this.inRView.fill(0);
|
|
||||||
|
|
||||||
this.instance.exports._nisps_engine_process_block(
|
|
||||||
this.engineHandle,
|
|
||||||
this.inLPtr, this.inRPtr,
|
|
||||||
this.outLPtr, this.outRPtr,
|
|
||||||
PROC_BLOCK,
|
|
||||||
);
|
|
||||||
|
|
||||||
outL.set(this.outLView.subarray(0, outL.length));
|
|
||||||
if (out.length > 1) outR.set(this.outRView.subarray(0, outR.length));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerProcessor('nisps-processor', NispsProcessor);
|
|
||||||
|
|
@ -1,516 +0,0 @@
|
||||||
/**
|
|
||||||
* Debug probe: window.__nisps
|
|
||||||
*
|
|
||||||
* Synchronous-or-immediate Promise API for Playwright tests and dev console
|
|
||||||
* use. Bypasses SolidJS reactivity by reading/writing the underlying stores
|
|
||||||
* and WASM directly — values propagate to the UI on the next reactive tick,
|
|
||||||
* but the probe always returns the freshest state.
|
|
||||||
*
|
|
||||||
* Stream 10 expanded the surface to cover stream 8/9 features:
|
|
||||||
* - Snapshot stack (push, pop, list)
|
|
||||||
* - A/B compare (capture, toggle, accept, revert)
|
|
||||||
* - Region pinning (add, remove, clear)
|
|
||||||
* - Auto-explore (toggle, interval, intensity)
|
|
||||||
* - Heatmap (refresh, color mode, getCells)
|
|
||||||
* - Weight health (histogram, status, layer stats)
|
|
||||||
* - Session presets (save, load, list, share URL)
|
|
||||||
* - Override application (read, write, clear)
|
|
||||||
*
|
|
||||||
* Test contract: every method either returns a value, returns null/empty,
|
|
||||||
* or returns an immediately-resolved Promise. None of them throw — bad
|
|
||||||
* input is silently ignored.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { batch, untrack } from 'solid-js';
|
|
||||||
import { mlStore } from '../stores/ml-store';
|
|
||||||
import { modeStore } from '../stores/mode-store';
|
|
||||||
import { sessionStore } from '../stores/session-store';
|
|
||||||
import { explorationStore } from '../stores/exploration-store';
|
|
||||||
import { controlStore } from '../stores/control-store';
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
import { outputStore } from '../stores/output-store';
|
|
||||||
import { coreBus } from '../stores/bus';
|
|
||||||
import {
|
|
||||||
autoSnapshot,
|
|
||||||
undoLastSnapshot,
|
|
||||||
captureA as snapCaptureA,
|
|
||||||
toggleAB as snapToggleAB,
|
|
||||||
acceptB as snapAcceptB,
|
|
||||||
revertToA as snapRevertToA,
|
|
||||||
} from '../features/snapshots';
|
|
||||||
import { HeatmapSampler } from '../features/heatmap-sampler';
|
|
||||||
import {
|
|
||||||
weightHistogram,
|
|
||||||
weightStatus as healthStatus,
|
|
||||||
layerNorms,
|
|
||||||
gradientStatuses,
|
|
||||||
layerWeightCounts,
|
|
||||||
} from '../features/weight-health';
|
|
||||||
import {
|
|
||||||
captureSessionPreset,
|
|
||||||
restoreSessionPreset,
|
|
||||||
saveNamedPreset,
|
|
||||||
loadNamedPreset,
|
|
||||||
buildShareUrl,
|
|
||||||
applyUrlParams,
|
|
||||||
type SessionPresetPayload,
|
|
||||||
} from '../features/session-preset';
|
|
||||||
import type { HeatmapColorMode } from '../features/heatmap-sampler';
|
|
||||||
|
|
||||||
export interface DebugProbe {
|
|
||||||
/** Current 126-element output vector (Float32Array). */
|
|
||||||
getOutputs(): Float32Array;
|
|
||||||
/** Current per-param post-override values (length = active mode params). */
|
|
||||||
getParamOutputs(): Float32Array;
|
|
||||||
/** Last training loss, or null if no training has occurred. */
|
|
||||||
getLoss(): number | null;
|
|
||||||
/** Loss history of last training run. */
|
|
||||||
getLossHistory(): ReadonlyArray<number>;
|
|
||||||
/** Flat weight array. */
|
|
||||||
getWeights(): Float32Array;
|
|
||||||
/** Number of training examples currently in the dataset. */
|
|
||||||
getExampleCount(): number;
|
|
||||||
/** Set joystick X/Y in [0,1] and run inference. */
|
|
||||||
setInputs(x: number, y: number): void;
|
|
||||||
/** Trigger thumbs-up RL feedback. */
|
|
||||||
thumbsUp(): void;
|
|
||||||
/** Trigger thumbs-down RL feedback. */
|
|
||||||
thumbsDown(): void;
|
|
||||||
/** Synchronous training; returns final loss. */
|
|
||||||
train(): number;
|
|
||||||
/** Async training; returns Promise<loss>. */
|
|
||||||
trainAsync(): Promise<number>;
|
|
||||||
/** Randomize weights with default spread. */
|
|
||||||
randomise(): void;
|
|
||||||
/** Clear all training examples. */
|
|
||||||
clearExamples(): void;
|
|
||||||
/** Force a save to localStorage now (no debounce). */
|
|
||||||
saveState(): void;
|
|
||||||
/** Non-destructive loss query against current dataset. */
|
|
||||||
evalLoss(): number | null;
|
|
||||||
/** Batch inference: input is Nx2 array of [x,y] pairs. */
|
|
||||||
inferBatch(points: ReadonlyArray<readonly [number, number]>): Float32Array;
|
|
||||||
/** Per-layer weight statistics: layerCount * 4 floats (mean|w|, max|w|, dead%, sat%). */
|
|
||||||
getLayerStats(): Float32Array;
|
|
||||||
|
|
||||||
// Snapshot / undo / A/B
|
|
||||||
pushSnapshot(tag: string): void;
|
|
||||||
popSnapshot(): boolean;
|
|
||||||
listSnapshots(): ReadonlyArray<{ id: string; tag: string; timestamp: number; noiseLevel: number }>;
|
|
||||||
clearSnapshots(): void;
|
|
||||||
captureA(): void;
|
|
||||||
toggleAB(): 'A' | 'B';
|
|
||||||
acceptB(): void;
|
|
||||||
revertToA(): void;
|
|
||||||
|
|
||||||
// Pins
|
|
||||||
addRegionPin(pin: { x: number; y: number; width: number; height: number }): string;
|
|
||||||
removeRegionPin(id: string): void;
|
|
||||||
toggleParamPin(modeId: string, paramName: string, outputIndex: number): boolean;
|
|
||||||
isParamPinned(modeId: string, paramName: string): boolean;
|
|
||||||
|
|
||||||
// Auto-explore
|
|
||||||
setAutoExplore(enabled: boolean): void;
|
|
||||||
setAutoExploreInterval(ms: number): void;
|
|
||||||
setAutoExploreIntensity(v: number): void;
|
|
||||||
|
|
||||||
// Pressure
|
|
||||||
setPressure(force: number, holdMs?: number): void;
|
|
||||||
|
|
||||||
// Heatmap
|
|
||||||
refreshHeatmap(force?: boolean): boolean;
|
|
||||||
getHeatmapCells(): Float32Array;
|
|
||||||
setHeatmapColorMode(mode: HeatmapColorMode): void;
|
|
||||||
|
|
||||||
// Weight health
|
|
||||||
getWeightHistogram(): number[];
|
|
||||||
getWeightStatus(): 'dead' | 'saturating' | 'healthy';
|
|
||||||
getGradientFlow(beforeWeights: Float32Array, afterWeights: Float32Array): { norms: number[]; status: ReturnType<typeof gradientStatuses> };
|
|
||||||
|
|
||||||
// Overrides
|
|
||||||
setOverride(modeId: string, paramName: string, override: Partial<{ min: number; max: number; curve: string; muted: boolean; pinned: boolean; frozen: boolean; fixedValue: number }>): void;
|
|
||||||
clearOverride(modeId: string, paramName: string): void;
|
|
||||||
clearAllOverrides(modeId: string): void;
|
|
||||||
getOverride(modeId: string, paramName: string): unknown;
|
|
||||||
|
|
||||||
// Compound axes
|
|
||||||
setAxis(axis: 'boldness' | 'memory' | 'precision', value: number): void;
|
|
||||||
applyControlPreset(id: string): boolean;
|
|
||||||
|
|
||||||
// Pipelines
|
|
||||||
setZoom(zoom: number): void;
|
|
||||||
setSpread(spread: number): void;
|
|
||||||
setOutputFreeze(frozen: boolean): void;
|
|
||||||
|
|
||||||
// Session presets
|
|
||||||
captureSessionPreset(withWeights?: boolean): SessionPresetPayload;
|
|
||||||
restoreSessionPreset(payload: SessionPresetPayload): boolean;
|
|
||||||
saveSessionPreset(name: string, withWeights?: boolean): void;
|
|
||||||
loadSessionPreset(id: string): boolean;
|
|
||||||
buildShareUrl(): string;
|
|
||||||
applyUrlParams(search?: string): boolean;
|
|
||||||
|
|
||||||
// Bus
|
|
||||||
emit(topic: string, data: unknown): void;
|
|
||||||
on(topic: string, handler: (data: unknown) => void): () => void;
|
|
||||||
|
|
||||||
/** True once the WASM is fully initialised. */
|
|
||||||
readonly __ready: boolean;
|
|
||||||
/** Force initialisation. Returns a promise that resolves when the WASM is ready. */
|
|
||||||
__init(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__nisps?: DebugProbe;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_F32 = new Float32Array(0);
|
|
||||||
|
|
||||||
let lazyInitPromise: Promise<void> | null = null;
|
|
||||||
function lazyInit(): Promise<void> {
|
|
||||||
if (mlStore.iml) return Promise.resolve();
|
|
||||||
if (!lazyInitPromise) {
|
|
||||||
lazyInitPromise = mlStore.initialize().then(() => undefined);
|
|
||||||
}
|
|
||||||
return lazyInitPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probe-local heatmap sampler (independent of any active mode runtime).
|
|
||||||
const probeHeatmap = new HeatmapSampler({ resolution: 16 });
|
|
||||||
|
|
||||||
const probe: DebugProbe = {
|
|
||||||
get __ready(): boolean {
|
|
||||||
return !!mlStore.iml && mlStore.state.ready;
|
|
||||||
},
|
|
||||||
|
|
||||||
__init(): Promise<void> {
|
|
||||||
return lazyInit();
|
|
||||||
},
|
|
||||||
|
|
||||||
getOutputs(): Float32Array {
|
|
||||||
return mlStore.outputs();
|
|
||||||
},
|
|
||||||
|
|
||||||
getParamOutputs(): Float32Array {
|
|
||||||
// The probe doesn't have a runtime reference; recompute from raw outputs.
|
|
||||||
const raw = mlStore.outputs();
|
|
||||||
if (raw.length === 0) return EMPTY_F32;
|
|
||||||
// Just return the raw outputs — actual override application requires
|
|
||||||
// schema knowledge that the probe doesn't track. Tests can read
|
|
||||||
// overrides via `getOverride` instead.
|
|
||||||
return raw;
|
|
||||||
},
|
|
||||||
|
|
||||||
getLoss(): number | null {
|
|
||||||
return mlStore.state.lastLoss;
|
|
||||||
},
|
|
||||||
|
|
||||||
getLossHistory(): ReadonlyArray<number> {
|
|
||||||
return mlStore.state.lossHistory;
|
|
||||||
},
|
|
||||||
|
|
||||||
getWeights(): Float32Array {
|
|
||||||
return mlStore.getWeights();
|
|
||||||
},
|
|
||||||
|
|
||||||
getExampleCount(): number {
|
|
||||||
return mlStore.state.exampleCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
setInputs(x: number, y: number): void {
|
|
||||||
if (!mlStore.iml) {
|
|
||||||
void lazyInit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
untrack(() => mlStore.iml!.inferXY(x, y));
|
|
||||||
},
|
|
||||||
|
|
||||||
thumbsUp(): void {
|
|
||||||
if (!mlStore.iml) return;
|
|
||||||
untrack(() => {
|
|
||||||
autoSnapshot('before thumbs-up (probe)');
|
|
||||||
mlStore.iml!.train(explorationStore.state.learningRate);
|
|
||||||
explorationStore.decayNoise(0.5);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
thumbsDown(): void {
|
|
||||||
if (!mlStore.iml) return;
|
|
||||||
untrack(() => {
|
|
||||||
autoSnapshot('before thumbs-down (probe)');
|
|
||||||
const cap = explorationStore.state.noiseCap;
|
|
||||||
const spread = explorationStore.state.spread;
|
|
||||||
mlStore.iml!.moveWeights(cap, spread);
|
|
||||||
explorationStore.growNoise(0.5);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
train(): number {
|
|
||||||
if (!mlStore.iml) {
|
|
||||||
void lazyInit();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return untrack(() => {
|
|
||||||
autoSnapshot('before train (probe)');
|
|
||||||
return mlStore.iml!.train(explorationStore.state.learningRate);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async trainAsync(): Promise<number> {
|
|
||||||
await lazyInit();
|
|
||||||
if (!mlStore.iml) return 0;
|
|
||||||
autoSnapshot('before trainAsync (probe)');
|
|
||||||
return mlStore.iml.trainAsync(explorationStore.state.learningRate);
|
|
||||||
},
|
|
||||||
|
|
||||||
randomise(): void {
|
|
||||||
if (!mlStore.iml) return;
|
|
||||||
untrack(() => {
|
|
||||||
autoSnapshot('before randomize (probe)');
|
|
||||||
mlStore.iml!.randomiseWeights(explorationStore.state.spread);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
clearExamples(): void {
|
|
||||||
mlStore.clearExamples();
|
|
||||||
},
|
|
||||||
|
|
||||||
saveState(): void {
|
|
||||||
mlStore.saveNow();
|
|
||||||
},
|
|
||||||
|
|
||||||
evalLoss(): number | null {
|
|
||||||
if (!mlStore.iml) return null;
|
|
||||||
return mlStore.iml.evalLoss();
|
|
||||||
},
|
|
||||||
|
|
||||||
inferBatch(points: ReadonlyArray<readonly [number, number]>): Float32Array {
|
|
||||||
if (!mlStore.iml) return new Float32Array(points.length * mlStore.state.outputSize);
|
|
||||||
return mlStore.iml.inferBatch(points);
|
|
||||||
},
|
|
||||||
|
|
||||||
getLayerStats(): Float32Array {
|
|
||||||
if (!mlStore.iml) return EMPTY_F32;
|
|
||||||
return mlStore.iml.getLayerStatsFlat();
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Snapshot / undo / A/B ---------------------------------------------
|
|
||||||
|
|
||||||
pushSnapshot(tag: string): void {
|
|
||||||
autoSnapshot(tag);
|
|
||||||
},
|
|
||||||
|
|
||||||
popSnapshot(): boolean {
|
|
||||||
return undoLastSnapshot();
|
|
||||||
},
|
|
||||||
|
|
||||||
listSnapshots() {
|
|
||||||
return sessionStore.listSnapshots().map((s) => ({
|
|
||||||
id: s.id,
|
|
||||||
tag: s.tag,
|
|
||||||
timestamp: s.timestamp,
|
|
||||||
noiseLevel: s.noiseLevel,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
clearSnapshots(): void {
|
|
||||||
sessionStore.clearSnapshots();
|
|
||||||
},
|
|
||||||
|
|
||||||
captureA(): void {
|
|
||||||
snapCaptureA();
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleAB(): 'A' | 'B' {
|
|
||||||
return snapToggleAB();
|
|
||||||
},
|
|
||||||
|
|
||||||
acceptB(): void {
|
|
||||||
snapAcceptB();
|
|
||||||
},
|
|
||||||
|
|
||||||
revertToA(): void {
|
|
||||||
snapRevertToA();
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Pins ---------------------------------------------------------------
|
|
||||||
|
|
||||||
addRegionPin(pin) {
|
|
||||||
const created = sessionStore.addRegionPin(pin);
|
|
||||||
return created.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRegionPin(id: string): void {
|
|
||||||
sessionStore.removeRegionPin(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleParamPin(modeId: string, paramName: string, outputIndex: number): boolean {
|
|
||||||
return sessionStore.toggleParamPin(`${modeId}:${paramName}`, outputIndex);
|
|
||||||
},
|
|
||||||
|
|
||||||
isParamPinned(modeId: string, paramName: string): boolean {
|
|
||||||
return sessionStore.isParamPinned(`${modeId}:${paramName}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Auto-explore -------------------------------------------------------
|
|
||||||
|
|
||||||
setAutoExplore(enabled: boolean): void {
|
|
||||||
explorationStore.setAutoExplore(enabled);
|
|
||||||
},
|
|
||||||
|
|
||||||
setAutoExploreInterval(ms: number): void {
|
|
||||||
explorationStore.setAutoExploreInterval(ms);
|
|
||||||
},
|
|
||||||
|
|
||||||
setAutoExploreIntensity(v: number): void {
|
|
||||||
explorationStore.setAutoExploreIntensity(v);
|
|
||||||
},
|
|
||||||
|
|
||||||
setPressure(force: number, holdMs: number = 0): void {
|
|
||||||
explorationStore.setPressure(force, holdMs);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Heatmap ------------------------------------------------------------
|
|
||||||
|
|
||||||
refreshHeatmap(force = false): boolean {
|
|
||||||
if (!mlStore.iml) return false;
|
|
||||||
const cfg = inputStore.config;
|
|
||||||
const z = cfg.zoom;
|
|
||||||
const cx = cfg.anchorMode === 'center' ? 0.5 : cfg.anchorX;
|
|
||||||
const cy = cfg.anchorMode === 'center' ? 0.5 : cfg.anchorY;
|
|
||||||
return probeHeatmap.update({ cx, cy, zoom: z }, force);
|
|
||||||
},
|
|
||||||
|
|
||||||
getHeatmapCells(): Float32Array {
|
|
||||||
return probeHeatmap.getCells();
|
|
||||||
},
|
|
||||||
|
|
||||||
setHeatmapColorMode(mode: HeatmapColorMode): void {
|
|
||||||
probeHeatmap.setColorMode(mode);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Weight health ------------------------------------------------------
|
|
||||||
|
|
||||||
getWeightHistogram(): number[] {
|
|
||||||
return weightHistogram(mlStore.weights());
|
|
||||||
},
|
|
||||||
|
|
||||||
getWeightStatus() {
|
|
||||||
return healthStatus(mlStore.weights());
|
|
||||||
},
|
|
||||||
|
|
||||||
getGradientFlow(beforeWeights: Float32Array, afterWeights: Float32Array) {
|
|
||||||
if (!mlStore.iml) return { norms: [], status: [] };
|
|
||||||
const arch = mlStore.iml.architecture;
|
|
||||||
const sizes = layerWeightCounts({
|
|
||||||
inputSize: arch.inputSize,
|
|
||||||
hidden: arch.hidden,
|
|
||||||
outputSize: arch.outputSize,
|
|
||||||
});
|
|
||||||
const norms = layerNorms(beforeWeights, afterWeights, sizes);
|
|
||||||
return { norms, status: gradientStatuses(norms) };
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Overrides ----------------------------------------------------------
|
|
||||||
|
|
||||||
setOverride(modeId: string, paramName: string, patch): void {
|
|
||||||
const existing = modeStore.getOverride(modeId, paramName);
|
|
||||||
const next = {
|
|
||||||
min: existing?.min ?? 0,
|
|
||||||
max: existing?.max ?? 1,
|
|
||||||
curve: existing?.curve ?? 'linear',
|
|
||||||
curveParam: existing?.curveParam,
|
|
||||||
muted: existing?.muted ?? false,
|
|
||||||
pinned: existing?.pinned ?? false,
|
|
||||||
frozen: existing?.frozen ?? false,
|
|
||||||
fixedValue: existing?.fixedValue ?? 0.5,
|
|
||||||
...patch,
|
|
||||||
} as Parameters<typeof modeStore.setOverride>[2];
|
|
||||||
modeStore.setOverride(modeId, paramName, next);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearOverride(modeId: string, paramName: string): void {
|
|
||||||
modeStore.clearOverride(modeId, paramName);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearAllOverrides(modeId: string): void {
|
|
||||||
modeStore.clearAllOverrides(modeId);
|
|
||||||
},
|
|
||||||
|
|
||||||
getOverride(modeId: string, paramName: string) {
|
|
||||||
return modeStore.getOverride(modeId, paramName);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Compound axes ------------------------------------------------------
|
|
||||||
|
|
||||||
setAxis(axis: 'boldness' | 'memory' | 'precision', value: number): void {
|
|
||||||
controlStore.setAxis(axis, value);
|
|
||||||
},
|
|
||||||
|
|
||||||
applyControlPreset(id: string): boolean {
|
|
||||||
return controlStore.applyPreset(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Pipelines ----------------------------------------------------------
|
|
||||||
|
|
||||||
setZoom(zoom: number): void {
|
|
||||||
inputStore.setZoom(zoom);
|
|
||||||
},
|
|
||||||
|
|
||||||
setSpread(spread: number): void {
|
|
||||||
explorationStore.setSpread(spread);
|
|
||||||
},
|
|
||||||
|
|
||||||
setOutputFreeze(frozen: boolean): void {
|
|
||||||
outputStore.setFreezeOutput(frozen);
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Session presets ----------------------------------------------------
|
|
||||||
|
|
||||||
captureSessionPreset(withWeights = false): SessionPresetPayload {
|
|
||||||
return captureSessionPreset(withWeights);
|
|
||||||
},
|
|
||||||
|
|
||||||
restoreSessionPreset(payload: SessionPresetPayload): boolean {
|
|
||||||
return batch(() => restoreSessionPreset(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
saveSessionPreset(name: string, withWeights = false): void {
|
|
||||||
saveNamedPreset(name, withWeights);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadSessionPreset(id: string): boolean {
|
|
||||||
return batch(() => loadNamedPreset(id));
|
|
||||||
},
|
|
||||||
|
|
||||||
buildShareUrl(): string {
|
|
||||||
return buildShareUrl();
|
|
||||||
},
|
|
||||||
|
|
||||||
applyUrlParams(search?: string): boolean {
|
|
||||||
return batch(() => applyUrlParams(search));
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----- Bus ----------------------------------------------------------------
|
|
||||||
|
|
||||||
emit(topic: string, data: unknown): void {
|
|
||||||
// Lossy cast — the probe is intentionally weakly typed.
|
|
||||||
coreBus.emit(topic as never, data as never);
|
|
||||||
},
|
|
||||||
|
|
||||||
on(topic: string, handler): () => void {
|
|
||||||
return coreBus.onPrefix(topic, handler);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install the probe on window. Idempotent — the probe object is a
|
|
||||||
* singleton, so capturing `window.__nisps` once is safe across hot
|
|
||||||
* reloads and re-installs.
|
|
||||||
*/
|
|
||||||
export function installDebugProbe(): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
window.__nisps = probe;
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
.root {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--fs-xl);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lede {
|
|
||||||
color: var(--fg-mute);
|
|
||||||
margin: 0;
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
||||||
gap: var(--sp-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--bg-1);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-3);
|
|
||||||
padding: var(--sp-4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
min-height: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardHead {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-1);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
padding-bottom: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--fs-md);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardDesc {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardBody {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
/**
|
|
||||||
* Primitives showcase — Storybook-equivalent at /dev/primitives.
|
|
||||||
*
|
|
||||||
* Each section wraps one primitive's demo. Demos use realistic interactive
|
|
||||||
* state so a quick visual scan answers "does this thing actually work?".
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component, For, JSX } from 'solid-js';
|
|
||||||
import styles from './PrimitivesShowcase.module.css';
|
|
||||||
|
|
||||||
import { SliderDemo } from '../primitives/Slider.demo';
|
|
||||||
import { SliderBankDemo } from '../primitives/SliderBank.demo';
|
|
||||||
import { VirtualJoystickDemo } from '../primitives/VirtualJoystick.demo';
|
|
||||||
import { XYPadDemo } from '../primitives/XYPad.demo';
|
|
||||||
import { HeatmapDemo } from '../primitives/Heatmap.demo';
|
|
||||||
import { OutputDisplayDemo } from '../primitives/OutputDisplay.demo';
|
|
||||||
import { TrainingControlsDemo } from '../primitives/TrainingControls.demo';
|
|
||||||
import { DrawerDemo } from '../primitives/Drawer.demo';
|
|
||||||
import { ControlAxisDemo } from '../primitives/ControlAxis.demo';
|
|
||||||
import { ProgressRingDemo } from '../primitives/ProgressRing.demo';
|
|
||||||
import { PillToggleDemo } from '../primitives/PillToggle.demo';
|
|
||||||
import { ParamEditorDemo } from '../primitives/ParamEditor.demo';
|
|
||||||
import { JoyMapDemo } from '../primitives/JoyMap.demo';
|
|
||||||
import { WeightHealthDemo } from '../primitives/WeightHealth.demo';
|
|
||||||
import { GradientFlowDemo } from '../primitives/GradientFlow.demo';
|
|
||||||
import { LossPlotDemo } from '../primitives/LossPlot.demo';
|
|
||||||
|
|
||||||
interface Section {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
Demo: Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SECTIONS: ReadonlyArray<Section> = [
|
|
||||||
{ name: 'Slider', description: 'Single-value slider with optional curve mapping.', Demo: SliderDemo },
|
|
||||||
{ name: 'SliderBank', description: 'Vertical stack with collapsible sections.', Demo: SliderBankDemo },
|
|
||||||
{ name: 'VirtualJoystick', description: 'Touch + pointer 2D input, returns [0,1]^2.', Demo: VirtualJoystickDemo },
|
|
||||||
{ name: 'XYPad', description: 'Rectangular 2D input pad.', Demo: XYPadDemo },
|
|
||||||
{ name: 'Heatmap', description: 'NxN colored grid, three color modes.', Demo: HeatmapDemo },
|
|
||||||
{ name: 'OutputDisplay', description: 'Bar chart of N output values.', Demo: OutputDisplayDemo },
|
|
||||||
{ name: 'TrainingControls', description: 'RL feedback + status panel.', Demo: TrainingControlsDemo },
|
|
||||||
{ name: 'Drawer', description: 'Slide-in panel from left/right.', Demo: DrawerDemo },
|
|
||||||
{ name: 'ControlAxis', description: 'Compound axis slider (Boldness/Memory/Precision shape).', Demo: ControlAxisDemo },
|
|
||||||
{ name: 'ProgressRing', description: 'Circular progress indicator.', Demo: ProgressRingDemo },
|
|
||||||
{ name: 'PillToggle', description: 'Segmented control.', Demo: PillToggleDemo },
|
|
||||||
{ name: 'ParamEditor', description: 'Range/curve/mute/pin for one parameter.', Demo: ParamEditorDemo },
|
|
||||||
{ name: 'JoyMap', description: 'Zoom minimap with trail, noise rings, region pins.', Demo: JoyMapDemo },
|
|
||||||
{ name: 'WeightHealth', description: 'Histogram with status glow.', Demo: WeightHealthDemo },
|
|
||||||
{ name: 'GradientFlow', description: 'Per-layer gradient bars.', Demo: GradientFlowDemo },
|
|
||||||
{ name: 'LossPlot', description: 'Training-loss line chart over time.', Demo: LossPlotDemo },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PrimitivesShowcase: Component = () => {
|
|
||||||
return (
|
|
||||||
<div class={styles.root}>
|
|
||||||
<header class={styles.header}>
|
|
||||||
<h1>Primitive Library</h1>
|
|
||||||
<p class={styles.lede}>
|
|
||||||
Each card is a live demo. Use this page as a sanity-check during the rewrite.
|
|
||||||
Modes (stream 9) and feature parity (stream 10) compose these primitives.
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
<div class={styles.grid}>
|
|
||||||
<For each={SECTIONS}>{(section) => (
|
|
||||||
<article class={styles.card}>
|
|
||||||
<header class={styles.cardHead}>
|
|
||||||
<h2 class={styles.cardTitle}>{section.name}</h2>
|
|
||||||
<p class={styles.cardDesc}>{section.description}</p>
|
|
||||||
</header>
|
|
||||||
<div class={styles.cardBody}>
|
|
||||||
{section.Demo({}) as unknown as JSX.Element}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
)}</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PrimitivesShowcase;
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
/**
|
|
||||||
* Tiny smoke check that exercises the debug probe API. Run from the dev
|
|
||||||
* console after the page loads:
|
|
||||||
*
|
|
||||||
* import('./dev/probe-smoke.ts').then((m) => m.smokeCheck());
|
|
||||||
*
|
|
||||||
* Returns a list of pass/fail results. Useful for manual verification when
|
|
||||||
* Playwright isn't available.
|
|
||||||
*
|
|
||||||
* This file is NOT bundled by default — Vite only includes referenced
|
|
||||||
* modules and `dev/probe-smoke.ts` is opt-in.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface SmokeResult {
|
|
||||||
name: string;
|
|
||||||
ok: boolean;
|
|
||||||
detail?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function smokeCheck(): Promise<SmokeResult[]> {
|
|
||||||
const results: SmokeResult[] = [];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const probe = (window as unknown as { __nisps?: any }).__nisps;
|
|
||||||
if (!probe) {
|
|
||||||
results.push({ name: 'probe installed', ok: false, detail: 'window.__nisps missing' });
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
results.push({ name: 'probe installed', ok: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await probe.__init();
|
|
||||||
results.push({ name: '__init', ok: true });
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: '__init', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
probe.setInputs(0.5, 0.5);
|
|
||||||
const outs = probe.getOutputs();
|
|
||||||
results.push({
|
|
||||||
name: 'setInputs(0.5, 0.5)',
|
|
||||||
ok: outs.length > 0,
|
|
||||||
detail: `outputs.length = ${outs.length}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'setInputs', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
probe.thumbsUp();
|
|
||||||
results.push({ name: 'thumbsUp', ok: true });
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'thumbsUp', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
probe.thumbsDown();
|
|
||||||
results.push({ name: 'thumbsDown', ok: true });
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'thumbsDown', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const before = probe.getWeights();
|
|
||||||
probe.pushSnapshot('smoke');
|
|
||||||
probe.randomise();
|
|
||||||
probe.popSnapshot();
|
|
||||||
const after = probe.getWeights();
|
|
||||||
const restored = before.length > 0 && after.length === before.length;
|
|
||||||
results.push({ name: 'snapshot push/pop', ok: restored });
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'snapshot push/pop', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
probe.setAxis('boldness', 0.7);
|
|
||||||
probe.setAxis('memory', 0.3);
|
|
||||||
probe.setAxis('precision', 0.5);
|
|
||||||
results.push({ name: 'compound axes', ok: true });
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'compound axes', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ok = probe.refreshHeatmap(true);
|
|
||||||
const cells = probe.getHeatmapCells();
|
|
||||||
results.push({
|
|
||||||
name: 'heatmap',
|
|
||||||
ok: cells.length > 0,
|
|
||||||
detail: `took=${ok} cells=${cells.length}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'heatmap', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const hist = probe.getWeightHistogram();
|
|
||||||
const status = probe.getWeightStatus();
|
|
||||||
results.push({
|
|
||||||
name: 'weight health',
|
|
||||||
ok: hist.length === 10,
|
|
||||||
detail: `status=${status} bins=${hist.length}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'weight health', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = probe.buildShareUrl();
|
|
||||||
const ok = typeof url === 'string' && url.startsWith('?session=');
|
|
||||||
results.push({ name: 'buildShareUrl', ok, detail: url.slice(0, 60) });
|
|
||||||
} catch (err) {
|
|
||||||
results.push({ name: 'buildShareUrl', ok: false, detail: String(err) });
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
(window as unknown as { smokeCheck?: typeof smokeCheck }).smokeCheck = smokeCheck;
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
/**
|
|
||||||
* Compound-axis → store routing.
|
|
||||||
*
|
|
||||||
* Converts the resolved parameter map from `controlStore.resolveParams()`
|
|
||||||
* into concrete writes against:
|
|
||||||
* - `inputStore` (zoom, deadzone, inputCurve, smoothing, momentumZoom)
|
|
||||||
* - `outputStore` (slewRate)
|
|
||||||
* - `explorationStore` (noiseCap, noiseGrowth, noiseDecay, learningRate, weightDecay)
|
|
||||||
*
|
|
||||||
* Called as a Solid `createEffect` from the mode runtime; whenever any axis
|
|
||||||
* changes, this re-applies. Per-store setters internally clamp the values.
|
|
||||||
*
|
|
||||||
* Note: `inputCurve` and `slewRate` from the Precision axis intentionally
|
|
||||||
* use the centered_power exponent and slew-rate-per-frame contracts; the
|
|
||||||
* resolver mirrors the legacy table values.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { controlStore } from '../stores/control-store';
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
import { outputStore } from '../stores/output-store';
|
|
||||||
import { explorationStore } from '../stores/exploration-store';
|
|
||||||
import type { MomentumZoomMode } from '../input/pipeline';
|
|
||||||
|
|
||||||
let lastApplied = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the resolved compound-axis params. Returns true if any store was
|
|
||||||
* modified.
|
|
||||||
*/
|
|
||||||
export function applyControlRouting(): boolean {
|
|
||||||
const params = controlStore.resolveParams();
|
|
||||||
// Coalesce: only re-apply if any value changed.
|
|
||||||
const sig = JSON.stringify({
|
|
||||||
z: params['zoom'],
|
|
||||||
nc: params['noiseCap'],
|
|
||||||
ng: params['noiseGrowth'],
|
|
||||||
nd: params['noiseDecay'],
|
|
||||||
lr: params['learningRate'],
|
|
||||||
wd: params['weightDecay'],
|
|
||||||
ic: params['inputCurve'],
|
|
||||||
dz: params['deadzone'],
|
|
||||||
sm: params['smoothing'],
|
|
||||||
sr: params['slewRate'],
|
|
||||||
mz: params['momentumZoom'],
|
|
||||||
});
|
|
||||||
if (sig === lastApplied) return false;
|
|
||||||
lastApplied = sig;
|
|
||||||
|
|
||||||
const num = (k: string): number | undefined =>
|
|
||||||
typeof params[k] === 'number' ? (params[k] as number) : undefined;
|
|
||||||
|
|
||||||
// Input pipeline
|
|
||||||
const z = num('zoom');
|
|
||||||
if (z !== undefined) inputStore.setZoom(z);
|
|
||||||
const dz = num('deadzone');
|
|
||||||
if (dz !== undefined) inputStore.setDeadzone(dz);
|
|
||||||
const ic = num('inputCurve');
|
|
||||||
if (ic !== undefined) inputStore.setInputCurve(ic);
|
|
||||||
const ism = num('smoothing');
|
|
||||||
if (ism !== undefined) inputStore.setSmoothing(ism);
|
|
||||||
const mz = params['momentumZoom'];
|
|
||||||
if (typeof mz === 'string') {
|
|
||||||
const valid: MomentumZoomMode[] = ['off', 'gentle', 'strong'];
|
|
||||||
if (valid.includes(mz as MomentumZoomMode)) {
|
|
||||||
inputStore.setMomentumZoom(mz as MomentumZoomMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output pipeline
|
|
||||||
const sr = num('slewRate');
|
|
||||||
if (sr !== undefined) outputStore.setSlewRate(sr);
|
|
||||||
|
|
||||||
// Exploration / RL
|
|
||||||
explorationStore.applyCompoundParams(params);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
/**
|
|
||||||
* Heatmap sampler — samples MLP across a 2D input window, reduces outputs
|
|
||||||
* to a scalar per cell.
|
|
||||||
*
|
|
||||||
* Three color modes:
|
|
||||||
* - 'luminance' → mean output magnitude
|
|
||||||
* - 'variance' → output variance
|
|
||||||
* - 'divergence' → distance from a reference (center) output
|
|
||||||
*
|
|
||||||
* Sampler is throttled: `update` no-ops if called more than once per
|
|
||||||
* `MIN_INTERVAL_MS`. Caller drives updates via `coreBus.on('ml.delta_update')`
|
|
||||||
* and `coreBus.on('ml.trained')` events plus the input `zoom` accessor.
|
|
||||||
*
|
|
||||||
* The sampler returns a `Float32Array` of length `resolution * resolution`
|
|
||||||
* directly consumable by the `Heatmap` primitive. Stale grid values stay
|
|
||||||
* around between updates so the canvas doesn't blank.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mlStore } from '../stores/ml-store';
|
|
||||||
|
|
||||||
export type HeatmapColorMode = 'luminance' | 'variance' | 'divergence';
|
|
||||||
|
|
||||||
const MIN_INTERVAL_MS = 200; // 5 updates / sec
|
|
||||||
|
|
||||||
export interface HeatmapSamplerOptions {
|
|
||||||
/** Grid size N×N. Default 16. */
|
|
||||||
resolution?: number;
|
|
||||||
/** Initial color mode. */
|
|
||||||
colorMode?: HeatmapColorMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapWindow {
|
|
||||||
/** Anchor point in input space [0,1]. */
|
|
||||||
cx: number;
|
|
||||||
cy: number;
|
|
||||||
/** Side length of the sampled window (zoom level). */
|
|
||||||
zoom: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HeatmapSampler {
|
|
||||||
resolution: number;
|
|
||||||
colorMode: HeatmapColorMode;
|
|
||||||
private cells: Float32Array;
|
|
||||||
private lastSampleMs = 0;
|
|
||||||
private pending = false;
|
|
||||||
|
|
||||||
constructor(opts: HeatmapSamplerOptions = {}) {
|
|
||||||
this.resolution = Math.max(2, Math.min(64, opts.resolution ?? 16));
|
|
||||||
this.colorMode = opts.colorMode ?? 'luminance';
|
|
||||||
this.cells = new Float32Array(this.resolution * this.resolution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Current grid for binding to the Heatmap primitive. */
|
|
||||||
getCells(): Float32Array {
|
|
||||||
return this.cells;
|
|
||||||
}
|
|
||||||
|
|
||||||
setColorMode(mode: HeatmapColorMode): void {
|
|
||||||
this.colorMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
setResolution(n: number): void {
|
|
||||||
const r = Math.max(2, Math.min(64, n | 0));
|
|
||||||
if (r === this.resolution) return;
|
|
||||||
this.resolution = r;
|
|
||||||
this.cells = new Float32Array(r * r);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the grid. Throttled to MIN_INTERVAL_MS.
|
|
||||||
*
|
|
||||||
* Returns `true` if a fresh sample was taken, `false` if throttled.
|
|
||||||
* Reads MLP via `mlStore.inferBatch` (no-op if WASM not ready).
|
|
||||||
*/
|
|
||||||
update(window: HeatmapWindow, force: boolean = false): boolean {
|
|
||||||
const now = performance.now();
|
|
||||||
if (!force && now - this.lastSampleMs < MIN_INTERVAL_MS) {
|
|
||||||
this.pending = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.lastSampleMs = now;
|
|
||||||
this.pending = false;
|
|
||||||
|
|
||||||
const N = this.resolution;
|
|
||||||
const total = N * N;
|
|
||||||
const points: Array<readonly [number, number]> = new Array(total);
|
|
||||||
|
|
||||||
const z = Math.max(0.01, Math.min(1, window.zoom));
|
|
||||||
const half = z * 0.5;
|
|
||||||
for (let y = 0; y < N; y++) {
|
|
||||||
for (let x = 0; x < N; x++) {
|
|
||||||
const u = N > 1 ? x / (N - 1) : 0.5;
|
|
||||||
const v = N > 1 ? y / (N - 1) : 0.5;
|
|
||||||
const px = window.cx + (u - 0.5) * z; // could go negative; the sampler clamps
|
|
||||||
const py = window.cy + (v - 0.5) * z;
|
|
||||||
points[y * N + x] = [
|
|
||||||
Math.max(0, Math.min(1, px)),
|
|
||||||
Math.max(0, Math.min(1, py)),
|
|
||||||
];
|
|
||||||
// Note: we'd love to compute half-based bounds but the existing input
|
|
||||||
// pipeline already clamps, so just clamp here.
|
|
||||||
void half;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const flat = mlStore.inferBatch(points);
|
|
||||||
if (flat.length === 0) {
|
|
||||||
// WASM not ready — cells remain at last value.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const outSize = mlStore.state.outputSize;
|
|
||||||
|
|
||||||
// Reduce per cell.
|
|
||||||
let referenceMean = 0;
|
|
||||||
if (this.colorMode === 'divergence') {
|
|
||||||
// Reference = center cell (closest to N/2, N/2).
|
|
||||||
const cy = Math.floor(N / 2);
|
|
||||||
const cx = Math.floor(N / 2);
|
|
||||||
const base = (cy * N + cx) * outSize;
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < outSize; i++) sum += flat[base + i] ?? 0;
|
|
||||||
referenceMean = sum / outSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < total; i++) {
|
|
||||||
const base = i * outSize;
|
|
||||||
let sum = 0;
|
|
||||||
let sumSq = 0;
|
|
||||||
for (let j = 0; j < outSize; j++) {
|
|
||||||
const v = flat[base + j] ?? 0;
|
|
||||||
sum += v;
|
|
||||||
sumSq += v * v;
|
|
||||||
}
|
|
||||||
const mean = sum / outSize;
|
|
||||||
const variance = Math.max(0, sumSq / outSize - mean * mean);
|
|
||||||
switch (this.colorMode) {
|
|
||||||
case 'luminance':
|
|
||||||
this.cells[i] = mean;
|
|
||||||
break;
|
|
||||||
case 'variance':
|
|
||||||
this.cells[i] = variance;
|
|
||||||
break;
|
|
||||||
case 'divergence':
|
|
||||||
this.cells[i] = Math.abs(mean - referenceMean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** True if a call is queued behind throttling. */
|
|
||||||
hasPendingUpdate(): boolean {
|
|
||||||
return this.pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.cells.fill(0);
|
|
||||||
this.lastSampleMs = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
/**
|
|
||||||
* Microphone input — captures user audio and emits audio analysis features
|
|
||||||
* for modes that take audio_in as primary input (XIASRI, SoundAnalysisMIDI).
|
|
||||||
*
|
|
||||||
* Uses an `AudioWorkletNode` for the analysis. Keeps things simple by
|
|
||||||
* computing four lightweight features in the main thread via AnalyserNode:
|
|
||||||
*
|
|
||||||
* - energy: RMS amplitude (0..1)
|
|
||||||
* - brightness: spectral centroid normalised to 0..1
|
|
||||||
* - pitch: dominant peak frequency normalised log scale
|
|
||||||
* - aperiodicity: 1 - peak_strength (0..1)
|
|
||||||
*
|
|
||||||
* Stream 10 doesn't ship a fully featured DSP analyser — this is a
|
|
||||||
* pragmatic pipeline that gets values into the MLP. Firmware-equivalent
|
|
||||||
* XIASRI analysis is a deeper port (out of scope here).
|
|
||||||
*
|
|
||||||
* Usage: call `start()` from a user gesture; subscribe via `getFeatures()`
|
|
||||||
* (Float32Array of length 4). `stop()` releases the mic.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const MIN_FREQ = 80;
|
|
||||||
const MAX_FREQ = 1200;
|
|
||||||
const FFT_SIZE = 1024;
|
|
||||||
|
|
||||||
export interface MicFeatures {
|
|
||||||
energy: number;
|
|
||||||
brightness: number;
|
|
||||||
pitch: number;
|
|
||||||
aperiodicity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MicInputOptions {
|
|
||||||
/** Optional shared AudioContext. If absent, creates one. */
|
|
||||||
audioContext?: AudioContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MicInput {
|
|
||||||
private ctx: AudioContext | null = null;
|
|
||||||
private stream: MediaStream | null = null;
|
|
||||||
private analyser: AnalyserNode | null = null;
|
|
||||||
private freqBuf: Float32Array<ArrayBuffer> = new Float32Array(new ArrayBuffer(FFT_SIZE / 2 * 4));
|
|
||||||
private timeBuf: Float32Array<ArrayBuffer> = new Float32Array(new ArrayBuffer(FFT_SIZE * 4));
|
|
||||||
private features: MicFeatures = { energy: 0, brightness: 0, pitch: 0, aperiodicity: 1 };
|
|
||||||
private running = false;
|
|
||||||
|
|
||||||
constructor(private opts: MicInputOptions = {}) {}
|
|
||||||
|
|
||||||
/** Start mic capture. Resolves once audio is flowing. */
|
|
||||||
async start(): Promise<void> {
|
|
||||||
if (this.running) return;
|
|
||||||
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
|
|
||||||
throw new Error('[mic-input] navigator.mediaDevices unavailable');
|
|
||||||
}
|
|
||||||
this.ctx = this.opts.audioContext ?? new AudioContext();
|
|
||||||
if (this.ctx.state === 'suspended') await this.ctx.resume();
|
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
const src = this.ctx.createMediaStreamSource(this.stream);
|
|
||||||
this.analyser = this.ctx.createAnalyser();
|
|
||||||
this.analyser.fftSize = FFT_SIZE;
|
|
||||||
this.analyser.smoothingTimeConstant = 0.6;
|
|
||||||
src.connect(this.analyser);
|
|
||||||
this.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
|
||||||
if (!this.running) return;
|
|
||||||
this.running = false;
|
|
||||||
if (this.stream) {
|
|
||||||
for (const t of this.stream.getTracks()) t.stop();
|
|
||||||
this.stream = null;
|
|
||||||
}
|
|
||||||
if (this.ctx && !this.opts.audioContext) {
|
|
||||||
try { await this.ctx.close(); } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
this.ctx = null;
|
|
||||||
this.analyser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRunning(): boolean {
|
|
||||||
return this.running;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Compute and return the latest feature vector. */
|
|
||||||
getFeatures(): MicFeatures {
|
|
||||||
if (!this.running || !this.analyser || !this.ctx) return this.features;
|
|
||||||
const sr = this.ctx.sampleRate;
|
|
||||||
const a = this.analyser;
|
|
||||||
a.getFloatFrequencyData(this.freqBuf);
|
|
||||||
a.getFloatTimeDomainData(this.timeBuf);
|
|
||||||
|
|
||||||
// RMS energy
|
|
||||||
let sumSq = 0;
|
|
||||||
for (let i = 0; i < this.timeBuf.length; ++i) {
|
|
||||||
const s = this.timeBuf[i]!;
|
|
||||||
sumSq += s * s;
|
|
||||||
}
|
|
||||||
const rms = Math.sqrt(sumSq / this.timeBuf.length);
|
|
||||||
const energy = Math.min(1, rms * 4);
|
|
||||||
|
|
||||||
// Convert dB → linear, find spectral centroid + peak
|
|
||||||
let sumMag = 0;
|
|
||||||
let sumWeighted = 0;
|
|
||||||
let peakBin = 0;
|
|
||||||
let peakMag = -Infinity;
|
|
||||||
const binHz = sr / FFT_SIZE;
|
|
||||||
const minBin = Math.floor(MIN_FREQ / binHz);
|
|
||||||
const maxBin = Math.min(this.freqBuf.length - 1, Math.ceil(MAX_FREQ / binHz));
|
|
||||||
for (let i = 0; i < this.freqBuf.length; ++i) {
|
|
||||||
const db = this.freqBuf[i]!;
|
|
||||||
const lin = Math.pow(10, db / 20);
|
|
||||||
sumMag += lin;
|
|
||||||
sumWeighted += i * binHz * lin;
|
|
||||||
if (i >= minBin && i <= maxBin && db > peakMag) {
|
|
||||||
peakMag = db;
|
|
||||||
peakBin = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const centroidHz = sumMag > 1e-12 ? sumWeighted / sumMag : 0;
|
|
||||||
const brightness = Math.min(1, centroidHz / (sr * 0.25));
|
|
||||||
|
|
||||||
// Pitch (log scale 80–1200 Hz)
|
|
||||||
const pitchHz = peakBin * binHz;
|
|
||||||
const pitchNorm = pitchHz <= MIN_FREQ
|
|
||||||
? 0
|
|
||||||
: pitchHz >= MAX_FREQ
|
|
||||||
? 1
|
|
||||||
: (Math.log(pitchHz / MIN_FREQ) / Math.log(MAX_FREQ / MIN_FREQ));
|
|
||||||
|
|
||||||
// Aperiodicity: 1 - relative peak height (vs spectral mean)
|
|
||||||
const meanLin = sumMag / this.freqBuf.length;
|
|
||||||
const peakLin = Math.pow(10, peakMag / 20);
|
|
||||||
const ratio = meanLin > 1e-12 ? peakLin / meanLin : 0;
|
|
||||||
const aperiodicity = Math.max(0, Math.min(1, 1 - Math.min(1, ratio / 30)));
|
|
||||||
|
|
||||||
this.features = { energy, brightness, pitch: pitchNorm, aperiodicity };
|
|
||||||
return this.features;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
/**
|
|
||||||
* Override application — apply per-parameter overrides between MLP outputs
|
|
||||||
* and the engine's parameter destination.
|
|
||||||
*
|
|
||||||
* The MLP emits values in [0, 1]. The mode schema specifies a per-parameter
|
|
||||||
* `min`, `max`, and `curve`. Modes can layer overrides on top via
|
|
||||||
* `modeStore.setOverride` to:
|
|
||||||
* - Re-curve the value (`curve` and `curveParam`).
|
|
||||||
* - Mute it (replace with `fixedValue` ∈ [hardMin, hardMax]).
|
|
||||||
* - Freeze it (hold last processed value).
|
|
||||||
* - Tighten the active range (`min`/`max` ⊂ schema bounds).
|
|
||||||
*
|
|
||||||
* `applyOverrides` walks the schema params in order, looks up overrides for
|
|
||||||
* each, and writes results into a target Float32Array of the same length as
|
|
||||||
* the MLP slice. The function is pure — caller manages buffer reuse.
|
|
||||||
*
|
|
||||||
* The output buffer values are NORMALISED to [0, 1] in their effective
|
|
||||||
* range so the mode's audio engine, MIDI router, or visualiser can scale
|
|
||||||
* uniformly. Engines/MIDI consumers that need actual min/max units can use
|
|
||||||
* `paramsToSliderValues` from `mode-helpers`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ParamOverride } from '../stores/mode-store';
|
|
||||||
import type { Param } from '../modes/generated/types';
|
|
||||||
import { applyCurve, type CurveName, clamp01 } from '../output/curves';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of applying overrides:
|
|
||||||
* - `values` is a Float32Array of length `params.length`, each in [0, 1]
|
|
||||||
* representing the post-override normalised parameter value.
|
|
||||||
* - `freezeMask` (optional) marks per-output frozen flags. Only allocated
|
|
||||||
* when at least one param is frozen — saves work on the typical case.
|
|
||||||
*/
|
|
||||||
export interface OverrideResult {
|
|
||||||
values: Float32Array;
|
|
||||||
freezeMask: Uint8Array | null;
|
|
||||||
/** Number of muted/frozen entries (for telemetry). */
|
|
||||||
affectedCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply per-param overrides to the slice of MLP outputs corresponding to
|
|
||||||
* the schema's params.
|
|
||||||
*
|
|
||||||
* @param raw processed (post output-pipeline) MLP outputs in [0,1]
|
|
||||||
* @param prev previous post-override outputs (for freeze hold)
|
|
||||||
* @param params mode schema params
|
|
||||||
* @param overrides per-name override map (sparse; missing keys fall through)
|
|
||||||
* @param dest optional destination Float32Array; allocated if absent
|
|
||||||
*/
|
|
||||||
export function applyOverrides(
|
|
||||||
raw: Float32Array,
|
|
||||||
prev: Float32Array | null,
|
|
||||||
params: ReadonlyArray<Param>,
|
|
||||||
overrides: Record<string, ParamOverride>,
|
|
||||||
dest?: Float32Array,
|
|
||||||
): OverrideResult {
|
|
||||||
const n = params.length;
|
|
||||||
const out = dest && dest.length === n ? dest : new Float32Array(n);
|
|
||||||
let affected = 0;
|
|
||||||
let freezeMask: Uint8Array | null = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < n; ++i) {
|
|
||||||
const p = params[i]!;
|
|
||||||
const ov = overrides[p.name];
|
|
||||||
const r = clamp01(raw[i] ?? 0);
|
|
||||||
|
|
||||||
if (!ov) {
|
|
||||||
// No override → identity (raw normalised value).
|
|
||||||
out[i] = r;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ov.frozen) {
|
|
||||||
// Freeze: hold last processed value. Track in freezeMask so the
|
|
||||||
// output pipeline can also honor it next frame.
|
|
||||||
if (!freezeMask) freezeMask = new Uint8Array(n);
|
|
||||||
freezeMask[i] = 1;
|
|
||||||
out[i] = prev?.[i] ?? r;
|
|
||||||
affected++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ov.muted) {
|
|
||||||
// Mute: replace with fixedValue. fixedValue is stored in schema units
|
|
||||||
// [min, max] — normalise back to [0, 1] in the override's effective range.
|
|
||||||
const range = ov.max - ov.min;
|
|
||||||
const norm = range > 0 ? (ov.fixedValue - ov.min) / range : 0.5;
|
|
||||||
out[i] = clamp01(norm);
|
|
||||||
affected++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply curve (if any), then clamp.
|
|
||||||
let v = r;
|
|
||||||
if (ov.curve && ov.curve !== 'linear') {
|
|
||||||
v = applyCurve(ov.curve as CurveName, r, ov.curveParam);
|
|
||||||
}
|
|
||||||
out[i] = clamp01(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { values: out, freezeMask, affectedCount: affected };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a Uint8Array of length `outputSize` marking every pinned output
|
|
||||||
* with a 1. Used by `WasmIML.moveWeights` to skip pinned weights during RL
|
|
||||||
* perturbation. Combines:
|
|
||||||
* - explicit param-pins from sessionStore
|
|
||||||
* - per-param `pinned` flag in modeStore overrides
|
|
||||||
*/
|
|
||||||
export function buildPinMask(
|
|
||||||
outputSize: number,
|
|
||||||
modeId: string,
|
|
||||||
params: ReadonlyArray<Param>,
|
|
||||||
overrides: Record<string, ParamOverride>,
|
|
||||||
paramPins: ReadonlyArray<{ key: string; outputIndex: number }>,
|
|
||||||
): Uint8Array {
|
|
||||||
const mask = new Uint8Array(outputSize);
|
|
||||||
for (let i = 0; i < params.length && i < outputSize; ++i) {
|
|
||||||
const ov = overrides[params[i]!.name];
|
|
||||||
if (ov && ov.pinned) mask[i] = 1;
|
|
||||||
}
|
|
||||||
for (const pin of paramPins) {
|
|
||||||
if (!pin.key.startsWith(modeId + ':')) continue;
|
|
||||||
if (pin.outputIndex >= 0 && pin.outputIndex < outputSize) {
|
|
||||||
mask[pin.outputIndex] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
/**
|
|
||||||
* Session preset — full app-state snapshot for save/load and URL sharing.
|
|
||||||
*
|
|
||||||
* A preset captures: control axes + offsets, exploration config, output
|
|
||||||
* pipeline settings, input pipeline settings, mode override map, the
|
|
||||||
* active mode id. ML weights are NOT included by default (large), but
|
|
||||||
* `serializeWithWeights` is provided for power users.
|
|
||||||
*
|
|
||||||
* URL sharing uses a compact encoding:
|
|
||||||
* ?session=<base64url(JSON)>
|
|
||||||
* ?boldness=...&memory=...&precision=...
|
|
||||||
* Either form rehydrates on next load.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { controlStore } from '../stores/control-store';
|
|
||||||
import { explorationStore } from '../stores/exploration-store';
|
|
||||||
import { modeStore } from '../stores/mode-store';
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
import { outputStore } from '../stores/output-store';
|
|
||||||
import { mlStore } from '../stores/ml-store';
|
|
||||||
import { sessionStore } from '../stores/session-store';
|
|
||||||
|
|
||||||
export interface SessionPresetPayload {
|
|
||||||
v: 1;
|
|
||||||
modeId: string | null;
|
|
||||||
control: {
|
|
||||||
boldness: number;
|
|
||||||
memory: number;
|
|
||||||
precision: number;
|
|
||||||
presetId: string | null;
|
|
||||||
offsets: typeof controlStore.state.offsets;
|
|
||||||
};
|
|
||||||
exploration: {
|
|
||||||
spread: number;
|
|
||||||
noiseFloor: number;
|
|
||||||
noiseCap: number;
|
|
||||||
noiseGrowth: number;
|
|
||||||
noiseDecay: number;
|
|
||||||
learningRate: number;
|
|
||||||
weightDecay: number;
|
|
||||||
};
|
|
||||||
output: {
|
|
||||||
globalCurve: number;
|
|
||||||
smoothing: number;
|
|
||||||
slewRate: number | null; // null encodes Infinity
|
|
||||||
freezeOutput: boolean;
|
|
||||||
};
|
|
||||||
input: {
|
|
||||||
zoom: number;
|
|
||||||
anchorMode: 'auto' | 'sticky' | 'center';
|
|
||||||
deadzone: number;
|
|
||||||
inputCurve: number;
|
|
||||||
smoothing: number;
|
|
||||||
momentumZoom: 'off' | 'gentle' | 'strong';
|
|
||||||
invertX: boolean;
|
|
||||||
invertY: boolean;
|
|
||||||
};
|
|
||||||
modeOverrides: typeof modeStore.state.overrides;
|
|
||||||
/** Base64-encoded weights, optional. */
|
|
||||||
weights?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encode(s: string): string {
|
|
||||||
// base64url encoding
|
|
||||||
const b64 = btoa(s);
|
|
||||||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function decode(s: string): string | null {
|
|
||||||
try {
|
|
||||||
let b64 = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
while (b64.length % 4 !== 0) b64 += '=';
|
|
||||||
return atob(b64);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function f32ToBase64(arr: Float32Array): string {
|
|
||||||
const u8 = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
||||||
let s = '';
|
|
||||||
for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]!);
|
|
||||||
return encode(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64ToF32(s: string): Float32Array | null {
|
|
||||||
const raw = decode(s);
|
|
||||||
if (!raw) return null;
|
|
||||||
const u8 = new Uint8Array(raw.length);
|
|
||||||
for (let i = 0; i < raw.length; i++) u8[i] = raw.charCodeAt(i);
|
|
||||||
// Round to 4 bytes
|
|
||||||
const len = u8.byteLength - (u8.byteLength % 4);
|
|
||||||
return new Float32Array(u8.buffer, u8.byteOffset, len / 4).slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read every store and assemble a SessionPresetPayload. If
|
|
||||||
* `withWeights = true`, also encodes the current MLP weight blob.
|
|
||||||
*/
|
|
||||||
export function captureSessionPreset(withWeights: boolean = false): SessionPresetPayload {
|
|
||||||
const c = controlStore.state;
|
|
||||||
const e = explorationStore.state;
|
|
||||||
const o = outputStore.config;
|
|
||||||
const i = inputStore.config;
|
|
||||||
|
|
||||||
const payload: SessionPresetPayload = {
|
|
||||||
v: 1,
|
|
||||||
modeId: modeStore.state.activeModeId,
|
|
||||||
control: {
|
|
||||||
boldness: c.boldness,
|
|
||||||
memory: c.memory,
|
|
||||||
precision: c.precision,
|
|
||||||
presetId: c.presetId,
|
|
||||||
offsets: JSON.parse(JSON.stringify(c.offsets)),
|
|
||||||
},
|
|
||||||
exploration: {
|
|
||||||
spread: e.spread,
|
|
||||||
noiseFloor: e.noiseFloor,
|
|
||||||
noiseCap: e.noiseCap,
|
|
||||||
noiseGrowth: e.noiseGrowth,
|
|
||||||
noiseDecay: e.noiseDecay,
|
|
||||||
learningRate: e.learningRate,
|
|
||||||
weightDecay: e.weightDecay,
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
globalCurve: o.globalCurve,
|
|
||||||
smoothing: o.smoothing,
|
|
||||||
slewRate: isFinite(o.slewRate) ? o.slewRate : null,
|
|
||||||
freezeOutput: o.freezeOutput,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
zoom: i.zoom,
|
|
||||||
anchorMode: i.anchorMode,
|
|
||||||
deadzone: i.deadzone,
|
|
||||||
inputCurve: i.inputCurve,
|
|
||||||
smoothing: i.smoothing,
|
|
||||||
momentumZoom: i.momentumZoom,
|
|
||||||
invertX: i.invertX,
|
|
||||||
invertY: i.invertY,
|
|
||||||
},
|
|
||||||
modeOverrides: JSON.parse(JSON.stringify(modeStore.state.overrides)),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (withWeights) {
|
|
||||||
const w = mlStore.getWeights();
|
|
||||||
if (w.length > 0) {
|
|
||||||
payload.weights = f32ToBase64(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore a SessionPresetPayload into all stores. Best-effort: missing
|
|
||||||
* fields fall back to current values. Returns true if at least one store
|
|
||||||
* was modified.
|
|
||||||
*/
|
|
||||||
export function restoreSessionPreset(payload: SessionPresetPayload): boolean {
|
|
||||||
if (!payload || payload.v !== 1) return false;
|
|
||||||
|
|
||||||
// Mode first (so override map is keyed correctly).
|
|
||||||
if (payload.modeId && payload.modeId !== modeStore.state.activeModeId) {
|
|
||||||
modeStore.switchMode(payload.modeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control axes & offsets.
|
|
||||||
if (payload.control) {
|
|
||||||
controlStore.setAxis('boldness', payload.control.boldness);
|
|
||||||
controlStore.setAxis('memory', payload.control.memory);
|
|
||||||
controlStore.setAxis('precision', payload.control.precision);
|
|
||||||
if (payload.control.presetId) controlStore.applyPreset(payload.control.presetId);
|
|
||||||
// Manually rewrite offsets — the store doesn't have a public bulk setter
|
|
||||||
// (control-store doesn't expose it because it's a private field).
|
|
||||||
// Use clearOffsets + setOffset roundtrip.
|
|
||||||
for (const axis of ['boldness', 'memory', 'precision'] as const) {
|
|
||||||
controlStore.clearOffsets(axis);
|
|
||||||
const off = payload.control.offsets[axis] ?? {};
|
|
||||||
for (const [k, v] of Object.entries(off)) {
|
|
||||||
if (typeof v === 'number') controlStore.setOffset(axis, k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.exploration) {
|
|
||||||
explorationStore.setSpread(payload.exploration.spread);
|
|
||||||
explorationStore.setNoiseFloor(payload.exploration.noiseFloor);
|
|
||||||
explorationStore.setNoiseCap(payload.exploration.noiseCap);
|
|
||||||
explorationStore.setNoiseGrowth(payload.exploration.noiseGrowth);
|
|
||||||
explorationStore.setNoiseDecay(payload.exploration.noiseDecay);
|
|
||||||
explorationStore.setLearningRate(payload.exploration.learningRate);
|
|
||||||
explorationStore.setWeightDecay(payload.exploration.weightDecay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.output) {
|
|
||||||
outputStore.setGlobalCurve(payload.output.globalCurve);
|
|
||||||
outputStore.setSmoothing(payload.output.smoothing);
|
|
||||||
outputStore.setSlewRate(payload.output.slewRate ?? Infinity);
|
|
||||||
outputStore.setFreezeOutput(payload.output.freezeOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.input) {
|
|
||||||
inputStore.setZoom(payload.input.zoom);
|
|
||||||
inputStore.setAnchorMode(payload.input.anchorMode);
|
|
||||||
inputStore.setDeadzone(payload.input.deadzone);
|
|
||||||
inputStore.setInputCurve(payload.input.inputCurve);
|
|
||||||
inputStore.setSmoothing(payload.input.smoothing);
|
|
||||||
inputStore.setMomentumZoom(payload.input.momentumZoom);
|
|
||||||
inputStore.setInvert(payload.input.invertX, payload.input.invertY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.modeOverrides) {
|
|
||||||
for (const modeId of Object.keys(payload.modeOverrides)) {
|
|
||||||
const map = payload.modeOverrides[modeId] ?? {};
|
|
||||||
modeStore.clearAllOverrides(modeId);
|
|
||||||
for (const [name, ov] of Object.entries(map)) {
|
|
||||||
modeStore.setOverride(modeId, name, ov);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.weights) {
|
|
||||||
const w = base64ToF32(payload.weights);
|
|
||||||
if (w && mlStore.iml && w.length > 0) {
|
|
||||||
try { mlStore.setWeights(w); } catch { /* ignore size mismatch */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Save preset to sessionStore (named blob in localStorage). */
|
|
||||||
export function saveNamedPreset(name: string, withWeights: boolean = false): void {
|
|
||||||
const payload = captureSessionPreset(withWeights);
|
|
||||||
sessionStore.savePreset(name, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load a named preset by id from sessionStore. */
|
|
||||||
export function loadNamedPreset(id: string): boolean {
|
|
||||||
const preset = sessionStore.state.presets.find((p) => p.id === id);
|
|
||||||
if (!preset) return false;
|
|
||||||
return restoreSessionPreset(preset.payload as SessionPresetPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// URL sharing
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Build a query-string fragment from current state (without weights). */
|
|
||||||
export function buildShareUrl(): string {
|
|
||||||
const payload = captureSessionPreset(false);
|
|
||||||
const json = JSON.stringify(payload);
|
|
||||||
const enc = encode(json);
|
|
||||||
return `?session=${enc}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parse `window.location.search` and apply a session if present. */
|
|
||||||
export function applyUrlParams(search: string = window.location.search): boolean {
|
|
||||||
const sp = new URLSearchParams(search);
|
|
||||||
// Compact ?boldness=...&memory=... form.
|
|
||||||
const cb = sp.get('boldness');
|
|
||||||
const cm = sp.get('memory');
|
|
||||||
const cp = sp.get('precision');
|
|
||||||
let applied = false;
|
|
||||||
if (cb !== null || cm !== null || cp !== null) {
|
|
||||||
if (cb !== null) controlStore.setAxis('boldness', clamp01(parseFloat(cb)));
|
|
||||||
if (cm !== null) controlStore.setAxis('memory', clamp01(parseFloat(cm)));
|
|
||||||
if (cp !== null) controlStore.setAxis('precision', clamp01(parseFloat(cp)));
|
|
||||||
applied = true;
|
|
||||||
}
|
|
||||||
const sess = sp.get('session');
|
|
||||||
if (sess) {
|
|
||||||
const json = decode(sess);
|
|
||||||
if (json) {
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(json) as SessionPresetPayload;
|
|
||||||
applied = restoreSessionPreset(payload) || applied;
|
|
||||||
} catch {
|
|
||||||
// Ignore malformed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return applied;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp01(v: number): number {
|
|
||||||
if (!isFinite(v)) return 0;
|
|
||||||
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
/**
|
|
||||||
* Snapshot helpers — bridge sessionStore.pushSnapshot/popSnapshot to the
|
|
||||||
* actual MLP weights via mlStore. Auto-snapshot before train/randomize/
|
|
||||||
* thumbs-down.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mlStore } from '../stores/ml-store';
|
|
||||||
import { sessionStore } from '../stores/session-store';
|
|
||||||
import { explorationStore } from '../stores/exploration-store';
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
|
|
||||||
/** Capture current weights + noise level + zoom level. */
|
|
||||||
export function autoSnapshot(tag: string): void {
|
|
||||||
if (!mlStore.iml) return;
|
|
||||||
const w = mlStore.getWeights();
|
|
||||||
if (w.length === 0) return;
|
|
||||||
sessionStore.pushSnapshot(tag, {
|
|
||||||
noiseLevel: explorationStore.state.noiseLevel,
|
|
||||||
zoomLevel: inputStore.config.zoom,
|
|
||||||
weights: w,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pop the most recent snapshot and restore weights. Returns true on success.
|
|
||||||
*/
|
|
||||||
export function undoLastSnapshot(): boolean {
|
|
||||||
const snap = sessionStore.popSnapshot();
|
|
||||||
if (!snap || !snap.weights) return false;
|
|
||||||
if (!mlStore.iml) return false;
|
|
||||||
try {
|
|
||||||
mlStore.setWeights(snap.weights);
|
|
||||||
explorationStore.setNoiseLevel(snap.noiseLevel);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Restore a specific snapshot by id. */
|
|
||||||
export function restoreSnapshotById(id: string): boolean {
|
|
||||||
const snap = sessionStore.jumpToSnapshot(id);
|
|
||||||
if (!snap || !snap.weights) return false;
|
|
||||||
if (!mlStore.iml) return false;
|
|
||||||
try {
|
|
||||||
mlStore.setWeights(snap.weights);
|
|
||||||
explorationStore.setNoiseLevel(snap.noiseLevel);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A/B compare helpers — capture the current weights as A, the next state
|
|
||||||
* becomes B. Toggle swaps in O(1).
|
|
||||||
*/
|
|
||||||
export function captureA(): void {
|
|
||||||
if (!mlStore.iml) return;
|
|
||||||
const w = mlStore.getWeights();
|
|
||||||
if (w.length === 0) return;
|
|
||||||
sessionStore.captureA({
|
|
||||||
id: `a-${Date.now().toString(36)}`,
|
|
||||||
tag: 'A',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
noiseLevel: explorationStore.state.noiseLevel,
|
|
||||||
zoomLevel: inputStore.config.zoom,
|
|
||||||
weights: w,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleAB(): 'A' | 'B' {
|
|
||||||
if (!mlStore.iml) return sessionStore.state.ab.live;
|
|
||||||
// Capture the current live state on first toggle.
|
|
||||||
const cur = mlStore.getWeights();
|
|
||||||
const live = sessionStore.state.ab.live;
|
|
||||||
if (live === 'B' && !sessionStore.state.ab.b) {
|
|
||||||
// First toggle from B → A: capture the current state as B.
|
|
||||||
sessionStore.captureB({
|
|
||||||
id: `b-${Date.now().toString(36)}`,
|
|
||||||
tag: 'B',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
noiseLevel: explorationStore.state.noiseLevel,
|
|
||||||
zoomLevel: inputStore.config.zoom,
|
|
||||||
weights: cur,
|
|
||||||
});
|
|
||||||
} else if (live === 'A') {
|
|
||||||
// Going back from A → B: update B with the current edits before flipping.
|
|
||||||
sessionStore.captureB({
|
|
||||||
id: `b-${Date.now().toString(36)}`,
|
|
||||||
tag: 'B',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
noiseLevel: explorationStore.state.noiseLevel,
|
|
||||||
zoomLevel: inputStore.config.zoom,
|
|
||||||
weights: cur,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const next = sessionStore.toggleAB();
|
|
||||||
// Apply the side we just switched to.
|
|
||||||
const target = next === 'A' ? sessionStore.state.ab.a : sessionStore.state.ab.b;
|
|
||||||
if (target?.weights) {
|
|
||||||
try { mlStore.setWeights(target.weights); } catch { /* ignore */ }
|
|
||||||
explorationStore.setNoiseLevel(target.noiseLevel);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function acceptB(): void {
|
|
||||||
sessionStore.acceptB();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function revertToA(): void {
|
|
||||||
if (!mlStore.iml) {
|
|
||||||
sessionStore.revertToA();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const a = sessionStore.state.ab.a;
|
|
||||||
if (a?.weights) {
|
|
||||||
try { mlStore.setWeights(a.weights); } catch { /* ignore */ }
|
|
||||||
explorationStore.setNoiseLevel(a.noiseLevel);
|
|
||||||
}
|
|
||||||
sessionStore.revertToA();
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
/**
|
|
||||||
* Trail tracking — fixed-size ring of recent input positions for the JoyMap
|
|
||||||
* vanishing trail and tap-to-return.
|
|
||||||
*
|
|
||||||
* Uses `performance.now()` for timestamps. The JoyMap primitive filters by
|
|
||||||
* age internally (5-second window), so the buffer can be larger than the
|
|
||||||
* displayed window.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createSignal, type Accessor } from 'solid-js';
|
|
||||||
|
|
||||||
export interface TrailPoint {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
t: number; // performance.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_POINTS = 300;
|
|
||||||
const MIN_DELTA_PX = 0.005; // skip near-duplicate points (in 0..1 space)
|
|
||||||
|
|
||||||
export interface TrailRing {
|
|
||||||
/** Reactive accessor for current points. */
|
|
||||||
points: Accessor<ReadonlyArray<TrailPoint>>;
|
|
||||||
push(x: number, y: number): void;
|
|
||||||
clear(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTrailRing(): TrailRing {
|
|
||||||
const [points, setPoints] = createSignal<ReadonlyArray<TrailPoint>>([], { equals: false });
|
|
||||||
|
|
||||||
let buf: TrailPoint[] = [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
points,
|
|
||||||
push(x, y) {
|
|
||||||
const now = performance.now();
|
|
||||||
// Skip if too close to last point.
|
|
||||||
const last = buf.length > 0 ? buf[buf.length - 1] : null;
|
|
||||||
if (last) {
|
|
||||||
const dx = x - last.x;
|
|
||||||
const dy = y - last.y;
|
|
||||||
if (dx * dx + dy * dy < MIN_DELTA_PX * MIN_DELTA_PX && now - last.t < 50) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf.push({ x, y, t: now });
|
|
||||||
if (buf.length > MAX_POINTS) buf.shift();
|
|
||||||
setPoints(buf.slice());
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
buf = [];
|
|
||||||
setPoints([]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
/**
|
|
||||||
* Weight health + gradient flow analysis.
|
|
||||||
*
|
|
||||||
* Pure functions over Float32Array weight buffers and `LayerStats[]` from
|
|
||||||
* the WasmIML. Mode UI binds these to the WeightHealth and GradientFlow
|
|
||||||
* primitives.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { LayerStats } from '../ml/types';
|
|
||||||
import type { WeightStatus } from '../primitives/WeightHealth';
|
|
||||||
import type { GradientStatus } from '../primitives/GradientFlow';
|
|
||||||
|
|
||||||
const HISTOGRAM_BINS = 10;
|
|
||||||
/** Magnitude bin edges: [0, 0.01, 0.05, 0.15, 0.4, 0.8, 1.5, 2.5, 4, 7, ∞] */
|
|
||||||
const BIN_EDGES = [0, 0.01, 0.05, 0.15, 0.4, 0.8, 1.5, 2.5, 4, 7];
|
|
||||||
|
|
||||||
/** Build a 10-bin histogram of weight magnitudes from a flat weight array. */
|
|
||||||
export function weightHistogram(weights: Float32Array): number[] {
|
|
||||||
const bins = new Array<number>(HISTOGRAM_BINS).fill(0);
|
|
||||||
if (weights.length === 0) return bins;
|
|
||||||
for (let i = 0; i < weights.length; ++i) {
|
|
||||||
const m = Math.abs(weights[i]!);
|
|
||||||
let bin = HISTOGRAM_BINS - 1;
|
|
||||||
for (let b = 0; b < HISTOGRAM_BINS - 1; ++b) {
|
|
||||||
if (m < BIN_EDGES[b + 1]!) {
|
|
||||||
bin = b;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bins[bin]++;
|
|
||||||
}
|
|
||||||
return bins;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Determine overall network health status. */
|
|
||||||
export function weightStatus(weights: Float32Array): WeightStatus {
|
|
||||||
if (weights.length === 0) return 'healthy';
|
|
||||||
let dead = 0;
|
|
||||||
let saturating = 0;
|
|
||||||
for (let i = 0; i < weights.length; ++i) {
|
|
||||||
const m = Math.abs(weights[i]!);
|
|
||||||
if (m < 0.01) dead++;
|
|
||||||
else if (m > 3.0) saturating++;
|
|
||||||
}
|
|
||||||
const deadFrac = dead / weights.length;
|
|
||||||
const satFrac = saturating / weights.length;
|
|
||||||
if (satFrac > 0.4) return 'saturating';
|
|
||||||
if (deadFrac > 0.3) return 'dead';
|
|
||||||
return 'healthy';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Per-layer L2 norm of weight delta (after - before). Used to populate
|
|
||||||
* GradientFlow primitive.
|
|
||||||
*/
|
|
||||||
export function layerNorms(beforeWeights: Float32Array, afterWeights: Float32Array, layerSizes: ReadonlyArray<number>): number[] {
|
|
||||||
const out: number[] = [];
|
|
||||||
let offset = 0;
|
|
||||||
for (const sz of layerSizes) {
|
|
||||||
let sum = 0;
|
|
||||||
const end = Math.min(beforeWeights.length, afterWeights.length, offset + sz);
|
|
||||||
for (let i = offset; i < end; ++i) {
|
|
||||||
const d = (afterWeights[i] ?? 0) - (beforeWeights[i] ?? 0);
|
|
||||||
sum += d * d;
|
|
||||||
}
|
|
||||||
out.push(Math.sqrt(sum));
|
|
||||||
offset += sz;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect vanishing/exploding/converged gradient flow given per-layer norms.
|
|
||||||
*/
|
|
||||||
export function gradientStatuses(norms: ReadonlyArray<number>): GradientStatus[] {
|
|
||||||
if (norms.length === 0) return [];
|
|
||||||
// Converged check first: all near-zero.
|
|
||||||
const allTiny = norms.every((n) => n < 1e-6);
|
|
||||||
if (allTiny) return norms.map(() => 'converged' as GradientStatus);
|
|
||||||
|
|
||||||
const out: GradientStatus[] = [];
|
|
||||||
for (let i = 0; i < norms.length; ++i) {
|
|
||||||
if (i === 0) {
|
|
||||||
out.push('healthy');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const prev = norms[i - 1] ?? 0;
|
|
||||||
const curr = norms[i] ?? 0;
|
|
||||||
if (prev <= 1e-12) {
|
|
||||||
out.push('healthy');
|
|
||||||
} else if (curr < 0.5 * prev) {
|
|
||||||
out.push('vanishing');
|
|
||||||
} else if (curr > 2.0 * prev) {
|
|
||||||
out.push('exploding');
|
|
||||||
} else {
|
|
||||||
out.push('healthy');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate per-layer weight count given an MLP architecture.
|
|
||||||
*
|
|
||||||
* MLP layout: layer i has weights = inputs * outputs + outputs (bias).
|
|
||||||
* `[inputSize, ...hidden, outputSize]` — N+1 layer sizes, N layers.
|
|
||||||
*/
|
|
||||||
export function layerWeightCounts(arch: { inputSize: number; hidden: ReadonlyArray<number>; outputSize: number }): number[] {
|
|
||||||
const sizes = [arch.inputSize, ...arch.hidden, arch.outputSize];
|
|
||||||
const counts: number[] = [];
|
|
||||||
for (let i = 0; i < sizes.length - 1; ++i) {
|
|
||||||
const ins = sizes[i]!;
|
|
||||||
const outs = sizes[i + 1]!;
|
|
||||||
counts.push(ins * outs + outs);
|
|
||||||
}
|
|
||||||
return counts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Map LayerStats to a healthy/dead/saturating per-layer status. */
|
|
||||||
export function layerStatsToStatus(stats: LayerStats): WeightStatus {
|
|
||||||
if (stats.saturatingFrac > 0.4) return 'saturating';
|
|
||||||
if (stats.deadFrac > 0.3) return 'dead';
|
|
||||||
return 'healthy';
|
|
||||||
}
|
|
||||||
|
|
@ -1,307 +0,0 @@
|
||||||
/**
|
|
||||||
* Input pipeline — pure TS port of legacy `js/ui/input-pipeline.js`.
|
|
||||||
*
|
|
||||||
* Stages (in order), each input/output in [0,1]:
|
|
||||||
* 0. Invert (per-axis flip)
|
|
||||||
* 1. Deadzone (suppress jitter near center, remap live zone to [0,1])
|
|
||||||
* 2. Circular clamp (constrain to unit disk centered at 0.5,0.5)
|
|
||||||
* 3. Zoom (narrow window around anchor, modulated by momentum)
|
|
||||||
* 4. Centered power curve (per-axis exponent)
|
|
||||||
* 5. EMA smoothing (frame-rate-independent)
|
|
||||||
* 6. Momentum-as-zoom update (consumed next frame)
|
|
||||||
*
|
|
||||||
* `processInput` is a pure function over (raw, cfg, prev): returns the new
|
|
||||||
* processed coordinate plus the next-frame state. Consumer (input-store)
|
|
||||||
* holds the state and calls this each frame.
|
|
||||||
*
|
|
||||||
* Math is intentionally bit-for-bit equivalent to the legacy implementation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { clamp, curveCenteredPower } from '../output/curves';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Constants
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const ZOOM_MIN = 0.01;
|
|
||||||
export const ZOOM_MAX = 1.0;
|
|
||||||
export const FREEZE_THRESHOLD = ZOOM_MIN;
|
|
||||||
|
|
||||||
export const DEADZONE_MAX = 0.4;
|
|
||||||
export const INPUT_CURVE_MIN = 0.2;
|
|
||||||
export const INPUT_CURVE_MAX = 5.0;
|
|
||||||
export const SMOOTHING_MAX = 0.95;
|
|
||||||
export const VELOCITY_WINDOW_DEFAULT = 150; // ms
|
|
||||||
|
|
||||||
const REFERENCE_DT = 1 / 60;
|
|
||||||
|
|
||||||
export type MomentumZoomMode = 'off' | 'gentle' | 'strong';
|
|
||||||
export type AnchorMode = 'auto' | 'sticky' | 'center';
|
|
||||||
|
|
||||||
interface MomentumPreset {
|
|
||||||
factor: number;
|
|
||||||
minZoomMul: number;
|
|
||||||
maxZoomMul: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MOMENTUM_PRESETS: Record<MomentumZoomMode, MomentumPreset | null> = {
|
|
||||||
off: null,
|
|
||||||
gentle: { factor: 0.6, minZoomMul: 0.3, maxZoomMul: 1.0 },
|
|
||||||
strong: { factor: 1.5, minZoomMul: 0.15, maxZoomMul: 1.0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Types
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export interface InputConfig {
|
|
||||||
/** Global zoom level [0.01, 1.0] */
|
|
||||||
zoom: number;
|
|
||||||
/** Optional per-axis zoom; overrides global when not null */
|
|
||||||
zoomX: number | null;
|
|
||||||
zoomY: number | null;
|
|
||||||
/** Anchor point [0,1]^2 (used in sticky/auto modes) */
|
|
||||||
anchorX: number;
|
|
||||||
anchorY: number;
|
|
||||||
anchorMode: AnchorMode;
|
|
||||||
/** Deadzone fraction of half-travel [0, 0.4] */
|
|
||||||
deadzone: number;
|
|
||||||
/** Centered power curve exponent [0.2, 5.0] (1.0 = linear) */
|
|
||||||
inputCurve: number;
|
|
||||||
inputCurveX: number | null;
|
|
||||||
inputCurveY: number | null;
|
|
||||||
/** EMA smoothing factor [0, 0.95] */
|
|
||||||
smoothing: number;
|
|
||||||
/** Momentum-as-zoom preset */
|
|
||||||
momentumZoom: MomentumZoomMode;
|
|
||||||
velocityWindow: number;
|
|
||||||
/** Per-axis inversion */
|
|
||||||
invertX: boolean;
|
|
||||||
invertY: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InputState {
|
|
||||||
/** Last smoothed output x; seed at 0.5 */
|
|
||||||
smoothedX: number;
|
|
||||||
smoothedY: number;
|
|
||||||
/** Velocity history ring used for momentum-zoom */
|
|
||||||
velocityHistory: ReadonlyArray<{ x: number; y: number; t: number }>;
|
|
||||||
/** Most recent momentum-zoom multiplier (1 = no scale) */
|
|
||||||
momentumZoomMultiplier: number;
|
|
||||||
/** Whether last process call returned frozen=true */
|
|
||||||
frozen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProcessResult {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
frozen: boolean;
|
|
||||||
/** Next frame's state — consumer should keep this and pass it back. */
|
|
||||||
state: InputState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Defaults
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export function defaultInputConfig(): InputConfig {
|
|
||||||
return {
|
|
||||||
zoom: 1.0,
|
|
||||||
zoomX: null,
|
|
||||||
zoomY: null,
|
|
||||||
anchorX: 0.5,
|
|
||||||
anchorY: 0.5,
|
|
||||||
anchorMode: 'center',
|
|
||||||
deadzone: 0,
|
|
||||||
inputCurve: 1.0,
|
|
||||||
inputCurveX: null,
|
|
||||||
inputCurveY: null,
|
|
||||||
smoothing: 0,
|
|
||||||
momentumZoom: 'off',
|
|
||||||
velocityWindow: VELOCITY_WINDOW_DEFAULT,
|
|
||||||
invertX: false,
|
|
||||||
invertY: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultInputState(): InputState {
|
|
||||||
return {
|
|
||||||
smoothedX: 0.5,
|
|
||||||
smoothedY: 0.5,
|
|
||||||
velocityHistory: [],
|
|
||||||
momentumZoomMultiplier: 1,
|
|
||||||
frozen: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function applyDeadzone(input: number, deadzone: number): number {
|
|
||||||
if (deadzone <= 0) return input;
|
|
||||||
const offset = input - 0.5;
|
|
||||||
const absOff = Math.abs(offset);
|
|
||||||
const halfDz = deadzone * 0.5;
|
|
||||||
if (absOff <= halfDz) return 0.5;
|
|
||||||
const sign = offset < 0 ? -1 : 1;
|
|
||||||
const remapped = ((absOff - halfDz) / (0.5 - halfDz)) * 0.5;
|
|
||||||
return 0.5 + sign * remapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyZoom(input: number, anchor: number, zoomLevel: number): number {
|
|
||||||
return clamp(anchor + (input - 0.5) * zoomLevel, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emaSmooth(prev: number, raw: number, smoothing: number, dt: number): number {
|
|
||||||
if (smoothing <= 0) return raw;
|
|
||||||
const effectiveDt = dt > 0 ? dt : REFERENCE_DT;
|
|
||||||
const alpha = 1 - smoothing;
|
|
||||||
const alphaEff = 1 - Math.pow(1 - alpha, effectiveDt / REFERENCE_DT);
|
|
||||||
return prev + alphaEff * (raw - prev);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMomentumZoomMultiplier(
|
|
||||||
cfg: InputConfig,
|
|
||||||
state: InputState,
|
|
||||||
rawX: number,
|
|
||||||
rawY: number,
|
|
||||||
dt: number,
|
|
||||||
): { multiplier: number; history: InputState['velocityHistory'] } {
|
|
||||||
const preset = MOMENTUM_PRESETS[cfg.momentumZoom];
|
|
||||||
if (!preset) {
|
|
||||||
return { multiplier: 1, history: [] };
|
|
||||||
}
|
|
||||||
const now = performance.now();
|
|
||||||
const window = cfg.velocityWindow;
|
|
||||||
// Append, drop entries older than `window` ms
|
|
||||||
const trimmed = state.velocityHistory.filter((p) => now - p.t <= window);
|
|
||||||
const newHist = [...trimmed, { x: rawX, y: rawY, t: now }];
|
|
||||||
if (newHist.length < 2) {
|
|
||||||
return { multiplier: 1, history: newHist };
|
|
||||||
}
|
|
||||||
const a = newHist[0]!;
|
|
||||||
const b = newHist[newHist.length - 1]!;
|
|
||||||
const dtMs = b.t - a.t;
|
|
||||||
if (dtMs <= 0) return { multiplier: state.momentumZoomMultiplier, history: newHist };
|
|
||||||
const dx = b.x - a.x;
|
|
||||||
const dy = b.y - a.y;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
const speed = dist / (dtMs / 1000); // [0,1]-space units per second
|
|
||||||
const normSpeed = clamp(speed * preset.factor, 0, 1);
|
|
||||||
// Higher speed → smaller multiplier (zoom out faster movements)
|
|
||||||
const target = preset.maxZoomMul - (preset.maxZoomMul - preset.minZoomMul) * normSpeed;
|
|
||||||
// Smooth toward target so the zoom doesn't jitter
|
|
||||||
const smoothCoeff = clamp(dt * 6, 0, 1);
|
|
||||||
const next = state.momentumZoomMultiplier + smoothCoeff * (target - state.momentumZoomMultiplier);
|
|
||||||
return { multiplier: next, history: newHist };
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAnchorX(cfg: InputConfig, state: InputState): number {
|
|
||||||
if (cfg.anchorMode === 'center') return 0.5;
|
|
||||||
if (cfg.anchorMode === 'sticky') return cfg.anchorX;
|
|
||||||
// auto: use stored anchor (input-store updates it on zoom changes)
|
|
||||||
return cfg.anchorX;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAnchorY(cfg: InputConfig, state: InputState): number {
|
|
||||||
if (cfg.anchorMode === 'center') return 0.5;
|
|
||||||
if (cfg.anchorMode === 'sticky') return cfg.anchorY;
|
|
||||||
return cfg.anchorY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Public API
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process raw 2D input through the pipeline.
|
|
||||||
*
|
|
||||||
* @param raw raw input [x, y] in [0,1]
|
|
||||||
* @param cfg pipeline configuration
|
|
||||||
* @param state prior state (use {@link defaultInputState} on first call)
|
|
||||||
* @param dt seconds since last call (default 1/60)
|
|
||||||
*/
|
|
||||||
export function processInput(
|
|
||||||
raw: readonly [number, number],
|
|
||||||
cfg: InputConfig,
|
|
||||||
state: InputState,
|
|
||||||
dt: number = REFERENCE_DT,
|
|
||||||
): ProcessResult {
|
|
||||||
const safeDt = Math.max(0, dt);
|
|
||||||
const baseZoomX = cfg.zoomX ?? cfg.zoom;
|
|
||||||
const baseZoomY = cfg.zoomY ?? cfg.zoom;
|
|
||||||
|
|
||||||
const frozenX = baseZoomX <= FREEZE_THRESHOLD;
|
|
||||||
const frozenY = baseZoomY <= FREEZE_THRESHOLD;
|
|
||||||
const fullyFrozen = frozenX && frozenY;
|
|
||||||
|
|
||||||
if (fullyFrozen) {
|
|
||||||
return {
|
|
||||||
x: state.smoothedX,
|
|
||||||
y: state.smoothedY,
|
|
||||||
frozen: true,
|
|
||||||
state: { ...state, frozen: true },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let [rawX, rawY] = raw;
|
|
||||||
|
|
||||||
// 0. Invert
|
|
||||||
let x = cfg.invertX ? 1 - rawX : rawX;
|
|
||||||
let y = cfg.invertY ? 1 - rawY : rawY;
|
|
||||||
|
|
||||||
// 1. Deadzone
|
|
||||||
x = applyDeadzone(x, cfg.deadzone);
|
|
||||||
y = applyDeadzone(y, cfg.deadzone);
|
|
||||||
|
|
||||||
// 2. Circular clamp to unit disk centered at (0.5, 0.5)
|
|
||||||
{
|
|
||||||
const cx = x - 0.5;
|
|
||||||
const cy = y - 0.5;
|
|
||||||
const dist = Math.sqrt(cx * cx + cy * cy);
|
|
||||||
if (dist > 0.5 && dist > 1e-12) {
|
|
||||||
const scale = 0.5 / dist;
|
|
||||||
x = 0.5 + cx * scale;
|
|
||||||
y = 0.5 + cy * scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Zoom around anchor (with momentum modulation)
|
|
||||||
const anchorX = resolveAnchorX(cfg, state);
|
|
||||||
const anchorY = resolveAnchorY(cfg, state);
|
|
||||||
const effZoomX = frozenX
|
|
||||||
? FREEZE_THRESHOLD
|
|
||||||
: clamp(baseZoomX * state.momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX);
|
|
||||||
const effZoomY = frozenY
|
|
||||||
? FREEZE_THRESHOLD
|
|
||||||
: clamp(baseZoomY * state.momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX);
|
|
||||||
x = frozenX ? state.smoothedX : applyZoom(x, anchorX, effZoomX);
|
|
||||||
y = frozenY ? state.smoothedY : applyZoom(y, anchorY, effZoomY);
|
|
||||||
|
|
||||||
// 4. Centered power curve
|
|
||||||
const curveX = cfg.inputCurveX ?? cfg.inputCurve;
|
|
||||||
const curveY = cfg.inputCurveY ?? cfg.inputCurve;
|
|
||||||
if (!frozenX) x = curveCenteredPower(x, curveX);
|
|
||||||
if (!frozenY) y = curveCenteredPower(y, curveY);
|
|
||||||
|
|
||||||
// 5. EMA smoothing
|
|
||||||
const smoothedX = frozenX ? state.smoothedX : emaSmooth(state.smoothedX, x, cfg.smoothing, safeDt);
|
|
||||||
const smoothedY = frozenY ? state.smoothedY : emaSmooth(state.smoothedY, y, cfg.smoothing, safeDt);
|
|
||||||
|
|
||||||
// 6. Update momentum-zoom for next frame
|
|
||||||
const { multiplier, history } = updateMomentumZoomMultiplier(cfg, state, rawX, rawY, safeDt);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: smoothedX,
|
|
||||||
y: smoothedY,
|
|
||||||
frozen: false,
|
|
||||||
state: {
|
|
||||||
smoothedX,
|
|
||||||
smoothedY,
|
|
||||||
velocityHistory: history,
|
|
||||||
momentumZoomMultiplier: multiplier,
|
|
||||||
frozen: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/* @refresh reload */
|
|
||||||
import { render } from 'solid-js/web';
|
|
||||||
import App from './App';
|
|
||||||
import './styles/tokens.css';
|
|
||||||
import { installDebugProbe } from './debug/probe';
|
|
||||||
import { applyUrlParams } from './features/session-preset';
|
|
||||||
|
|
||||||
const root = document.getElementById('root');
|
|
||||||
if (!root) {
|
|
||||||
throw new Error('Root element #root not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install debug probe early. Playwright tests use it to drive the ML
|
|
||||||
// engine programmatically.
|
|
||||||
installDebugProbe();
|
|
||||||
|
|
||||||
// Apply URL params (?session=..., ?boldness=..., etc) before mounting so
|
|
||||||
// stores read the resolved values on first render.
|
|
||||||
try {
|
|
||||||
applyUrlParams();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[main] applyUrlParams failed:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(() => <App />, root);
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
/**
|
|
||||||
* Dataset — JS-side training-example store.
|
|
||||||
*
|
|
||||||
* Why duplicate the C++ ring buffer? Two reasons:
|
|
||||||
* 1. Sample-weight computation (recency / spatial / combined) lives in JS so
|
|
||||||
* that adjusting weighting modes doesn't burn a WASM round-trip.
|
|
||||||
* 2. The dataset is part of session state we serialize to localStorage —
|
|
||||||
* the WASM heap is wiped on reload.
|
|
||||||
*
|
|
||||||
* On train() we ship features + labels into WASM via `addExample` calls. The
|
|
||||||
* order of insertion is preserved; FIFO eviction matches the C++ MLP's
|
|
||||||
* `dataset_head_` pointer so weighting stays consistent.
|
|
||||||
*
|
|
||||||
* The implementation is a faithful TypeScript port of the legacy
|
|
||||||
* `playground/_archive/js/nisps/dataset.js` with:
|
|
||||||
* - Float32Array backing instead of `Array<Array<number>>`
|
|
||||||
* - Stricter types
|
|
||||||
* - No `withBias` flag (the WASM bindings don't take a bias term)
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type WeightMode = 'global' | 'local' | 'combined' | 'uniform';
|
|
||||||
|
|
||||||
export interface ComputeWeightsParams {
|
|
||||||
/** [0,1] — how strongly to bias toward newest examples (global/combined). */
|
|
||||||
recencyBias?: number;
|
|
||||||
/** Current input position, used for local/combined spatial weighting. */
|
|
||||||
queryInput?: ReadonlyArray<number>;
|
|
||||||
/** Spatial radius in input space (local/combined). */
|
|
||||||
radius?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Dataset {
|
|
||||||
/** Maximum number of examples retained. FIFO eviction beyond this. */
|
|
||||||
readonly maxSize: number;
|
|
||||||
/** Length of feature vectors. Set on first add(); locked thereafter. */
|
|
||||||
private inputSize_ = 0;
|
|
||||||
/** Length of label vectors. Set on first add(); locked thereafter. */
|
|
||||||
private outputSize_ = 0;
|
|
||||||
|
|
||||||
/** Flat arrays — entries `[i*inputSize, (i+1)*inputSize)` belong to example i. */
|
|
||||||
private features_: Float32Array = new Float32Array(0);
|
|
||||||
private labels_: Float32Array = new Float32Array(0);
|
|
||||||
private size_ = 0;
|
|
||||||
|
|
||||||
constructor(maxSize = 100) {
|
|
||||||
if (maxSize <= 0) throw new Error('Dataset.maxSize must be > 0');
|
|
||||||
this.maxSize = maxSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Number of examples currently stored. */
|
|
||||||
get size(): number {
|
|
||||||
return this.size_;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.size_ === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a feature/label pair. Returns true on success, false if the
|
|
||||||
* dimensions don't match a previously-added example.
|
|
||||||
*
|
|
||||||
* Eviction: when at capacity, the oldest example is removed (shift),
|
|
||||||
* then the new one is appended. This matches the legacy JS behaviour
|
|
||||||
* (and is conceptually equivalent to the C++ side's ring buffer with
|
|
||||||
* `head_` advancement).
|
|
||||||
*/
|
|
||||||
add(features: ReadonlyArray<number>, labels: ReadonlyArray<number>): boolean {
|
|
||||||
if (this.size_ === 0) {
|
|
||||||
this.inputSize_ = features.length;
|
|
||||||
this.outputSize_ = labels.length;
|
|
||||||
// Allocate full-capacity buffers up front to avoid growth thrash.
|
|
||||||
this.features_ = new Float32Array(this.maxSize * this.inputSize_);
|
|
||||||
this.labels_ = new Float32Array(this.maxSize * this.outputSize_);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (features.length !== this.inputSize_ || labels.length !== this.outputSize_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.size_ >= this.maxSize) {
|
|
||||||
// FIFO: shift left in place. This is O(n*dim) and could be replaced
|
|
||||||
// with a head pointer; for maxSize ≤ a few hundred it's fine.
|
|
||||||
this.features_.copyWithin(0, this.inputSize_);
|
|
||||||
this.labels_.copyWithin(0, this.outputSize_);
|
|
||||||
this.size_ = this.maxSize - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fOff = this.size_ * this.inputSize_;
|
|
||||||
const lOff = this.size_ * this.outputSize_;
|
|
||||||
for (let i = 0; i < this.inputSize_; ++i) this.features_[fOff + i] = features[i];
|
|
||||||
for (let i = 0; i < this.outputSize_; ++i) this.labels_[lOff + i] = labels[i];
|
|
||||||
this.size_++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.size_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read-only view of the i-th feature vector. */
|
|
||||||
feature(i: number): Float32Array {
|
|
||||||
if (i < 0 || i >= this.size_) throw new RangeError(`feature index ${i} out of bounds`);
|
|
||||||
return this.features_.subarray(i * this.inputSize_, (i + 1) * this.inputSize_);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read-only view of the i-th label vector. */
|
|
||||||
label(i: number): Float32Array {
|
|
||||||
if (i < 0 || i >= this.size_) throw new RangeError(`label index ${i} out of bounds`);
|
|
||||||
return this.labels_.subarray(i * this.outputSize_, (i + 1) * this.outputSize_);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Flat view of all features (size * inputSize). */
|
|
||||||
featuresFlat(): Float32Array {
|
|
||||||
return this.features_.subarray(0, this.size_ * this.inputSize_);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Flat view of all labels (size * outputSize). */
|
|
||||||
labelsFlat(): Float32Array {
|
|
||||||
return this.labels_.subarray(0, this.size_ * this.outputSize_);
|
|
||||||
}
|
|
||||||
|
|
||||||
get inputSize(): number {
|
|
||||||
return this.inputSize_;
|
|
||||||
}
|
|
||||||
get outputSize(): number {
|
|
||||||
return this.outputSize_;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute per-sample training weights. Returns Float32Array (size=this.size)
|
|
||||||
* normalized to sum to 1. For an empty dataset returns a 0-length array;
|
|
||||||
* for a singleton, [1.0].
|
|
||||||
*
|
|
||||||
* Modes:
|
|
||||||
* - `uniform` — every weight = 1/n.
|
|
||||||
* - `global` — exponential recency decay over insertion order.
|
|
||||||
* - `local` — within `radius` of `queryInput`, suppress older neighbours.
|
|
||||||
* - `combined` — global × local.
|
|
||||||
*/
|
|
||||||
computeWeights(mode: WeightMode = 'uniform', params: ComputeWeightsParams = {}): Float32Array {
|
|
||||||
const n = this.size_;
|
|
||||||
if (n === 0) return new Float32Array(0);
|
|
||||||
if (n === 1) return new Float32Array([1.0]);
|
|
||||||
|
|
||||||
const weights = new Float32Array(n).fill(1.0);
|
|
||||||
|
|
||||||
if (mode === 'global' || mode === 'combined') {
|
|
||||||
const bias = params.recencyBias ?? 0.6;
|
|
||||||
if (bias > 0) {
|
|
||||||
const decay = 1 - 0.3 * bias;
|
|
||||||
for (let i = n - 2; i >= 0; --i) weights[i] = weights[i + 1] * decay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((mode === 'local' || mode === 'combined') && params.queryInput) {
|
|
||||||
const query = params.queryInput;
|
|
||||||
const radius = params.radius ?? 0.15;
|
|
||||||
const radiusSq = radius * radius;
|
|
||||||
const dim = this.inputSize_;
|
|
||||||
|
|
||||||
for (let i = 0; i < n; ++i) {
|
|
||||||
const fOffI = i * dim;
|
|
||||||
let distSq = 0;
|
|
||||||
for (let d = 0; d < dim; ++d) {
|
|
||||||
const diff = this.features_[fOffI + d] - (query[d] ?? 0);
|
|
||||||
distSq += diff * diff;
|
|
||||||
}
|
|
||||||
if (distSq < radiusSq) {
|
|
||||||
const proximity = 1 - Math.sqrt(distSq) / radius;
|
|
||||||
let newerNearby = 0;
|
|
||||||
for (let j = i + 1; j < n; ++j) {
|
|
||||||
const fOffJ = j * dim;
|
|
||||||
let djSq = 0;
|
|
||||||
for (let d = 0; d < dim; ++d) {
|
|
||||||
const diff = this.features_[fOffI + d] - this.features_[fOffJ + d];
|
|
||||||
djSq += diff * diff;
|
|
||||||
}
|
|
||||||
if (djSq < radiusSq) ++newerNearby;
|
|
||||||
}
|
|
||||||
if (newerNearby > 0) {
|
|
||||||
weights[i] *= Math.pow(1 - proximity, newerNearby);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < n; ++i) sum += weights[i];
|
|
||||||
if (sum > 0) for (let i = 0; i < n; ++i) weights[i] /= sum;
|
|
||||||
return weights;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
/**
|
|
||||||
* jolt.ts — "Jolt": held-button continuous weight morph.
|
|
||||||
*
|
|
||||||
* Faithful TS port of `nisps/ml/jolt.hpp` (which itself ports upstream
|
|
||||||
* memllib InterfaceRL "jolts"). 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).
|
|
||||||
*
|
|
||||||
* This operates on the MLP's FLAT weight buffer (the same buffer reached
|
|
||||||
* via `mlStore.getWeights()` / `setWeights()` / weight_count), so it is
|
|
||||||
* architecture-agnostic.
|
|
||||||
*
|
|
||||||
* RNG note: the C++ owns a per-instance deterministic xoshiro256+ for
|
|
||||||
* firmware↔browser parity. This is a browser-only exploration aid that
|
|
||||||
* never round-trips through WASM, so we use `Math.random()` — the exact
|
|
||||||
* sequence does not need to match firmware. Constants are reproduced
|
|
||||||
* verbatim from JoltParams.
|
|
||||||
*
|
|
||||||
* DEFAULT STATE IS INERT: a freshly constructed Jolt is inactive and
|
|
||||||
* `step()` is a no-op until `press()` is called.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Upstream JoltParams defaults (kJolt* constants). */
|
|
||||||
export interface JoltParams {
|
|
||||||
/** kJoltNumWeights — number of weights morphed at once. */
|
|
||||||
numWeights: number;
|
|
||||||
/** kJoltMorphRate — EMA glide per tick (~1s @200Hz). */
|
|
||||||
morphRate: number;
|
|
||||||
/** kJoltWeightMin — lower bound of a random target. */
|
|
||||||
targetMin: number;
|
|
||||||
/** kJoltWeightMax — upper bound of a random target. */
|
|
||||||
targetMax: number;
|
|
||||||
/** kJoltTargetEpsilon — re-roll target once within this distance. */
|
|
||||||
targetEpsilon: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_JOLT_PARAMS: JoltParams = {
|
|
||||||
numWeights: 40,
|
|
||||||
morphRate: 0.017,
|
|
||||||
targetMin: -1.2,
|
|
||||||
targetMax: 0.9,
|
|
||||||
targetEpsilon: 0.05,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Jolt {
|
|
||||||
private params_: JoltParams = { ...DEFAULT_JOLT_PARAMS };
|
|
||||||
private active_ = false;
|
|
||||||
private n_ = 0;
|
|
||||||
private idx_: Int32Array = new Int32Array(0);
|
|
||||||
private target_: Float32Array = new Float32Array(0);
|
|
||||||
|
|
||||||
constructor(params?: Partial<JoltParams>) {
|
|
||||||
if (params) this.params_ = { ...this.params_, ...params };
|
|
||||||
}
|
|
||||||
|
|
||||||
setParams(p: Partial<JoltParams>): void {
|
|
||||||
this.params_ = { ...this.params_, ...p };
|
|
||||||
}
|
|
||||||
|
|
||||||
get params(): Readonly<JoltParams> {
|
|
||||||
return this.params_;
|
|
||||||
}
|
|
||||||
|
|
||||||
active(): boolean {
|
|
||||||
return this.active_;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Begin a jolt over a flat weight buffer of `weightCount` entries: pick
|
|
||||||
* `numWeights` random global indices and a bounded random target each.
|
|
||||||
*/
|
|
||||||
press(weightCount: number): void {
|
|
||||||
this.active_ = true;
|
|
||||||
this.n_ = this.params_.numWeights;
|
|
||||||
if (weightCount <= 0) {
|
|
||||||
this.n_ = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.idx_.length < this.n_) this.idx_ = new Int32Array(this.n_);
|
|
||||||
if (this.target_.length < this.n_) this.target_ = new Float32Array(this.n_);
|
|
||||||
for (let i = 0; i < this.n_; ++i) {
|
|
||||||
this.idx_[i] = Math.floor(Math.random() * weightCount) % weightCount;
|
|
||||||
this.target_[i] = this.rollTarget_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Per control tick while held: EMA-glide each selected weight toward its
|
|
||||||
* target, re-rolling targets that have been reached. No-op when inactive.
|
|
||||||
* Mutates `weights` in place.
|
|
||||||
*/
|
|
||||||
step(weights: Float32Array): void {
|
|
||||||
if (!this.active_) return;
|
|
||||||
const wc = weights.length;
|
|
||||||
const rate = this.params_.morphRate;
|
|
||||||
const eps = this.params_.targetEpsilon;
|
|
||||||
for (let i = 0; i < this.n_; ++i) {
|
|
||||||
const k = this.idx_[i]!;
|
|
||||||
if (k < 0 || k >= wc) continue;
|
|
||||||
let w = weights[k]!;
|
|
||||||
w += rate * (this.target_[i]! - w);
|
|
||||||
weights[k] = w;
|
|
||||||
if (Math.abs(this.target_[i]! - w) < eps) {
|
|
||||||
this.target_[i] = this.rollTarget_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Release: freeze weights where they are (permanent). */
|
|
||||||
release(): void {
|
|
||||||
this.active_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private rollTarget_(): number {
|
|
||||||
return (
|
|
||||||
this.params_.targetMin +
|
|
||||||
Math.random() * (this.params_.targetMax - this.params_.targetMin)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
/**
|
|
||||||
* TypeScript types matching the C API surface in `nisps/wasm/bindings.cpp`.
|
|
||||||
*
|
|
||||||
* These types are intentionally minimal: they describe the JS-visible shape
|
|
||||||
* of the Emscripten module, the heap views we read/write, and the per-layer
|
|
||||||
* stats record. They DO NOT mirror any internal C++ struct.
|
|
||||||
*
|
|
||||||
* The Emscripten glue produced by `scripts/build-wasm.sh` exposes a factory
|
|
||||||
* function, `createNispsModule(opts?) => Promise<NispsModule>`, which we
|
|
||||||
* call from `wasm-iml.ts` and `wasm-worker.ts`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shape of the loaded WASM module — the subset we use.
|
|
||||||
* Emscripten generates more on it; we type only what we need.
|
|
||||||
*
|
|
||||||
* NOTE: `_*` prefixed methods are the raw exported C functions (Emscripten
|
|
||||||
* naming convention). They take/return numbers (pointers + primitives).
|
|
||||||
*/
|
|
||||||
export interface NispsModule {
|
|
||||||
// Memory views (re-bound after grow).
|
|
||||||
HEAP8: Int8Array;
|
|
||||||
HEAP16: Int16Array;
|
|
||||||
HEAP32: Int32Array;
|
|
||||||
HEAPU8: Uint8Array;
|
|
||||||
HEAPU16: Uint16Array;
|
|
||||||
HEAPU32: Uint32Array;
|
|
||||||
HEAPF32: Float32Array;
|
|
||||||
HEAPF64: Float64Array;
|
|
||||||
|
|
||||||
_malloc(bytes: number): number;
|
|
||||||
_free(ptr: number): void;
|
|
||||||
|
|
||||||
// ML lifecycle.
|
|
||||||
// Seed is a uint32_t (not 64-bit) — see bindings.cpp file comment.
|
|
||||||
_nisps_ml_create(input_size: number, output_size: number, hidden_ptr: number, n_hidden: number, seed: number): number;
|
|
||||||
_nisps_ml_destroy(ml: number): void;
|
|
||||||
_nisps_ml_reset(ml: number): void;
|
|
||||||
|
|
||||||
// ML inference.
|
|
||||||
_nisps_ml_set_input(ml: number, idx: number, v: number): void;
|
|
||||||
_nisps_ml_process(ml: number): void;
|
|
||||||
_nisps_ml_outputs(ml: number): number; // returns float* into HEAPF32
|
|
||||||
_nisps_ml_infer_batch(ml: number, points_ptr: number, n_points: number, out_ptr: number): void;
|
|
||||||
|
|
||||||
// ML training.
|
|
||||||
_nisps_ml_add_example(ml: number, features_ptr: number, labels_ptr: number): void;
|
|
||||||
_nisps_ml_train(ml: number, lr: number, max_iter: number, min_err: number, sample_weights_ptr: number): number;
|
|
||||||
_nisps_ml_eval_loss(ml: number): number;
|
|
||||||
|
|
||||||
// ML examples.
|
|
||||||
_nisps_ml_clear_examples(ml: number): void;
|
|
||||||
_nisps_ml_example_count(ml: number): number;
|
|
||||||
|
|
||||||
// ML weights.
|
|
||||||
_nisps_ml_weight_count(ml: number): number;
|
|
||||||
_nisps_ml_get_weights(ml: number, out_ptr: number): void;
|
|
||||||
_nisps_ml_set_weights(ml: number, in_ptr: number): void;
|
|
||||||
_nisps_ml_draw_weights(ml: number, spread: number): void;
|
|
||||||
_nisps_ml_move_weights(ml: number, speed: number, spread: number, mask_ptr: number): void;
|
|
||||||
_nisps_ml_get_layer_stats(ml: number, out_ptr: number): void;
|
|
||||||
_nisps_ml_describe(out_ptr: number): void;
|
|
||||||
|
|
||||||
// Engines.
|
|
||||||
_nisps_engine_create(id_ptr: number, sample_rate: number): number;
|
|
||||||
_nisps_engine_destroy(engine: number): void;
|
|
||||||
_nisps_engine_set_params(engine: number, params_ptr: number, n_params: number): void;
|
|
||||||
_nisps_engine_process_block(
|
|
||||||
engine: number,
|
|
||||||
in_l_ptr: number, in_r_ptr: number,
|
|
||||||
out_l_ptr: number, out_r_ptr: number,
|
|
||||||
n_samples: number,
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Factory function exposed by the Emscripten glue. */
|
|
||||||
export type NispsModuleFactory = (opts?: {
|
|
||||||
locateFile?: (path: string, prefix: string) => string;
|
|
||||||
wasmBinary?: ArrayBuffer | Uint8Array;
|
|
||||||
print?: (msg: string) => void;
|
|
||||||
printErr?: (msg: string) => void;
|
|
||||||
}) => Promise<NispsModule>;
|
|
||||||
|
|
||||||
/** Architecture descriptor returned from `nisps_ml_describe`. */
|
|
||||||
export interface MLArchitecture {
|
|
||||||
inputSize: number;
|
|
||||||
hidden: [number, number, number];
|
|
||||||
outputSize: number;
|
|
||||||
numLayers: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Per-layer weight health record (one per layer). */
|
|
||||||
export interface LayerStats {
|
|
||||||
meanAbs: number;
|
|
||||||
maxAbs: number;
|
|
||||||
deadFrac: number;
|
|
||||||
saturatingFrac: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The `engine_id` strings the C++ side recognises. Anything else falls back to "thru". */
|
|
||||||
export type EngineId =
|
|
||||||
| 'thru'
|
|
||||||
| 'paf_synth'
|
|
||||||
| 'channel_strip'
|
|
||||||
| 'xiasri'
|
|
||||||
| 'verb_fx'
|
|
||||||
| 'memlcelium'
|
|
||||||
| 'breakor'
|
|
||||||
| 'elysiamorf'
|
|
||||||
| 'analysis';
|
|
||||||
|
|
||||||
/** Message protocol between main thread and `wasm-worker.ts`. */
|
|
||||||
export type WorkerRequest =
|
|
||||||
| {
|
|
||||||
kind: 'init';
|
|
||||||
seed: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'train';
|
|
||||||
requestId: number;
|
|
||||||
// Flat features: nExamples * inputSize floats.
|
|
||||||
features: Float32Array;
|
|
||||||
// Flat labels: nExamples * outputSize floats.
|
|
||||||
labels: Float32Array;
|
|
||||||
// Optional per-example weights, sums to 1. Empty = uniform.
|
|
||||||
sampleWeights: Float32Array;
|
|
||||||
// Current weights to seed worker MLP.
|
|
||||||
weights: Float32Array;
|
|
||||||
lr: number;
|
|
||||||
maxIter: number;
|
|
||||||
minErr: number;
|
|
||||||
inputSize: number;
|
|
||||||
outputSize: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'dispose';
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkerResponse =
|
|
||||||
| {
|
|
||||||
kind: 'ready';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'result';
|
|
||||||
requestId: number;
|
|
||||||
loss: number;
|
|
||||||
weights: Float32Array;
|
|
||||||
// Loss curve (per-iteration). Currently always empty — the C++ MLP
|
|
||||||
// exposes loss_history but the WASM bridge does not yet plumb it.
|
|
||||||
lossHistory: Float32Array;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
kind: 'error';
|
|
||||||
requestId: number;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
@ -1,650 +0,0 @@
|
||||||
/**
|
|
||||||
* WasmIML — main-thread ML interface backed by `nisps.wasm`.
|
|
||||||
*
|
|
||||||
* Owns:
|
|
||||||
* - One `nisps.wasm` instance.
|
|
||||||
* - One MLP handle.
|
|
||||||
* - A JS-side `Dataset` (mirrors the C++ ring buffer; see dataset.ts).
|
|
||||||
* - Pre-allocated heap buffers for inputs/outputs/weights/etc.
|
|
||||||
* - A lazy `WasmTrainer` worker for off-thread async training.
|
|
||||||
*
|
|
||||||
* Threading model (architecture.md §6.4):
|
|
||||||
* - Inference + sync training run on this main-thread instance.
|
|
||||||
* - `trainAsync()` spawns/uses a worker which holds a SECOND wasm
|
|
||||||
* instance; weights round-trip through `getWeights()`/`setWeights()`.
|
|
||||||
*
|
|
||||||
* Side effects: every mutation that should be visible to the UI calls into
|
|
||||||
* `mlStore`. The store is the single source of truth for Solid components.
|
|
||||||
*
|
|
||||||
* Concept compatibility: this class re-implements the legacy WasmIML
|
|
||||||
* surface area documented in `recon/04-playground.md §6` so the existing
|
|
||||||
* Playwright debug-probe tests can keep passing once the new probe is
|
|
||||||
* wired up.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { produce } from 'solid-js/store';
|
|
||||||
|
|
||||||
import { mlStore } from '../stores/ml-store';
|
|
||||||
import { coreBus } from '../stores/bus';
|
|
||||||
import { Dataset } from './dataset';
|
|
||||||
import type {
|
|
||||||
LayerStats,
|
|
||||||
MLArchitecture,
|
|
||||||
NispsModule,
|
|
||||||
NispsModuleFactory,
|
|
||||||
} from './types';
|
|
||||||
import { createTrainer, type WasmTrainer } from './wasm-worker';
|
|
||||||
|
|
||||||
const NISPS_JS_URL = '/nisps.js';
|
|
||||||
const NISPS_WASM_URL = '/nisps.wasm';
|
|
||||||
|
|
||||||
/** Default architecture matches `nisps/wasm/bindings.cpp` instantiation. */
|
|
||||||
const DEFAULT_INPUT_SIZE = 2;
|
|
||||||
const DEFAULT_OUTPUT_SIZE = 126;
|
|
||||||
|
|
||||||
let cachedFactory: NispsModuleFactory | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the Emscripten glue once, cache the factory.
|
|
||||||
*
|
|
||||||
* The glue is served from `playground/public/nisps.js` (committed). We
|
|
||||||
* dynamically import it so the WASM only loads when the ML system is first
|
|
||||||
* used — `/dev/primitives` doesn't pay for it.
|
|
||||||
*/
|
|
||||||
async function getFactory(): Promise<NispsModuleFactory> {
|
|
||||||
if (cachedFactory) return cachedFactory;
|
|
||||||
// Vite serves `/nisps.js` as a normal asset; we use a dynamic-eval import
|
|
||||||
// to avoid Vite trying to resolve it at build time.
|
|
||||||
const url = new URL(NISPS_JS_URL, window.location.origin).toString();
|
|
||||||
const mod = await import(/* @vite-ignore */ url);
|
|
||||||
// Emscripten MODULARIZE=1 default export key is `default`.
|
|
||||||
// tslint:disable-next-line:no-any
|
|
||||||
const factory = (mod as { default?: NispsModuleFactory; createNispsModule?: NispsModuleFactory })
|
|
||||||
.default ?? (mod as { createNispsModule?: NispsModuleFactory }).createNispsModule;
|
|
||||||
if (!factory) throw new Error('[wasm-iml] nisps.js does not export a module factory');
|
|
||||||
cachedFactory = factory;
|
|
||||||
return factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Aligned float-array allocation helper. Returns ptr + a view. */
|
|
||||||
class HeapBuffer {
|
|
||||||
readonly ptr: number;
|
|
||||||
readonly view: Float32Array;
|
|
||||||
constructor(private mod: NispsModule, public readonly count: number) {
|
|
||||||
this.ptr = mod._malloc(count * 4);
|
|
||||||
if (!this.ptr) throw new Error(`malloc(${count * 4}) failed`);
|
|
||||||
this.view = new Float32Array(mod.HEAPF32.buffer, this.ptr, count);
|
|
||||||
}
|
|
||||||
/** After memory growth, refresh the view onto the new ArrayBuffer. */
|
|
||||||
rebind(): void {
|
|
||||||
// Re-create the view with the (possibly new) underlying buffer.
|
|
||||||
Object.defineProperty(this, 'view', {
|
|
||||||
value: new Float32Array(this.mod.HEAPF32.buffer, this.ptr, this.count),
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
free(): void {
|
|
||||||
this.mod._free(this.ptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HeapU8 {
|
|
||||||
readonly ptr: number;
|
|
||||||
readonly view: Uint8Array;
|
|
||||||
constructor(private mod: NispsModule, public readonly count: number) {
|
|
||||||
this.ptr = mod._malloc(count);
|
|
||||||
if (!this.ptr) throw new Error(`malloc(${count}) failed`);
|
|
||||||
this.view = new Uint8Array(mod.HEAPU8.buffer, this.ptr, count);
|
|
||||||
}
|
|
||||||
rebind(): void {
|
|
||||||
Object.defineProperty(this, 'view', {
|
|
||||||
value: new Uint8Array(this.mod.HEAPU8.buffer, this.ptr, this.count),
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
free(): void {
|
|
||||||
this.mod._free(this.ptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WasmIMLOptions {
|
|
||||||
inputSize?: number;
|
|
||||||
outputSize?: number;
|
|
||||||
hiddenLayers?: ReadonlyArray<number>;
|
|
||||||
seed?: number;
|
|
||||||
/** localStorage key the loaded weights/dataset will be persisted under. */
|
|
||||||
storageKey?: string;
|
|
||||||
maxExamples?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WasmIML {
|
|
||||||
// WASM binding state.
|
|
||||||
private module!: NispsModule;
|
|
||||||
private mlHandle = 0;
|
|
||||||
private weightCount_ = 0;
|
|
||||||
|
|
||||||
// Architecture descriptor (resolved post-init from the WASM build).
|
|
||||||
private arch_: MLArchitecture = {
|
|
||||||
inputSize: DEFAULT_INPUT_SIZE,
|
|
||||||
hidden: [10, 14, 18],
|
|
||||||
outputSize: DEFAULT_OUTPUT_SIZE,
|
|
||||||
numLayers: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pre-allocated heap buffers.
|
|
||||||
private featuresBuf!: HeapBuffer;
|
|
||||||
private labelsBuf!: HeapBuffer;
|
|
||||||
private weightsBuf!: HeapBuffer;
|
|
||||||
private statsBuf!: HeapBuffer;
|
|
||||||
private batchInBuf!: HeapBuffer;
|
|
||||||
private batchOutBuf!: HeapBuffer;
|
|
||||||
private pinMaskBuf!: HeapU8;
|
|
||||||
private describeBuf!: HeapBuffer; // 6 ints, reused as 6 floats on the heap is wrong;
|
|
||||||
// We use HEAP32 directly via a tiny scratch malloc:
|
|
||||||
private describePtr = 0;
|
|
||||||
|
|
||||||
// JS-side state.
|
|
||||||
readonly dataset: Dataset;
|
|
||||||
private lastLoss_: number | null = null;
|
|
||||||
private trainer: WasmTrainer | null = null;
|
|
||||||
private storageKey: string;
|
|
||||||
private saveTimer: number | null = null;
|
|
||||||
private destroyed = false;
|
|
||||||
|
|
||||||
static MAX_BATCH = 4096;
|
|
||||||
|
|
||||||
private constructor(opts: WasmIMLOptions) {
|
|
||||||
this.dataset = new Dataset(opts.maxExamples ?? 100);
|
|
||||||
this.storageKey = opts.storageKey ?? 'nisps:wasm-iml';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async factory. Loads the wasm, creates the MLP handle, allocates heap
|
|
||||||
* buffers, and rehydrates persisted state.
|
|
||||||
*/
|
|
||||||
static async create(opts: WasmIMLOptions = {}): Promise<WasmIML> {
|
|
||||||
const inst = new WasmIML(opts);
|
|
||||||
await inst.init_(opts);
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init_(opts: WasmIMLOptions): Promise<void> {
|
|
||||||
const factory = await getFactory();
|
|
||||||
this.module = await factory({
|
|
||||||
// Vite serves /nisps.wasm at the root; the default locateFile would
|
|
||||||
// resolve relative to nisps.js (also at root) so this is the same
|
|
||||||
// result, but explicit is better.
|
|
||||||
locateFile: (path: string) => {
|
|
||||||
if (path.endsWith('.wasm')) return new URL(NISPS_WASM_URL, window.location.origin).toString();
|
|
||||||
return path;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Resolve architecture via the WASM module (compile-time fixed; we
|
|
||||||
// stash the values for callers that need them).
|
|
||||||
this.describePtr = this.module._malloc(6 * 4);
|
|
||||||
this.module._nisps_ml_describe(this.describePtr);
|
|
||||||
const dims = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 6);
|
|
||||||
this.arch_ = {
|
|
||||||
inputSize: dims[0],
|
|
||||||
hidden: [dims[1], dims[2], dims[3]],
|
|
||||||
outputSize: dims[4],
|
|
||||||
numLayers: dims[5],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Caller-supplied dimensions are accepted but ignored; warn if mismatch.
|
|
||||||
const wantedIn = opts.inputSize ?? this.arch_.inputSize;
|
|
||||||
const wantedOut = opts.outputSize ?? this.arch_.outputSize;
|
|
||||||
if (wantedIn !== this.arch_.inputSize || wantedOut !== this.arch_.outputSize) {
|
|
||||||
console.warn(
|
|
||||||
`[wasm-iml] requested ${wantedIn}->${wantedOut} but WASM build is fixed at ` +
|
|
||||||
`${this.arch_.inputSize}->${this.arch_.outputSize}; extras are ignored.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the MLP. We pass dummy hidden ptr/count — the binding ignores them.
|
|
||||||
const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0;
|
|
||||||
this.mlHandle = this.module._nisps_ml_create(
|
|
||||||
this.arch_.inputSize,
|
|
||||||
this.arch_.outputSize,
|
|
||||||
0, // hidden ptr (unused)
|
|
||||||
0, // n_hidden (unused)
|
|
||||||
seed,
|
|
||||||
);
|
|
||||||
if (!this.mlHandle) throw new Error('[wasm-iml] nisps_ml_create returned null');
|
|
||||||
|
|
||||||
this.weightCount_ = this.module._nisps_ml_weight_count(this.mlHandle);
|
|
||||||
|
|
||||||
// Heap buffers (created after we know the architecture).
|
|
||||||
this.featuresBuf = new HeapBuffer(this.module, this.arch_.inputSize);
|
|
||||||
this.labelsBuf = new HeapBuffer(this.module, this.arch_.outputSize);
|
|
||||||
this.weightsBuf = new HeapBuffer(this.module, this.weightCount_);
|
|
||||||
this.statsBuf = new HeapBuffer(this.module, this.arch_.numLayers * 4);
|
|
||||||
this.batchInBuf = new HeapBuffer(this.module, WasmIML.MAX_BATCH * this.arch_.inputSize);
|
|
||||||
this.batchOutBuf = new HeapBuffer(this.module, WasmIML.MAX_BATCH * this.arch_.outputSize);
|
|
||||||
this.pinMaskBuf = new HeapU8(this.module, this.arch_.outputSize);
|
|
||||||
|
|
||||||
// Push initial state to the store.
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.inputSize = this.arch_.inputSize;
|
|
||||||
s.outputSize = this.arch_.outputSize;
|
|
||||||
s.exampleCount = 0;
|
|
||||||
s.lastLoss = null;
|
|
||||||
s.lossHistory = [];
|
|
||||||
s.training = false;
|
|
||||||
s.ready = true;
|
|
||||||
}));
|
|
||||||
mlStore.__setOutputs(new Float32Array(this.arch_.outputSize));
|
|
||||||
this.publishWeights_();
|
|
||||||
|
|
||||||
// Load persisted state if present (best-effort).
|
|
||||||
this.tryLoadFromStorage_();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Lifecycle
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
if (this.destroyed) return;
|
|
||||||
this.destroyed = true;
|
|
||||||
if (this.saveTimer !== null) {
|
|
||||||
clearTimeout(this.saveTimer);
|
|
||||||
this.saveTimer = null;
|
|
||||||
}
|
|
||||||
if (this.trainer) {
|
|
||||||
this.trainer.dispose();
|
|
||||||
this.trainer = null;
|
|
||||||
}
|
|
||||||
if (this.module && this.mlHandle) {
|
|
||||||
this.module._nisps_ml_destroy(this.mlHandle);
|
|
||||||
this.mlHandle = 0;
|
|
||||||
}
|
|
||||||
if (this.featuresBuf) this.featuresBuf.free();
|
|
||||||
if (this.labelsBuf) this.labelsBuf.free();
|
|
||||||
if (this.weightsBuf) this.weightsBuf.free();
|
|
||||||
if (this.statsBuf) this.statsBuf.free();
|
|
||||||
if (this.batchInBuf) this.batchInBuf.free();
|
|
||||||
if (this.batchOutBuf) this.batchOutBuf.free();
|
|
||||||
if (this.pinMaskBuf) this.pinMaskBuf.free();
|
|
||||||
if (this.describePtr) this.module._free(this.describePtr);
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.ready = false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
get architecture(): MLArchitecture {
|
|
||||||
return this.arch_;
|
|
||||||
}
|
|
||||||
get weightCount(): number {
|
|
||||||
return this.weightCount_;
|
|
||||||
}
|
|
||||||
get exampleCount(): number {
|
|
||||||
return this.dataset.size;
|
|
||||||
}
|
|
||||||
get lastLoss(): number | null {
|
|
||||||
return this.lastLoss_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Inference
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
setInput(idx: number, value: number): void {
|
|
||||||
this.module._nisps_ml_set_input(this.mlHandle, idx, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
process(): Float32Array {
|
|
||||||
this.module._nisps_ml_process(this.mlHandle);
|
|
||||||
const ptr = this.module._nisps_ml_outputs(this.mlHandle);
|
|
||||||
// Copy out so the caller can hold onto it across memory growth.
|
|
||||||
const view = new Float32Array(this.module.HEAPF32.buffer, ptr, this.arch_.outputSize);
|
|
||||||
const out = new Float32Array(view); // copy
|
|
||||||
mlStore.__setOutputs(out);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convenience: setInput(0,x); setInput(1,y); process(). */
|
|
||||||
inferXY(x: number, y: number): Float32Array {
|
|
||||||
this.setInput(0, x);
|
|
||||||
this.setInput(1, y);
|
|
||||||
return this.process();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch inference. `points` is an array of [x,y] tuples (or any vector
|
|
||||||
* length <= inputSize; trailing entries zero-padded). Returns a flat
|
|
||||||
* Float32Array of length n * outputSize.
|
|
||||||
*
|
|
||||||
* Larger requests than `MAX_BATCH` are chunked transparently.
|
|
||||||
*/
|
|
||||||
inferBatch(points: ReadonlyArray<ReadonlyArray<number>>): Float32Array {
|
|
||||||
const n = points.length;
|
|
||||||
const inSz = this.arch_.inputSize;
|
|
||||||
const outSz = this.arch_.outputSize;
|
|
||||||
const result = new Float32Array(n * outSz);
|
|
||||||
|
|
||||||
let written = 0;
|
|
||||||
for (let offset = 0; offset < n; offset += WasmIML.MAX_BATCH) {
|
|
||||||
const chunk = Math.min(WasmIML.MAX_BATCH, n - offset);
|
|
||||||
// Pack into batchInBuf.
|
|
||||||
for (let i = 0; i < chunk; ++i) {
|
|
||||||
const src = points[offset + i];
|
|
||||||
const base = i * inSz;
|
|
||||||
for (let j = 0; j < inSz; ++j) this.batchInBuf.view[base + j] = src[j] ?? 0;
|
|
||||||
}
|
|
||||||
this.module._nisps_ml_infer_batch(
|
|
||||||
this.mlHandle,
|
|
||||||
this.batchInBuf.ptr,
|
|
||||||
chunk,
|
|
||||||
this.batchOutBuf.ptr,
|
|
||||||
);
|
|
||||||
// Copy out into result.
|
|
||||||
const slice = this.batchOutBuf.view.subarray(0, chunk * outSz);
|
|
||||||
result.set(slice, written);
|
|
||||||
written += chunk * outSz;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Training
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a feature/label pair to BOTH the JS dataset and the WASM ring
|
|
||||||
* buffer. The two stay in sync because every train() call pushes the
|
|
||||||
* full JS dataset back into WASM (in case of weight recompute, undo
|
|
||||||
* restore, etc.). For the sake of correctness, we re-sync on each add
|
|
||||||
* too — cheap relative to training.
|
|
||||||
*/
|
|
||||||
addExample(features: ReadonlyArray<number>, labels: ReadonlyArray<number>): boolean {
|
|
||||||
const ok = this.dataset.add(features, labels);
|
|
||||||
if (!ok) return false;
|
|
||||||
this.copyExampleToWasm_(features, labels);
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.exampleCount = this.dataset.size;
|
|
||||||
}));
|
|
||||||
coreBus.emit('ml.example_added', { count: this.dataset.size });
|
|
||||||
this.scheduleSave_();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private copyExampleToWasm_(features: ReadonlyArray<number>, labels: ReadonlyArray<number>): void {
|
|
||||||
const fv = this.featuresBuf.view;
|
|
||||||
const lv = this.labelsBuf.view;
|
|
||||||
const inSz = this.arch_.inputSize;
|
|
||||||
const outSz = this.arch_.outputSize;
|
|
||||||
for (let i = 0; i < inSz; ++i) fv[i] = features[i] ?? 0;
|
|
||||||
for (let i = 0; i < outSz; ++i) lv[i] = labels[i] ?? 0;
|
|
||||||
this.module._nisps_ml_add_example(this.mlHandle, this.featuresBuf.ptr, this.labelsBuf.ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronous training. Returns the final loss (also stored in
|
|
||||||
* `lastLoss`). Updates `mlStore.lastLoss` and emits `ml.trained`.
|
|
||||||
*
|
|
||||||
* Caller can pass per-sample weights; if omitted the WASM side uses
|
|
||||||
* uniform 1/n weighting.
|
|
||||||
*/
|
|
||||||
train(lr = 1.0, maxIter = 1000, minErr = 0.001, sampleWeights?: Float32Array): number {
|
|
||||||
if (this.dataset.isEmpty()) {
|
|
||||||
this.lastLoss_ = 0;
|
|
||||||
mlStore.__setState(produce((s) => { s.lastLoss = 0; }));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let weightsPtr = 0;
|
|
||||||
let weightsHandle: HeapBuffer | null = null;
|
|
||||||
if (sampleWeights && sampleWeights.length === this.dataset.size) {
|
|
||||||
weightsHandle = new HeapBuffer(this.module, sampleWeights.length);
|
|
||||||
weightsHandle.view.set(sampleWeights);
|
|
||||||
weightsPtr = weightsHandle.ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
mlStore.__setState(produce((s) => { s.training = true; }));
|
|
||||||
let loss = 0;
|
|
||||||
try {
|
|
||||||
loss = this.module._nisps_ml_train(this.mlHandle, lr, maxIter, minErr, weightsPtr);
|
|
||||||
} finally {
|
|
||||||
if (weightsHandle) weightsHandle.free();
|
|
||||||
mlStore.__setState(produce((s) => { s.training = false; }));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastLoss_ = loss;
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.lastLoss = loss;
|
|
||||||
// The C++ MLP stores per-iter history but we don't currently expose
|
|
||||||
// it via the WASM bindings. Stream 9 may add nisps_ml_loss_history.
|
|
||||||
s.lossHistory = [loss];
|
|
||||||
}));
|
|
||||||
this.publishWeights_();
|
|
||||||
coreBus.emit('ml.trained', { loss });
|
|
||||||
this.scheduleSave_();
|
|
||||||
return loss;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async training via worker. The worker holds a SECOND wasm instance,
|
|
||||||
* receives current weights + dataset, runs SGD, and returns updated
|
|
||||||
* weights. Main-thread weights are then `setWeights()`-restored.
|
|
||||||
*/
|
|
||||||
async trainAsync(lr = 1.0, maxIter = 1000, minErr = 0.001, sampleWeights?: Float32Array): Promise<number> {
|
|
||||||
if (this.dataset.isEmpty()) {
|
|
||||||
this.lastLoss_ = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (!this.trainer) this.trainer = await createTrainer();
|
|
||||||
|
|
||||||
const weights = this.getWeights();
|
|
||||||
const features = new Float32Array(this.dataset.featuresFlat());
|
|
||||||
const labels = new Float32Array(this.dataset.labelsFlat());
|
|
||||||
const sw = sampleWeights ? new Float32Array(sampleWeights) : new Float32Array(0);
|
|
||||||
|
|
||||||
mlStore.__setState(produce((s) => { s.training = true; }));
|
|
||||||
try {
|
|
||||||
const result = await this.trainer.train({
|
|
||||||
weights,
|
|
||||||
features,
|
|
||||||
labels,
|
|
||||||
sampleWeights: sw,
|
|
||||||
lr,
|
|
||||||
maxIter,
|
|
||||||
minErr,
|
|
||||||
inputSize: this.arch_.inputSize,
|
|
||||||
outputSize: this.arch_.outputSize,
|
|
||||||
});
|
|
||||||
this.setWeights(result.weights);
|
|
||||||
this.lastLoss_ = result.loss;
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.lastLoss = result.loss;
|
|
||||||
s.lossHistory = Array.from(result.lossHistory);
|
|
||||||
}));
|
|
||||||
coreBus.emit('ml.trained', { loss: result.loss });
|
|
||||||
this.scheduleSave_();
|
|
||||||
return result.loss;
|
|
||||||
} finally {
|
|
||||||
mlStore.__setState(produce((s) => { s.training = false; }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Non-destructive loss query. */
|
|
||||||
evalLoss(): number {
|
|
||||||
return this.module._nisps_ml_eval_loss(this.mlHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearExamples(): void {
|
|
||||||
this.dataset.clear();
|
|
||||||
this.module._nisps_ml_clear_examples(this.mlHandle);
|
|
||||||
mlStore.__setState(produce((s) => { s.exampleCount = 0; }));
|
|
||||||
coreBus.emit('ml.examples_cleared', undefined);
|
|
||||||
this.scheduleSave_();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// RL ops
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
randomiseWeights(spread = 0.6): void {
|
|
||||||
this.module._nisps_ml_draw_weights(this.mlHandle, spread);
|
|
||||||
this.publishWeights_();
|
|
||||||
coreBus.emit('ml.delta_update', { reason: 'randomize' });
|
|
||||||
this.scheduleSave_();
|
|
||||||
}
|
|
||||||
|
|
||||||
moveWeights(speed: number, spread: number, pinMask?: Uint8Array): void {
|
|
||||||
let maskPtr = 0;
|
|
||||||
if (pinMask) {
|
|
||||||
const sz = Math.min(pinMask.length, this.arch_.outputSize);
|
|
||||||
for (let i = 0; i < sz; ++i) this.pinMaskBuf.view[i] = pinMask[i];
|
|
||||||
for (let i = sz; i < this.arch_.outputSize; ++i) this.pinMaskBuf.view[i] = 0;
|
|
||||||
maskPtr = this.pinMaskBuf.ptr;
|
|
||||||
}
|
|
||||||
this.module._nisps_ml_move_weights(this.mlHandle, speed, spread, maskPtr);
|
|
||||||
this.publishWeights_();
|
|
||||||
// The caller (RL handler) decides whether this is a thumbs-up/down;
|
|
||||||
// we emit a generic delta_update.
|
|
||||||
coreBus.emit('ml.delta_update', { reason: 'thumbs_down' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Weights I/O
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
getWeights(): Float32Array {
|
|
||||||
this.module._nisps_ml_get_weights(this.mlHandle, this.weightsBuf.ptr);
|
|
||||||
// Copy out so caller can mutate freely.
|
|
||||||
return new Float32Array(this.weightsBuf.view);
|
|
||||||
}
|
|
||||||
|
|
||||||
setWeights(w: Float32Array | Uint8Array): void {
|
|
||||||
if (w.length < this.weightCount_) {
|
|
||||||
throw new Error(`setWeights: expected ${this.weightCount_} floats, got ${w.length}`);
|
|
||||||
}
|
|
||||||
this.weightsBuf.view.set(w as Float32Array, 0);
|
|
||||||
this.module._nisps_ml_set_weights(this.mlHandle, this.weightsBuf.ptr);
|
|
||||||
this.publishWeights_();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Per-layer weight stats. Returns one record per layer. */
|
|
||||||
getLayerStats(): LayerStats[] {
|
|
||||||
this.module._nisps_ml_get_layer_stats(this.mlHandle, this.statsBuf.ptr);
|
|
||||||
const out: LayerStats[] = [];
|
|
||||||
for (let i = 0; i < this.arch_.numLayers; ++i) {
|
|
||||||
const base = i * 4;
|
|
||||||
out.push({
|
|
||||||
meanAbs: this.statsBuf.view[base],
|
|
||||||
maxAbs: this.statsBuf.view[base + 1],
|
|
||||||
deadFrac: this.statsBuf.view[base + 2],
|
|
||||||
saturatingFrac: this.statsBuf.view[base + 3],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Flat layer-stats Float32Array (numLayers * 4). For probe API. */
|
|
||||||
getLayerStatsFlat(): Float32Array {
|
|
||||||
this.module._nisps_ml_get_layer_stats(this.mlHandle, this.statsBuf.ptr);
|
|
||||||
return new Float32Array(this.statsBuf.view);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Misc
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.module._nisps_ml_reset(this.mlHandle);
|
|
||||||
this.dataset.clear();
|
|
||||||
this.lastLoss_ = null;
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.exampleCount = 0;
|
|
||||||
s.lastLoss = null;
|
|
||||||
s.lossHistory = [];
|
|
||||||
}));
|
|
||||||
this.publishWeights_();
|
|
||||||
coreBus.emit('ml.examples_cleared', undefined);
|
|
||||||
this.scheduleSave_();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Persistence
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
private scheduleSave_(): void {
|
|
||||||
if (this.saveTimer !== null) clearTimeout(this.saveTimer);
|
|
||||||
this.saveTimer = window.setTimeout(() => this.saveNow(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveNow(): void {
|
|
||||||
if (this.destroyed) return;
|
|
||||||
if (this.saveTimer !== null) {
|
|
||||||
clearTimeout(this.saveTimer);
|
|
||||||
this.saveTimer = null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const weights = this.getWeights();
|
|
||||||
const payload = {
|
|
||||||
v: 1,
|
|
||||||
arch: this.arch_,
|
|
||||||
weights: Array.from(weights),
|
|
||||||
features: Array.from(this.dataset.featuresFlat()),
|
|
||||||
labels: Array.from(this.dataset.labelsFlat()),
|
|
||||||
size: this.dataset.size,
|
|
||||||
lastLoss: this.lastLoss_,
|
|
||||||
};
|
|
||||||
localStorage.setItem(this.storageKey, JSON.stringify(payload));
|
|
||||||
} catch (err) {
|
|
||||||
// localStorage might be full or unavailable; not fatal.
|
|
||||||
console.warn('[wasm-iml] saveNow failed:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryLoadFromStorage_(): void {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(this.storageKey);
|
|
||||||
if (!raw) return;
|
|
||||||
const payload = JSON.parse(raw) as {
|
|
||||||
v: number;
|
|
||||||
weights: number[];
|
|
||||||
features: number[];
|
|
||||||
labels: number[];
|
|
||||||
size: number;
|
|
||||||
lastLoss: number | null;
|
|
||||||
};
|
|
||||||
if (payload.v !== 1) return;
|
|
||||||
// Restore dataset (rebuild via add()).
|
|
||||||
const inSz = this.arch_.inputSize;
|
|
||||||
const outSz = this.arch_.outputSize;
|
|
||||||
if (payload.size > 0 && payload.features.length === payload.size * inSz &&
|
|
||||||
payload.labels.length === payload.size * outSz) {
|
|
||||||
for (let i = 0; i < payload.size; ++i) {
|
|
||||||
const f = payload.features.slice(i * inSz, (i + 1) * inSz);
|
|
||||||
const l = payload.labels.slice(i * outSz, (i + 1) * outSz);
|
|
||||||
this.dataset.add(f, l);
|
|
||||||
// Also push to the WASM ring buffer.
|
|
||||||
this.copyExampleToWasm_(f, l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Restore weights.
|
|
||||||
if (payload.weights.length === this.weightCount_) {
|
|
||||||
this.setWeights(new Float32Array(payload.weights));
|
|
||||||
}
|
|
||||||
this.lastLoss_ = payload.lastLoss;
|
|
||||||
mlStore.__setState(produce((s) => {
|
|
||||||
s.exampleCount = this.dataset.size;
|
|
||||||
s.lastLoss = this.lastLoss_;
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[wasm-iml] tryLoadFromStorage failed:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// -------------------------------------------------------------------
|
|
||||||
|
|
||||||
private publishWeights_(): void {
|
|
||||||
const w = this.getWeights();
|
|
||||||
mlStore.__setWeights(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
/**
|
|
||||||
* Disposable Web Worker that runs SGD off the main thread.
|
|
||||||
*
|
|
||||||
* The worker holds its own `nisps.wasm` instance (architecture.md §6.4).
|
|
||||||
* The main thread sends:
|
|
||||||
* - current weights (so the worker is in the same state as the UI),
|
|
||||||
* - dataset (features + labels),
|
|
||||||
* - SGD hyperparameters,
|
|
||||||
* and receives updated weights + final loss.
|
|
||||||
*
|
|
||||||
* This module exposes:
|
|
||||||
* - `createTrainer()` - factory that spawns the worker, loads its WASM,
|
|
||||||
* and returns a `WasmTrainer` handle.
|
|
||||||
* - The worker entry-point itself (when this file runs in a Worker).
|
|
||||||
*
|
|
||||||
* The worker is implemented inline so a single TS file becomes both the
|
|
||||||
* main-thread API and the worker bundle. Vite's `new Worker(new URL(...,
|
|
||||||
* import.meta.url))` pattern packs it correctly.
|
|
||||||
*
|
|
||||||
* Lifecycle: each `WasmTrainer` is disposable via `.dispose()` which
|
|
||||||
* terminates the worker. Tests should always dispose to avoid leaks.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { NispsModule, NispsModuleFactory, WorkerRequest, WorkerResponse } from './types';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Main-thread side
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export interface TrainArgs {
|
|
||||||
weights: Float32Array;
|
|
||||||
features: Float32Array;
|
|
||||||
labels: Float32Array;
|
|
||||||
/** Optional; pass empty for uniform weighting. */
|
|
||||||
sampleWeights: Float32Array;
|
|
||||||
lr: number;
|
|
||||||
maxIter: number;
|
|
||||||
minErr: number;
|
|
||||||
inputSize: number;
|
|
||||||
outputSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrainResult {
|
|
||||||
loss: number;
|
|
||||||
weights: Float32Array;
|
|
||||||
lossHistory: Float32Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WasmTrainer {
|
|
||||||
private worker: Worker;
|
|
||||||
private nextId = 1;
|
|
||||||
private pending = new Map<number, { resolve: (r: TrainResult) => void; reject: (e: unknown) => void }>();
|
|
||||||
private disposed = false;
|
|
||||||
|
|
||||||
static async create(): Promise<WasmTrainer> {
|
|
||||||
const trainer = new WasmTrainer();
|
|
||||||
await trainer.init_();
|
|
||||||
return trainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.worker = new Worker(new URL('./wasm-worker.ts', import.meta.url), { type: 'module' });
|
|
||||||
this.worker.onmessage = (ev) => this.onMessage_(ev.data as WorkerResponse);
|
|
||||||
this.worker.onerror = (ev) => {
|
|
||||||
// Fail any pending requests.
|
|
||||||
for (const { reject } of this.pending.values()) reject(ev.message ?? 'worker error');
|
|
||||||
this.pending.clear();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private init_(): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const handler = (ev: MessageEvent) => {
|
|
||||||
const msg = ev.data as WorkerResponse;
|
|
||||||
if (msg.kind === 'ready') {
|
|
||||||
this.worker.removeEventListener('message', handler);
|
|
||||||
resolve();
|
|
||||||
} else if (msg.kind === 'error') {
|
|
||||||
this.worker.removeEventListener('message', handler);
|
|
||||||
reject(new Error(msg.message));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.worker.addEventListener('message', handler);
|
|
||||||
const seed = (Date.now() ^ Math.floor(Math.random() * 0xffffffff)) >>> 0;
|
|
||||||
this.worker.postMessage({ kind: 'init', seed } satisfies WorkerRequest);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
train(args: TrainArgs): Promise<TrainResult> {
|
|
||||||
if (this.disposed) return Promise.reject(new Error('WasmTrainer disposed'));
|
|
||||||
const requestId = this.nextId++;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.pending.set(requestId, { resolve, reject });
|
|
||||||
const msg: WorkerRequest = {
|
|
||||||
kind: 'train',
|
|
||||||
requestId,
|
|
||||||
weights: args.weights,
|
|
||||||
features: args.features,
|
|
||||||
labels: args.labels,
|
|
||||||
sampleWeights: args.sampleWeights,
|
|
||||||
lr: args.lr,
|
|
||||||
maxIter: args.maxIter,
|
|
||||||
minErr: args.minErr,
|
|
||||||
inputSize: args.inputSize,
|
|
||||||
outputSize: args.outputSize,
|
|
||||||
};
|
|
||||||
// Transfer all the typed-array buffers we no longer need on the
|
|
||||||
// main thread; faster than copying. Caller has already cloned.
|
|
||||||
this.worker.postMessage(msg, [
|
|
||||||
args.weights.buffer,
|
|
||||||
args.features.buffer,
|
|
||||||
args.labels.buffer,
|
|
||||||
args.sampleWeights.buffer,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
if (this.disposed) return;
|
|
||||||
this.disposed = true;
|
|
||||||
try {
|
|
||||||
this.worker.postMessage({ kind: 'dispose' } satisfies WorkerRequest);
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
this.worker.terminate();
|
|
||||||
for (const { reject } of this.pending.values()) reject(new Error('disposed'));
|
|
||||||
this.pending.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMessage_(msg: WorkerResponse): void {
|
|
||||||
if (msg.kind === 'result') {
|
|
||||||
const p = this.pending.get(msg.requestId);
|
|
||||||
if (p) {
|
|
||||||
this.pending.delete(msg.requestId);
|
|
||||||
p.resolve({ loss: msg.loss, weights: msg.weights, lossHistory: msg.lossHistory });
|
|
||||||
}
|
|
||||||
} else if (msg.kind === 'error') {
|
|
||||||
const p = this.pending.get(msg.requestId);
|
|
||||||
if (p) {
|
|
||||||
this.pending.delete(msg.requestId);
|
|
||||||
p.reject(new Error(msg.message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 'ready' handled in init_().
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTrainer(): Promise<WasmTrainer> {
|
|
||||||
return WasmTrainer.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Worker-thread side
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// When this module is loaded in a Worker, `self` is `WorkerGlobalScope` and
|
|
||||||
// `window` is undefined. We use that as the dispatch.
|
|
||||||
//
|
|
||||||
// We import the wasm via a same-origin fetch (no DOM available so we can't
|
|
||||||
// use a regular import URL — but Vite's bundler treats `new Worker(...)`
|
|
||||||
// specially and `?url` imports work for assets).
|
|
||||||
|
|
||||||
// Inside a Worker, `self` is a `DedicatedWorkerGlobalScope`. To keep TS
|
|
||||||
// happy in both build contexts we use a structural cast.
|
|
||||||
declare const self: {
|
|
||||||
postMessage: (msg: unknown, transfer?: Transferable[]) => void;
|
|
||||||
addEventListener: (event: string, handler: (ev: MessageEvent) => void) => void;
|
|
||||||
location: { origin: string };
|
|
||||||
importScripts?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWorker =
|
|
||||||
typeof window === 'undefined' &&
|
|
||||||
typeof self !== 'undefined' &&
|
|
||||||
typeof (self as { importScripts?: unknown }).importScripts !== 'undefined';
|
|
||||||
|
|
||||||
if (isWorker) {
|
|
||||||
// Module-level state in worker scope.
|
|
||||||
let mod: NispsModule | null = null;
|
|
||||||
let mlHandle = 0;
|
|
||||||
let weightCount = 0;
|
|
||||||
|
|
||||||
// Heap buffers (allocated on first train).
|
|
||||||
let weightsPtr = 0;
|
|
||||||
let weightsViewLen = 0;
|
|
||||||
let featuresPtr = 0;
|
|
||||||
let featuresLen = 0;
|
|
||||||
let labelsPtr = 0;
|
|
||||||
let labelsLen = 0;
|
|
||||||
let sampleWeightsPtr = 0;
|
|
||||||
let sampleWeightsLen = 0;
|
|
||||||
|
|
||||||
async function loadModule(seed: number): Promise<void> {
|
|
||||||
// Same-origin fetch to /nisps.js. The worker is served by the dev
|
|
||||||
// server with COOP/COEP set, so this works.
|
|
||||||
const factoryMod = await import(/* @vite-ignore */ new URL('/nisps.js', self.location.origin).toString());
|
|
||||||
const factory: NispsModuleFactory =
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(factoryMod as any).default ?? (factoryMod as any).createNispsModule;
|
|
||||||
mod = await factory({
|
|
||||||
locateFile: (path: string) => {
|
|
||||||
if (path.endsWith('.wasm')) return new URL('/nisps.wasm', self.location.origin).toString();
|
|
||||||
return path;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
mlHandle = mod._nisps_ml_create(0, 0, 0, 0, seed >>> 0);
|
|
||||||
weightCount = mod._nisps_ml_weight_count(mlHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureBuffers(features: Float32Array, labels: Float32Array, sampleWeights: Float32Array, weights: Float32Array): void {
|
|
||||||
if (!mod) throw new Error('worker module not loaded');
|
|
||||||
|
|
||||||
if (weightsViewLen !== weightCount) {
|
|
||||||
if (weightsPtr) mod._free(weightsPtr);
|
|
||||||
weightsPtr = mod._malloc(weightCount * 4);
|
|
||||||
weightsViewLen = weightCount;
|
|
||||||
}
|
|
||||||
if (features.length !== featuresLen) {
|
|
||||||
if (featuresPtr) mod._free(featuresPtr);
|
|
||||||
featuresPtr = mod._malloc(features.length * 4);
|
|
||||||
featuresLen = features.length;
|
|
||||||
}
|
|
||||||
if (labels.length !== labelsLen) {
|
|
||||||
if (labelsPtr) mod._free(labelsPtr);
|
|
||||||
labelsPtr = mod._malloc(labels.length * 4);
|
|
||||||
labelsLen = labels.length;
|
|
||||||
}
|
|
||||||
if (sampleWeights.length !== sampleWeightsLen) {
|
|
||||||
if (sampleWeightsPtr) mod._free(sampleWeightsPtr);
|
|
||||||
sampleWeightsPtr = sampleWeights.length > 0 ? mod._malloc(sampleWeights.length * 4) : 0;
|
|
||||||
sampleWeightsLen = sampleWeights.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
new Float32Array(mod.HEAPF32.buffer, weightsPtr, weightCount).set(weights);
|
|
||||||
new Float32Array(mod.HEAPF32.buffer, featuresPtr, features.length).set(features);
|
|
||||||
new Float32Array(mod.HEAPF32.buffer, labelsPtr, labels.length).set(labels);
|
|
||||||
if (sampleWeightsPtr) {
|
|
||||||
new Float32Array(mod.HEAPF32.buffer, sampleWeightsPtr, sampleWeights.length).set(sampleWeights);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function trainOnce(req: Extract<WorkerRequest, { kind: 'train' }>): WorkerResponse {
|
|
||||||
if (!mod) {
|
|
||||||
return { kind: 'error', requestId: req.requestId, message: 'worker not initialised' };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ensureBuffers(req.features, req.labels, req.sampleWeights, req.weights);
|
|
||||||
// Push current weights into our MLP.
|
|
||||||
mod._nisps_ml_set_weights(mlHandle, weightsPtr);
|
|
||||||
|
|
||||||
// Seed the example ring buffer. We must clear first because past
|
|
||||||
// train calls may have left examples there.
|
|
||||||
mod._nisps_ml_clear_examples(mlHandle);
|
|
||||||
const inSz = req.inputSize;
|
|
||||||
const outSz = req.outputSize;
|
|
||||||
const n = req.features.length / inSz;
|
|
||||||
// Allocate small per-example scratch (re-used across iterations of
|
|
||||||
// this loop).
|
|
||||||
// We use stack-equivalents by allocating once, then shifting pointers.
|
|
||||||
for (let i = 0; i < n; ++i) {
|
|
||||||
const fPtr = featuresPtr + i * inSz * 4;
|
|
||||||
const lPtr = labelsPtr + i * outSz * 4;
|
|
||||||
mod._nisps_ml_add_example(mlHandle, fPtr, lPtr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run training.
|
|
||||||
const swPtr = req.sampleWeights.length > 0 ? sampleWeightsPtr : 0;
|
|
||||||
const loss = mod._nisps_ml_train(mlHandle, req.lr, req.maxIter, req.minErr, swPtr);
|
|
||||||
|
|
||||||
// Read out final weights.
|
|
||||||
mod._nisps_ml_get_weights(mlHandle, weightsPtr);
|
|
||||||
const view = new Float32Array(mod.HEAPF32.buffer, weightsPtr, weightCount);
|
|
||||||
const outWeights = new Float32Array(view); // copy
|
|
||||||
|
|
||||||
// Loss history not yet plumbed via WASM; emit just final loss.
|
|
||||||
const lossHistory = new Float32Array([loss]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
kind: 'result',
|
|
||||||
requestId: req.requestId,
|
|
||||||
loss,
|
|
||||||
weights: outWeights,
|
|
||||||
lossHistory,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
kind: 'error',
|
|
||||||
requestId: req.requestId,
|
|
||||||
message: err instanceof Error ? err.message : String(err),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disposeModule(): void {
|
|
||||||
if (!mod) return;
|
|
||||||
if (mlHandle) {
|
|
||||||
mod._nisps_ml_destroy(mlHandle);
|
|
||||||
mlHandle = 0;
|
|
||||||
}
|
|
||||||
if (weightsPtr) { mod._free(weightsPtr); weightsPtr = 0; }
|
|
||||||
if (featuresPtr) { mod._free(featuresPtr); featuresPtr = 0; }
|
|
||||||
if (labelsPtr) { mod._free(labelsPtr); labelsPtr = 0; }
|
|
||||||
if (sampleWeightsPtr) { mod._free(sampleWeightsPtr); sampleWeightsPtr = 0; }
|
|
||||||
mod = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener('message', async (ev: MessageEvent<WorkerRequest>) => {
|
|
||||||
const req = ev.data;
|
|
||||||
if (req.kind === 'init') {
|
|
||||||
try {
|
|
||||||
await loadModule(req.seed);
|
|
||||||
self.postMessage({ kind: 'ready' } satisfies WorkerResponse);
|
|
||||||
} catch (err) {
|
|
||||||
self.postMessage({
|
|
||||||
kind: 'error',
|
|
||||||
requestId: 0,
|
|
||||||
message: err instanceof Error ? err.message : String(err),
|
|
||||||
} satisfies WorkerResponse);
|
|
||||||
}
|
|
||||||
} else if (req.kind === 'train') {
|
|
||||||
const res = trainOnce(req);
|
|
||||||
// Transfer weights back to main thread to avoid copy.
|
|
||||||
if (res.kind === 'result') {
|
|
||||||
self.postMessage(res, [res.weights.buffer, res.lossHistory.buffer]);
|
|
||||||
} else {
|
|
||||||
self.postMessage(res);
|
|
||||||
}
|
|
||||||
} else if (req.kind === 'dispose') {
|
|
||||||
disposeModule();
|
|
||||||
// Worker terminates from main thread side via .terminate().
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* BreakOrMode — drum/breakbeat synthesis (xy_pad input, 56 outputs).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component } 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 { BreakorSchema } from './generated/breakor_schema';
|
|
||||||
|
|
||||||
export const BreakOrMode: Component = () => {
|
|
||||||
const schema = BreakorSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
drawerTitle="Breakor settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<XYPad
|
|
||||||
size={280}
|
|
||||||
ariaLabel="Breakor pad"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Drag through drum space.
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="var(--good)"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BreakOrMode;
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
/**
|
|
||||||
* C15Mode — browser-only stub.
|
|
||||||
*
|
|
||||||
* The C15 (Nonlinear Labs C15) WASM synthesizer is not yet ported into the
|
|
||||||
* SolidJS playground; only firmware modes have schemas + WASM engines so
|
|
||||||
* far. This file exists so the mode switcher can list all eight firmware
|
|
||||||
* modes plus a "C15" entry that surfaces a clear "TODO" rather than 404'ing.
|
|
||||||
*
|
|
||||||
* Stream 11+ is expected to add a `c15` schema and wire the existing
|
|
||||||
* `playground/c15` directory into `nisps/wasm/engines/`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component } from 'solid-js';
|
|
||||||
|
|
||||||
export const C15Mode: Component = () => {
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
'flex-direction': 'column',
|
|
||||||
'align-items': 'center',
|
|
||||||
gap: 'var(--sp-4)',
|
|
||||||
padding: 'var(--sp-6)',
|
|
||||||
margin: 'var(--sp-5) auto',
|
|
||||||
'max-width': '720px',
|
|
||||||
'background': 'var(--bg-1)',
|
|
||||||
border: '1px dashed var(--line-strong)',
|
|
||||||
'border-radius': 'var(--r-3)',
|
|
||||||
color: 'var(--fg)',
|
|
||||||
'text-align': 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 style={{ color: 'var(--accent-2)', margin: 0 }}>C15 mode — TODO</h2>
|
|
||||||
<p style={{ color: 'var(--fg-mute)', margin: 0 }}>
|
|
||||||
The C15 WASM synthesizer hasn't been ported into the SolidJS rewrite
|
|
||||||
yet. The legacy bridge lives in <code>playground/c15/</code> and
|
|
||||||
<code>playground/js/synth/c15-bridge.js</code>; stream 11+ will wrap
|
|
||||||
it as an AudioWorklet engine alongside the firmware-derived ones.
|
|
||||||
</p>
|
|
||||||
<p style={{ color: 'var(--fg-dim)', 'font-size': 'var(--fs-sm)', margin: 0 }}>
|
|
||||||
Until then, pick one of the firmware modes (PAFSynth, Channel Strip,
|
|
||||||
Verb FX, etc.) from the mode switcher above.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default C15Mode;
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
/**
|
|
||||||
* ChannelStripMode — channel-strip processor controlled by joystick.
|
|
||||||
*
|
|
||||||
* 24 outputs feed EQ / dynamics / gain. Settings drawer is the default
|
|
||||||
* SettingsDrawer (input/training/exploration/output/overrides/advanced).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component } from 'solid-js';
|
|
||||||
import { ModeShell } from './ModeShell';
|
|
||||||
import { useModeRuntime } from './mode-runtime';
|
|
||||||
import { VirtualJoystick } from '../primitives/VirtualJoystick';
|
|
||||||
import { OutputDisplay } from '../primitives/OutputDisplay';
|
|
||||||
import { LossPlot } from '../primitives/LossPlot';
|
|
||||||
import { ChannelStripSchema } from './generated/channel_strip_schema';
|
|
||||||
|
|
||||||
export const ChannelStripMode: Component = () => {
|
|
||||||
const schema = ChannelStripSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
drawerTitle="Channel strip settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<VirtualJoystick
|
|
||||||
size={260}
|
|
||||||
ariaLabel="Channel strip joystick"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Mix EQ + dynamics by moving the joystick.
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="var(--accent-3)"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChannelStripMode;
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* ElysiamorfMode — granular / morphing synth (xy_pad input, 40 outputs).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component } 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 { ElysiamorfSchema } from './generated/elysiamorf_schema';
|
|
||||||
|
|
||||||
export const ElysiamorfMode: Component = () => {
|
|
||||||
const schema = ElysiamorfSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
drawerTitle="Elysiamorf settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<XYPad
|
|
||||||
size={280}
|
|
||||||
ariaLabel="Elysiamorf pad"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Drag to morph through grain space.
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="#ffb060"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ElysiamorfMode;
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* MEMLCeliumMode — uSEQ-Celium dual-MLP CV/gate output (56 outputs).
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 { MemlceliumSchema } from './generated/memlcelium_schema';
|
|
||||||
|
|
||||||
export const MEMLCeliumMode: Component = () => {
|
|
||||||
const schema = MemlceliumSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
activeVoiceSpace={voiceSpace}
|
|
||||||
onVoiceSpaceChange={setVoiceSpace}
|
|
||||||
drawerTitle="MEMLCelium settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<XYPad
|
|
||||||
size={280}
|
|
||||||
ariaLabel="MEMLCelium pad"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Sculpt voice + CV/gate. CV/gate streaming over USB serial requires
|
|
||||||
firmware (browser preview only).
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="#5b9eef"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MEMLCeliumMode;
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
.shell {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr auto auto;
|
|
||||||
gap: var(--sp-4);
|
|
||||||
min-height: 100%;
|
|
||||||
padding: var(--sp-4);
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding-bottom: var(--sp-3);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--fs-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audioToggle {
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audioBtn {
|
|
||||||
background: var(--bg-2);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: var(--sp-2) var(--sp-3);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
color: var(--fg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audioBtn.on {
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audioBtn:hover {
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voiceSpaces {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voiceSpacesLabel {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(320px, 1fr) minmax(280px, 380px);
|
|
||||||
gap: var(--sp-4);
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.body {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.primaryArea {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-4);
|
|
||||||
background: var(--bg-1);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-2);
|
|
||||||
min-height: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.outputArea {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-3);
|
|
||||||
background: var(--bg-1);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-2);
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-4);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: var(--sp-3);
|
|
||||||
background: var(--bg-1);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.controlAxes {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
||||||
gap: var(--sp-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trainingPanel {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerToggleBar {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerToggleBtn {
|
|
||||||
background: var(--bg-2);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: var(--sp-2) var(--sp-3);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerToggleBtn:hover {
|
|
||||||
color: var(--fg);
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusLine {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-dim);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.frozen {
|
|
||||||
color: var(--accent-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notReady {
|
|
||||||
color: var(--warn);
|
|
||||||
}
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
/**
|
|
||||||
* ModeShell — common scaffolding shared by every concrete mode.
|
|
||||||
*
|
|
||||||
* Provides:
|
|
||||||
* - Header (mode name, voice space pill, audio start/stop, drawer button).
|
|
||||||
* - Primary input area (mode-supplied) + JoyMap navigator on its right.
|
|
||||||
* - Output area (mode-supplied).
|
|
||||||
* - Control axes bar (Boldness/Memory/Precision wired to controlStore).
|
|
||||||
* - Training controls — undo, A/B, region/param pin all wired through.
|
|
||||||
* - SettingsDrawer (collapsible sections, per-param overrides, advanced).
|
|
||||||
*
|
|
||||||
* Modes only have to author the primary input + output JSX. Everything
|
|
||||||
* else is owned here so behaviour stays consistent across modes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component, createSignal, JSX, Show, For, createMemo } from 'solid-js';
|
|
||||||
import { TrainingControls } from '../primitives/TrainingControls';
|
|
||||||
import { ControlAxis } from '../primitives/ControlAxis';
|
|
||||||
import { PillToggle } from '../primitives/PillToggle';
|
|
||||||
import { Drawer } from '../primitives/Drawer';
|
|
||||||
import { JoyMap } from '../primitives/JoyMap';
|
|
||||||
import { controlStore } from '../stores/control-store';
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
import { sessionStore } from '../stores/session-store';
|
|
||||||
import { explorationStore } from '../stores/exploration-store';
|
|
||||||
import type { ModeRuntime } from './mode-runtime';
|
|
||||||
import type { ModeSchema } from './generated';
|
|
||||||
import { SettingsDrawer } from './SettingsDrawer';
|
|
||||||
import styles from './ModeShell.module.css';
|
|
||||||
|
|
||||||
export interface ModeShellProps {
|
|
||||||
schema: ModeSchema;
|
|
||||||
runtime: ModeRuntime;
|
|
||||||
/** Primary input renderer (joystick / xy-pad / etc.). */
|
|
||||||
primaryInput: () => JSX.Element;
|
|
||||||
/** Output / visualisation area. */
|
|
||||||
outputArea: () => JSX.Element;
|
|
||||||
/** Optional drawer body for mode-specific settings (replaces default). */
|
|
||||||
drawerContent?: () => JSX.Element;
|
|
||||||
drawerTitle?: string;
|
|
||||||
/** Active voice space index (only used if schema.voice_spaces is non-empty). */
|
|
||||||
activeVoiceSpace?: () => number;
|
|
||||||
onVoiceSpaceChange?: (idx: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModeShell: Component<ModeShellProps> = (props) => {
|
|
||||||
const [drawerOpen, setDrawerOpen] = createSignal(false);
|
|
||||||
const [snapshotPopupOpen, setSnapshotPopupOpen] = createSignal(false);
|
|
||||||
const showVoiceSpaces = () =>
|
|
||||||
props.schema.ui.show_voice_space_selector && props.schema.voice_spaces.length > 0;
|
|
||||||
|
|
||||||
const voiceSpaceOptions = () =>
|
|
||||||
props.schema.voice_spaces.map((label, idx) => ({
|
|
||||||
value: String(idx),
|
|
||||||
label,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const noiseRings = createMemo(() => {
|
|
||||||
// Outer = noiseCap, inner = noiseLevel — both as fractions of half-window.
|
|
||||||
const cap = explorationStore.state.noiseCap;
|
|
||||||
const cur = explorationStore.state.noiseLevel;
|
|
||||||
if (cap <= 0) return null;
|
|
||||||
return [cap * 0.4, cur * 0.4] as const;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section class={styles.shell} aria-label={`${props.schema.mode_id} mode`}>
|
|
||||||
<header class={styles.header}>
|
|
||||||
<div>
|
|
||||||
<h2 class={styles.title}>{formatModeName(props.schema.mode_id)}</h2>
|
|
||||||
<p class={styles.subtitle}>
|
|
||||||
{props.schema.ml.input_size} in → {props.schema.ml.output_size} out
|
|
||||||
{' · '}
|
|
||||||
engine: <code>{props.schema.engine_id}</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={showVoiceSpaces()}>
|
|
||||||
<div class={styles.voiceSpaces}>
|
|
||||||
<span class={styles.voiceSpacesLabel}>Voice space</span>
|
|
||||||
<PillToggle
|
|
||||||
options={voiceSpaceOptions()}
|
|
||||||
value={() => String(props.activeVoiceSpace?.() ?? 0)}
|
|
||||||
onChange={(v) => props.onVoiceSpaceChange?.(Number(v))}
|
|
||||||
ariaLabel="Voice space"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div class={styles.audioToggle}>
|
|
||||||
<Show
|
|
||||||
when={props.schema.ui.primary_input === 'audio_in'}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={props.runtime.mic.started()}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.audioBtn}
|
|
||||||
onClick={() => void props.runtime.mic.start()}
|
|
||||||
aria-label="Enable microphone"
|
|
||||||
>🎤 Mic</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`${styles.audioBtn} ${styles.on}`}
|
|
||||||
onClick={() => void props.runtime.mic.stop()}
|
|
||||||
aria-label="Disable microphone"
|
|
||||||
>🎤 Mic on</button>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
<Show
|
|
||||||
when={props.runtime.audio.started()}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.audioBtn}
|
|
||||||
onClick={() => void props.runtime.audio.start()}
|
|
||||||
aria-label="Start audio engine"
|
|
||||||
>▶ Start audio</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={`${styles.audioBtn} ${styles.on}`}
|
|
||||||
onClick={() => void props.runtime.audio.stop()}
|
|
||||||
aria-label="Stop audio engine"
|
|
||||||
>⏹ Stop audio</button>
|
|
||||||
</Show>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.drawerToggleBtn}
|
|
||||||
onClick={() => setDrawerOpen(true)}
|
|
||||||
aria-label="Open settings drawer"
|
|
||||||
>⚙</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class={styles.body}>
|
|
||||||
<div class={styles.primaryArea}>
|
|
||||||
{props.primaryInput()}
|
|
||||||
<JoyMap
|
|
||||||
size={150}
|
|
||||||
zoom={() => inputStore.config.zoom}
|
|
||||||
anchor={() => [
|
|
||||||
inputStore.config.anchorMode === 'center' ? 0.5 : inputStore.config.anchorX,
|
|
||||||
inputStore.config.anchorMode === 'center' ? 0.5 : inputStore.config.anchorY,
|
|
||||||
]}
|
|
||||||
position={props.runtime.pipedInput}
|
|
||||||
trail={props.runtime.trail}
|
|
||||||
regionPins={() => sessionStore.state.regionPins}
|
|
||||||
noiseRings={noiseRings as () => readonly [number, number] | null}
|
|
||||||
frozen={props.runtime.frozen}
|
|
||||||
onTrailTap={(p) => props.runtime.snapToTrail(p)}
|
|
||||||
onLongPress={() => props.runtime.pinCurrentRegion()}
|
|
||||||
ariaLabel="Joy map navigator"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class={styles.outputArea}>{props.outputArea()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={styles.controls}>
|
|
||||||
<div class={styles.controlAxes}>
|
|
||||||
<For each={AXES}>
|
|
||||||
{(axis) => (
|
|
||||||
<ControlAxis
|
|
||||||
label={axis.label}
|
|
||||||
endpoints={axis.endpoints}
|
|
||||||
value={() => controlStore.state[axis.key]}
|
|
||||||
onChange={(v) => controlStore.setAxis(axis.key, v)}
|
|
||||||
preset={() => controlStore.state.presetId}
|
|
||||||
onDoubleTap={() => controlStore.clearOffsets(axis.key)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div class={styles.trainingPanel}>
|
|
||||||
<TrainingControls
|
|
||||||
onTrain={() => props.runtime.trainOnCurrent()}
|
|
||||||
onRandomize={() => props.runtime.randomize()}
|
|
||||||
onThumbsUp={() => props.runtime.thumbsUp()}
|
|
||||||
onThumbsDown={() => props.runtime.thumbsDown()}
|
|
||||||
onUndo={() => {
|
|
||||||
// Click = pop one snapshot. Long-press = open list popup.
|
|
||||||
props.runtime.undo();
|
|
||||||
}}
|
|
||||||
exampleCount={() => props.runtime.training.examples()}
|
|
||||||
lastLoss={() => props.runtime.training.lastLoss()}
|
|
||||||
busy={() => props.runtime.training.busy()}
|
|
||||||
canUndo={() => props.runtime.canUndo()}
|
|
||||||
/>
|
|
||||||
<Show when={sessionStore.state.snapshots.length > 0}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.drawerToggleBtn}
|
|
||||||
onClick={() => setSnapshotPopupOpen((b) => !b)}
|
|
||||||
aria-label="Show snapshot history"
|
|
||||||
title={`History (${sessionStore.state.snapshots.length})`}
|
|
||||||
style={{ 'margin-top': 'var(--sp-1)' }}
|
|
||||||
>📚 {sessionStore.state.snapshots.length}</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class={styles.statusLine}>
|
|
||||||
<Show when={!props.runtime.ready()}>
|
|
||||||
<span class={styles.notReady}>Loading WASM ML…</span>{' '}
|
|
||||||
</Show>
|
|
||||||
<Show when={props.runtime.frozen()}>
|
|
||||||
<span class={styles.frozen}>frozen</span>{' '}
|
|
||||||
</Show>
|
|
||||||
<span>
|
|
||||||
input ({props.runtime.pipedInput()[0].toFixed(2)},
|
|
||||||
{' '}
|
|
||||||
{props.runtime.pipedInput()[1].toFixed(2)})
|
|
||||||
{' · '}
|
|
||||||
noise {explorationStore.state.noiseLevel.toFixed(3)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Drawer
|
|
||||||
open={drawerOpen()}
|
|
||||||
onClose={() => setDrawerOpen(false)}
|
|
||||||
side="right"
|
|
||||||
title={props.drawerTitle ?? 'Mode settings'}
|
|
||||||
width={460}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={props.drawerContent}
|
|
||||||
fallback={
|
|
||||||
<SettingsDrawer schema={props.schema} runtime={props.runtime} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.drawerContent!()}
|
|
||||||
</Show>
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
{/* Snapshot list popup (long-press undo equivalent) */}
|
|
||||||
<Drawer
|
|
||||||
open={snapshotPopupOpen()}
|
|
||||||
onClose={() => setSnapshotPopupOpen(false)}
|
|
||||||
side="right"
|
|
||||||
title="Snapshot history"
|
|
||||||
width={320}
|
|
||||||
>
|
|
||||||
<ul style={{ 'list-style': 'none', padding: 0, margin: 0, display: 'flex', 'flex-direction': 'column', gap: 'var(--sp-1)' }}>
|
|
||||||
<For each={sessionStore.listSnapshots().slice().reverse()}>{(s) => (
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
const map = sessionStore.state.snapshots;
|
|
||||||
const idx = map.findIndex((m) => m.id === s.id);
|
|
||||||
if (idx < 0) return;
|
|
||||||
for (let i = map.length - 1; i > idx; i--) sessionStore.popSnapshot();
|
|
||||||
props.runtime.undo();
|
|
||||||
setSnapshotPopupOpen(false);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
'flex-direction': 'column',
|
|
||||||
width: '100%',
|
|
||||||
background: 'var(--bg-2)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
'border-radius': 'var(--r-1)',
|
|
||||||
padding: 'var(--sp-1) var(--sp-2)',
|
|
||||||
'text-align': 'left',
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: 'var(--fg)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ 'font-family': 'var(--font-mono)', 'font-size': 'var(--fs-sm)' }}>{s.tag}</span>
|
|
||||||
<span style={{ 'font-family': 'var(--font-mono)', 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
noise {s.noiseLevel.toFixed(3)}
|
|
||||||
{s.zoomLevel !== null ? ` · zoom ${s.zoomLevel.toFixed(2)}` : ''}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}</For>
|
|
||||||
</ul>
|
|
||||||
</Drawer>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AXES = [
|
|
||||||
{ key: 'boldness' as const, label: 'Boldness', endpoints: ['Caution', 'Bold'] as const },
|
|
||||||
{ key: 'memory' as const, label: 'Memory', endpoints: ['Amnesia', 'Elephant'] as const },
|
|
||||||
{ key: 'precision' as const, label: 'Precision', endpoints: ['Raw', 'Precise'] as const },
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatModeName(id: string): string {
|
|
||||||
return id
|
|
||||||
.split('_')
|
|
||||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModeShell;
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
.bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-2) var(--sp-4);
|
|
||||||
background: var(--bg-1);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
background: var(--bg-2);
|
|
||||||
color: var(--fg);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: var(--sp-1) var(--sp-3);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select:hover {
|
|
||||||
border-color: var(--line-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select:focus {
|
|
||||||
outline: 1px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
color: var(--warn);
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* ModeSwitcher — top-level select that picks the active mode.
|
|
||||||
*
|
|
||||||
* Writes through `modeStore.switchMode(id)` so persistence + the bus event
|
|
||||||
* fire correctly. Reads the current selection back from the store so it
|
|
||||||
* stays in sync with persisted state on first paint.
|
|
||||||
*
|
|
||||||
* Audio engine switching is handled inside the mode runtime (each mode
|
|
||||||
* routes its own engine_id through `EngineHost.setEngine` when started).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component, createMemo, For, Show } from 'solid-js';
|
|
||||||
import { modeStore } from '../stores/mode-store';
|
|
||||||
import { MODE_REGISTRY, getModeById } from './index';
|
|
||||||
import styles from './ModeSwitcher.module.css';
|
|
||||||
|
|
||||||
export const ModeSwitcher: Component = () => {
|
|
||||||
const activeId = () => modeStore.state.activeModeId ?? MODE_REGISTRY[0]!.id;
|
|
||||||
const active = createMemo(() => getModeById(activeId()));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={styles.bar} role="region" aria-label="Mode selector">
|
|
||||||
<span class={styles.label}>Mode</span>
|
|
||||||
<select
|
|
||||||
class={styles.select}
|
|
||||||
value={activeId()}
|
|
||||||
onChange={(e) => modeStore.switchMode(e.currentTarget.value)}
|
|
||||||
aria-label="Select active mode"
|
|
||||||
>
|
|
||||||
<For each={MODE_REGISTRY}>
|
|
||||||
{(m) => (
|
|
||||||
<option value={m.id}>
|
|
||||||
{m.label}
|
|
||||||
{m.placeholder ? ' (TODO)' : ''}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
<span
|
|
||||||
class={styles.description}
|
|
||||||
classList={{ [styles.placeholder]: !!active().placeholder }}
|
|
||||||
>
|
|
||||||
{active().description}
|
|
||||||
</span>
|
|
||||||
<Show when={active().placeholder}>
|
|
||||||
<span class={styles.placeholder} aria-hidden="true">
|
|
||||||
⚠
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModeSwitcher;
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/**
|
|
||||||
* PAFSynthMode — Phase Aligned Formant synth (XY pad input, 33 outputs).
|
|
||||||
*
|
|
||||||
* Uses the xy_pad as primary input (as per schema). Voice spaces are
|
|
||||||
* exposed via the shell header. A drawer renders the SliderBank for
|
|
||||||
* monitoring (and eventually editing) per-parameter values.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 { PafSynthSchema } from './generated/paf_synth_schema';
|
|
||||||
|
|
||||||
export const PAFSynthMode: Component = () => {
|
|
||||||
const schema = PafSynthSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
|
|
||||||
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
activeVoiceSpace={voiceSpace}
|
|
||||||
onVoiceSpaceChange={setVoiceSpace}
|
|
||||||
drawerTitle="PAF synth settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<XYPad
|
|
||||||
size={280}
|
|
||||||
ariaLabel="PAF synth control pad"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Drag to sculpt formants. Voice space:
|
|
||||||
<strong>{schema.voice_spaces[voiceSpace()] ?? 'Default'}</strong>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="var(--accent)"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PAFSynthMode;
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
/**
|
|
||||||
* 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 { Slider } from '../primitives/Slider';
|
|
||||||
import { SlpWorkshopSchema } from './generated/slp_workshop_schema';
|
|
||||||
|
|
||||||
export const SLPWorkshopMode: Component = () => {
|
|
||||||
const schema = SlpWorkshopSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
|
||||||
const [exploreLevel, setExploreLevel] = createSignal(0);
|
|
||||||
|
|
||||||
const setExplore = (v: number): void => {
|
|
||||||
setExploreLevel(v);
|
|
||||||
runtime.explore.setIntensity(v);
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Adaptive-learning gestures. */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
'flex-direction': 'column',
|
|
||||||
gap: 'var(--sp-2)',
|
|
||||||
width: '280px',
|
|
||||||
'margin-top': 'var(--sp-2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Jolt — hold to morph weights live, release to freeze. */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Jolt — hold to morph the network's weights"
|
|
||||||
aria-pressed={runtime.jolt.active()}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
runtime.jolt.press();
|
|
||||||
}}
|
|
||||||
onPointerUp={() => runtime.jolt.release()}
|
|
||||||
onPointerLeave={() => runtime.jolt.release()}
|
|
||||||
onPointerCancel={() => runtime.jolt.release()}
|
|
||||||
style={{
|
|
||||||
padding: 'var(--sp-2)',
|
|
||||||
'border-radius': 'var(--r-1)',
|
|
||||||
border: '1px solid var(--line)',
|
|
||||||
background: runtime.jolt.active() ? 'var(--accent, #ef9e5b)' : 'var(--bg-2)',
|
|
||||||
color: runtime.jolt.active() ? '#000' : 'var(--fg)',
|
|
||||||
'font-weight': 600,
|
|
||||||
cursor: 'pointer',
|
|
||||||
'touch-action': 'none',
|
|
||||||
'user-select': 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
⚡ Jolt {runtime.jolt.active() ? '(hold…)' : '(hold)'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Explore — OU random-walk exploration intensity. */}
|
|
||||||
<Slider
|
|
||||||
label="Explore"
|
|
||||||
ariaLabel="Explore — exploration random-walk intensity"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
value={exploreLevel()}
|
|
||||||
onChange={setExplore}
|
|
||||||
decimals={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="#ef9e5b"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SLPWorkshopMode;
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
.wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
padding-top: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section:first-of-type {
|
|
||||||
border-top: none;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: var(--sp-2) 0;
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--fg-mute);
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionHeader:hover {
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.caret {
|
|
||||||
color: var(--fg-dim);
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
width: 12px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
background: var(--bg-3);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-2) 0 var(--sp-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldLabel {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
min-width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-1);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
color: var(--fg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subhead {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--fg-mute);
|
|
||||||
margin: var(--sp-2) 0 var(--sp-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
color: var(--fg-dim);
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnAlt {
|
|
||||||
background: var(--bg-2);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: var(--sp-1) var(--sp-3);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
color: var(--fg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnAlt:hover:not(:disabled) {
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnAlt:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnTiny {
|
|
||||||
background: var(--bg-2);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnTiny:hover {
|
|
||||||
color: var(--fg);
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshotList,
|
|
||||||
.presetList {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
max-height: 220px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshotBtn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--bg-2);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: var(--sp-1) var(--sp-2);
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshotBtn:hover {
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshotTag {
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshotMeta {
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
color: var(--fg-mute);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.presetRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
padding: var(--sp-1);
|
|
||||||
border-bottom: 1px dashed var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.presetRow > span {
|
|
||||||
flex: 1;
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.paramList {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
max-height: 360px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layerStats {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-1);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layerStatRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
padding: 2px var(--sp-2);
|
|
||||||
background: var(--bg-2);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layerStatLabel {
|
|
||||||
color: var(--accent);
|
|
||||||
font-weight: 600;
|
|
||||||
min-width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layerStatValue {
|
|
||||||
flex: 1;
|
|
||||||
color: var(--fg-mute);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layerStatStatus {
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layerStatStatus[data-status="dead"] { color: #999; }
|
|
||||||
.layerStatStatus[data-status="saturating"] { color: var(--warn); }
|
|
||||||
.layerStatStatus[data-status="healthy"] { color: var(--good); }
|
|
||||||
|
|
||||||
.textInput {
|
|
||||||
background: var(--bg-2);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
color: var(--fg);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
padding: var(--sp-1) var(--sp-2);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px dashed var(--line);
|
|
||||||
margin: var(--sp-2) 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,656 +0,0 @@
|
||||||
/**
|
|
||||||
* SettingsDrawer — settings panel rendered inside ModeShell's drawer.
|
|
||||||
*
|
|
||||||
* Sections:
|
|
||||||
* - Input: deadzone, curve, smoothing, momentum, anchor mode, invert
|
|
||||||
* - Training: learning rate, weight decay
|
|
||||||
* - Exploration: spread, noise floor/cap/growth/decay,
|
|
||||||
* auto-explore (toggle, interval, intensity)
|
|
||||||
* - Output: global curve, smoothing, slew rate, freeze
|
|
||||||
* - Per-param overrides: ParamEditor list
|
|
||||||
* - Advanced: weight health histogram, gradient flow, session presets
|
|
||||||
*
|
|
||||||
* Sections are collapsible. Per-param-override editor renders inline (also
|
|
||||||
* accessible via double-tap on slider in the live readout — wired in
|
|
||||||
* ModeShell). The drawer is a child of `ModeShell`'s `drawerContent` slot.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component, createSignal, For, Show, createMemo, createEffect } from 'solid-js';
|
|
||||||
import { ParamEditor, type ParamOverride as PrimitiveParamOverride } from '../primitives/ParamEditor';
|
|
||||||
import { Slider } from '../primitives/Slider';
|
|
||||||
import { PillToggle } from '../primitives/PillToggle';
|
|
||||||
import { WeightHealth } from '../primitives/WeightHealth';
|
|
||||||
import { GradientFlow } from '../primitives/GradientFlow';
|
|
||||||
import { Heatmap } from '../primitives/Heatmap';
|
|
||||||
import { ProgressRing } from '../primitives/ProgressRing';
|
|
||||||
import { LossPlot } from '../primitives/LossPlot';
|
|
||||||
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
import { outputStore } from '../stores/output-store';
|
|
||||||
import { explorationStore } from '../stores/exploration-store';
|
|
||||||
import { modeStore, defaultParamOverride, type ParamOverride } from '../stores/mode-store';
|
|
||||||
import { sessionStore } from '../stores/session-store';
|
|
||||||
import { mlStore } from '../stores/ml-store';
|
|
||||||
import { coreBus } from '../stores/bus';
|
|
||||||
import {
|
|
||||||
ZOOM_MIN,
|
|
||||||
ZOOM_MAX,
|
|
||||||
DEADZONE_MAX,
|
|
||||||
INPUT_CURVE_MIN,
|
|
||||||
INPUT_CURVE_MAX,
|
|
||||||
SMOOTHING_MAX,
|
|
||||||
} from '../input/pipeline';
|
|
||||||
import { GLOBAL_CURVE_MIN, GLOBAL_CURVE_MAX } from '../output/pipeline';
|
|
||||||
import {
|
|
||||||
layerNorms,
|
|
||||||
gradientStatuses,
|
|
||||||
weightHistogram,
|
|
||||||
weightStatus,
|
|
||||||
layerStatsToStatus,
|
|
||||||
layerWeightCounts,
|
|
||||||
} from '../features/weight-health';
|
|
||||||
import { saveNamedPreset, loadNamedPreset, buildShareUrl } from '../features/session-preset';
|
|
||||||
import { captureA, toggleAB, acceptB, revertToA } from '../features/snapshots';
|
|
||||||
import type { ModeRuntime } from './mode-runtime';
|
|
||||||
import type { ModeSchema } from './generated';
|
|
||||||
import type { Param } from './generated/types';
|
|
||||||
import type { CurveName } from '../output/curves';
|
|
||||||
import type { HeatmapColorMode } from '../features/heatmap-sampler';
|
|
||||||
|
|
||||||
import styles from './SettingsDrawer.module.css';
|
|
||||||
|
|
||||||
export interface SettingsDrawerProps {
|
|
||||||
schema: ModeSchema;
|
|
||||||
runtime: ModeRuntime;
|
|
||||||
/** Show advanced section (weight health, gradient flow, session presets). */
|
|
||||||
showAdvanced?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Section {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
render: () => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsDrawer: Component<SettingsDrawerProps> = (props) => {
|
|
||||||
const [open, setOpen] = createSignal<Record<string, boolean>>({
|
|
||||||
input: false,
|
|
||||||
training: false,
|
|
||||||
exploration: false,
|
|
||||||
output: false,
|
|
||||||
overrides: false,
|
|
||||||
advanced: false,
|
|
||||||
snapshots: false,
|
|
||||||
});
|
|
||||||
const toggle = (id: string) => setOpen((s) => ({ ...s, [id]: !s[id] }));
|
|
||||||
|
|
||||||
// ----- Per-param overrides UI ----------------------------------------
|
|
||||||
const overrides = () => modeStore.state.overrides[props.schema.mode_id] ?? {};
|
|
||||||
|
|
||||||
const getOverride = (param: Param): PrimitiveParamOverride => {
|
|
||||||
const cur = overrides()[param.name];
|
|
||||||
if (cur) {
|
|
||||||
return {
|
|
||||||
min: cur.min,
|
|
||||||
max: cur.max,
|
|
||||||
curve: cur.curve as CurveName,
|
|
||||||
curveParam: cur.curveParam,
|
|
||||||
muted: cur.muted,
|
|
||||||
pinned: cur.pinned,
|
|
||||||
fixedValue: cur.fixedValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
min: param.min,
|
|
||||||
max: param.max,
|
|
||||||
curve: param.curve as CurveName,
|
|
||||||
muted: false,
|
|
||||||
pinned: false,
|
|
||||||
fixedValue: param.default,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeOverride = (param: Param, next: PrimitiveParamOverride) => {
|
|
||||||
const stored: ParamOverride = {
|
|
||||||
min: next.min,
|
|
||||||
max: next.max,
|
|
||||||
curve: next.curve,
|
|
||||||
curveParam: next.curveParam,
|
|
||||||
muted: next.muted,
|
|
||||||
pinned: next.pinned,
|
|
||||||
fixedValue: next.fixedValue,
|
|
||||||
// Frozen flag is local to the runtime (set via output-store freezeMask)
|
|
||||||
// — keep it false in the persistent override unless the user explicitly
|
|
||||||
// toggles it.
|
|
||||||
frozen: overrides()[param.name]?.frozen ?? false,
|
|
||||||
};
|
|
||||||
modeStore.setOverride(props.schema.mode_id, param.name, stored);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----- Per-output health (advanced section) -----------------------------
|
|
||||||
const [advancedExpanded, setAdvancedExpanded] = createSignal(props.showAdvanced ?? false);
|
|
||||||
const histogram = createMemo<number[]>(() => {
|
|
||||||
const w = mlStore.weights();
|
|
||||||
return weightHistogram(w);
|
|
||||||
});
|
|
||||||
const status = createMemo(() => weightStatus(mlStore.weights()));
|
|
||||||
|
|
||||||
// Gradient flow: capture before/after weights on training events.
|
|
||||||
const [normHistory, setNormHistory] = createSignal<number[]>([]);
|
|
||||||
const [gradStatuses, setGradStatuses] = createSignal<ReturnType<typeof gradientStatuses>>([]);
|
|
||||||
let lastBeforeWeights: Float32Array | null = null;
|
|
||||||
|
|
||||||
const offTrainStart = coreBus.on('snap.push', (e) => {
|
|
||||||
if (e.tag === 'before train' && mlStore.iml) {
|
|
||||||
lastBeforeWeights = mlStore.getWeights();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const offTrainEnd = coreBus.on('ml.trained', () => {
|
|
||||||
if (lastBeforeWeights && mlStore.iml) {
|
|
||||||
const after = mlStore.getWeights();
|
|
||||||
const arch = mlStore.iml.architecture;
|
|
||||||
const sizes = layerWeightCounts({
|
|
||||||
inputSize: arch.inputSize,
|
|
||||||
hidden: arch.hidden,
|
|
||||||
outputSize: arch.outputSize,
|
|
||||||
});
|
|
||||||
const norms = layerNorms(lastBeforeWeights, after, sizes);
|
|
||||||
setNormHistory(norms);
|
|
||||||
setGradStatuses(gradientStatuses(norms));
|
|
||||||
lastBeforeWeights = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Session-preset save / share UI state
|
|
||||||
const [presetName, setPresetName] = createSignal('');
|
|
||||||
const [shareUrl, setShareUrl] = createSignal('');
|
|
||||||
|
|
||||||
// Snapshot list (long-press-undo equivalent)
|
|
||||||
const snapshots = () => sessionStore.listSnapshots();
|
|
||||||
|
|
||||||
// Cleanup on unmount (Solid handles via owner; we only need to detach bus
|
|
||||||
// subs explicitly because they live across renders).
|
|
||||||
// We can't use onCleanup here because this component is recreated per-mode,
|
|
||||||
// but bus.on returns an unsubscribe. Defer cleanup to when the component
|
|
||||||
// unmounts via createEffect cleanup function:
|
|
||||||
createEffect(() => {
|
|
||||||
return () => { offTrainStart(); offTrainEnd(); };
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- A/B compare visibility ------------------------------------------
|
|
||||||
const ab = () => sessionStore.state.ab;
|
|
||||||
const live = () => ab().live;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
|
||||||
// Render helpers
|
|
||||||
// ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
const SectionHeader = (h: { id: string; title: string; count?: number }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.sectionHeader}
|
|
||||||
onClick={() => toggle(h.id)}
|
|
||||||
aria-expanded={!!open()[h.id]}
|
|
||||||
>
|
|
||||||
<span class={styles.caret}>{open()[h.id] ? '▾' : '▸'}</span>
|
|
||||||
<span>{h.title}</span>
|
|
||||||
<Show when={h.count !== undefined}><span class={styles.badge}>{h.count}</span></Show>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={styles.wrap}>
|
|
||||||
{/* Snapshots / undo / A/B */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader id="snapshots" title="History (snapshots, A/B)" count={snapshots().length} />
|
|
||||||
<Show when={open().snapshots}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
disabled={!props.runtime.canUndo()}
|
|
||||||
onClick={() => props.runtime.undo()}
|
|
||||||
>Undo last</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
onClick={() => sessionStore.clearSnapshots()}
|
|
||||||
>Clear stack</button>
|
|
||||||
</div>
|
|
||||||
<Show when={snapshots().length > 0}>
|
|
||||||
<ul class={styles.snapshotList}>
|
|
||||||
<For each={snapshots().slice().reverse()}>{(s) => (
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.snapshotBtn}
|
|
||||||
onClick={() => {
|
|
||||||
const ok = props.runtime.undo();
|
|
||||||
if (!ok) {
|
|
||||||
// Restore by id instead.
|
|
||||||
const map = sessionStore.state.snapshots;
|
|
||||||
const idx = map.findIndex((m) => m.id === s.id);
|
|
||||||
if (idx >= 0) {
|
|
||||||
// walk back N times to make it the top of the stack
|
|
||||||
for (let i = map.length - 1; i > idx; i--) sessionStore.popSnapshot();
|
|
||||||
props.runtime.undo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class={styles.snapshotTag}>{s.tag}</span>
|
|
||||||
<span class={styles.snapshotMeta}>
|
|
||||||
noise {s.noiseLevel.toFixed(3)}
|
|
||||||
{s.zoomLevel !== null ? ` · zoom ${s.zoomLevel.toFixed(2)}` : ''}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}</For>
|
|
||||||
</ul>
|
|
||||||
</Show>
|
|
||||||
<hr class={styles.hr} />
|
|
||||||
<h4 class={styles.subhead}>A / B compare</h4>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<Show when={!ab().a} fallback={
|
|
||||||
<span class={styles.muted}>active: <strong>{live()}</strong></span>
|
|
||||||
}>
|
|
||||||
<span class={styles.muted}>no capture yet</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
onClick={() => captureA()}
|
|
||||||
>Capture A</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
disabled={!ab().a}
|
|
||||||
onClick={() => toggleAB()}
|
|
||||||
>Toggle A/B</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
disabled={!ab().a}
|
|
||||||
onClick={() => acceptB()}
|
|
||||||
>Accept B</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
disabled={!ab().a}
|
|
||||||
onClick={() => revertToA()}
|
|
||||||
>Revert to A</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Input pipeline */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader id="input" title="Input pipeline" />
|
|
||||||
<Show when={open().input}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<Slider
|
|
||||||
label="Zoom" min={ZOOM_MIN} max={ZOOM_MAX}
|
|
||||||
value={inputStore.config.zoom}
|
|
||||||
onChange={(v) => inputStore.setZoom(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Deadzone" min={0} max={DEADZONE_MAX}
|
|
||||||
value={inputStore.config.deadzone}
|
|
||||||
onChange={(v) => inputStore.setDeadzone(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Input curve" min={INPUT_CURVE_MIN} max={INPUT_CURVE_MAX}
|
|
||||||
value={inputStore.config.inputCurve}
|
|
||||||
onChange={(v) => inputStore.setInputCurve(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Smoothing" min={0} max={SMOOTHING_MAX}
|
|
||||||
value={inputStore.config.smoothing}
|
|
||||||
onChange={(v) => inputStore.setSmoothing(v)}
|
|
||||||
/>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<label class={styles.fieldLabel}>Anchor mode</label>
|
|
||||||
<PillToggle
|
|
||||||
options={[
|
|
||||||
{ value: 'auto', label: 'Auto' },
|
|
||||||
{ value: 'sticky', label: 'Sticky' },
|
|
||||||
{ value: 'center', label: 'Center' },
|
|
||||||
]}
|
|
||||||
value={() => inputStore.config.anchorMode}
|
|
||||||
onChange={(v) => inputStore.setAnchorMode(v as 'auto' | 'sticky' | 'center')}
|
|
||||||
ariaLabel="Anchor mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<label class={styles.fieldLabel}>Momentum zoom</label>
|
|
||||||
<PillToggle
|
|
||||||
options={[
|
|
||||||
{ value: 'off', label: 'Off' },
|
|
||||||
{ value: 'gentle', label: 'Gentle' },
|
|
||||||
{ value: 'strong', label: 'Strong' },
|
|
||||||
]}
|
|
||||||
value={() => inputStore.config.momentumZoom}
|
|
||||||
onChange={(v) => inputStore.setMomentumZoom(v as 'off' | 'gentle' | 'strong')}
|
|
||||||
ariaLabel="Momentum zoom"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<label class={styles.checkbox}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={inputStore.config.invertX}
|
|
||||||
onChange={(e) => inputStore.setInvert(e.currentTarget.checked, inputStore.config.invertY)}
|
|
||||||
/>
|
|
||||||
Invert X
|
|
||||||
</label>
|
|
||||||
<label class={styles.checkbox}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={inputStore.config.invertY}
|
|
||||||
onChange={(e) => inputStore.setInvert(inputStore.config.invertX, e.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
Invert Y
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Training */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader id="training" title="Training" />
|
|
||||||
<Show when={open().training}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<Slider
|
|
||||||
label="Learning rate" min={0.01} max={5}
|
|
||||||
value={explorationStore.state.learningRate}
|
|
||||||
onChange={(v) => explorationStore.setLearningRate(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Weight decay" min={0} max={0.3}
|
|
||||||
value={explorationStore.state.weightDecay}
|
|
||||||
onChange={(v) => explorationStore.setWeightDecay(v)}
|
|
||||||
/>
|
|
||||||
<LossPlot history={() => mlStore.state.lossHistory} width={320} height={70} />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Exploration */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader id="exploration" title="Exploration / RL" />
|
|
||||||
<Show when={open().exploration}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<Slider
|
|
||||||
label="Spread" min={0} max={1}
|
|
||||||
value={explorationStore.state.spread}
|
|
||||||
onChange={(v) => explorationStore.setSpread(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Noise floor" min={0} max={0.5}
|
|
||||||
value={explorationStore.state.noiseFloor}
|
|
||||||
onChange={(v) => explorationStore.setNoiseFloor(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Noise cap" min={explorationStore.state.noiseFloor} max={1}
|
|
||||||
value={explorationStore.state.noiseCap}
|
|
||||||
onChange={(v) => explorationStore.setNoiseCap(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Noise growth" min={1} max={3}
|
|
||||||
value={explorationStore.state.noiseGrowth}
|
|
||||||
onChange={(v) => explorationStore.setNoiseGrowth(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Noise decay" min={0.7} max={1}
|
|
||||||
value={explorationStore.state.noiseDecay}
|
|
||||||
onChange={(v) => explorationStore.setNoiseDecay(v)}
|
|
||||||
/>
|
|
||||||
<hr class={styles.hr} />
|
|
||||||
<h4 class={styles.subhead}>Auto-explore</h4>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<label class={styles.checkbox}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={explorationStore.state.autoExploreEnabled}
|
|
||||||
onChange={(e) => explorationStore.setAutoExplore(e.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
Enable auto-explore
|
|
||||||
</label>
|
|
||||||
<Show when={explorationStore.state.autoExploreEnabled}>
|
|
||||||
<ProgressRing
|
|
||||||
size={28}
|
|
||||||
showLabel={false}
|
|
||||||
progress={() => 1}
|
|
||||||
ariaLabel="Auto-explore active"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
label="Interval (ms)" min={500} max={10000} step={100}
|
|
||||||
value={explorationStore.state.autoExploreIntervalMs}
|
|
||||||
onChange={(v) => explorationStore.setAutoExploreInterval(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Intensity" min={0.1} max={1}
|
|
||||||
value={explorationStore.state.autoExploreIntensity}
|
|
||||||
onChange={(v) => explorationStore.setAutoExploreIntensity(v)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Output */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader id="output" title="Output pipeline" />
|
|
||||||
<Show when={open().output}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<Slider
|
|
||||||
label="Global curve" min={GLOBAL_CURVE_MIN} max={GLOBAL_CURVE_MAX}
|
|
||||||
value={outputStore.config.globalCurve}
|
|
||||||
onChange={(v) => outputStore.setGlobalCurve(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Output smoothing" min={0} max={0.95}
|
|
||||||
value={outputStore.config.smoothing}
|
|
||||||
onChange={(v) => outputStore.setSmoothing(v)}
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
label="Slew rate (per sec)" min={0.005} max={1} step={0.005}
|
|
||||||
value={isFinite(outputStore.config.slewRate) ? outputStore.config.slewRate : 1}
|
|
||||||
onChange={(v) => outputStore.setSlewRate(v)}
|
|
||||||
/>
|
|
||||||
<label class={styles.checkbox}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={outputStore.config.freezeOutput}
|
|
||||||
onChange={(e) => outputStore.setFreezeOutput(e.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
Freeze output
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Per-param overrides */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader
|
|
||||||
id="overrides"
|
|
||||||
title="Parameter overrides"
|
|
||||||
count={Object.keys(overrides()).length}
|
|
||||||
/>
|
|
||||||
<Show when={open().overrides}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
onClick={() => modeStore.clearAllOverrides(props.schema.mode_id)}
|
|
||||||
>Clear all</button>
|
|
||||||
</div>
|
|
||||||
<div class={styles.paramList}>
|
|
||||||
<For each={props.schema.params}>{(p) => (
|
|
||||||
<ParamEditor
|
|
||||||
param={{
|
|
||||||
name: p.name,
|
|
||||||
label: p.label,
|
|
||||||
hardMin: p.min,
|
|
||||||
hardMax: p.max,
|
|
||||||
default: p.default,
|
|
||||||
curve: p.curve as CurveName,
|
|
||||||
group: p.group,
|
|
||||||
}}
|
|
||||||
override={() => getOverride(p)}
|
|
||||||
onChange={(next) => writeOverride(p, next)}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
)}</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Advanced: weight health, gradient flow, heatmap */}
|
|
||||||
<section class={styles.section}>
|
|
||||||
<SectionHeader id="advanced" title="Advanced" />
|
|
||||||
<Show when={open().advanced}>
|
|
||||||
<div class={styles.body}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
onClick={() => setAdvancedExpanded((b) => !b)}
|
|
||||||
>{advancedExpanded() ? 'Hide diagnostics' : 'Show diagnostics'}</button>
|
|
||||||
<Show when={advancedExpanded()}>
|
|
||||||
<h4 class={styles.subhead}>Weight health</h4>
|
|
||||||
<WeightHealth
|
|
||||||
histogram={histogram}
|
|
||||||
status={status}
|
|
||||||
width={300}
|
|
||||||
height={60}
|
|
||||||
/>
|
|
||||||
<h4 class={styles.subhead}>Gradient flow (last train)</h4>
|
|
||||||
<Show
|
|
||||||
when={normHistory().length > 0}
|
|
||||||
fallback={<p class={styles.muted}>No training run yet.</p>}
|
|
||||||
>
|
|
||||||
<GradientFlow
|
|
||||||
layerNorms={normHistory}
|
|
||||||
status={gradStatuses}
|
|
||||||
width={300}
|
|
||||||
height={70}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<h4 class={styles.subhead}>Per-layer stats</h4>
|
|
||||||
<div class={styles.layerStats}>
|
|
||||||
<For each={mlStore.getLayerStatsRecords()}>{(stats, idx) => (
|
|
||||||
<div class={styles.layerStatRow}>
|
|
||||||
<span class={styles.layerStatLabel}>L{idx()}</span>
|
|
||||||
<span class={styles.layerStatValue}>
|
|
||||||
mean|w| {stats.meanAbs.toFixed(3)}
|
|
||||||
· max|w| {stats.maxAbs.toFixed(3)}
|
|
||||||
· {(stats.deadFrac * 100).toFixed(0)}% dead
|
|
||||||
· {(stats.saturatingFrac * 100).toFixed(0)}% sat
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class={styles.layerStatStatus}
|
|
||||||
data-status={layerStatsToStatus(stats)}
|
|
||||||
>{layerStatsToStatus(stats)}</span>
|
|
||||||
</div>
|
|
||||||
)}</For>
|
|
||||||
</div>
|
|
||||||
<h4 class={styles.subhead}>Input heatmap</h4>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<label class={styles.fieldLabel}>Color</label>
|
|
||||||
<PillToggle
|
|
||||||
options={[
|
|
||||||
{ value: 'luminance', label: 'Lumi' },
|
|
||||||
{ value: 'variance', label: 'Var' },
|
|
||||||
{ value: 'divergence', label: 'Div' },
|
|
||||||
]}
|
|
||||||
value={() => props.runtime.heatmap.colorMode()}
|
|
||||||
onChange={(v) => props.runtime.heatmap.setColorMode(v as HeatmapColorMode)}
|
|
||||||
ariaLabel="Heatmap color mode"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
onClick={() => props.runtime.heatmap.refresh(true)}
|
|
||||||
>Refresh</button>
|
|
||||||
</div>
|
|
||||||
<Heatmap
|
|
||||||
samples={props.runtime.heatmap.cells}
|
|
||||||
resolution={props.runtime.heatmap.resolution()}
|
|
||||||
colorMode={props.runtime.heatmap.colorMode() as 'luminance' | 'variance' | 'divergence'}
|
|
||||||
size={240}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<hr class={styles.hr} />
|
|
||||||
<h4 class={styles.subhead}>Session preset</h4>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<input
|
|
||||||
class={styles.textInput}
|
|
||||||
placeholder="preset name"
|
|
||||||
value={presetName()}
|
|
||||||
onInput={(e) => setPresetName(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
disabled={!presetName().trim()}
|
|
||||||
onClick={() => {
|
|
||||||
const n = presetName().trim();
|
|
||||||
if (!n) return;
|
|
||||||
saveNamedPreset(n, false);
|
|
||||||
setPresetName('');
|
|
||||||
}}
|
|
||||||
>Save</button>
|
|
||||||
</div>
|
|
||||||
<Show when={sessionStore.state.presets.length > 0}>
|
|
||||||
<ul class={styles.presetList}>
|
|
||||||
<For each={sessionStore.state.presets}>{(p) => (
|
|
||||||
<li class={styles.presetRow}>
|
|
||||||
<span>{p.name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnTiny}
|
|
||||||
onClick={() => loadNamedPreset(p.id)}
|
|
||||||
>Load</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnTiny}
|
|
||||||
onClick={() => sessionStore.removePreset(p.id)}
|
|
||||||
>Del</button>
|
|
||||||
</li>
|
|
||||||
)}</For>
|
|
||||||
</ul>
|
|
||||||
</Show>
|
|
||||||
<div class={styles.row}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.btnAlt}
|
|
||||||
onClick={() => {
|
|
||||||
setShareUrl(window.location.origin + window.location.pathname + buildShareUrl());
|
|
||||||
}}
|
|
||||||
>Build share URL</button>
|
|
||||||
<Show when={shareUrl()}>
|
|
||||||
<input
|
|
||||||
class={styles.textInput}
|
|
||||||
readOnly
|
|
||||||
value={shareUrl()}
|
|
||||||
onClick={(e) => e.currentTarget.select()}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsDrawer;
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
/**
|
|
||||||
* SoundAnalysisMIDIMode — sound analysis → MIDI CC output (audio_in input).
|
|
||||||
*
|
|
||||||
* Schema declares `primary_input: 'audio_in'` and `engine_id: 'thru'` (no
|
|
||||||
* synthesis). Stream 10 wires the mic into channels 2..5 via ModeShell's
|
|
||||||
* Mic button + the runtime's MicInput. The joystick is used as a fallback
|
|
||||||
* for channels 0..1 and during testing without mic permissions.
|
|
||||||
*
|
|
||||||
* MIDI output routing is read-only here — the SettingsDrawer's per-param
|
|
||||||
* override editor lets users tighten the active range / mute params; the
|
|
||||||
* actual MIDI device wiring is left for the engine host to handle (or for
|
|
||||||
* a future stream that adds WebMIDI sender). For now CC values are
|
|
||||||
* visible in the live OutputDisplay.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component, Show } from 'solid-js';
|
|
||||||
import { ModeShell } from './ModeShell';
|
|
||||||
import { useModeRuntime } from './mode-runtime';
|
|
||||||
import { VirtualJoystick } from '../primitives/VirtualJoystick';
|
|
||||||
import { OutputDisplay } from '../primitives/OutputDisplay';
|
|
||||||
import { LossPlot } from '../primitives/LossPlot';
|
|
||||||
import { SoundAnalysisMidiSchema } from './generated/sound_analysis_midi_schema';
|
|
||||||
|
|
||||||
export const SoundAnalysisMIDIMode: Component = () => {
|
|
||||||
const schema = SoundAnalysisMidiSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
drawerTitle="Sound analysis → MIDI settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<Show
|
|
||||||
when={schema.ui.primary_input === 'audio_in'}
|
|
||||||
fallback={
|
|
||||||
<VirtualJoystick
|
|
||||||
size={260}
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
ariaLabel="Sound analysis joystick"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
'flex-direction': 'column',
|
|
||||||
'align-items': 'center',
|
|
||||||
gap: 'var(--sp-3)',
|
|
||||||
padding: 'var(--sp-4)',
|
|
||||||
background: 'var(--bg-2)',
|
|
||||||
border: `1px ${runtime.mic.started() ? 'solid var(--accent-2)' : 'dashed var(--line-strong)'}`,
|
|
||||||
'border-radius': 'var(--r-2)',
|
|
||||||
'min-width': '260px',
|
|
||||||
'min-height': '200px',
|
|
||||||
'justify-content': 'center',
|
|
||||||
color: 'var(--fg-mute)',
|
|
||||||
'text-align': 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong style={{ color: runtime.mic.started() ? 'var(--accent-2)' : 'var(--fg-mute)' }}>
|
|
||||||
{runtime.mic.started() ? '🎤 Mic active' : 'Mic input'}
|
|
||||||
</strong>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)' }}>
|
|
||||||
{runtime.mic.started()
|
|
||||||
? 'Audio features → channels 2..5. Joystick below drives channels 0..1.'
|
|
||||||
: 'Tap "Mic" in the header to feed mic features into the model.'}
|
|
||||||
</span>
|
|
||||||
<VirtualJoystick
|
|
||||||
size={200}
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
ariaLabel="Manual joystick fallback"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="var(--info)"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SoundAnalysisMIDIMode;
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* VerbFXMode — verb / fx unit (joystick input, ~47 outputs).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component, createSignal } from 'solid-js';
|
|
||||||
import { ModeShell } from './ModeShell';
|
|
||||||
import { useModeRuntime } from './mode-runtime';
|
|
||||||
import { VirtualJoystick } from '../primitives/VirtualJoystick';
|
|
||||||
import { OutputDisplay } from '../primitives/OutputDisplay';
|
|
||||||
import { LossPlot } from '../primitives/LossPlot';
|
|
||||||
import { VerbFxSchema } from './generated/verb_fx_schema';
|
|
||||||
|
|
||||||
export const VerbFXMode: Component = () => {
|
|
||||||
const schema = VerbFxSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
activeVoiceSpace={voiceSpace}
|
|
||||||
onVoiceSpaceChange={setVoiceSpace}
|
|
||||||
drawerTitle="Verb / FX settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<VirtualJoystick
|
|
||||||
size={260}
|
|
||||||
ariaLabel="Verb FX joystick"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Sweep through verb space. Voice space:
|
|
||||||
<strong>{schema.voice_spaces[voiceSpace()] ?? 'Default'}</strong>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="#b464ff"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default VerbFXMode;
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* XIASRIMode — audio-reactive verb / pitch effects.
|
|
||||||
*
|
|
||||||
* Schema declares `primary_input: 'joystick'` and feeds the MLP from
|
|
||||||
* joy_x/joy_y plus optional mic features. ModeShell exposes a Mic toggle
|
|
||||||
* that opt-in feeds analysis features into channels 2..5.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component } from 'solid-js';
|
|
||||||
import { ModeShell } from './ModeShell';
|
|
||||||
import { useModeRuntime } from './mode-runtime';
|
|
||||||
import { VirtualJoystick } from '../primitives/VirtualJoystick';
|
|
||||||
import { OutputDisplay } from '../primitives/OutputDisplay';
|
|
||||||
import { LossPlot } from '../primitives/LossPlot';
|
|
||||||
import { XiasriSchema } from './generated/xiasri_schema';
|
|
||||||
|
|
||||||
export const XIASRIMode: Component = () => {
|
|
||||||
const schema = XiasriSchema;
|
|
||||||
const runtime = useModeRuntime(schema);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModeShell
|
|
||||||
schema={schema}
|
|
||||||
runtime={runtime}
|
|
||||||
drawerTitle="XIASRI settings"
|
|
||||||
primaryInput={() => (
|
|
||||||
<>
|
|
||||||
<VirtualJoystick
|
|
||||||
size={260}
|
|
||||||
ariaLabel="XIASRI joystick"
|
|
||||||
onMove={(x, y) => runtime.setInput(x, y)}
|
|
||||||
position={runtime.pipedInput}
|
|
||||||
/>
|
|
||||||
<span style={{ 'font-size': 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
Joystick → verb / pitch space. Enable mic for audio-reactive features.
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
outputArea={() => (
|
|
||||||
<>
|
|
||||||
<OutputDisplay
|
|
||||||
values={runtime.paramOutputs}
|
|
||||||
width={360}
|
|
||||||
height={120}
|
|
||||||
color="var(--accent-2)"
|
|
||||||
/>
|
|
||||||
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default XIASRIMode;
|
|
||||||
|
|
@ -1,596 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/breakor.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface BreakorParams {
|
|
||||||
readonly kick_ratio0: number;
|
|
||||||
readonly kick_ratio1: number;
|
|
||||||
readonly kick_ratio2: number;
|
|
||||||
readonly kick_pmul: number;
|
|
||||||
readonly kick_poff: number;
|
|
||||||
readonly kick_amp0: number;
|
|
||||||
readonly kick_amp1: number;
|
|
||||||
readonly snare_ratio0: number;
|
|
||||||
readonly snare_ratio1: number;
|
|
||||||
readonly snare_ratio2: number;
|
|
||||||
readonly snare_pmul: number;
|
|
||||||
readonly snare_poff: number;
|
|
||||||
readonly snare_amp0: number;
|
|
||||||
readonly snare_amp1: number;
|
|
||||||
readonly tom_ratio0: number;
|
|
||||||
readonly tom_ratio1: number;
|
|
||||||
readonly tom_ratio2: number;
|
|
||||||
readonly tom_pmul: number;
|
|
||||||
readonly tom_poff: number;
|
|
||||||
readonly tom_amp0: number;
|
|
||||||
readonly tom_amp1: number;
|
|
||||||
readonly lowtom_ratio0: number;
|
|
||||||
readonly lowtom_ratio1: number;
|
|
||||||
readonly lowtom_ratio2: number;
|
|
||||||
readonly lowtom_pmul: number;
|
|
||||||
readonly lowtom_poff: number;
|
|
||||||
readonly lowtom_amp0: number;
|
|
||||||
readonly lowtom_amp1: number;
|
|
||||||
readonly rim_ratio0: number;
|
|
||||||
readonly rim_ratio1: number;
|
|
||||||
readonly rim_ratio2: number;
|
|
||||||
readonly rim_pmul: number;
|
|
||||||
readonly rim_poff: number;
|
|
||||||
readonly rim_amp0: number;
|
|
||||||
readonly rim_amp1: number;
|
|
||||||
readonly hat_ratio0: number;
|
|
||||||
readonly hat_ratio1: number;
|
|
||||||
readonly hat_ratio2: number;
|
|
||||||
readonly hat_pmul: number;
|
|
||||||
readonly hat_poff: number;
|
|
||||||
readonly hat_amp0: number;
|
|
||||||
readonly hat_amp1: number;
|
|
||||||
readonly ophat_ratio0: number;
|
|
||||||
readonly ophat_ratio1: number;
|
|
||||||
readonly ophat_ratio2: number;
|
|
||||||
readonly ophat_pmul: number;
|
|
||||||
readonly ophat_poff: number;
|
|
||||||
readonly ophat_amp0: number;
|
|
||||||
readonly ophat_amp1: number;
|
|
||||||
readonly perc_ratio0: number;
|
|
||||||
readonly perc_ratio1: number;
|
|
||||||
readonly perc_ratio2: number;
|
|
||||||
readonly perc_pmul: number;
|
|
||||||
readonly perc_poff: number;
|
|
||||||
readonly perc_amp0: number;
|
|
||||||
readonly perc_amp1: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BreakorSchema: ModeSchema = {
|
|
||||||
mode_id: 'breakor',
|
|
||||||
engine_id: 'breakor',
|
|
||||||
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: 'kick_ratio0',
|
|
||||||
label: 'Kick Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'kick_ratio1',
|
|
||||||
label: 'Kick Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'kick_ratio2',
|
|
||||||
label: 'Kick Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'kick_pmul',
|
|
||||||
label: 'Kick Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'kick_poff',
|
|
||||||
label: 'Kick Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'kick_amp0',
|
|
||||||
label: 'Kick Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'kick_amp1',
|
|
||||||
label: 'Kick Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'kick',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_ratio0',
|
|
||||||
label: 'Snare Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_ratio1',
|
|
||||||
label: 'Snare Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_ratio2',
|
|
||||||
label: 'Snare Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_pmul',
|
|
||||||
label: 'Snare Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_poff',
|
|
||||||
label: 'Snare Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_amp0',
|
|
||||||
label: 'Snare Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'snare_amp1',
|
|
||||||
label: 'Snare Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'snare',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_ratio0',
|
|
||||||
label: 'Tom Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_ratio1',
|
|
||||||
label: 'Tom Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_ratio2',
|
|
||||||
label: 'Tom Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_pmul',
|
|
||||||
label: 'Tom Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_poff',
|
|
||||||
label: 'Tom Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_amp0',
|
|
||||||
label: 'Tom Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tom_amp1',
|
|
||||||
label: 'Tom Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'tom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_ratio0',
|
|
||||||
label: 'LowTom Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_ratio1',
|
|
||||||
label: 'LowTom Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_ratio2',
|
|
||||||
label: 'LowTom Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_pmul',
|
|
||||||
label: 'LowTom Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_poff',
|
|
||||||
label: 'LowTom Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_amp0',
|
|
||||||
label: 'LowTom Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowtom_amp1',
|
|
||||||
label: 'LowTom Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'lowtom',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_ratio0',
|
|
||||||
label: 'Rim Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_ratio1',
|
|
||||||
label: 'Rim Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_ratio2',
|
|
||||||
label: 'Rim Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_pmul',
|
|
||||||
label: 'Rim Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_poff',
|
|
||||||
label: 'Rim Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_amp0',
|
|
||||||
label: 'Rim Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rim_amp1',
|
|
||||||
label: 'Rim Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'rim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_ratio0',
|
|
||||||
label: 'Hat Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_ratio1',
|
|
||||||
label: 'Hat Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_ratio2',
|
|
||||||
label: 'Hat Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_pmul',
|
|
||||||
label: 'Hat Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_poff',
|
|
||||||
label: 'Hat Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_amp0',
|
|
||||||
label: 'Hat Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hat_amp1',
|
|
||||||
label: 'Hat Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'hat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_ratio0',
|
|
||||||
label: 'OpenHat Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_ratio1',
|
|
||||||
label: 'OpenHat Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_ratio2',
|
|
||||||
label: 'OpenHat Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_pmul',
|
|
||||||
label: 'OpenHat Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_poff',
|
|
||||||
label: 'OpenHat Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_amp0',
|
|
||||||
label: 'OpenHat Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ophat_amp1',
|
|
||||||
label: 'OpenHat Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'openhat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_ratio0',
|
|
||||||
label: 'Perc Ratio 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_ratio1',
|
|
||||||
label: 'Perc Ratio 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_ratio2',
|
|
||||||
label: 'Perc Ratio 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_pmul',
|
|
||||||
label: 'Perc Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_poff',
|
|
||||||
label: 'Perc Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_amp0',
|
|
||||||
label: 'Perc Amp 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'perc_amp1',
|
|
||||||
label: 'Perc Amp 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'perc',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'xy_pad',
|
|
||||||
show_voice_space_selector: false,
|
|
||||||
show_synth_visualizer: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/channel_strip.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface ChannelStripParams {
|
|
||||||
readonly pre_gain: number;
|
|
||||||
readonly peak0_freq: number;
|
|
||||||
readonly p02: number;
|
|
||||||
readonly p03: number;
|
|
||||||
readonly peak1_freq: number;
|
|
||||||
readonly peak_q: number;
|
|
||||||
readonly peak_gain: number;
|
|
||||||
readonly in_lpf_cutoff: number;
|
|
||||||
readonly in_hpf_cutoff: number;
|
|
||||||
readonly p09: number;
|
|
||||||
readonly comp_threshold: number;
|
|
||||||
readonly comp_ratio: number;
|
|
||||||
readonly comp_attack: number;
|
|
||||||
readonly comp_release: number;
|
|
||||||
readonly lowshelf_freq: number;
|
|
||||||
readonly lowshelf_q: number;
|
|
||||||
readonly lowshelf_gain: number;
|
|
||||||
readonly highshelf_freq: number;
|
|
||||||
readonly highshelf_q: number;
|
|
||||||
readonly highshelf_gain: number;
|
|
||||||
readonly p20: number;
|
|
||||||
readonly p21: number;
|
|
||||||
readonly p22: number;
|
|
||||||
readonly post_gain: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChannelStripSchema: ModeSchema = {
|
|
||||||
mode_id: 'channel_strip',
|
|
||||||
engine_id: 'channel_strip',
|
|
||||||
ml: {
|
|
||||||
input_channels: [
|
|
||||||
'joy_x',
|
|
||||||
'joy_y',
|
|
||||||
'joy_z',
|
|
||||||
'joy_w',
|
|
||||||
],
|
|
||||||
input_size: 4,
|
|
||||||
hidden_layers: [
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
],
|
|
||||||
output_size: 24,
|
|
||||||
default_spread: 0.6,
|
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'pre_gain',
|
|
||||||
label: 'Pre Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'gain',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'peak0_freq',
|
|
||||||
label: 'Peak0 Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p02',
|
|
||||||
label: 'Param 02',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p03',
|
|
||||||
label: 'Param 03',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'peak1_freq',
|
|
||||||
label: 'Peak1 Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'peak_q',
|
|
||||||
label: 'Peak Q',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'peak_gain',
|
|
||||||
label: 'Peak Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'in_lpf_cutoff',
|
|
||||||
label: 'Input LPF',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.7,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'filters',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'in_hpf_cutoff',
|
|
||||||
label: 'Input HPF',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.2,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'filters',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p09',
|
|
||||||
label: 'Param 09',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comp_threshold',
|
|
||||||
label: 'Comp Threshold',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'comp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comp_ratio',
|
|
||||||
label: 'Comp Ratio',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'comp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comp_attack',
|
|
||||||
label: 'Comp Attack',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.1,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'comp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comp_release',
|
|
||||||
label: 'Comp Release',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'comp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowshelf_freq',
|
|
||||||
label: 'Lowshelf Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowshelf_q',
|
|
||||||
label: 'Lowshelf Q',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lowshelf_gain',
|
|
||||||
label: 'Lowshelf Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'highshelf_freq',
|
|
||||||
label: 'Highshelf Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'highshelf_q',
|
|
||||||
label: 'Highshelf Q',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'highshelf_gain',
|
|
||||||
label: 'Highshelf Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'eq',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p20',
|
|
||||||
label: 'Param 20',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p21',
|
|
||||||
label: 'Param 21',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p22',
|
|
||||||
label: 'Param 22',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'post_gain',
|
|
||||||
label: 'Post Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'gain',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [
|
|
||||||
'WannabeNeve66',
|
|
||||||
'SSL 4K G-ist',
|
|
||||||
'SSL 9K-inda',
|
|
||||||
'MaleVox',
|
|
||||||
'FemaleVox',
|
|
||||||
'Neve 80',
|
|
||||||
],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'joystick',
|
|
||||||
show_voice_space_selector: true,
|
|
||||||
show_synth_visualizer: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,436 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/elysiamorf.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface ElysiamorfParams {
|
|
||||||
readonly fm0_carrier: number;
|
|
||||||
readonly fm0_mod_freq: number;
|
|
||||||
readonly fm0_mod_index: number;
|
|
||||||
readonly fm0_phasor_mul: number;
|
|
||||||
readonly fm0_phase_off: number;
|
|
||||||
readonly fm1_carrier: number;
|
|
||||||
readonly fm1_mod_freq: number;
|
|
||||||
readonly fm1_mod_index: number;
|
|
||||||
readonly fm1_phasor_mul: number;
|
|
||||||
readonly fm1_phase_off: number;
|
|
||||||
readonly fm2_carrier: number;
|
|
||||||
readonly fm2_mod_freq: number;
|
|
||||||
readonly fm2_mod_index: number;
|
|
||||||
readonly fm2_phasor_mul: number;
|
|
||||||
readonly fm2_phase_off: number;
|
|
||||||
readonly fm3_carrier: number;
|
|
||||||
readonly fm3_mod_freq: number;
|
|
||||||
readonly fm3_mod_index: number;
|
|
||||||
readonly fm3_phasor_mul: number;
|
|
||||||
readonly fm3_phase_off: number;
|
|
||||||
readonly fm4_carrier: number;
|
|
||||||
readonly fm4_mod_freq: number;
|
|
||||||
readonly fm4_mod_index: number;
|
|
||||||
readonly fm4_phasor_mul: number;
|
|
||||||
readonly fm4_phase_off: number;
|
|
||||||
readonly fm5_carrier: number;
|
|
||||||
readonly fm5_mod_freq: number;
|
|
||||||
readonly fm5_mod_index: number;
|
|
||||||
readonly fm5_phasor_mul: number;
|
|
||||||
readonly fm5_phase_off: number;
|
|
||||||
readonly fm6_carrier: number;
|
|
||||||
readonly fm6_mod_freq: number;
|
|
||||||
readonly fm6_mod_index: number;
|
|
||||||
readonly fm6_phasor_mul: number;
|
|
||||||
readonly fm6_phase_off: number;
|
|
||||||
readonly fm7_carrier: number;
|
|
||||||
readonly fm7_mod_freq: number;
|
|
||||||
readonly fm7_mod_index: number;
|
|
||||||
readonly fm7_phasor_mul: number;
|
|
||||||
readonly fm7_phase_off: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ElysiamorfSchema: ModeSchema = {
|
|
||||||
mode_id: 'elysiamorf',
|
|
||||||
engine_id: 'elysiamorf',
|
|
||||||
ml: {
|
|
||||||
input_channels: [
|
|
||||||
'joy_x',
|
|
||||||
'joy_y',
|
|
||||||
'joy_z',
|
|
||||||
'joy_w',
|
|
||||||
],
|
|
||||||
input_size: 4,
|
|
||||||
hidden_layers: [
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
18,
|
|
||||||
],
|
|
||||||
output_size: 40,
|
|
||||||
default_spread: 0.6,
|
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'fm0_carrier',
|
|
||||||
label: 'FM0 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm0_mod_freq',
|
|
||||||
label: 'FM0 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm0_mod_index',
|
|
||||||
label: 'FM0 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm0_phasor_mul',
|
|
||||||
label: 'FM0 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm0_phase_off',
|
|
||||||
label: 'FM0 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm1_carrier',
|
|
||||||
label: 'FM1 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm1_mod_freq',
|
|
||||||
label: 'FM1 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm1_mod_index',
|
|
||||||
label: 'FM1 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm1_phasor_mul',
|
|
||||||
label: 'FM1 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm1_phase_off',
|
|
||||||
label: 'FM1 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm2_carrier',
|
|
||||||
label: 'FM2 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm2_mod_freq',
|
|
||||||
label: 'FM2 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm2_mod_index',
|
|
||||||
label: 'FM2 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm2_phasor_mul',
|
|
||||||
label: 'FM2 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm2_phase_off',
|
|
||||||
label: 'FM2 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm3_carrier',
|
|
||||||
label: 'FM3 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm3_mod_freq',
|
|
||||||
label: 'FM3 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm3_mod_index',
|
|
||||||
label: 'FM3 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm3_phasor_mul',
|
|
||||||
label: 'FM3 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm3_phase_off',
|
|
||||||
label: 'FM3 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm4_carrier',
|
|
||||||
label: 'FM4 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm4_mod_freq',
|
|
||||||
label: 'FM4 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm4_mod_index',
|
|
||||||
label: 'FM4 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm4_phasor_mul',
|
|
||||||
label: 'FM4 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm4_phase_off',
|
|
||||||
label: 'FM4 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm5_carrier',
|
|
||||||
label: 'FM5 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm5_mod_freq',
|
|
||||||
label: 'FM5 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm5_mod_index',
|
|
||||||
label: 'FM5 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm5_phasor_mul',
|
|
||||||
label: 'FM5 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm5_phase_off',
|
|
||||||
label: 'FM5 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm6_carrier',
|
|
||||||
label: 'FM6 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm6',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm6_mod_freq',
|
|
||||||
label: 'FM6 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm6',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm6_mod_index',
|
|
||||||
label: 'FM6 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm6',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm6_phasor_mul',
|
|
||||||
label: 'FM6 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm6',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm6_phase_off',
|
|
||||||
label: 'FM6 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm6',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm7_carrier',
|
|
||||||
label: 'FM7 Carrier',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm7',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm7_mod_freq',
|
|
||||||
label: 'FM7 Mod Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm7',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm7_mod_index',
|
|
||||||
label: 'FM7 Mod Index',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm7',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm7_phasor_mul',
|
|
||||||
label: 'FM7 Phasor Mul',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm7',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fm7_phase_off',
|
|
||||||
label: 'FM7 Phase Off',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'fm7',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'xy_pad',
|
|
||||||
show_voice_space_selector: false,
|
|
||||||
show_synth_visualizer: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
// Re-exports every generated mode schema.
|
|
||||||
|
|
||||||
export * from './types';
|
|
||||||
export * from './breakor_schema';
|
|
||||||
export * from './channel_strip_schema';
|
|
||||||
export * from './elysiamorf_schema';
|
|
||||||
export * from './memlcelium_schema';
|
|
||||||
export * from './paf_synth_schema';
|
|
||||||
export * from './slp_workshop_schema';
|
|
||||||
export * from './sound_analysis_midi_schema';
|
|
||||||
export * from './verb_fx_schema';
|
|
||||||
export * from './xiasri_schema';
|
|
||||||
|
|
@ -1,598 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/memlcelium.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface MemlceliumParams {
|
|
||||||
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 MemlceliumSchema: ModeSchema = {
|
|
||||||
mode_id: 'memlcelium',
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/paf_synth.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface PafSynthParams {
|
|
||||||
readonly p00: number;
|
|
||||||
readonly p01: number;
|
|
||||||
readonly paf0_cf: number;
|
|
||||||
readonly paf1_cf: number;
|
|
||||||
readonly p04: number;
|
|
||||||
readonly paf0_bw: number;
|
|
||||||
readonly paf1_bw: number;
|
|
||||||
readonly p07: number;
|
|
||||||
readonly paf0_vib: number;
|
|
||||||
readonly paf1_vib: number;
|
|
||||||
readonly p10: number;
|
|
||||||
readonly paf0_vfr: number;
|
|
||||||
readonly paf1_vfr: number;
|
|
||||||
readonly p13: number;
|
|
||||||
readonly paf0_shift: number;
|
|
||||||
readonly paf1_shift: number;
|
|
||||||
readonly p16: number;
|
|
||||||
readonly dl1mix: number;
|
|
||||||
readonly p18: number;
|
|
||||||
readonly dlfb: number;
|
|
||||||
readonly env_decay: number;
|
|
||||||
readonly p21: number;
|
|
||||||
readonly p22: number;
|
|
||||||
readonly p23: number;
|
|
||||||
readonly p24: number;
|
|
||||||
readonly p25: number;
|
|
||||||
readonly shape_gain: number;
|
|
||||||
readonly shape_asym: number;
|
|
||||||
readonly shape_mix: number;
|
|
||||||
readonly rm_gain: number;
|
|
||||||
readonly env_attack: number;
|
|
||||||
readonly env_sustain: number;
|
|
||||||
readonly env_release: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PafSynthSchema: ModeSchema = {
|
|
||||||
mode_id: 'paf_synth',
|
|
||||||
engine_id: 'paf_synth',
|
|
||||||
ml: {
|
|
||||||
input_channels: [
|
|
||||||
'joy_x',
|
|
||||||
'joy_y',
|
|
||||||
'joy_z',
|
|
||||||
'joy_w',
|
|
||||||
],
|
|
||||||
input_size: 4,
|
|
||||||
hidden_layers: [
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
],
|
|
||||||
output_size: 33,
|
|
||||||
default_spread: 0.6,
|
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'p00',
|
|
||||||
label: 'Param 00',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p01',
|
|
||||||
label: 'Param 01',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf0_cf',
|
|
||||||
label: 'PAF0 Center Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'operators',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf1_cf',
|
|
||||||
label: 'PAF1 Center Freq',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'operators',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p04',
|
|
||||||
label: 'Param 04',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf0_bw',
|
|
||||||
label: 'PAF0 Bandwidth',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'operators',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf1_bw',
|
|
||||||
label: 'PAF1 Bandwidth',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'operators',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p07',
|
|
||||||
label: 'Param 07',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf0_vib',
|
|
||||||
label: 'PAF0 Vibrato Depth',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'modulation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf1_vib',
|
|
||||||
label: 'PAF1 Vibrato Depth',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'modulation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p10',
|
|
||||||
label: 'Param 10',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf0_vfr',
|
|
||||||
label: 'PAF0 Vibrato Rate',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'modulation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf1_vfr',
|
|
||||||
label: 'PAF1 Vibrato Rate',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'modulation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p13',
|
|
||||||
label: 'Param 13',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf0_shift',
|
|
||||||
label: 'PAF0 Shift',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'operators',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paf1_shift',
|
|
||||||
label: 'PAF1 Shift',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'operators',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p16',
|
|
||||||
label: 'Param 16',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl1mix',
|
|
||||||
label: 'Delay Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'delay',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p18',
|
|
||||||
label: 'Param 18',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dlfb',
|
|
||||||
label: 'Delay Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delay',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'env_decay',
|
|
||||||
label: 'Env Decay',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'envelope',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p21',
|
|
||||||
label: 'Param 21',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p22',
|
|
||||||
label: 'Param 22',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p23',
|
|
||||||
label: 'Param 23',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p24',
|
|
||||||
label: 'Param 24',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p25',
|
|
||||||
label: 'Param 25',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'shape_gain',
|
|
||||||
label: 'Sine Shape Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'shaper',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'shape_asym',
|
|
||||||
label: 'Sine Shape Asym',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'shaper',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'shape_mix',
|
|
||||||
label: 'Sine Shape Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'shaper',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rm_gain',
|
|
||||||
label: 'Ring Mod Gain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'shaper',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'env_attack',
|
|
||||||
label: 'Env Attack',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'envelope',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'env_sustain',
|
|
||||||
label: 'Env Sustain',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'envelope',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'env_release',
|
|
||||||
label: 'Env Release',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.3,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'envelope',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [
|
|
||||||
'Ellipticacacia',
|
|
||||||
'Rowantares',
|
|
||||||
'Neemeda',
|
|
||||||
'Aquillow',
|
|
||||||
'Magnetarch',
|
|
||||||
'Elderstar',
|
|
||||||
'Ipeleiades',
|
|
||||||
],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'xy_pad',
|
|
||||||
show_voice_space_selector: true,
|
|
||||||
show_synth_visualizer: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,598 +0,0 @@
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/sound_analysis_midi.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface SoundAnalysisMidiParams {
|
|
||||||
readonly midi_cc0: number;
|
|
||||||
readonly midi_cc1: number;
|
|
||||||
readonly midi_cc2: number;
|
|
||||||
readonly midi_cc3: number;
|
|
||||||
readonly midi_cc4: number;
|
|
||||||
readonly midi_cc5: number;
|
|
||||||
readonly midi_cc6: number;
|
|
||||||
readonly midi_cc7: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SoundAnalysisMidiSchema: ModeSchema = {
|
|
||||||
mode_id: 'sound_analysis_midi',
|
|
||||||
engine_id: 'thru',
|
|
||||||
ml: {
|
|
||||||
input_channels: [
|
|
||||||
'audio_pitch',
|
|
||||||
'audio_aperiodicity',
|
|
||||||
'audio_energy',
|
|
||||||
'audio_attack',
|
|
||||||
'audio_brightness',
|
|
||||||
'audio_energy_crude',
|
|
||||||
'joy_x',
|
|
||||||
'joy_y',
|
|
||||||
'joy_z',
|
|
||||||
'joy_w',
|
|
||||||
],
|
|
||||||
input_size: 10,
|
|
||||||
hidden_layers: [
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
],
|
|
||||||
output_size: 8,
|
|
||||||
default_spread: 0.6,
|
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'midi_cc0',
|
|
||||||
label: 'MIDI CC 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc1',
|
|
||||||
label: 'MIDI CC 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc2',
|
|
||||||
label: 'MIDI CC 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc3',
|
|
||||||
label: 'MIDI CC 3',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc4',
|
|
||||||
label: 'MIDI CC 4',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc5',
|
|
||||||
label: 'MIDI CC 5',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc6',
|
|
||||||
label: 'MIDI CC 6',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'midi_cc7',
|
|
||||||
label: 'MIDI CC 7',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'midi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'audio_in',
|
|
||||||
show_voice_space_selector: false,
|
|
||||||
show_synth_visualizer: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
// Shared TypeScript types for generated mode schemas.
|
|
||||||
|
|
||||||
export type Curve =
|
|
||||||
| 'linear'
|
|
||||||
| 'exp'
|
|
||||||
| 'log'
|
|
||||||
| 'square'
|
|
||||||
| 'sqrt'
|
|
||||||
| 'sigmoid'
|
|
||||||
| 'cubic';
|
|
||||||
|
|
||||||
export type PrimaryInput =
|
|
||||||
| 'xy_pad'
|
|
||||||
| 'joystick'
|
|
||||||
| 'sliders'
|
|
||||||
| 'audio_in'
|
|
||||||
| 'midi_in'
|
|
||||||
| 'none';
|
|
||||||
|
|
||||||
export interface Param {
|
|
||||||
readonly name: string;
|
|
||||||
readonly label: string;
|
|
||||||
readonly min: number;
|
|
||||||
readonly max: number;
|
|
||||||
readonly default: number;
|
|
||||||
readonly curve: Curve;
|
|
||||||
readonly group: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MLConfig {
|
|
||||||
readonly input_channels: readonly string[];
|
|
||||||
readonly input_size: number;
|
|
||||||
readonly hidden_layers: readonly number[];
|
|
||||||
readonly output_size: number;
|
|
||||||
readonly default_spread: number;
|
|
||||||
readonly default_learning_rate: number;
|
|
||||||
readonly default_max_iterations: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UIConfig {
|
|
||||||
readonly primary_input: PrimaryInput;
|
|
||||||
readonly show_voice_space_selector: boolean;
|
|
||||||
readonly show_synth_visualizer: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModeSchema {
|
|
||||||
readonly mode_id: string;
|
|
||||||
readonly engine_id: string;
|
|
||||||
readonly ml: MLConfig;
|
|
||||||
readonly params: readonly Param[];
|
|
||||||
readonly voice_spaces: readonly string[];
|
|
||||||
readonly ui: UIConfig;
|
|
||||||
}
|
|
||||||
|
|
@ -1,519 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/verb_fx.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface VerbFxParams {
|
|
||||||
readonly fb_delay_xfade: number;
|
|
||||||
readonly lp0_fb: number;
|
|
||||||
readonly lp0_cutoff: number;
|
|
||||||
readonly lp1_fb: number;
|
|
||||||
readonly lp1_cutoff: number;
|
|
||||||
readonly lp2_fb: number;
|
|
||||||
readonly lp2_cutoff: number;
|
|
||||||
readonly lp3_fb: number;
|
|
||||||
readonly lp3_cutoff: number;
|
|
||||||
readonly lp4_fb: number;
|
|
||||||
readonly lp4_cutoff: number;
|
|
||||||
readonly lp5_fb: number;
|
|
||||||
readonly lp5_cutoff: number;
|
|
||||||
readonly lp6_fb: number;
|
|
||||||
readonly lp6_cutoff: number;
|
|
||||||
readonly lp7_fb: number;
|
|
||||||
readonly lp7_cutoff: number;
|
|
||||||
readonly allp0_fb: number;
|
|
||||||
readonly allp1_fb: number;
|
|
||||||
readonly allp2_fb: number;
|
|
||||||
readonly allp3_fb: number;
|
|
||||||
readonly fbank_f0: number;
|
|
||||||
readonly fbank_f1: number;
|
|
||||||
readonly fbank_f2: number;
|
|
||||||
readonly fbank_f3: number;
|
|
||||||
readonly fbank_f4: number;
|
|
||||||
readonly fbank_f5: number;
|
|
||||||
readonly fbank_f6: number;
|
|
||||||
readonly fbank_f7: number;
|
|
||||||
readonly fbank_res0: number;
|
|
||||||
readonly fbank_res1: number;
|
|
||||||
readonly fbank_res2: number;
|
|
||||||
readonly fbank_res3: number;
|
|
||||||
readonly fbank_res4: number;
|
|
||||||
readonly fbank_res5: number;
|
|
||||||
readonly fbank_res6: number;
|
|
||||||
readonly fbank_res7: number;
|
|
||||||
readonly delay0_time: number;
|
|
||||||
readonly delay0_fb: number;
|
|
||||||
readonly delay1_time: number;
|
|
||||||
readonly delay1_fb: number;
|
|
||||||
readonly delay2_time: number;
|
|
||||||
readonly delay2_fb: number;
|
|
||||||
readonly verb_vs_delay: number;
|
|
||||||
readonly delay_to_verb: number;
|
|
||||||
readonly delay_morph: number;
|
|
||||||
readonly delay_blend: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VerbFxSchema: ModeSchema = {
|
|
||||||
mode_id: 'verb_fx',
|
|
||||||
engine_id: 'verb_fx',
|
|
||||||
ml: {
|
|
||||||
input_channels: [
|
|
||||||
'joy_x',
|
|
||||||
'joy_y',
|
|
||||||
'joy_z',
|
|
||||||
'joy_w',
|
|
||||||
],
|
|
||||||
input_size: 4,
|
|
||||||
hidden_layers: [
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
18,
|
|
||||||
],
|
|
||||||
output_size: 47,
|
|
||||||
default_spread: 0.6,
|
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'fb_delay_xfade',
|
|
||||||
label: 'Bank/Delay XFade',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'routing',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp0_fb',
|
|
||||||
label: 'LP Comb 0 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp0_cutoff',
|
|
||||||
label: 'LP Comb 0 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp1_fb',
|
|
||||||
label: 'LP Comb 1 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp1_cutoff',
|
|
||||||
label: 'LP Comb 1 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp2_fb',
|
|
||||||
label: 'LP Comb 2 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp2_cutoff',
|
|
||||||
label: 'LP Comb 2 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp3_fb',
|
|
||||||
label: 'LP Comb 3 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp3_cutoff',
|
|
||||||
label: 'LP Comb 3 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp4_fb',
|
|
||||||
label: 'LP Comb 4 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp4_cutoff',
|
|
||||||
label: 'LP Comb 4 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp5_fb',
|
|
||||||
label: 'LP Comb 5 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp5_cutoff',
|
|
||||||
label: 'LP Comb 5 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp6_fb',
|
|
||||||
label: 'LP Comb 6 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp6_cutoff',
|
|
||||||
label: 'LP Comb 6 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp7_fb',
|
|
||||||
label: 'LP Comb 7 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lp7_cutoff',
|
|
||||||
label: 'LP Comb 7 Cutoff',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp0_fb',
|
|
||||||
label: 'Allpass 0 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp1_fb',
|
|
||||||
label: 'Allpass 1 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp2_fb',
|
|
||||||
label: 'Allpass 2 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp3_fb',
|
|
||||||
label: 'Allpass 3 FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f0',
|
|
||||||
label: 'Filterbank Freq 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f1',
|
|
||||||
label: 'Filterbank Freq 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f2',
|
|
||||||
label: 'Filterbank Freq 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f3',
|
|
||||||
label: 'Filterbank Freq 3',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f4',
|
|
||||||
label: 'Filterbank Freq 4',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f5',
|
|
||||||
label: 'Filterbank Freq 5',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f6',
|
|
||||||
label: 'Filterbank Freq 6',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_f7',
|
|
||||||
label: 'Filterbank Freq 7',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res0',
|
|
||||||
label: 'Filterbank Res 0',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res1',
|
|
||||||
label: 'Filterbank Res 1',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res2',
|
|
||||||
label: 'Filterbank Res 2',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res3',
|
|
||||||
label: 'Filterbank Res 3',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res4',
|
|
||||||
label: 'Filterbank Res 4',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res5',
|
|
||||||
label: 'Filterbank Res 5',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res6',
|
|
||||||
label: 'Filterbank Res 6',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fbank_res7',
|
|
||||||
label: 'Filterbank Res 7',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'filterbank',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay0_time',
|
|
||||||
label: 'Long Delay Time',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay0_fb',
|
|
||||||
label: 'Long Delay FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay1_time',
|
|
||||||
label: 'Med Delay Time',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay1_fb',
|
|
||||||
label: 'Med Delay FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay2_time',
|
|
||||||
label: 'Short Delay Time',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay2_fb',
|
|
||||||
label: 'Short Delay FB',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'verb_vs_delay',
|
|
||||||
label: 'Verb vs Delay',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'routing',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay_to_verb',
|
|
||||||
label: 'Delay→Verb Send',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'routing',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay_morph',
|
|
||||||
label: 'Delay Morph',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delay_blend',
|
|
||||||
label: 'Delay Blend',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [
|
|
||||||
'Default',
|
|
||||||
'Resonant',
|
|
||||||
'Soft',
|
|
||||||
'Cathedral',
|
|
||||||
'Shimmer',
|
|
||||||
'Chamber',
|
|
||||||
'Metallic',
|
|
||||||
'Granular',
|
|
||||||
'Diffuse',
|
|
||||||
'Dark',
|
|
||||||
'Bright',
|
|
||||||
'Harmonic',
|
|
||||||
],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'joystick',
|
|
||||||
show_voice_space_selector: true,
|
|
||||||
show_synth_visualizer: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
// AUTOGENERATED — do not edit. Source: schemas/modes/xiasri.json. Run `bun run codegen/generate.ts` to regenerate.
|
|
||||||
import type { ModeSchema } from './types';
|
|
||||||
|
|
||||||
export interface XiasriParams {
|
|
||||||
readonly dl1_mix: number;
|
|
||||||
readonly dl2_mix: number;
|
|
||||||
readonly dl3_mix: number;
|
|
||||||
readonly p03: number;
|
|
||||||
readonly allp1_fb: number;
|
|
||||||
readonly allp2_fb: number;
|
|
||||||
readonly comb1_fb: number;
|
|
||||||
readonly comb2_fb: number;
|
|
||||||
readonly dl1_fb: number;
|
|
||||||
readonly dl2_fb: number;
|
|
||||||
readonly dl3_fb: number;
|
|
||||||
readonly wetdry_mix: number;
|
|
||||||
readonly pitch_transp: number;
|
|
||||||
readonly pitch_mix: number;
|
|
||||||
readonly allp3_fb: number;
|
|
||||||
readonly allp4_fb: number;
|
|
||||||
readonly allp5_fb: number;
|
|
||||||
readonly allp6_fb: number;
|
|
||||||
readonly allp3_mix: number;
|
|
||||||
readonly allp4_mix: number;
|
|
||||||
readonly allp5_mix: number;
|
|
||||||
readonly allp6_mix: number;
|
|
||||||
readonly dl4_mix: number;
|
|
||||||
readonly dl4_fb: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const XiasriSchema: ModeSchema = {
|
|
||||||
mode_id: 'xiasri',
|
|
||||||
engine_id: 'xiasri',
|
|
||||||
ml: {
|
|
||||||
input_channels: [
|
|
||||||
'joy_x',
|
|
||||||
'joy_y',
|
|
||||||
'joy_z',
|
|
||||||
'joy_w',
|
|
||||||
],
|
|
||||||
input_size: 4,
|
|
||||||
hidden_layers: [
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
14,
|
|
||||||
],
|
|
||||||
output_size: 24,
|
|
||||||
default_spread: 0.6,
|
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
name: 'dl1_mix',
|
|
||||||
label: 'Delay 1 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl2_mix',
|
|
||||||
label: 'Delay 2 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl3_mix',
|
|
||||||
label: 'Delay 3 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'p03',
|
|
||||||
label: 'Param 03',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'general',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp1_fb',
|
|
||||||
label: 'Allpass 1 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp2_fb',
|
|
||||||
label: 'Allpass 2 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comb1_fb',
|
|
||||||
label: 'Comb 1 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'comb2_fb',
|
|
||||||
label: 'Comb 2 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl1_fb',
|
|
||||||
label: 'Delay 1 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl2_fb',
|
|
||||||
label: 'Delay 2 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl3_fb',
|
|
||||||
label: 'Delay 3 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'wetdry_mix',
|
|
||||||
label: 'Wet/Dry Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'mix',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pitch_transp',
|
|
||||||
label: 'Pitch Transpose',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0.5,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'pitch',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pitch_mix',
|
|
||||||
label: 'Pitch Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'pitch',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp3_fb',
|
|
||||||
label: 'Allpass 3 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp4_fb',
|
|
||||||
label: 'Allpass 4 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp5_fb',
|
|
||||||
label: 'Allpass 5 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp6_fb',
|
|
||||||
label: 'Allpass 6 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp3_mix',
|
|
||||||
label: 'Allpass 3 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp4_mix',
|
|
||||||
label: 'Allpass 4 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp5_mix',
|
|
||||||
label: 'Allpass 5 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allp6_mix',
|
|
||||||
label: 'Allpass 6 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'verb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl4_mix',
|
|
||||||
label: 'Delay 4 Mix',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'square',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'dl4_fb',
|
|
||||||
label: 'Delay 4 Feedback',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
default: 0,
|
|
||||||
curve: 'linear',
|
|
||||||
group: 'delays',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
voice_spaces: [
|
|
||||||
'Direct',
|
|
||||||
],
|
|
||||||
ui: {
|
|
||||||
primary_input: 'joystick',
|
|
||||||
show_voice_space_selector: false,
|
|
||||||
show_synth_visualizer: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
/**
|
|
||||||
* Mode registry — maps mode_id (and the special "c15" placeholder) to the
|
|
||||||
* TSX component that renders it. The ModeSwitcher reads from this list to
|
|
||||||
* populate its dropdown, and `App.tsx` looks up the active mode here.
|
|
||||||
*
|
|
||||||
* Order matters — the switcher renders modes in this order.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Component } from 'solid-js';
|
|
||||||
import { PAFSynthMode } from './PAFSynthMode';
|
|
||||||
import { ChannelStripMode } from './ChannelStripMode';
|
|
||||||
import { XIASRIMode } from './XIASRIMode';
|
|
||||||
import { VerbFXMode } from './VerbFXMode';
|
|
||||||
import { MEMLCeliumMode } from './MEMLCeliumMode';
|
|
||||||
import { SLPWorkshopMode } from './SLPWorkshopMode';
|
|
||||||
import { BreakOrMode } from './BreakOrMode';
|
|
||||||
import { ElysiamorfMode } from './ElysiamorfMode';
|
|
||||||
import { SoundAnalysisMIDIMode } from './SoundAnalysisMIDIMode';
|
|
||||||
import { C15Mode } from './C15Mode';
|
|
||||||
|
|
||||||
export interface ModeRegistration {
|
|
||||||
/** Stable id; matches the schema's `mode_id` for firmware modes. */
|
|
||||||
id: string;
|
|
||||||
/** Human-readable label for the switcher. */
|
|
||||||
label: string;
|
|
||||||
/** Short description for the switcher tooltip. */
|
|
||||||
description: string;
|
|
||||||
/** TSX component rendering the mode. */
|
|
||||||
Component: Component;
|
|
||||||
/** True for browser-only placeholders (currently just C15). */
|
|
||||||
placeholder?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MODE_REGISTRY: ReadonlyArray<ModeRegistration> = [
|
|
||||||
{
|
|
||||||
id: 'paf_synth',
|
|
||||||
label: 'PAF Synth',
|
|
||||||
description: 'Phase-aligned formant synth (XY pad).',
|
|
||||||
Component: PAFSynthMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'channel_strip',
|
|
||||||
label: 'Channel Strip',
|
|
||||||
description: 'EQ + dynamics processing channel.',
|
|
||||||
Component: ChannelStripMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'xiasri',
|
|
||||||
label: 'XIASRI',
|
|
||||||
description: 'Audio-reactive verb / pitch engine.',
|
|
||||||
Component: XIASRIMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'verb_fx',
|
|
||||||
label: 'Verb FX',
|
|
||||||
description: 'Reverb / multi-effects unit.',
|
|
||||||
Component: VerbFXMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'memlcelium',
|
|
||||||
label: 'MEML Celium',
|
|
||||||
description: 'Voice + dual-MLP CV/gate via uSEQ.',
|
|
||||||
Component: MEMLCeliumMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'slp_workshop',
|
|
||||||
label: 'SLP-Workshop',
|
|
||||||
description: 'MEMLCelium voice + live Jolt / Explore learning (Synth Library Portland).',
|
|
||||||
Component: SLPWorkshopMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'breakor',
|
|
||||||
label: 'Breakor',
|
|
||||||
description: 'Drum / breakbeat synthesis.',
|
|
||||||
Component: BreakOrMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'elysiamorf',
|
|
||||||
label: 'Elysiamorf',
|
|
||||||
description: 'Granular morphing synth.',
|
|
||||||
Component: ElysiamorfMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sound_analysis_midi',
|
|
||||||
label: 'Sound Analysis → MIDI',
|
|
||||||
description: 'Audio features → MIDI CC routing.',
|
|
||||||
Component: SoundAnalysisMIDIMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'c15',
|
|
||||||
label: 'C15 (browser-only)',
|
|
||||||
description: 'C15 WASM synth. Not yet ported.',
|
|
||||||
Component: C15Mode,
|
|
||||||
placeholder: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Look up a mode by id, falling back to the first registration. */
|
|
||||||
export function getModeById(id: string | null): ModeRegistration {
|
|
||||||
if (!id) return MODE_REGISTRY[0]!;
|
|
||||||
return MODE_REGISTRY.find((m) => m.id === id) ?? MODE_REGISTRY[0]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ModeShell } from './ModeShell';
|
|
||||||
export { useModeRuntime } from './mode-runtime';
|
|
||||||
export type { ModeRuntime } from './mode-runtime';
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/**
|
|
||||||
* Helpers shared across mode TSX files. Pure functions; no Solid state.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Param } from './generated/types';
|
|
||||||
import type { SliderConfig } from '../primitives/SliderBank';
|
|
||||||
import type { CurveName } from '../output/curves';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a schema param list into SliderConfig entries that the SliderBank
|
|
||||||
* primitive understands. Sliders are grouped by the schema's `group` field
|
|
||||||
* so the bank renders collapsible sections.
|
|
||||||
*/
|
|
||||||
export function paramsToSliderConfig(params: ReadonlyArray<Param>): SliderConfig[] {
|
|
||||||
let lastGroup: string | null = null;
|
|
||||||
return params.map((p) => {
|
|
||||||
const isNewGroup = p.group !== lastGroup;
|
|
||||||
lastGroup = p.group;
|
|
||||||
const cfg: SliderConfig = {
|
|
||||||
id: p.name,
|
|
||||||
label: p.label,
|
|
||||||
min: p.min,
|
|
||||||
max: p.max,
|
|
||||||
curve: p.curve as CurveName,
|
|
||||||
};
|
|
||||||
if (isNewGroup) cfg.section = formatGroupName(p.group);
|
|
||||||
return cfg;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a Float32Array (length = N) of normalized values [0,1] to a flat
|
|
||||||
* array sized to match the slider config (already in min/max range).
|
|
||||||
*
|
|
||||||
* If `overrides` is supplied, per-param min/max overrides are honoured
|
|
||||||
* instead of the schema defaults — this is what the mode UI sliders
|
|
||||||
* display when the user has tightened the active range.
|
|
||||||
*/
|
|
||||||
export function outputsToSliderValues(
|
|
||||||
outputs: Float32Array,
|
|
||||||
params: ReadonlyArray<Param>,
|
|
||||||
overrides?: Record<string, { min: number; max: number; muted?: boolean; fixedValue?: number }>,
|
|
||||||
): number[] {
|
|
||||||
const out: number[] = [];
|
|
||||||
for (let i = 0; i < params.length; ++i) {
|
|
||||||
const v = outputs[i] ?? 0;
|
|
||||||
const p = params[i]!;
|
|
||||||
const ov = overrides?.[p.name];
|
|
||||||
if (ov && ov.muted && ov.fixedValue !== undefined) {
|
|
||||||
out.push(ov.fixedValue);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const min = ov?.min ?? p.min;
|
|
||||||
const max = ov?.max ?? p.max;
|
|
||||||
out.push(min + v * (max - min));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatGroupName(group: string): string {
|
|
||||||
if (!group) return '';
|
|
||||||
return group
|
|
||||||
.split(/[_\s]+/)
|
|
||||||
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
@ -1,785 +0,0 @@
|
||||||
/**
|
|
||||||
* Mode runtime — shared wiring between mode TSX components and the
|
|
||||||
* playground's stores / WASM ML / audio engine host.
|
|
||||||
*
|
|
||||||
* Every mode does the same dance:
|
|
||||||
* 1. Hold a primary 2D input position (joystick / xy-pad / external feed).
|
|
||||||
* 2. Push it through the input pipeline.
|
|
||||||
* 3. Forward the processed (x, y) to the WASM MLP as input channels [0..N].
|
|
||||||
* Modes with input_size > 2 zero-pad the unused channels (or the mic
|
|
||||||
* analyser fills 0..3 if active).
|
|
||||||
* 4. Pull the WASM outputs (Float32Array of 126), slice to the schema's
|
|
||||||
* `output_size`, and run them through the output pipeline.
|
|
||||||
* 5. Apply per-param overrides (modeStore.activeOverrides).
|
|
||||||
* 6. Throttle + ship the processed slice to the AudioWorklet engine.
|
|
||||||
*
|
|
||||||
* Stream 10 wires:
|
|
||||||
* - Compound axes → underlying stores (input/output/exploration).
|
|
||||||
* - Per-param overrides between MLP outputs and engine.
|
|
||||||
* - Snapshot + undo + A/B compare on RL events.
|
|
||||||
* - Pin mask passed to moveWeights.
|
|
||||||
* - Auto-explore interval timer.
|
|
||||||
* - Trail tracking for JoyMap.
|
|
||||||
* - Heatmap sampler invalidated on weight-change events.
|
|
||||||
* - Mic features pushed into MLP for audio-input modes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { batch, createEffect, createMemo, createSignal, onCleanup, onMount, untrack } from 'solid-js';
|
|
||||||
|
|
||||||
import { mlStore, modeStore, controlStore, sessionStore, explorationStore } from '../stores';
|
|
||||||
import { inputStore } from '../stores/input-store';
|
|
||||||
import { outputStore } from '../stores/output-store';
|
|
||||||
import { coreBus } from '../stores/bus';
|
|
||||||
import { processInput, defaultInputState, type InputState } from '../input/pipeline';
|
|
||||||
import {
|
|
||||||
processOutput,
|
|
||||||
defaultOutputState,
|
|
||||||
type OutputState,
|
|
||||||
} from '../output/pipeline';
|
|
||||||
import { EngineHost } from '../audio/engine-host';
|
|
||||||
import type { EngineId } from '../ml/types';
|
|
||||||
import type { ModeSchema } from './generated';
|
|
||||||
|
|
||||||
import { applyOverrides, buildPinMask } from '../features/overrides';
|
|
||||||
import { applyControlRouting } from '../features/control-routing';
|
|
||||||
import { Jolt } from '../ml/jolt';
|
|
||||||
import { OUExplore } from '../output/ou-explore';
|
|
||||||
import { createTrailRing, type TrailPoint } from '../features/trail';
|
|
||||||
import { autoSnapshot, undoLastSnapshot } from '../features/snapshots';
|
|
||||||
import { HeatmapSampler, type HeatmapColorMode } from '../features/heatmap-sampler';
|
|
||||||
import { MicInput } from '../features/mic-input';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throttle interval for engine param updates (ms). 50ms ≈ 20fps which
|
|
||||||
* matches the legacy playground's C15 update cadence.
|
|
||||||
*/
|
|
||||||
const ENGINE_PARAM_THROTTLE_MS = 50;
|
|
||||||
|
|
||||||
/** A single shared EngineHost. Audio only starts on user gesture. */
|
|
||||||
let engineHost: EngineHost | null = null;
|
|
||||||
function getEngineHost(): EngineHost {
|
|
||||||
if (!engineHost) engineHost = new EngineHost();
|
|
||||||
return engineHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModeRuntime {
|
|
||||||
/** Driver — call from joystick/xy-pad/etc. */
|
|
||||||
setInput: (x: number, y: number) => void;
|
|
||||||
|
|
||||||
/** Most recent processed input (after pipeline). */
|
|
||||||
pipedInput: () => readonly [number, number];
|
|
||||||
|
|
||||||
/** Whether the input is currently frozen by zoom. */
|
|
||||||
frozen: () => boolean;
|
|
||||||
|
|
||||||
/** Raw 126-output ML vector (live). */
|
|
||||||
rawOutputs: () => Float32Array;
|
|
||||||
|
|
||||||
/** Output-sliced + pipeline-processed vector (length = schema.output_size). */
|
|
||||||
processedOutputs: () => Float32Array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Final per-param values after override application (length = params.length).
|
|
||||||
* These are what get shipped to the engine and visualised in sliders.
|
|
||||||
*/
|
|
||||||
paramOutputs: () => Float32Array;
|
|
||||||
|
|
||||||
/** True iff WASM has loaded and the MLP is ready. */
|
|
||||||
ready: () => boolean;
|
|
||||||
|
|
||||||
/** Audio host control. */
|
|
||||||
audio: {
|
|
||||||
started: () => boolean;
|
|
||||||
start: () => Promise<void>;
|
|
||||||
stop: () => Promise<void>;
|
|
||||||
setMuted: (muted: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Mic input control (for audio_in modes; safe to call on others — no-op). */
|
|
||||||
mic: {
|
|
||||||
started: () => boolean;
|
|
||||||
start: () => Promise<void>;
|
|
||||||
stop: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Loss / training plumbing surfaced from mlStore. */
|
|
||||||
training: {
|
|
||||||
busy: () => boolean;
|
|
||||||
examples: () => number;
|
|
||||||
lastLoss: () => number | null;
|
|
||||||
lossHistory: () => ReadonlyArray<number>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Trigger a sync train + push the current pipeline-processed sample. */
|
|
||||||
trainOnCurrent: () => void;
|
|
||||||
|
|
||||||
/** RL callbacks. */
|
|
||||||
thumbsUp: () => void;
|
|
||||||
thumbsDown: () => void;
|
|
||||||
randomize: () => void;
|
|
||||||
/** Pop snapshot stack and restore weights. Returns true on success. */
|
|
||||||
undo: () => boolean;
|
|
||||||
|
|
||||||
/** Whether the snapshot stack has anything to undo. */
|
|
||||||
canUndo: () => boolean;
|
|
||||||
|
|
||||||
/** Trail ring for JoyMap binding. */
|
|
||||||
trail: () => ReadonlyArray<TrailPoint>;
|
|
||||||
/** Snap input back to a trail point. */
|
|
||||||
snapToTrail: (p: { x: number; y: number }) => void;
|
|
||||||
|
|
||||||
/** Heatmap sampler. Returns the underlying cells; modes pass to <Heatmap>. */
|
|
||||||
heatmap: {
|
|
||||||
cells: () => Float32Array;
|
|
||||||
setColorMode: (m: HeatmapColorMode) => void;
|
|
||||||
colorMode: () => HeatmapColorMode;
|
|
||||||
refresh: (force?: boolean) => void;
|
|
||||||
resolution: () => number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Region pin: pin the current zoom window (long-press handler). */
|
|
||||||
pinCurrentRegion: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jolt — held-button continuous weight morph (port of nisps/ml/jolt.hpp).
|
|
||||||
* `press()` starts morphing the network's weights; a control-rate timer
|
|
||||||
* glides them while held; `release()` freezes them in place. Inert until
|
|
||||||
* pressed; modes that never call `press()` are unaffected.
|
|
||||||
*/
|
|
||||||
jolt: {
|
|
||||||
/** Begin the jolt (button press / toggle on). */
|
|
||||||
press: () => void;
|
|
||||||
/** Freeze the jolt (button release / toggle off). */
|
|
||||||
release: () => void;
|
|
||||||
/** Whether the jolt is currently active. */
|
|
||||||
active: () => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Explore — OU random-walk exploration intensity in [0,1] (port of
|
|
||||||
* nisps/ml/ou_noise.hpp). Added to the mode's output vector before audio.
|
|
||||||
* Intensity 0 = disabled passthrough; modes that never set it are
|
|
||||||
* unaffected.
|
|
||||||
*/
|
|
||||||
explore: {
|
|
||||||
setIntensity: (level: number) => void;
|
|
||||||
intensity: () => number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RuntimeOptions {
|
|
||||||
/** Override the engine id (defaults to schema.engine_id). */
|
|
||||||
engineOverride?: EngineId;
|
|
||||||
/** Skip starting the audio engine even on user gesture. */
|
|
||||||
audioDisabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the runtime for a given mode schema. Call from inside a Solid
|
|
||||||
* component (uses createSignal/onCleanup).
|
|
||||||
*/
|
|
||||||
export function useModeRuntime(
|
|
||||||
schema: ModeSchema,
|
|
||||||
opts: RuntimeOptions = {},
|
|
||||||
): ModeRuntime {
|
|
||||||
// ----- WASM init ---------------------------------------------------------
|
|
||||||
const [ready, setReady] = createSignal(mlStore.state.ready);
|
|
||||||
|
|
||||||
// Lazy initialise WASM. Idempotent across remounts.
|
|
||||||
void mlStore
|
|
||||||
.initialize(schema.ml.input_size, schema.ml.output_size)
|
|
||||||
.then(() => setReady(true))
|
|
||||||
.catch((err) => {
|
|
||||||
// Best-effort; UI keeps running without ML.
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('[mode-runtime] mlStore.initialize failed:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Mode-store side effects ------------------------------------------
|
|
||||||
// Make sure the active mode in the store matches what's actually rendered.
|
|
||||||
if (modeStore.state.activeModeId !== schema.mode_id) {
|
|
||||||
modeStore.switchMode(schema.mode_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Compound axis routing -------------------------------------------
|
|
||||||
// Whenever any axis or offset changes, re-derive the underlying store
|
|
||||||
// values. Track axis reads explicitly; do the writes inside untrack so
|
|
||||||
// we don't pick up stale dependencies on write-targets (input/output/
|
|
||||||
// exploration stores).
|
|
||||||
createEffect(() => {
|
|
||||||
void controlStore.state.boldness;
|
|
||||||
void controlStore.state.memory;
|
|
||||||
void controlStore.state.precision;
|
|
||||||
void controlStore.state.offsets.boldness;
|
|
||||||
void controlStore.state.offsets.memory;
|
|
||||||
void controlStore.state.offsets.precision;
|
|
||||||
untrack(() => applyControlRouting());
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Input pipeline state --------------------------------------------
|
|
||||||
const [pipedInput, setPipedInput] = createSignal<readonly [number, number]>([0.5, 0.5]);
|
|
||||||
const [frozen, setFrozen] = createSignal(false);
|
|
||||||
let inputState: InputState = defaultInputState();
|
|
||||||
let lastFrameMs = performance.now();
|
|
||||||
|
|
||||||
// Trail ring
|
|
||||||
const trailRing = createTrailRing();
|
|
||||||
|
|
||||||
// Pressure tracking (set externally via setPressure on touch).
|
|
||||||
let pressDownAt: number | null = null;
|
|
||||||
|
|
||||||
const setInput = (rawX: number, rawY: number): void => {
|
|
||||||
const now = performance.now();
|
|
||||||
const dt = Math.max(0.001, (now - lastFrameMs) / 1000);
|
|
||||||
lastFrameMs = now;
|
|
||||||
|
|
||||||
const result = processInput([rawX, rawY], inputStore.config, inputState, dt);
|
|
||||||
inputState = result.state;
|
|
||||||
inputStore.__setLiveState(result.state);
|
|
||||||
setPipedInput([result.x, result.y]);
|
|
||||||
setFrozen(result.frozen);
|
|
||||||
trailRing.push(result.x, result.y);
|
|
||||||
|
|
||||||
if (!ready()) return;
|
|
||||||
// Push input to the MLP. Channels beyond [x,y] are zeroed out (or
|
|
||||||
// overridden with mic features in audio-input modes).
|
|
||||||
const inSz = schema.ml.input_size;
|
|
||||||
mlStore.setInput(0, result.x);
|
|
||||||
if (inSz > 1) mlStore.setInput(1, result.y);
|
|
||||||
// Mic features (if active) take channels 2..5.
|
|
||||||
if (mic.isRunning() && inSz > 2) {
|
|
||||||
const f = mic.getFeatures();
|
|
||||||
const fv = [f.energy, f.brightness, f.pitch, f.aperiodicity];
|
|
||||||
for (let i = 2; i < inSz; ++i) {
|
|
||||||
mlStore.setInput(i, fv[i - 2] ?? 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 2; i < inSz; ++i) mlStore.setInput(i, 0);
|
|
||||||
}
|
|
||||||
mlStore.process();
|
|
||||||
|
|
||||||
// Update pressure-feedback hold timer.
|
|
||||||
if (pressDownAt !== null) {
|
|
||||||
explorationStore.setPressure(
|
|
||||||
explorationStore.state.pressureForce,
|
|
||||||
now - pressDownAt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----- Output pipeline + override application --------------------------
|
|
||||||
let outputState: OutputState = defaultOutputState();
|
|
||||||
const sliceLen = schema.ml.output_size;
|
|
||||||
const [processedOutputs, setProcessedOutputs] = createSignal<Float32Array>(
|
|
||||||
new Float32Array(sliceLen),
|
|
||||||
{ equals: false },
|
|
||||||
);
|
|
||||||
const paramCount = schema.params.length;
|
|
||||||
let prevParamOutputs: Float32Array | null = null;
|
|
||||||
const [paramOutputs, setParamOutputs] = createSignal<Float32Array>(
|
|
||||||
new Float32Array(paramCount),
|
|
||||||
{ equals: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
// ----- Explore (OU exploration noise) ----------------------------------
|
|
||||||
// Inert by default (intensity 0). Applied to the processed slice below,
|
|
||||||
// after the output pipeline and before override application, so it rides
|
|
||||||
// on top of the mode's mapping output. Modes that never set an intensity
|
|
||||||
// leave the output untouched (parity-safe).
|
|
||||||
const ouExplore = new OUExplore();
|
|
||||||
const [exploreIntensity, setExploreIntensitySig] = createSignal(0);
|
|
||||||
|
|
||||||
// Run the output pipeline whenever raw outputs change.
|
|
||||||
const rawOutputsAccessor = mlStore.outputs;
|
|
||||||
let lastOutFrameMs = performance.now();
|
|
||||||
|
|
||||||
const recomputeOutputs = () => {
|
|
||||||
const raw = rawOutputsAccessor();
|
|
||||||
if (raw.length === 0) return;
|
|
||||||
const now = performance.now();
|
|
||||||
const dtMs = Math.max(1, now - lastOutFrameMs);
|
|
||||||
lastOutFrameMs = now;
|
|
||||||
// Slice to mode's output_size up front.
|
|
||||||
const slice = raw.length === sliceLen ? raw : raw.subarray(0, sliceLen);
|
|
||||||
const result = processOutput(slice as Float32Array, outputStore.config, outputState, dtMs);
|
|
||||||
outputState = result.state;
|
|
||||||
// Explore (OU) noise: temporally-correlated random walk added on top of
|
|
||||||
// the processed output, clamped to [0,1]. No-op when intensity is 0.
|
|
||||||
ouExplore.apply(result.processed);
|
|
||||||
setProcessedOutputs(result.processed);
|
|
||||||
|
|
||||||
// Apply per-param overrides for the per-param consumer (engine, sliders).
|
|
||||||
// Only the first `params.length` outputs are user-visible parameters; the
|
|
||||||
// remainder is reserved for engine internals (none right now).
|
|
||||||
const paramSlice = result.processed.length === paramCount
|
|
||||||
? result.processed
|
|
||||||
: result.processed.subarray(0, paramCount);
|
|
||||||
const overrides = modeStore.state.overrides[schema.mode_id] ?? {};
|
|
||||||
const applied = applyOverrides(
|
|
||||||
paramSlice as Float32Array,
|
|
||||||
prevParamOutputs,
|
|
||||||
schema.params,
|
|
||||||
overrides,
|
|
||||||
);
|
|
||||||
prevParamOutputs = applied.values;
|
|
||||||
setParamOutputs(applied.values);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Trigger recompute on any raw-output change. Wrap in untrack so reading
|
|
||||||
// outputStore.config inside processOutput doesn't add a tracked dep here.
|
|
||||||
createEffect(() => {
|
|
||||||
rawOutputsAccessor();
|
|
||||||
untrack(recomputeOutputs);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-run override application when overrides change (no new ML output).
|
|
||||||
createEffect(() => {
|
|
||||||
void modeStore.state.overrides[schema.mode_id];
|
|
||||||
untrack(() => {
|
|
||||||
const raw = rawOutputsAccessor();
|
|
||||||
if (raw.length > 0) recomputeOutputs();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Push the freeze mask into outputStore whenever overrides change.
|
|
||||||
createEffect(() => {
|
|
||||||
const ovs = modeStore.state.overrides[schema.mode_id] ?? {};
|
|
||||||
let mask: Uint8Array | null = null;
|
|
||||||
for (let i = 0; i < schema.params.length; ++i) {
|
|
||||||
const ov = ovs[schema.params[i]!.name];
|
|
||||||
if (ov && ov.frozen) {
|
|
||||||
if (!mask) mask = new Uint8Array(schema.params.length);
|
|
||||||
mask[i] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
untrack(() => outputStore.setFreezeMask(mask));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Engine wiring (audio) -------------------------------------------
|
|
||||||
const host = getEngineHost();
|
|
||||||
const [audioStarted, setAudioStarted] = createSignal(host.isStarted);
|
|
||||||
let pendingParams: Float32Array | null = null;
|
|
||||||
let throttleTimer: number | null = null;
|
|
||||||
|
|
||||||
const flushParams = () => {
|
|
||||||
throttleTimer = null;
|
|
||||||
if (!pendingParams || !host.isStarted) {
|
|
||||||
pendingParams = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const copy = new Float32Array(pendingParams);
|
|
||||||
pendingParams = null;
|
|
||||||
try {
|
|
||||||
host.setParams(copy);
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[mode-runtime] setParams failed:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleParamFlush = (params: Float32Array) => {
|
|
||||||
pendingParams = params;
|
|
||||||
if (throttleTimer === null) {
|
|
||||||
throttleTimer = window.setTimeout(flushParams, ENGINE_PARAM_THROTTLE_MS);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pipe paramOutputs into the engine host whenever they change.
|
|
||||||
createEffect(() => {
|
|
||||||
const out = paramOutputs();
|
|
||||||
if (out.length === 0) return;
|
|
||||||
if (!host.isStarted) return;
|
|
||||||
scheduleParamFlush(out);
|
|
||||||
});
|
|
||||||
|
|
||||||
const engineId = (opts.engineOverride ?? (schema.engine_id as EngineId));
|
|
||||||
|
|
||||||
const startAudio = async (): Promise<void> => {
|
|
||||||
if (opts.audioDisabled) return;
|
|
||||||
try {
|
|
||||||
await host.start(engineId);
|
|
||||||
setAudioStarted(true);
|
|
||||||
const out = paramOutputs();
|
|
||||||
if (out.length > 0) host.setParams(new Float32Array(out));
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('[mode-runtime] audio start failed:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopAudio = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await host.stop();
|
|
||||||
} finally {
|
|
||||||
setAudioStarted(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Switch engine if mode changes engine_id (e.g. on remount).
|
|
||||||
onMount(() => {
|
|
||||||
if (host.isStarted) host.setEngine(engineId);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (throttleTimer !== null) {
|
|
||||||
clearTimeout(throttleTimer);
|
|
||||||
throttleTimer = null;
|
|
||||||
}
|
|
||||||
pendingParams = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Mic input -------------------------------------------------------
|
|
||||||
const mic = new MicInput();
|
|
||||||
const [micStarted, setMicStarted] = createSignal(false);
|
|
||||||
const startMic = async () => {
|
|
||||||
try {
|
|
||||||
await mic.start();
|
|
||||||
setMicStarted(true);
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('[mode-runtime] mic start failed:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const stopMic = async () => {
|
|
||||||
try {
|
|
||||||
await mic.stop();
|
|
||||||
} finally {
|
|
||||||
setMicStarted(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onCleanup(() => { void stopMic(); });
|
|
||||||
|
|
||||||
// ----- Snapshot stack helpers ------------------------------------------
|
|
||||||
const canUndoMemo = createMemo(() => sessionStore.state.snapshots.length > 0);
|
|
||||||
|
|
||||||
// ----- Heatmap sampler -------------------------------------------------
|
|
||||||
const sampler = new HeatmapSampler({ resolution: 16 });
|
|
||||||
const [heatmapColorMode, setHeatmapColorMode] = createSignal<HeatmapColorMode>('luminance');
|
|
||||||
const [heatmapCells, setHeatmapCells] = createSignal<Float32Array>(sampler.getCells(), { equals: false });
|
|
||||||
const refreshHeatmap = (force = false): void => {
|
|
||||||
untrack(() => {
|
|
||||||
if (!ready()) return;
|
|
||||||
const cfg = inputStore.config;
|
|
||||||
const z = cfg.zoom;
|
|
||||||
const cx = cfg.anchorMode === 'center' ? 0.5 : cfg.anchorX;
|
|
||||||
const cy = cfg.anchorMode === 'center' ? 0.5 : cfg.anchorY;
|
|
||||||
const took = sampler.update({ cx, cy, zoom: z }, force);
|
|
||||||
if (took) {
|
|
||||||
setHeatmapCells(new Float32Array(sampler.getCells()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Refresh heatmap on weight changes (training, RL feedback, randomize).
|
|
||||||
const offTrained = coreBus.on('ml.trained', () => refreshHeatmap());
|
|
||||||
const offDelta = coreBus.on('ml.delta_update', () => refreshHeatmap());
|
|
||||||
onCleanup(() => { offTrained(); offDelta(); });
|
|
||||||
|
|
||||||
// Initial heatmap refresh once WASM is ready.
|
|
||||||
createEffect(() => {
|
|
||||||
if (ready()) refreshHeatmap(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Auto-explore loop -----------------------------------------------
|
|
||||||
let autoExploreTimer: number | null = null;
|
|
||||||
const tickAutoExplore = () => {
|
|
||||||
untrack(() => {
|
|
||||||
if (!ready()) return;
|
|
||||||
if (!explorationStore.state.autoExploreEnabled) return;
|
|
||||||
const intensity = explorationStore.state.autoExploreIntensity;
|
|
||||||
// Zoom-scaled intensity: smaller zoom = gentler.
|
|
||||||
const zoom = inputStore.config.zoom;
|
|
||||||
const scaledIntensity = intensity * (0.3 + 0.7 * zoom);
|
|
||||||
const cap = explorationStore.state.noiseCap * scaledIntensity;
|
|
||||||
autoSnapshot('before auto-explore');
|
|
||||||
const spread = explorationStore.state.spread;
|
|
||||||
const overrides = modeStore.state.overrides[schema.mode_id] ?? {};
|
|
||||||
const pinMask = buildPinMask(
|
|
||||||
mlStore.state.outputSize,
|
|
||||||
schema.mode_id,
|
|
||||||
schema.params,
|
|
||||||
overrides,
|
|
||||||
sessionStore.state.paramPins,
|
|
||||||
);
|
|
||||||
mlStore.moveWeights(cap, spread, pinMask);
|
|
||||||
explorationStore.growNoise(0.5);
|
|
||||||
// Re-run inference to update the heatmap and visuals.
|
|
||||||
const [x, y] = pipedInput();
|
|
||||||
setInput(x, y);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
createEffect(() => {
|
|
||||||
const enabled = explorationStore.state.autoExploreEnabled;
|
|
||||||
const interval = explorationStore.state.autoExploreIntervalMs;
|
|
||||||
if (autoExploreTimer !== null) {
|
|
||||||
window.clearInterval(autoExploreTimer);
|
|
||||||
autoExploreTimer = null;
|
|
||||||
}
|
|
||||||
if (enabled) {
|
|
||||||
autoExploreTimer = window.setInterval(tickAutoExplore, interval);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onCleanup(() => {
|
|
||||||
if (autoExploreTimer !== null) {
|
|
||||||
window.clearInterval(autoExploreTimer);
|
|
||||||
autoExploreTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Jolt (held-button continuous weight morph) ----------------------
|
|
||||||
// Inert until press(). While active, a ~200Hz control-rate timer glides a
|
|
||||||
// scatter of the network's weights toward random targets (port of
|
|
||||||
// nisps/ml/jolt.hpp). release() freezes them where they landed.
|
|
||||||
const jolt = new Jolt();
|
|
||||||
let joltTimer: number | null = null;
|
|
||||||
// ~200Hz to match the upstream firmware control rate the constants assume.
|
|
||||||
const JOLT_TICK_MS = 5;
|
|
||||||
|
|
||||||
const tickJolt = () => {
|
|
||||||
if (!ready() || !jolt.active()) return;
|
|
||||||
const w = mlStore.getWeights();
|
|
||||||
if (w.length === 0) return;
|
|
||||||
jolt.step(w);
|
|
||||||
mlStore.setWeights(w);
|
|
||||||
coreBus.emit('ml.delta_update', { reason: 'jolt' });
|
|
||||||
// Re-run inference so the audio + visuals reflect the morphed weights.
|
|
||||||
const [x, y] = pipedInput();
|
|
||||||
setInput(x, y);
|
|
||||||
};
|
|
||||||
|
|
||||||
const joltPress = () => {
|
|
||||||
if (!ready()) return;
|
|
||||||
autoSnapshot('before jolt');
|
|
||||||
jolt.press(mlStore.iml?.weightCount ?? mlStore.getWeights().length);
|
|
||||||
if (joltTimer === null) {
|
|
||||||
joltTimer = window.setInterval(tickJolt, JOLT_TICK_MS);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const joltRelease = () => {
|
|
||||||
jolt.release();
|
|
||||||
if (joltTimer !== null) {
|
|
||||||
window.clearInterval(joltTimer);
|
|
||||||
joltTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onCleanup(() => {
|
|
||||||
if (joltTimer !== null) {
|
|
||||||
window.clearInterval(joltTimer);
|
|
||||||
joltTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Explore (OU) control-rate driver --------------------------------
|
|
||||||
// The OU walk advances inside recomputeOutputs (driven by setInput). When
|
|
||||||
// the input is static the walk would stall, so while Explore is active we
|
|
||||||
// keep ticking so the sound keeps roaming. Inert when intensity is 0.
|
|
||||||
let exploreTimer: number | null = null;
|
|
||||||
const EXPLORE_TICK_MS = 30;
|
|
||||||
const setExploreIntensity = (level: number) => {
|
|
||||||
ouExplore.setIntensity(level);
|
|
||||||
setExploreIntensitySig(ouExplore.intensity());
|
|
||||||
if (ouExplore.enabled() && exploreTimer === null) {
|
|
||||||
exploreTimer = window.setInterval(() => {
|
|
||||||
untrack(() => {
|
|
||||||
if (!ready()) return;
|
|
||||||
// Re-run the output pipeline (advances the OU state) and reship.
|
|
||||||
recomputeOutputs();
|
|
||||||
});
|
|
||||||
}, EXPLORE_TICK_MS);
|
|
||||||
} else if (!ouExplore.enabled() && exploreTimer !== null) {
|
|
||||||
window.clearInterval(exploreTimer);
|
|
||||||
exploreTimer = null;
|
|
||||||
ouExplore.reset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onCleanup(() => {
|
|
||||||
if (exploreTimer !== null) {
|
|
||||||
window.clearInterval(exploreTimer);
|
|
||||||
exploreTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----- Touch / pressure feedback ---------------------------------------
|
|
||||||
const onPointerDown = (e: PointerEvent) => {
|
|
||||||
pressDownAt = performance.now();
|
|
||||||
explorationStore.setPressure(
|
|
||||||
// Force is 0..1 on supported devices; default 0.5 elsewhere.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(e as any).pressure ?? 0.5,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const onPointerUp = () => {
|
|
||||||
pressDownAt = null;
|
|
||||||
explorationStore.setPressure(0, 0);
|
|
||||||
};
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('pointerdown', onPointerDown, { passive: true });
|
|
||||||
window.addEventListener('pointerup', onPointerUp);
|
|
||||||
window.addEventListener('pointercancel', onPointerUp);
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('pointerdown', onPointerDown);
|
|
||||||
window.removeEventListener('pointerup', onPointerUp);
|
|
||||||
window.removeEventListener('pointercancel', onPointerUp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Training helpers -------------------------------------------------
|
|
||||||
const trainOnCurrent = () => {
|
|
||||||
if (!ready()) return;
|
|
||||||
autoSnapshot('before train');
|
|
||||||
const lr = explorationStore.state.learningRate;
|
|
||||||
mlStore.train(lr, schema.ml.default_max_iterations, 0.001);
|
|
||||||
};
|
|
||||||
|
|
||||||
const thumbsUp = () => {
|
|
||||||
if (!ready()) return;
|
|
||||||
const [x, y] = pipedInput();
|
|
||||||
const out = paramOutputs();
|
|
||||||
if (out.length === 0) return;
|
|
||||||
const features = new Array(schema.ml.input_size).fill(0);
|
|
||||||
features[0] = x;
|
|
||||||
if (features.length > 1) features[1] = y;
|
|
||||||
// Mic features into the example too, if active.
|
|
||||||
if (mic.isRunning() && schema.ml.input_size > 2) {
|
|
||||||
const f = mic.getFeatures();
|
|
||||||
const fv = [f.energy, f.brightness, f.pitch, f.aperiodicity];
|
|
||||||
for (let i = 2; i < schema.ml.input_size; ++i) {
|
|
||||||
features[i] = fv[i - 2] ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Labels: the raw 126-vector targets (not the override-applied; the MLP
|
|
||||||
// doesn't know about overrides).
|
|
||||||
const labels = Array.from(processedOutputs());
|
|
||||||
autoSnapshot('before thumbs-up');
|
|
||||||
mlStore.addExample(features, labels);
|
|
||||||
explorationStore.decayNoise(explorationStore.state.pressureForce);
|
|
||||||
trainOnCurrent();
|
|
||||||
};
|
|
||||||
|
|
||||||
const thumbsDown = () => {
|
|
||||||
if (!ready()) return;
|
|
||||||
autoSnapshot('before thumbs-down');
|
|
||||||
const cap = explorationStore.state.noiseCap;
|
|
||||||
const spread = explorationStore.state.spread;
|
|
||||||
const overrides = modeStore.state.overrides[schema.mode_id] ?? {};
|
|
||||||
const pinMask = buildPinMask(
|
|
||||||
mlStore.state.outputSize,
|
|
||||||
schema.mode_id,
|
|
||||||
schema.params,
|
|
||||||
overrides,
|
|
||||||
sessionStore.state.paramPins,
|
|
||||||
);
|
|
||||||
mlStore.moveWeights(cap, spread, pinMask);
|
|
||||||
explorationStore.growNoise(explorationStore.state.pressureForce);
|
|
||||||
const [x, y] = pipedInput();
|
|
||||||
setInput(x, y);
|
|
||||||
};
|
|
||||||
|
|
||||||
const randomize = () => {
|
|
||||||
if (!ready()) return;
|
|
||||||
autoSnapshot('before randomize');
|
|
||||||
mlStore.drawWeights(explorationStore.state.spread);
|
|
||||||
const [x, y] = pipedInput();
|
|
||||||
setInput(x, y);
|
|
||||||
};
|
|
||||||
|
|
||||||
const undo = (): boolean => {
|
|
||||||
const ok = undoLastSnapshot();
|
|
||||||
if (ok) {
|
|
||||||
const [x, y] = pipedInput();
|
|
||||||
setInput(x, y);
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----- Region pins ------------------------------------------------------
|
|
||||||
const pinCurrentRegion = () => {
|
|
||||||
const cfg = inputStore.config;
|
|
||||||
const z = cfg.zoom;
|
|
||||||
const cx = cfg.anchorMode === 'center' ? 0.5 : cfg.anchorX;
|
|
||||||
const cy = cfg.anchorMode === 'center' ? 0.5 : cfg.anchorY;
|
|
||||||
const halfZ = z * 0.5;
|
|
||||||
sessionStore.addRegionPin({
|
|
||||||
x: Math.max(0, cx - halfZ),
|
|
||||||
y: Math.max(0, cy - halfZ),
|
|
||||||
width: Math.min(z, 1),
|
|
||||||
height: Math.min(z, 1),
|
|
||||||
});
|
|
||||||
autoSnapshot('pinned baseline');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Snap-to-trail
|
|
||||||
const snapToTrail = (p: { x: number; y: number }) => {
|
|
||||||
batch(() => {
|
|
||||||
setInput(p.x, p.y);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
setInput,
|
|
||||||
pipedInput,
|
|
||||||
frozen,
|
|
||||||
rawOutputs: rawOutputsAccessor,
|
|
||||||
processedOutputs,
|
|
||||||
paramOutputs,
|
|
||||||
ready,
|
|
||||||
audio: {
|
|
||||||
started: audioStarted,
|
|
||||||
start: startAudio,
|
|
||||||
stop: stopAudio,
|
|
||||||
setMuted: (muted) => host.setMuted(muted),
|
|
||||||
},
|
|
||||||
mic: {
|
|
||||||
started: micStarted,
|
|
||||||
start: startMic,
|
|
||||||
stop: stopMic,
|
|
||||||
},
|
|
||||||
training: {
|
|
||||||
busy: () => mlStore.state.training,
|
|
||||||
examples: () => mlStore.state.exampleCount,
|
|
||||||
lastLoss: () => mlStore.state.lastLoss,
|
|
||||||
lossHistory: () => mlStore.state.lossHistory,
|
|
||||||
},
|
|
||||||
trainOnCurrent,
|
|
||||||
thumbsUp,
|
|
||||||
thumbsDown,
|
|
||||||
randomize,
|
|
||||||
undo,
|
|
||||||
canUndo: () => canUndoMemo(),
|
|
||||||
trail: trailRing.points,
|
|
||||||
snapToTrail,
|
|
||||||
heatmap: {
|
|
||||||
cells: heatmapCells,
|
|
||||||
setColorMode: (m) => {
|
|
||||||
sampler.setColorMode(m);
|
|
||||||
setHeatmapColorMode(m);
|
|
||||||
refreshHeatmap(true);
|
|
||||||
},
|
|
||||||
colorMode: heatmapColorMode,
|
|
||||||
refresh: refreshHeatmap,
|
|
||||||
resolution: () => sampler.resolution,
|
|
||||||
},
|
|
||||||
pinCurrentRegion,
|
|
||||||
jolt: {
|
|
||||||
press: joltPress,
|
|
||||||
release: joltRelease,
|
|
||||||
active: () => jolt.active(),
|
|
||||||
},
|
|
||||||
explore: {
|
|
||||||
setIntensity: setExploreIntensity,
|
|
||||||
intensity: exploreIntensity,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposes the shared EngineHost. Test helper — production never calls this.
|
|
||||||
*/
|
|
||||||
export function __disposeEngineHost(): void {
|
|
||||||
if (engineHost) {
|
|
||||||
engineHost.dispose();
|
|
||||||
engineHost = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
/**
|
|
||||||
* Curve catalog — TypeScript mirror of the named curves from
|
|
||||||
* nisps/core/math.hpp (forthcoming, stream 1). All inputs and outputs are in
|
|
||||||
* [0, 1] unless noted otherwise.
|
|
||||||
*
|
|
||||||
* IMPORTANT: This file MUST stay in lockstep with the C++ side. The
|
|
||||||
* authoritative reference is `nisps/core/math.hpp`. Golden-vector tests
|
|
||||||
* (stream 11) compare WASM-computed vs TS-computed outputs and fail on
|
|
||||||
* any drift.
|
|
||||||
*
|
|
||||||
* Architecture §5.3:
|
|
||||||
* linear, exp, log, square, sqrt, sigmoid, cubic, centered_power
|
|
||||||
*
|
|
||||||
* The "centered_power" variant comes from the legacy input/output pipelines
|
|
||||||
* and shapes around 0.5 instead of 0.0. Kept as a named curve because both
|
|
||||||
* input and output pipelines use it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type CurveName =
|
|
||||||
| 'linear'
|
|
||||||
| 'exp'
|
|
||||||
| 'log'
|
|
||||||
| 'square'
|
|
||||||
| 'sqrt'
|
|
||||||
| 'sigmoid'
|
|
||||||
| 'cubic'
|
|
||||||
| 'centered_power';
|
|
||||||
|
|
||||||
/** Hard clamp to [0, 1]. */
|
|
||||||
export function clamp01(v: number): number {
|
|
||||||
if (v < 0) return 0;
|
|
||||||
if (v > 1) return 1;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generic clamp. */
|
|
||||||
export function clamp(v: number, lo: number, hi: number): number {
|
|
||||||
if (v < lo) return lo;
|
|
||||||
if (v > hi) return hi;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Linear: identity. */
|
|
||||||
export function curveLinear(x: number): number {
|
|
||||||
return clamp01(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Exponential: e^(k*x) - 1, normalized to [0,1] over [0,1] input. */
|
|
||||||
export function curveExp(x: number, k: number = 4.0): number {
|
|
||||||
if (x <= 0) return 0;
|
|
||||||
if (x >= 1) return 1;
|
|
||||||
const denom = Math.exp(k) - 1.0;
|
|
||||||
if (denom === 0) return x;
|
|
||||||
return (Math.exp(k * x) - 1.0) / denom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Inverse of curveExp. */
|
|
||||||
export function curveLog(x: number, k: number = 4.0): number {
|
|
||||||
if (x <= 0) return 0;
|
|
||||||
if (x >= 1) return 1;
|
|
||||||
const denom = Math.exp(k) - 1.0;
|
|
||||||
if (denom === 0) return x;
|
|
||||||
return Math.log(1 + x * denom) / k;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Square: x^2. */
|
|
||||||
export function curveSquare(x: number): number {
|
|
||||||
const v = clamp01(x);
|
|
||||||
return v * v;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Square-root. */
|
|
||||||
export function curveSqrt(x: number): number {
|
|
||||||
return Math.sqrt(clamp01(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Logistic sigmoid mapped onto [0,1] domain (centered at x=0.5). */
|
|
||||||
export function curveSigmoid(x: number, slope: number = 8.0): number {
|
|
||||||
// Sigmoid centered at 0.5 with given slope. Output is in (0, 1).
|
|
||||||
// Normalize so endpoints map exactly to 0 and 1.
|
|
||||||
const t = (x - 0.5) * slope;
|
|
||||||
const s = 1 / (1 + Math.exp(-t));
|
|
||||||
// Anchor: when x=0, t=-slope/2; when x=1, t=+slope/2
|
|
||||||
const sLo = 1 / (1 + Math.exp(slope / 2));
|
|
||||||
const sHi = 1 / (1 + Math.exp(-slope / 2));
|
|
||||||
return (s - sLo) / (sHi - sLo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cubic ease-in-out. */
|
|
||||||
export function curveCubic(x: number): number {
|
|
||||||
const v = clamp01(x);
|
|
||||||
// Smoothstep cubic: 3v^2 - 2v^3
|
|
||||||
return v * v * (3 - 2 * v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Centered power curve. Pivots around 0.5.
|
|
||||||
*
|
|
||||||
* exponent < 1 → push toward extremes
|
|
||||||
* exponent = 1 → identity
|
|
||||||
* exponent > 1 → pull toward center
|
|
||||||
*/
|
|
||||||
export function curveCenteredPower(x: number, exponent: number): number {
|
|
||||||
if (exponent === 1) return clamp01(x);
|
|
||||||
const offset = x - 0.5;
|
|
||||||
const sign = offset < 0 ? -1 : 1;
|
|
||||||
// Range [-0.5, 0.5] -> [-1, 1] for the power op, then halve back.
|
|
||||||
const shaped = (sign * Math.pow(Math.abs(offset) * 2, exponent)) / 2;
|
|
||||||
return clamp01(shaped + 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Apply by name. `param` interpretation depends on the curve. */
|
|
||||||
export function applyCurve(name: CurveName, x: number, param?: number): number {
|
|
||||||
switch (name) {
|
|
||||||
case 'linear': return curveLinear(x);
|
|
||||||
case 'exp': return curveExp(x, param ?? 4.0);
|
|
||||||
case 'log': return curveLog(x, param ?? 4.0);
|
|
||||||
case 'square': return curveSquare(x);
|
|
||||||
case 'sqrt': return curveSqrt(x);
|
|
||||||
case 'sigmoid': return curveSigmoid(x, param ?? 8.0);
|
|
||||||
case 'cubic': return curveCubic(x);
|
|
||||||
case 'centered_power': return curveCenteredPower(x, param ?? 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CURVE_NAMES: ReadonlyArray<CurveName> = [
|
|
||||||
'linear', 'exp', 'log', 'square', 'sqrt', 'sigmoid', 'cubic', 'centered_power',
|
|
||||||
];
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
/**
|
|
||||||
* ou-explore.ts — Ornstein-Uhlenbeck exploration noise on the output.
|
|
||||||
*
|
|
||||||
* Faithful TS port of `nisps/ml/ou_noise.hpp`. Adds a per-output-channel
|
|
||||||
* Ornstein-Uhlenbeck random walk to the network's action (output) vector
|
|
||||||
* before it reaches audio. Unlike i.i.d. per-frame noise, 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 (mu = 0):
|
|
||||||
* x += theta * (-x) * dt + noiseScale * N(0,1)
|
|
||||||
* out = clamp(out + x, 0, 1)
|
|
||||||
* where, to make the stationary std equal a requested `std`:
|
|
||||||
* noiseScale = std * sqrt(2 * theta * dt)
|
|
||||||
* and the exploration knob [0,1] maps std = level * kMaxAmplitude (0.65).
|
|
||||||
*
|
|
||||||
* RNG note: the C++ owns a per-instance deterministic xoshiro256+ for
|
|
||||||
* firmware↔browser parity. This is a browser-only exploration aid that
|
|
||||||
* never round-trips through WASM, so `Math.random()` is fine. The gaussian
|
|
||||||
* is reproduced as the same sum-of-three-uniforms shape the C++ `Rng` uses
|
|
||||||
* (`next_float_gaussian`).
|
|
||||||
*
|
|
||||||
* DEFAULT STATE IS INERT: intensity defaults to 0, `enabled()` is false, and
|
|
||||||
* `apply()` neither advances the RNG nor touches the output — so the output
|
|
||||||
* passes through bit-identically when intensity is 0 (parity-safe).
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Upstream kMaxAmplitude: the exploration knob's full-scale stationary std. */
|
|
||||||
export const OU_MAX_AMPLITUDE = 0.65;
|
|
||||||
|
|
||||||
/** Sum-of-three-uniforms gaussian (matches nisps::Rng::next_float_gaussian). */
|
|
||||||
function gaussian(stddev = 1): number {
|
|
||||||
// Three uniforms in [-1, 1): variance 1/3 each ⇒ sum has variance 1.
|
|
||||||
const a = Math.random() * 2 - 1;
|
|
||||||
const b = Math.random() * 2 - 1;
|
|
||||||
const c = Math.random() * 2 - 1;
|
|
||||||
return (a + b + c) * stddev;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OUExplore {
|
|
||||||
private theta_ = 0.02;
|
|
||||||
private dt_ = 0.001;
|
|
||||||
private stationaryStd_ = 0;
|
|
||||||
private noiseScale_ = 0;
|
|
||||||
private state_: Float32Array = new Float32Array(0);
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.recomputeScale_();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exploration amount in [0,1]; 0 disables (inert). Maps to the OU
|
|
||||||
* stationary std = level * kMaxAmplitude.
|
|
||||||
*/
|
|
||||||
setIntensity(level: number): void {
|
|
||||||
let l = level;
|
|
||||||
if (l < 0) l = 0;
|
|
||||||
else if (l > 1) l = 1;
|
|
||||||
this.stationaryStd_ = l * OU_MAX_AMPLITUDE;
|
|
||||||
this.recomputeScale_();
|
|
||||||
}
|
|
||||||
|
|
||||||
intensity(): number {
|
|
||||||
return this.stationaryStd_ / OU_MAX_AMPLITUDE;
|
|
||||||
}
|
|
||||||
|
|
||||||
enabled(): boolean {
|
|
||||||
return this.stationaryStd_ > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTheta(theta: number): void {
|
|
||||||
this.theta_ = theta;
|
|
||||||
this.recomputeScale_();
|
|
||||||
}
|
|
||||||
|
|
||||||
setDt(dt: number): void {
|
|
||||||
this.dt_ = dt;
|
|
||||||
this.recomputeScale_();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advance the per-channel OU state and add it (clamped) to `out`, mutating
|
|
||||||
* `out` in place. No-op (no state advance, no output change) when disabled.
|
|
||||||
* `out` is the post-inference parameter vector.
|
|
||||||
*/
|
|
||||||
apply(out: Float32Array): void {
|
|
||||||
if (!this.enabled()) return;
|
|
||||||
const n = out.length;
|
|
||||||
if (this.state_.length < n) {
|
|
||||||
// Grow the per-channel state, preserving existing drift.
|
|
||||||
const next = new Float32Array(n);
|
|
||||||
next.set(this.state_);
|
|
||||||
this.state_ = next;
|
|
||||||
}
|
|
||||||
const theta = this.theta_;
|
|
||||||
const dt = this.dt_;
|
|
||||||
const scale = this.noiseScale_;
|
|
||||||
for (let i = 0; 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.
|
|
||||||
let s = this.state_[i]!;
|
|
||||||
s += theta * -s * dt + scale * gaussian(1);
|
|
||||||
this.state_[i] = s;
|
|
||||||
let v = out[i]! + s;
|
|
||||||
if (v < 0) v = 0;
|
|
||||||
else if (v > 1) v = 1;
|
|
||||||
out[i] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.state_.fill(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private recomputeScale_(): void {
|
|
||||||
let k = 2 * this.theta_ * this.dt_;
|
|
||||||
if (k < 0) k = 0;
|
|
||||||
this.noiseScale_ = this.stationaryStd_ * Math.sqrt(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
/**
|
|
||||||
* Output pipeline — pure TS port of legacy `js/ui/output-pipeline.js`.
|
|
||||||
*
|
|
||||||
* Stages (in order) for each output:
|
|
||||||
* 1. Global power curve (raw^exponent, exponent in [0.2, 5.0])
|
|
||||||
* 2. Per-output EMA smoothing (frame-rate-independent)
|
|
||||||
* 3. Slew-rate limiting (max change per second per output)
|
|
||||||
* 4. Freeze gate (global) and per-output freeze mask
|
|
||||||
*
|
|
||||||
* `processOutput` is a pure function: takes the raw output vector, the prior
|
|
||||||
* processed vector (or null on first call), and config; returns a new
|
|
||||||
* Float32Array. Consumer (output-store) holds prev between frames.
|
|
||||||
*
|
|
||||||
* NOTE: Reuses an internal scratch buffer ONLY when a prev buffer of the
|
|
||||||
* exact same length is supplied AND `cfg.reuseBuffer === true`. Otherwise it
|
|
||||||
* allocates a fresh Float32Array (safer for cross-component sharing).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { clamp, clamp01 } from './curves';
|
|
||||||
|
|
||||||
export const GLOBAL_CURVE_MIN = 0.2;
|
|
||||||
export const GLOBAL_CURVE_MAX = 5.0;
|
|
||||||
export const SMOOTHING_MAX = 0.95;
|
|
||||||
export const SLEW_RATE_MIN = 0.005;
|
|
||||||
|
|
||||||
const REFERENCE_DT = 1 / 60;
|
|
||||||
|
|
||||||
export interface OutputConfig {
|
|
||||||
/** Power curve exponent applied to ALL outputs. 1 = linear. */
|
|
||||||
globalCurve: number;
|
|
||||||
/** EMA smoothing factor [0, 0.95]. */
|
|
||||||
smoothing: number;
|
|
||||||
/** Max change per second per output. Infinity = unlimited. */
|
|
||||||
slewRate: number;
|
|
||||||
/** Global freeze gate. */
|
|
||||||
freezeOutput: boolean;
|
|
||||||
/** Per-output freeze mask (1 = frozen). Length must match output vector. */
|
|
||||||
freezeMask: Uint8Array | null;
|
|
||||||
/** If true and prev buffer matches length, reuse it for processed output. */
|
|
||||||
reuseBuffer: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutputState {
|
|
||||||
/** Last processed output (kept here for slew/freeze logic). */
|
|
||||||
prev: Float32Array | null;
|
|
||||||
/** Last EMA-smoothed values per output. */
|
|
||||||
smoothed: Float32Array | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultOutputConfig(): OutputConfig {
|
|
||||||
return {
|
|
||||||
globalCurve: 1.0,
|
|
||||||
smoothing: 0,
|
|
||||||
slewRate: Infinity,
|
|
||||||
freezeOutput: false,
|
|
||||||
freezeMask: null,
|
|
||||||
reuseBuffer: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultOutputState(): OutputState {
|
|
||||||
return { prev: null, smoothed: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
function emaSmooth(prev: number, raw: number, smoothing: number, dt: number): number {
|
|
||||||
if (smoothing <= 0) return raw;
|
|
||||||
const effectiveDt = dt > 0 ? dt : REFERENCE_DT;
|
|
||||||
const alpha = 1 - smoothing;
|
|
||||||
const alphaEff = 1 - Math.pow(1 - alpha, effectiveDt / REFERENCE_DT);
|
|
||||||
return prev + alphaEff * (raw - prev);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process raw outputs through global curve → smoothing → slew → freeze gate.
|
|
||||||
*
|
|
||||||
* @param raw raw output vector (Float32Array of size N)
|
|
||||||
* @param cfg pipeline config
|
|
||||||
* @param state prior state (use {@link defaultOutputState} first call)
|
|
||||||
* @param dtMs time since last call in milliseconds
|
|
||||||
* @returns { processed, state } with the new outputs and updated state
|
|
||||||
*/
|
|
||||||
export function processOutput(
|
|
||||||
raw: Float32Array,
|
|
||||||
cfg: OutputConfig,
|
|
||||||
state: OutputState,
|
|
||||||
dtMs: number,
|
|
||||||
): { processed: Float32Array; state: OutputState } {
|
|
||||||
const n = raw.length;
|
|
||||||
const dt = Math.max(0, dtMs / 1000);
|
|
||||||
|
|
||||||
let prev = state.prev;
|
|
||||||
let smoothed = state.smoothed;
|
|
||||||
if (!prev || prev.length !== n) {
|
|
||||||
prev = new Float32Array(n);
|
|
||||||
// Seed from raw on first call
|
|
||||||
for (let i = 0; i < n; i++) prev[i] = clamp01(raw[i] ?? 0);
|
|
||||||
}
|
|
||||||
if (!smoothed || smoothed.length !== n) {
|
|
||||||
smoothed = new Float32Array(n);
|
|
||||||
for (let i = 0; i < n; i++) smoothed[i] = clamp01(raw[i] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let processed: Float32Array;
|
|
||||||
if (cfg.reuseBuffer && prev.length === n) {
|
|
||||||
processed = prev;
|
|
||||||
} else {
|
|
||||||
processed = new Float32Array(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage 1: global curve (mutates a working scratch via direct compute)
|
|
||||||
const exp = cfg.globalCurve;
|
|
||||||
|
|
||||||
if (cfg.freezeOutput) {
|
|
||||||
// Output frozen: hold prior values.
|
|
||||||
if (processed !== prev) {
|
|
||||||
processed.set(prev);
|
|
||||||
}
|
|
||||||
return { processed, state: { prev: processed, smoothed } };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const r = clamp01(raw[i] ?? 0);
|
|
||||||
const curved = exp === 1.0 ? r : Math.pow(r, exp);
|
|
||||||
|
|
||||||
// Per-output freeze
|
|
||||||
if (cfg.freezeMask && cfg.freezeMask[i]) {
|
|
||||||
processed[i] = prev[i] ?? curved;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage 2: EMA smoothing
|
|
||||||
let value = emaSmooth(smoothed[i] ?? curved, curved, cfg.smoothing, dt);
|
|
||||||
smoothed[i] = value;
|
|
||||||
|
|
||||||
// Stage 3: slew-rate limit
|
|
||||||
if (isFinite(cfg.slewRate) && cfg.slewRate > 0) {
|
|
||||||
const maxDelta = cfg.slewRate * dt;
|
|
||||||
const delta = value - (prev[i] ?? value);
|
|
||||||
if (Math.abs(delta) > maxDelta) {
|
|
||||||
value = (prev[i] ?? value) + Math.sign(delta) * maxDelta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processed[i] = clamp01(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update prev for next call
|
|
||||||
if (processed !== prev) {
|
|
||||||
prev = new Float32Array(processed); // copy so caller can hold processed buffer freely
|
|
||||||
}
|
|
||||||
|
|
||||||
return { processed, state: { prev, smoothed } };
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { Component, createSignal } from 'solid-js';
|
|
||||||
import { ControlAxis } from './ControlAxis';
|
|
||||||
|
|
||||||
export const ControlAxisDemo: Component = () => {
|
|
||||||
const [boldness, setBoldness] = createSignal(0.5);
|
|
||||||
const [memory, setMemory] = createSignal(0.7);
|
|
||||||
const [precision, setPrecision] = createSignal(0.3);
|
|
||||||
const [preset, setPreset] = createSignal<string | null>('default');
|
|
||||||
|
|
||||||
const onAny = () => setPreset(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '10px', 'min-width': '320px' }}>
|
|
||||||
<ControlAxis
|
|
||||||
label="Boldness"
|
|
||||||
value={boldness}
|
|
||||||
onChange={(v) => { setBoldness(v); onAny(); }}
|
|
||||||
preset={preset}
|
|
||||||
endpoints={['Caution', 'Bold']}
|
|
||||||
accentColor="var(--accent)"
|
|
||||||
onDoubleTap={() => setBoldness(0.5)}
|
|
||||||
/>
|
|
||||||
<ControlAxis
|
|
||||||
label="Memory"
|
|
||||||
value={memory}
|
|
||||||
onChange={(v) => { setMemory(v); onAny(); }}
|
|
||||||
preset={preset}
|
|
||||||
endpoints={['Amnesia', 'Elephant']}
|
|
||||||
accentColor="var(--accent-2)"
|
|
||||||
onDoubleTap={() => setMemory(0.5)}
|
|
||||||
/>
|
|
||||||
<ControlAxis
|
|
||||||
label="Precision"
|
|
||||||
value={precision}
|
|
||||||
onChange={(v) => { setPrecision(v); onAny(); }}
|
|
||||||
preset={preset}
|
|
||||||
endpoints={['Raw', 'Precise']}
|
|
||||||
accentColor="var(--good)"
|
|
||||||
onDoubleTap={() => setPrecision(0.3)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
.axis {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-1);
|
|
||||||
background: var(--bg-1);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-2);
|
|
||||||
padding: var(--sp-2) var(--sp-3);
|
|
||||||
--axis-accent: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--fg);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preset {
|
|
||||||
color: var(--axis-accent);
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
color: var(--fg-mute);
|
|
||||||
font-size: var(--fs-xs);
|
|
||||||
min-width: 4ch;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 24px;
|
|
||||||
background: transparent;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::-webkit-slider-runnable-track {
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::-moz-range-track {
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::-webkit-slider-thumb {
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--axis-accent);
|
|
||||||
margin-top: -6px;
|
|
||||||
box-shadow: 0 0 10px var(--axis-accent);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::-moz-range-thumb {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--axis-accent);
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 0 10px var(--axis-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:focus { outline: none; }
|
|
||||||
|
|
||||||
.endpoints {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import { Component, Show } from 'solid-js';
|
|
||||||
import styles from './ControlAxis.module.css';
|
|
||||||
|
|
||||||
export interface ControlAxisProps {
|
|
||||||
label: string;
|
|
||||||
value: () => number;
|
|
||||||
onChange: (v: number) => void;
|
|
||||||
/** Active control preset id (rendered as subtitle when set). */
|
|
||||||
preset?: () => string | null;
|
|
||||||
/** Endpoint labels (left, right). E.g. ['Caution', 'Bold']. */
|
|
||||||
endpoints?: readonly [string, string];
|
|
||||||
/** Called when user double-taps the axis (re-link offsets). */
|
|
||||||
onDoubleTap?: () => void;
|
|
||||||
/** Disabled state. */
|
|
||||||
disabled?: boolean;
|
|
||||||
/** Visual color hint (defaults to --accent). */
|
|
||||||
accentColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ControlAxis: Component<ControlAxisProps> = (props) => {
|
|
||||||
let lastTap = 0;
|
|
||||||
|
|
||||||
const onPointerDown = () => {
|
|
||||||
const now = performance.now();
|
|
||||||
if (now - lastTap < 350 && props.onDoubleTap) {
|
|
||||||
props.onDoubleTap();
|
|
||||||
lastTap = 0;
|
|
||||||
} else {
|
|
||||||
lastTap = now;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInput = (e: InputEvent & { currentTarget: HTMLInputElement }) => {
|
|
||||||
const v = parseFloat(e.currentTarget.value);
|
|
||||||
if (!Number.isNaN(v)) props.onChange(Math.max(0, Math.min(1, v)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const accent = () => props.accentColor ?? 'var(--accent)';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={styles.axis}
|
|
||||||
classList={{ [styles.disabled]: !!props.disabled }}
|
|
||||||
style={{ '--axis-accent': accent() }}
|
|
||||||
>
|
|
||||||
<div class={styles.head}>
|
|
||||||
<span class={styles.label}>{props.label}</span>
|
|
||||||
<Show when={props.preset?.()}>
|
|
||||||
<span class={styles.preset}>{props.preset!()}</span>
|
|
||||||
</Show>
|
|
||||||
<span class={styles.value}>{props.value().toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
class={styles.input}
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.001}
|
|
||||||
value={props.value()}
|
|
||||||
onInput={onInput}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
disabled={props.disabled}
|
|
||||||
aria-label={`${props.label} control axis`}
|
|
||||||
/>
|
|
||||||
<Show when={props.endpoints}>
|
|
||||||
<div class={styles.endpoints}>
|
|
||||||
<span>{props.endpoints![0]}</span>
|
|
||||||
<span>{props.endpoints![1]}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { Component, createSignal } from 'solid-js';
|
|
||||||
import { Drawer } from './Drawer';
|
|
||||||
|
|
||||||
export const DrawerDemo: Component = () => {
|
|
||||||
const [openR, setOpenR] = createSignal(false);
|
|
||||||
const [openL, setOpenL] = createSignal(false);
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', gap: '8px', 'flex-wrap': 'wrap' }}>
|
|
||||||
<button type="button" onClick={() => setOpenL(true)}>Open Left</button>
|
|
||||||
<button type="button" onClick={() => setOpenR(true)}>Open Right</button>
|
|
||||||
<Drawer open={openL()} onClose={() => setOpenL(false)} side="left" title="Left drawer">
|
|
||||||
<p>Slides in from the left.</p>
|
|
||||||
<p>Press Escape, click outside, or click × to close.</p>
|
|
||||||
</Drawer>
|
|
||||||
<Drawer open={openR()} onClose={() => setOpenR(false)} side="right" title="Right drawer">
|
|
||||||
<p>Slides in from the right.</p>
|
|
||||||
<p>Mode-specific drawers will use this primitive.</p>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
.root {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: var(--z-drawer);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrim {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
pointer-events: auto;
|
|
||||||
animation: fadeIn var(--dur-fast) var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: var(--bg-1);
|
|
||||||
border-left: 1px solid var(--line);
|
|
||||||
pointer-events: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 0 24px rgba(0, 0, 0, 0.5);
|
|
||||||
animation: slideIn var(--dur-med) var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.left .panel {
|
|
||||||
left: 0;
|
|
||||||
border-right: 1px solid var(--line);
|
|
||||||
border-left: 0;
|
|
||||||
animation-name: slideInLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right .panel {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-3) var(--sp-4);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: var(--fs-md);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
color: var(--fg-mute);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
color: var(--fg-mute);
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: var(--r-1);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close:hover {
|
|
||||||
background: var(--bg-2);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
padding: var(--sp-3) var(--sp-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateX(100%); }
|
|
||||||
to { transform: translateX(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInLeft {
|
|
||||||
from { transform: translateX(-100%); }
|
|
||||||
to { transform: translateX(0); }
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import { Component, JSX, onCleanup, onMount, Show } from 'solid-js';
|
|
||||||
import { Portal } from 'solid-js/web';
|
|
||||||
import styles from './Drawer.module.css';
|
|
||||||
|
|
||||||
export interface DrawerProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
side: 'left' | 'right';
|
|
||||||
title?: string;
|
|
||||||
children: JSX.Element;
|
|
||||||
/** Optional width in px (default 360). */
|
|
||||||
width?: number;
|
|
||||||
/** Set to false to disable click-outside / Escape close behaviour. */
|
|
||||||
dismissable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Drawer: Component<DrawerProps> = (props) => {
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (!props.open) return;
|
|
||||||
if (props.dismissable === false) return;
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.stopPropagation();
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
document.addEventListener('keydown', onKey);
|
|
||||||
});
|
|
||||||
onCleanup(() => {
|
|
||||||
document.removeEventListener('keydown', onKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={props.open}>
|
|
||||||
<Portal>
|
|
||||||
<div
|
|
||||||
class={styles.root}
|
|
||||||
classList={{ [styles.left]: props.side === 'left', [styles.right]: props.side === 'right' }}
|
|
||||||
role="dialog"
|
|
||||||
aria-label={props.title ?? 'drawer'}
|
|
||||||
aria-modal="false"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class={styles.scrim}
|
|
||||||
onClick={() => {
|
|
||||||
if (props.dismissable !== false) props.onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<aside
|
|
||||||
class={styles.panel}
|
|
||||||
style={{ width: `${props.width ?? 360}px` }}
|
|
||||||
>
|
|
||||||
<header class={styles.header}>
|
|
||||||
<span class={styles.title}>{props.title ?? ''}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.close}
|
|
||||||
onClick={props.onClose}
|
|
||||||
aria-label="Close drawer"
|
|
||||||
>×</button>
|
|
||||||
</header>
|
|
||||||
<div class={styles.body}>{props.children}</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
</Show>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { Component, createMemo, createSignal } from 'solid-js';
|
|
||||||
import { GradientFlow, type GradientStatus } from './GradientFlow';
|
|
||||||
import { PillToggle } from './PillToggle';
|
|
||||||
|
|
||||||
export const GradientFlowDemo: Component = () => {
|
|
||||||
const [pattern, setPattern] = createSignal<'healthy' | 'vanishing' | 'exploding'>('healthy');
|
|
||||||
|
|
||||||
const norms = createMemo<number[]>(() => {
|
|
||||||
const p = pattern();
|
|
||||||
if (p === 'vanishing') return [1.0, 0.4, 0.15, 0.04];
|
|
||||||
if (p === 'exploding') return [0.4, 0.8, 1.6, 3.2];
|
|
||||||
return [0.6, 0.55, 0.5, 0.5];
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = createMemo<GradientStatus[]>(() => {
|
|
||||||
const p = pattern();
|
|
||||||
if (p === 'vanishing') return ['healthy', 'vanishing', 'vanishing', 'vanishing'];
|
|
||||||
if (p === 'exploding') return ['healthy', 'healthy', 'exploding', 'exploding'];
|
|
||||||
return ['healthy', 'healthy', 'healthy', 'healthy'];
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px', 'align-items': 'flex-start' }}>
|
|
||||||
<GradientFlow layerNorms={norms} status={status} />
|
|
||||||
<PillToggle
|
|
||||||
options={[
|
|
||||||
{ value: 'healthy', label: 'Healthy' },
|
|
||||||
{ value: 'vanishing', label: 'Vanishing' },
|
|
||||||
{ value: 'exploding', label: 'Exploding' },
|
|
||||||
]}
|
|
||||||
value={pattern}
|
|
||||||
onChange={(v) => setPattern(v as 'healthy' | 'vanishing' | 'exploding')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
.canvas {
|
|
||||||
display: block;
|
|
||||||
background: var(--bg-1);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--r-2);
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import { Component, createEffect } from 'solid-js';
|
|
||||||
import styles from './GradientFlow.module.css';
|
|
||||||
|
|
||||||
export type GradientStatus = 'vanishing' | 'exploding' | 'converged' | 'healthy';
|
|
||||||
|
|
||||||
export interface GradientFlowProps {
|
|
||||||
/** Per-layer L2 norms of weight delta. */
|
|
||||||
layerNorms: () => ReadonlyArray<number>;
|
|
||||||
/** Per-layer status. Length must match layerNorms(). */
|
|
||||||
status: () => ReadonlyArray<GradientStatus>;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
ariaLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_COLOR: Record<GradientStatus, string> = {
|
|
||||||
vanishing: '#5b9eef',
|
|
||||||
exploding: '#ef5b5b',
|
|
||||||
converged: '#9a9a9a',
|
|
||||||
healthy: '#6bc26b',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GradientFlow: Component<GradientFlowProps> = (props) => {
|
|
||||||
let canvasEl: HTMLCanvasElement | undefined;
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!canvasEl) return;
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const w = (props.width ?? 240) * dpr;
|
|
||||||
const h = (props.height ?? 80) * dpr;
|
|
||||||
if (canvasEl.width !== w || canvasEl.height !== h) {
|
|
||||||
canvasEl.width = w;
|
|
||||||
canvasEl.height = h;
|
|
||||||
}
|
|
||||||
const ctx = canvasEl.getContext('2d');
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
const norms = props.layerNorms();
|
|
||||||
const status = props.status();
|
|
||||||
if (norms.length === 0) return;
|
|
||||||
|
|
||||||
let max = 0;
|
|
||||||
for (const n of norms) if (n > max) max = n;
|
|
||||||
if (max <= 0) max = 1;
|
|
||||||
|
|
||||||
const barW = w / norms.length;
|
|
||||||
for (let i = 0; i < norms.length; i++) {
|
|
||||||
const bh = (norms[i]! / max) * (h - 14 * dpr);
|
|
||||||
const s = status[i] ?? 'healthy';
|
|
||||||
ctx.fillStyle = STATUS_COLOR[s];
|
|
||||||
ctx.fillRect(i * barW + 2, h - bh - 2, barW - 4, bh);
|
|
||||||
ctx.fillStyle = '#5a5a5a';
|
|
||||||
ctx.font = `${10 * dpr}px var(--font-mono)`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(`L${i}`, i * barW + barW / 2, h - 2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={canvasEl}
|
|
||||||
class={styles.canvas}
|
|
||||||
style={{ width: `${props.width ?? 240}px`, height: `${props.height ?? 80}px` }}
|
|
||||||
role="img"
|
|
||||||
aria-label={props.ariaLabel ?? 'Gradient flow per layer'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { Component, createMemo, createSignal, onCleanup, onMount } from 'solid-js';
|
|
||||||
import { Heatmap, type HeatmapColorMode } from './Heatmap';
|
|
||||||
import { PillToggle } from './PillToggle';
|
|
||||||
|
|
||||||
const N = 16;
|
|
||||||
|
|
||||||
function generateField(seed: number): Float32Array {
|
|
||||||
const arr = new Float32Array(N * N);
|
|
||||||
for (let y = 0; y < N; y++) {
|
|
||||||
for (let x = 0; x < N; x++) {
|
|
||||||
const fx = (x - N / 2) / (N / 2);
|
|
||||||
const fy = (y - N / 2) / (N / 2);
|
|
||||||
const r = Math.sqrt(fx * fx + fy * fy);
|
|
||||||
const v = 0.5 + 0.5 * Math.sin(r * 3 + seed * 0.5) * Math.cos(fx * 4 + seed);
|
|
||||||
arr[y * N + x] = Math.max(0, Math.min(1, v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeatmapDemo: Component = () => {
|
|
||||||
const [mode, setMode] = createSignal<HeatmapColorMode>('luminance');
|
|
||||||
const [tick, setTick] = createSignal(0);
|
|
||||||
let timer: ReturnType<typeof setInterval>;
|
|
||||||
onMount(() => {
|
|
||||||
timer = setInterval(() => setTick((t) => t + 1), 200);
|
|
||||||
});
|
|
||||||
onCleanup(() => clearInterval(timer));
|
|
||||||
|
|
||||||
const samples = createMemo(() => generateField(tick()));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '10px', 'align-items': 'center' }}>
|
|
||||||
<Heatmap samples={samples} resolution={N} colorMode={mode()} size={240} showGrid={false} />
|
|
||||||
<PillToggle
|
|
||||||
options={[
|
|
||||||
{ value: 'luminance', label: 'Luminance' },
|
|
||||||
{ value: 'variance', label: 'Variance' },
|
|
||||||
{ value: 'divergence', label: 'Divergence' },
|
|
||||||
]}
|
|
||||||
value={mode}
|
|
||||||
onChange={(v) => setMode(v as HeatmapColorMode)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue