refactor(manifold): move MIDI controls onto output cards
This commit is contained in:
parent
5ed84cbf00
commit
5ab4d18523
5 changed files with 127 additions and 86 deletions
6
MAP.md
6
MAP.md
|
|
@ -59,8 +59,8 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
|
||||||
consts (mode_id, engine_id, ml dims, params, voice_spaces, ui) — the SOURCE OF TRUTH for `MF_MODES`.
|
consts (mode_id, engine_id, ml dims, params, voice_spaces, ui) — the SOURCE OF TRUTH for `MF_MODES`.
|
||||||
Switching mode reshapes the WASM net to the mode's `ml` dims (ConsoleApp P5.3; boot mode paf_synth →
|
Switching mode reshapes the WASM net to the mode's `ml` dims (ConsoleApp P5.3; boot mode paf_synth →
|
||||||
4→[10,10,14]→33).
|
4→[10,10,14]→33).
|
||||||
- `manifold/src/dock/` — `OutputControlRow` (add/delete card identity + off/fixed/live + mute + solo/arm + min/max/curve), `output-state.ts`,
|
- `manifold/src/dock/` — `OutputControlRow` (add/delete card identity + off/fixed/live + mute + solo/arm + min/max/curve, plus MIDI card fields), `output-state.ts`,
|
||||||
`OutputsBackendConfig.tsx` (per-backend specialised Outputs panel — the sole per-backend editor).
|
`OutputsBackendConfig.tsx` (per-backend specialised Outputs panel — the sole per-backend editor; the centered add-card control lives below the rows).
|
||||||
- `manifold/src/backends/` — `OutputBackend` adapter + `BackendManager` (spine consumer); `midi-backend.ts`
|
- `manifold/src/backends/` — `OutputBackend` adapter + `BackendManager` (spine consumer); `midi-backend.ts`
|
||||||
(WebMIDI), `osc-backend.ts`+`osc-client.ts` (OSC-over-WS), `vcv-backend.ts` (VCV-over-WS), `cv-backend.ts`
|
(WebMIDI), `osc-backend.ts`+`osc-client.ts` (OSC-over-WS), `vcv-backend.ts` (VCV-over-WS), `cv-backend.ts`
|
||||||
(`UseqCvBackend` — uSEQ CV/gate over USB Web Serial, backend id `cvgate`) + `useq-protocol.ts` (v2 wire
|
(`UseqCvBackend` — uSEQ CV/gate over USB Web Serial, backend id `cvgate`) + `useq-protocol.ts` (v2 wire
|
||||||
|
|
@ -68,7 +68,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set.
|
||||||
`particle-backend.ts`, `passthrough-backend.ts`, `presets.ts` (named presets), `manager.ts`.
|
`particle-backend.ts`, `passthrough-backend.ts`, `presets.ts` (named presets), `manager.ts`.
|
||||||
- `manifold/src/midi-devices/` — external-synth device templates. `generated/` is codegen output from
|
- `manifold/src/midi-devices/` — external-synth device templates. `generated/` is codegen output from
|
||||||
`schemas/midi_devices/` (`MIDI_DEVICES` catalogue + `MIDI_DEVICES_BY_ID`, params by name+CC). The MIDI Outputs
|
`schemas/midi_devices/` (`MIDI_DEVICES` catalogue + `MIDI_DEVICES_BY_ID`, params by name+CC). The MIDI Outputs
|
||||||
config (`dock/OutputsBackendConfig.tsx`) reads it for the device picker + param-select that fills the CC table.
|
config (`dock/OutputsBackendConfig.tsx`) reads it for the device picker + param-select that fills the per-card MIDI fields.
|
||||||
- `manifold/src/inputs/` — modular INPUT layer feeding the ML head. The Inputs dock picks ONE exclusive mode
|
- `manifold/src/inputs/` — modular INPUT layer feeding the ML head. The Inputs dock picks ONE exclusive mode
|
||||||
(`InputMode` = `internal` | `gamepad` | `midi`; Internal/XY-pad is default). `input-layer.ts` owns a single rAF
|
(`InputMode` = `internal` | `gamepad` | `midi`; Internal/XY-pad is default). `input-layer.ts` owns a single rAF
|
||||||
loop composing the active source's axes → **one dedicated engine input slot per axis, 1:1, no blending** → one
|
loop composing the active source's axes → **one dedicated engine input slot per axis, 1:1, no blending** → one
|
||||||
|
|
|
||||||
|
|
@ -166,9 +166,10 @@ for the narrow pane.
|
||||||
curve/fixedValue + backend specs `MidiCcSpec`/`OscSpec`/`VcvSpec`), `toOutputControl()`,
|
curve/fixedValue + backend specs `MidiCcSpec`/`OscSpec`/`VcvSpec`), `toOutputControl()`,
|
||||||
`buildArmMask()` (solo focus).
|
`buildArmMask()` (solo focus).
|
||||||
- `OutputControlRow.tsx` — one output row: name · M(mute) · S(solo/arm) · off|fixed|live · dual-range
|
- `OutputControlRow.tsx` — one output row: name · M(mute) · S(solo/arm) · off|fixed|live · dual-range
|
||||||
· curve pad · live value. **Writes eagerly to the shared `MFParam` store via `onChange`.**
|
· curve pad · live value; MIDI mode adds the card's name, CC#, and channel fields. **Writes eagerly
|
||||||
- `OutputsBackendConfig.tsx` — preset bar (save/restore/rename/delete) + per-backend config (MIDI
|
to the shared `MFParam` store via `onChange`.** The centered `+` control follows the last visible card.
|
||||||
CC#/channel, OSC path/range, VCV polarity).
|
- `OutputsBackendConfig.tsx` — preset bar (save/restore/rename/delete) + transport/device config (MIDI
|
||||||
|
port/templates, OSC path/range, VCV polarity); MIDI per-output fields live on `OutputControlRow`.
|
||||||
|
|
||||||
### Primitives — `src/primitives/` (barrel: `index.ts`)
|
### Primitives — `src/primitives/` (barrel: `index.ts`)
|
||||||
Seven: `Button`, `Slider`, `PillToggle`, `Badge`, `Switch`, `XYPad`, `VirtualJoystick`. Dumb,
|
Seven: `Button`, `Slider`, `PillToggle`, `Badge`, `Switch`, `XYPad`, `VirtualJoystick`. Dumb,
|
||||||
|
|
|
||||||
|
|
@ -757,9 +757,6 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
<Chip>off {counts.off || 0}</Chip>
|
<Chip>off {counts.off || 0}</Chip>
|
||||||
<Chip tone="var(--danger)">muted {mutedN}</Chip>
|
<Chip tone="var(--danger)">muted {mutedN}</Chip>
|
||||||
<BackendStatusChip ctx={ctx} />
|
<BackendStatusChip ctx={ctx} />
|
||||||
<Button size="sm" variant="ghost" onClick={ctx.addOutput}>
|
|
||||||
+ output
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{ModeConfig(ctx, depth)}
|
{ModeConfig(ctx, depth)}
|
||||||
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */}
|
{/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */}
|
||||||
|
|
@ -787,9 +784,32 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) {
|
||||||
onChange={(patch) => ctx.setParam(i, patch)}
|
onChange={(patch) => ctx.setParam(i, patch)}
|
||||||
onDelete={activeParams.length > 1 ? () => ctx.deleteOutput(i) : undefined}
|
onDelete={activeParams.length > 1 ? () => ctx.deleteOutput(i) : undefined}
|
||||||
showCurve={expanded}
|
showCurve={expanded}
|
||||||
|
showMidi={ctx.outputMode === 'midi'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="+ output"
|
||||||
|
title="Add parameter"
|
||||||
|
onClick={ctx.addOutput}
|
||||||
|
style={{
|
||||||
|
alignSelf: 'center',
|
||||||
|
width: 30,
|
||||||
|
height: 26,
|
||||||
|
margin: '4px 0 2px',
|
||||||
|
border: '1px solid var(--line)',
|
||||||
|
borderRadius: 'var(--r-1)',
|
||||||
|
background: 'var(--bg-2)',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 18,
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{!expanded && activeParams.length > 6 && (
|
{!expanded && activeParams.length > 6 && (
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{activeParams.length - 6} more — expand to edit</span>
|
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>+{activeParams.length - 6} more — expand to edit</span>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { GROUP_COLOR } from '../console/model';
|
||||||
import type { MFParam, ParamStatus } from '../console/model';
|
import type { MFParam, ParamStatus } from '../console/model';
|
||||||
import { CurvePad } from '../console/CurvePad';
|
import { CurvePad } from '../console/CurvePad';
|
||||||
import { DualRange } from '../console/DualRange';
|
import { DualRange } from '../console/DualRange';
|
||||||
|
import { defaultMidiSpec } from './output-state';
|
||||||
|
|
||||||
const STATE_META: { v: ParamStatus; label: string; color: string }[] = [
|
const STATE_META: { v: ParamStatus; label: string; color: string }[] = [
|
||||||
{ v: 'off', label: 'off', color: 'var(--fg-dim)' },
|
{ v: 'off', label: 'off', color: 'var(--fg-dim)' },
|
||||||
|
|
@ -64,6 +65,8 @@ export interface OutputControlRowProps {
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
/** Show the curve pad inline (expand depth); hidden in compact rows. */
|
/** Show the curve pad inline (expand depth); hidden in compact rows. */
|
||||||
showCurve?: boolean;
|
showCurve?: boolean;
|
||||||
|
/** Show the per-card MIDI name, CC, and channel controls. */
|
||||||
|
showMidi?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OutputControlRow({
|
export function OutputControlRow({
|
||||||
|
|
@ -72,12 +75,14 @@ export function OutputControlRow({
|
||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
showCurve = false,
|
showCurve = false,
|
||||||
|
showMidi = false,
|
||||||
}: OutputControlRowProps) {
|
}: OutputControlRowProps) {
|
||||||
const gc = `var(${GROUP_COLOR[param.group] || '--accent'})`;
|
const gc = `var(${GROUP_COLOR[param.group] || '--accent'})`;
|
||||||
const muted = param.muted ?? false;
|
const muted = param.muted ?? false;
|
||||||
const armed = param.armed ?? false;
|
const armed = param.armed ?? false;
|
||||||
const off = param.status === 'off';
|
const off = param.status === 'off';
|
||||||
const barVal = param.status === 'fixed' ? param.val : value;
|
const barVal = param.status === 'fixed' ? param.val : value;
|
||||||
|
const midi = param.midi ?? defaultMidiSpec(param.engineIndex ?? 0);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -93,19 +98,39 @@ export function OutputControlRow({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span
|
{showMidi ? (
|
||||||
style={{
|
<input
|
||||||
flex: 1,
|
aria-label={`MIDI name for ${param.name}`}
|
||||||
minWidth: 0,
|
value={midi.name}
|
||||||
fontSize: 'var(--fs-xs)',
|
onChange={(e) => onChange({ midi: { ...midi, name: e.target.value } })}
|
||||||
color: 'var(--fg)',
|
style={{
|
||||||
overflow: 'hidden',
|
flex: 1,
|
||||||
textOverflow: 'ellipsis',
|
minWidth: 0,
|
||||||
whiteSpace: 'nowrap',
|
width: 0,
|
||||||
}}
|
border: 0,
|
||||||
>
|
borderBottom: '1px solid var(--line)',
|
||||||
{param.name}
|
background: 'transparent',
|
||||||
</span>
|
color: 'var(--fg)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 'var(--fs-xs)',
|
||||||
|
padding: '1px 0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
fontSize: 'var(--fs-xs)',
|
||||||
|
color: 'var(--fg)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{param.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>{param.group}</span>
|
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>{param.group}</span>
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -145,6 +170,59 @@ export function OutputControlRow({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showMidi && (
|
||||||
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
||||||
|
CC
|
||||||
|
<input
|
||||||
|
aria-label={`MIDI CC for ${param.name}`}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={127}
|
||||||
|
value={midi.cc}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ midi: { ...midi, cc: Math.max(0, Math.min(127, Number(e.target.value) || 0)) } })
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 54,
|
||||||
|
background: 'var(--bg-1)',
|
||||||
|
border: '1px solid var(--line)',
|
||||||
|
borderRadius: 'var(--r-1)',
|
||||||
|
color: 'var(--fg)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 10,
|
||||||
|
padding: '2px 4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, color: 'var(--fg-mute)' }}>
|
||||||
|
ch
|
||||||
|
<input
|
||||||
|
aria-label={`MIDI channel for ${param.name}`}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={16}
|
||||||
|
value={midi.channel}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
midi: { ...midi, channel: Math.max(1, Math.min(16, Number(e.target.value) || 1)) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
width: 44,
|
||||||
|
background: 'var(--bg-1)',
|
||||||
|
border: '1px solid var(--line)',
|
||||||
|
borderRadius: 'var(--r-1)',
|
||||||
|
color: 'var(--fg)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 10,
|
||||||
|
padding: '2px 4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
{/* tri-state segmented */}
|
{/* tri-state segmented */}
|
||||||
<div style={{ display: 'flex', gap: 1, flex: '0 0 auto' }}>
|
<div style={{ display: 'flex', gap: 1, flex: '0 0 auto' }}>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
*
|
*
|
||||||
* Layout: ONE preset bar (save-as / restore / rename / delete, per-backend
|
* Layout: ONE preset bar (save-as / restore / rename / delete, per-backend
|
||||||
* namespace) on top, then a per-backend config section:
|
* namespace) on top, then a per-backend config section:
|
||||||
* - MIDI → output-port picker, number-of-CCs, per-output CC#/channel/name.
|
* - MIDI → output-port picker, number-of-CCs, and device templates; each
|
||||||
|
* output card owns its CC#/channel/name fields.
|
||||||
* - OSC → bridge URL + connect status + send-raw toggle, per-output path/range.
|
* - OSC → bridge URL + connect status + send-raw toggle, per-output path/range.
|
||||||
* - VCV/CV→ bridge URL + connect status + send-raw toggle, per-output polarity
|
* - VCV/CV→ bridge URL + connect status + send-raw toggle, per-output polarity
|
||||||
* (uni 0–10 V / bipolar ±5 V). The browser drives + trains the VCV
|
* (uni 0–10 V / bipolar ±5 V). The browser drives + trains the VCV
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { ConsoleCtx } from '../console/types';
|
import type { ConsoleCtx } from '../console/types';
|
||||||
import type { BackendId, CvChannelId } from './output-state';
|
import type { BackendId, CvChannelId } from './output-state';
|
||||||
import { CV_CHANNELS, defaultCvSpec, defaultMidiSpec, defaultOscSpec, defaultVcvSpec } from './output-state';
|
import { CV_CHANNELS, defaultCvSpec, defaultOscSpec, defaultVcvSpec } from './output-state';
|
||||||
import {
|
import {
|
||||||
applyPreset,
|
applyPreset,
|
||||||
deletePreset,
|
deletePreset,
|
||||||
|
|
@ -212,8 +213,8 @@ function PresetBar({ ctx, backend }: { ctx: ConsoleCtx; backend: BackendId }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pick an external synth (e.g. Moog Sub 37), see its parameters BY NAME, tick the
|
* Pick an external synth (e.g. Moog Sub 37), see its parameters BY NAME, tick the
|
||||||
* ones to control, and "Apply" — this fills the per-output CC table (CC#, channel,
|
* ones to control, and "Apply" — this fills the per-output card fields (CC#,
|
||||||
* name) from the device template and sets the CC count. Pure local state + the
|
* channel, name) from the device template and sets the CC count. Pure local state + the
|
||||||
* existing setParam/setMidiCcCount; the applied result lives in the shared params
|
* existing setParam/setMidiCcCount; the applied result lives in the shared params
|
||||||
* store, so it is saved/restored by the named-preset bar like any other config.
|
* store, so it is saved/restored by the named-preset bar like any other config.
|
||||||
*/
|
*/
|
||||||
|
|
@ -355,72 +356,13 @@ function MidiConfig({ ctx }: { ctx: ConsoleCtx }) {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>
|
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)' }}>
|
||||||
{ctx.midiCcCount} CC output{ctx.midiCcCount === 1 ? '' : 's'} · add/delete cards below
|
{ctx.midiCcCount} MIDI output card{ctx.midiCcCount === 1 ? '' : 's'}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 9, color: statusColor }}>{s.message}</span>
|
<span style={{ fontSize: 9, color: statusColor }}>{s.message}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeviceTemplatePicker ctx={ctx} />
|
<DeviceTemplatePicker ctx={ctx} />
|
||||||
|
|
||||||
<SectionLabel>Per-output CC · name · channel</SectionLabel>
|
|
||||||
<div style={{ maxHeight: 320, overflow: 'auto' }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>CC#</Th>
|
|
||||||
<Th>Ch</Th>
|
|
||||||
<Th>State</Th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{ctx.params.slice(0, ctx.midiCcCount).map((p, i) => {
|
|
||||||
const m = p.midi ?? defaultMidiSpec(i);
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
<td style={{ padding: '3px 6px' }}>
|
|
||||||
<input
|
|
||||||
style={cellInput}
|
|
||||||
value={m.name}
|
|
||||||
onChange={(e) => ctx.setParam(i, { midi: { ...m, name: e.target.value } })}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 70 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={127}
|
|
||||||
style={cellInput}
|
|
||||||
value={m.cc}
|
|
||||||
onChange={(e) =>
|
|
||||||
ctx.setParam(i, { midi: { ...m, cc: Math.max(0, Math.min(127, num(e.target.value, m.cc))) } })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', width: 60 }}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={16}
|
|
||||||
style={cellInput}
|
|
||||||
value={m.channel}
|
|
||||||
onChange={(e) =>
|
|
||||||
ctx.setParam(i, {
|
|
||||||
midi: { ...m, channel: Math.max(1, Math.min(16, num(e.target.value, m.channel))) },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '3px 6px', fontSize: 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
|
|
||||||
{p.status}
|
|
||||||
{p.muted ? ' · muted' : ''}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue