memlnaut-nisps/playground/src/modes/mode-helpers.ts
w1n5t0n ebd5dba4c0 feat(playground/modes): ModeShell + runtime + switcher scaffolding (meml-yt7)
Stream 9 (part 1/2) — shared infrastructure for the per-mode TSX layer.

  - ModeShell.tsx: common chrome (header, voice-space PillToggle, audio
    start/stop, control axes, training controls, drawer, status line).
    Modes only have to author primary input + output JSX.
  - mode-runtime.ts: useModeRuntime(schema) hook. Owns input pipeline,
    WASM ML calls, output pipeline, and a 50ms-throttled engine-host
    bridge. Exposes setInput/processedOutputs/training/audio surfaces
    so mode TSX stays declarative.
  - mode-helpers.ts: schema → SliderBank config + Float32Array →
    slider-range value mapping. Pure functions.
  - ModeSwitcher.tsx: top-of-page <select> wired to modeStore.

ModeShell and the runtime read controlStore (Boldness/Memory/Precision)
to derive learning rate + RL noise cap so axes affect training without
needing per-mode plumbing.
2026-04-29 17:00:43 +03:00

54 lines
1.5 KiB
TypeScript

/**
* 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).
*/
export function outputsToSliderValues(
outputs: Float32Array,
params: ReadonlyArray<Param>,
): number[] {
const out: number[] = [];
for (let i = 0; i < params.length; ++i) {
const v = outputs[i] ?? 0;
const p = params[i]!;
out.push(p.min + v * (p.max - p.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(' ');
}