feat(playground/modes): all 9 mode bindings + /modes route (meml-yt7)
Stream 9 (part 2/2) — concrete mode TSX components + routing.
Each firmware mode pairs the generated schema with the appropriate
primary input (XYPad for xy_pad schemas, VirtualJoystick for joystick
schemas) and feeds the runtime's processedOutputs into the shared
SliderBank/OutputDisplay/LossPlot layout. The set:
- PAFSynthMode (xy_pad, voice spaces)
- ChannelStripMode (joystick)
- XIASRIMode (joystick — mic-input wiring deferred to stream 10)
- VerbFXMode (joystick, voice spaces)
- MEMLCeliumMode (xy_pad, voice spaces)
- BreakOrMode (xy_pad)
- ElysiamorfMode (xy_pad)
- SoundAnalysisMIDIMode (audio_in fallback to joystick;
mic capture is a stream-10 task — UI scaffold renders today)
- C15Mode (browser-only placeholder; bridge not yet ported)
App.tsx adds a /modes route that renders <ModeSwitcher /> + the active
mode's component via Solid <Dynamic />. modes/index.ts exposes the
registry the switcher consumes.
This commit is contained in:
parent
ebd5dba4c0
commit
f916344c9b
11 changed files with 800 additions and 7 deletions
|
|
@ -1,11 +1,16 @@
|
||||||
import { Component, createSignal, lazy, onCleanup, Show } from 'solid-js';
|
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';
|
import styles from './App.module.css';
|
||||||
|
|
||||||
type Route = 'home' | 'primitives' | 'unknown';
|
type Route = 'home' | 'primitives' | 'modes' | 'unknown';
|
||||||
|
|
||||||
function parseRoute(path: string): Route {
|
function parseRoute(path: string): Route {
|
||||||
if (path === '' || path === '/' || path === '/index.html') return 'home';
|
if (path === '' || path === '/' || path === '/index.html') return 'home';
|
||||||
if (path === '/dev/primitives' || path === '/dev/primitives/') return 'primitives';
|
if (path === '/dev/primitives' || path === '/dev/primitives/') return 'primitives';
|
||||||
|
if (path === '/modes' || path === '/modes/' || path.startsWith('/modes/')) return 'modes';
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +44,13 @@ const App: Component = () => {
|
||||||
>
|
>
|
||||||
home
|
home
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={route() === 'modes' ? styles.active : ''}
|
||||||
|
onClick={() => navigate('/modes')}
|
||||||
|
>
|
||||||
|
/modes
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={route() === 'primitives' ? styles.active : ''}
|
class={route() === 'primitives' ? styles.active : ''}
|
||||||
|
|
@ -50,7 +62,10 @@ const App: Component = () => {
|
||||||
</header>
|
</header>
|
||||||
<main class={styles.main}>
|
<main class={styles.main}>
|
||||||
<Show when={route() === 'home'}>
|
<Show when={route() === 'home'}>
|
||||||
<Home />
|
<Home navigate={navigate} />
|
||||||
|
</Show>
|
||||||
|
<Show when={route() === 'modes'}>
|
||||||
|
<ModesPage />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={route() === 'primitives'}>
|
<Show when={route() === 'primitives'}>
|
||||||
<PrimitivesShowcase />
|
<PrimitivesShowcase />
|
||||||
|
|
@ -69,18 +84,59 @@ const App: Component = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Home: Component = () => {
|
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 (
|
return (
|
||||||
<div class={styles.home}>
|
<div class={styles.home}>
|
||||||
<h1 class={styles.title}>MEMLNaut Playground</h1>
|
<h1 class={styles.title}>MEMLNaut Playground</h1>
|
||||||
<p class={styles.tagline}>
|
<p class={styles.tagline}>
|
||||||
Interactive ML control of audio. SolidJS scaffold — modes coming online in stream 9.
|
Interactive ML control of audio. Stream 9: nine modes, one shell.
|
||||||
</p>
|
</p>
|
||||||
<ul class={styles.linkList}>
|
<ul class={styles.linkList}>
|
||||||
<li><a href="/dev/primitives" onClick={(e) => { e.preventDefault(); window.history.pushState({}, '', '/dev/primitives'); window.dispatchEvent(new PopStateEvent('popstate')); }}>Primitives showcase</a> — UI building blocks</li>
|
<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>
|
</ul>
|
||||||
<p class={styles.note}>
|
<p class={styles.note}>
|
||||||
This is a fresh scaffold. ML, WASM, and audio engines are not yet wired up.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
64
playground/src/modes/BreakOrMode.tsx
Normal file
64
playground/src/modes/BreakOrMode.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* 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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { BreakorSchema } from './generated/breakor_schema';
|
||||||
|
|
||||||
|
export const BreakOrMode: Component = () => {
|
||||||
|
const schema = BreakorSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
drawerTitle="Breakor params"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live drum params"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="var(--good)"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreakOrMode;
|
||||||
48
playground/src/modes/C15Mode.tsx
Normal file
48
playground/src/modes/C15Mode.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
68
playground/src/modes/ChannelStripMode.tsx
Normal file
68
playground/src/modes/ChannelStripMode.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* ChannelStripMode — channel-strip processor controlled by joystick.
|
||||||
|
*
|
||||||
|
* 24 outputs feed EQ / dynamics / gain. Schema has no voice spaces so the
|
||||||
|
* shell omits the selector. Drawer shows the live param sliders.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { ChannelStripSchema } from './generated/channel_strip_schema';
|
||||||
|
|
||||||
|
export const ChannelStripMode: Component = () => {
|
||||||
|
const schema = ChannelStripSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
drawerTitle="Channel strip params"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live parameter values"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only for now */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="var(--accent-3)"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChannelStripMode;
|
||||||
64
playground/src/modes/ElysiamorfMode.tsx
Normal file
64
playground/src/modes/ElysiamorfMode.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* 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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { ElysiamorfSchema } from './generated/elysiamorf_schema';
|
||||||
|
|
||||||
|
export const ElysiamorfMode: Component = () => {
|
||||||
|
const schema = ElysiamorfSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
drawerTitle="Elysiamorf params"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live morph params"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="#ffb060"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ElysiamorfMode;
|
||||||
68
playground/src/modes/MEMLCeliumMode.tsx
Normal file
68
playground/src/modes/MEMLCeliumMode.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* 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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { MemlceliumSchema } from './generated/memlcelium_schema';
|
||||||
|
|
||||||
|
export const MEMLCeliumMode: Component = () => {
|
||||||
|
const schema = MemlceliumSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
activeVoiceSpace={voiceSpace}
|
||||||
|
onVoiceSpaceChange={setVoiceSpace}
|
||||||
|
drawerTitle="MEMLCelium voice + CV"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live voice + CV params"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="#5b9eef"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MEMLCeliumMode;
|
||||||
74
playground/src/modes/PAFSynthMode.tsx
Normal file
74
playground/src/modes/PAFSynthMode.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* 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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { PafSynthSchema } from './generated/paf_synth_schema';
|
||||||
|
|
||||||
|
export const PAFSynthMode: Component = () => {
|
||||||
|
const schema = PafSynthSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
|
||||||
|
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
activeVoiceSpace={voiceSpace}
|
||||||
|
onVoiceSpaceChange={setVoiceSpace}
|
||||||
|
drawerTitle="PAF synth params"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live parameter values"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
// Sliders are display-only here. Stream 10 wires the per-param
|
||||||
|
// override editor which writes through modeStore.setOverride.
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="var(--accent)"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PAFSynthMode;
|
||||||
115
playground/src/modes/SoundAnalysisMIDIMode.tsx
Normal file
115
playground/src/modes/SoundAnalysisMIDIMode.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* SoundAnalysisMIDIMode — sound analysis → MIDI CC output (audio_in input).
|
||||||
|
*
|
||||||
|
* Schema declares `primary_input: 'audio_in'` and `engine_id: 'thru'` (no
|
||||||
|
* synthesis). The full firmware pipeline feeds audio analysis features
|
||||||
|
* (pitch / aperiodicity / energy / brightness / etc.) into the first 6
|
||||||
|
* input channels and joystick coords into the last 4. Stream 9 ships a
|
||||||
|
* scaffold UI; mic capture + analysis wiring is a stream-10 task.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { SoundAnalysisMidiSchema } from './generated/sound_analysis_midi_schema';
|
||||||
|
|
||||||
|
export const SoundAnalysisMIDIMode: Component = () => {
|
||||||
|
const schema = SoundAnalysisMidiSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
drawerTitle="MIDI CC routing"
|
||||||
|
drawerContent={() => (
|
||||||
|
<div>
|
||||||
|
<SliderBank
|
||||||
|
title="MIDI CC values"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
'font-size': 'var(--fs-xs)',
|
||||||
|
color: 'var(--fg-mute)',
|
||||||
|
'margin-top': 'var(--sp-3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
WebMIDI routing is wired up in stream 10. For now CC values are
|
||||||
|
visible in the live readout.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
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 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: 'var(--accent-2)' }}>Mic input — TODO</strong>
|
||||||
|
<span style={{ 'font-size': 'var(--fs-xs)' }}>
|
||||||
|
Stream 10 will request `getUserMedia` and feed audio analysis
|
||||||
|
features into the MLP. For now you can still drive the model
|
||||||
|
manually with the joystick below.
|
||||||
|
</span>
|
||||||
|
<VirtualJoystick
|
||||||
|
size={200}
|
||||||
|
onMove={(x, y) => runtime.setInput(x, y)}
|
||||||
|
position={runtime.pipedInput}
|
||||||
|
ariaLabel="Manual joystick fallback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
outputArea={() => (
|
||||||
|
<>
|
||||||
|
<OutputDisplay
|
||||||
|
values={runtime.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="var(--info)"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SoundAnalysisMIDIMode;
|
||||||
68
playground/src/modes/VerbFXMode.tsx
Normal file
68
playground/src/modes/VerbFXMode.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* 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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { VerbFxSchema } from './generated/verb_fx_schema';
|
||||||
|
|
||||||
|
export const VerbFXMode: Component = () => {
|
||||||
|
const schema = VerbFxSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const [voiceSpace, setVoiceSpace] = createSignal(0);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
activeVoiceSpace={voiceSpace}
|
||||||
|
onVoiceSpaceChange={setVoiceSpace}
|
||||||
|
drawerTitle="Verb / FX params"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live FX params"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="#b464ff"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerbFXMode;
|
||||||
69
playground/src/modes/XIASRIMode.tsx
Normal file
69
playground/src/modes/XIASRIMode.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* XIASRIMode — audio-reactive verb / pitch effects driven by joystick.
|
||||||
|
*
|
||||||
|
* Although the firmware variant historically used audio analysis as input,
|
||||||
|
* the playground schema declares `primary_input: 'joystick'` and feeds the
|
||||||
|
* MLP from joy_x/joy_y/joy_z/joy_w. The audio-reactive flavour is left to
|
||||||
|
* stream 10 (mic input wiring).
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 { SliderBank } from '../primitives/SliderBank';
|
||||||
|
import { LossPlot } from '../primitives/LossPlot';
|
||||||
|
import { paramsToSliderConfig, outputsToSliderValues } from './mode-helpers';
|
||||||
|
import { XiasriSchema } from './generated/xiasri_schema';
|
||||||
|
|
||||||
|
export const XIASRIMode: Component = () => {
|
||||||
|
const schema = XiasriSchema;
|
||||||
|
const runtime = useModeRuntime(schema);
|
||||||
|
const sliderConfig = paramsToSliderConfig(schema.params);
|
||||||
|
const sliderValues = () => outputsToSliderValues(runtime.processedOutputs(), schema.params);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModeShell
|
||||||
|
schema={schema}
|
||||||
|
runtime={runtime}
|
||||||
|
drawerTitle="XIASRI params"
|
||||||
|
drawerContent={() => (
|
||||||
|
<SliderBank
|
||||||
|
title="Live verb / pitch params"
|
||||||
|
sliders={sliderConfig}
|
||||||
|
values={sliderValues}
|
||||||
|
onChange={() => {
|
||||||
|
/* read-only */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
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. Mic input wiring is a stream-10 task.
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
outputArea={() => (
|
||||||
|
<>
|
||||||
|
<OutputDisplay
|
||||||
|
values={runtime.processedOutputs}
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
color="var(--accent-2)"
|
||||||
|
/>
|
||||||
|
<LossPlot history={runtime.training.lossHistory} width={360} height={70} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default XIASRIMode;
|
||||||
99
playground/src/modes/index.ts
Normal file
99
playground/src/modes/index.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* 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 { 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: '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';
|
||||||
Loading…
Reference in a new issue