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`.
|
||||
Switching mode reshapes the WASM net to the mode's `ml` dims (ConsoleApp P5.3; boot mode paf_synth →
|
||||
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`,
|
||||
`OutputsBackendConfig.tsx` (per-backend specialised Outputs panel — the sole per-backend editor).
|
||||
- `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; the centered add-card control lives below the rows).
|
||||
- `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`
|
||||
(`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`.
|
||||
- `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
|
||||
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
|
||||
(`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
|
||||
|
|
|
|||
|
|
@ -166,9 +166,10 @@ for the narrow pane.
|
|||
curve/fixedValue + backend specs `MidiCcSpec`/`OscSpec`/`VcvSpec`), `toOutputControl()`,
|
||||
`buildArmMask()` (solo focus).
|
||||
- `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`.**
|
||||
- `OutputsBackendConfig.tsx` — preset bar (save/restore/rename/delete) + per-backend config (MIDI
|
||||
CC#/channel, OSC path/range, VCV polarity).
|
||||
· curve pad · live value; MIDI mode adds the card's name, CC#, and channel fields. **Writes eagerly
|
||||
to the shared `MFParam` store via `onChange`.** The centered `+` control follows the last visible card.
|
||||
- `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`)
|
||||
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 tone="var(--danger)">muted {mutedN}</Chip>
|
||||
<BackendStatusChip ctx={ctx} />
|
||||
<Button size="sm" variant="ghost" onClick={ctx.addOutput}>
|
||||
+ output
|
||||
</Button>
|
||||
</div>
|
||||
{ModeConfig(ctx, depth)}
|
||||
{/* 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)}
|
||||
onDelete={activeParams.length > 1 ? () => ctx.deleteOutput(i) : undefined}
|
||||
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>
|
||||
{!expanded && activeParams.length > 6 && (
|
||||
<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 { CurvePad } from '../console/CurvePad';
|
||||
import { DualRange } from '../console/DualRange';
|
||||
import { defaultMidiSpec } from './output-state';
|
||||
|
||||
const STATE_META: { v: ParamStatus; label: string; color: string }[] = [
|
||||
{ v: 'off', label: 'off', color: 'var(--fg-dim)' },
|
||||
|
|
@ -64,6 +65,8 @@ export interface OutputControlRowProps {
|
|||
onDelete?: () => void;
|
||||
/** Show the curve pad inline (expand depth); hidden in compact rows. */
|
||||
showCurve?: boolean;
|
||||
/** Show the per-card MIDI name, CC, and channel controls. */
|
||||
showMidi?: boolean;
|
||||
}
|
||||
|
||||
export function OutputControlRow({
|
||||
|
|
@ -72,12 +75,14 @@ export function OutputControlRow({
|
|||
onChange,
|
||||
onDelete,
|
||||
showCurve = false,
|
||||
showMidi = false,
|
||||
}: OutputControlRowProps) {
|
||||
const gc = `var(${GROUP_COLOR[param.group] || '--accent'})`;
|
||||
const muted = param.muted ?? false;
|
||||
const armed = param.armed ?? false;
|
||||
const off = param.status === 'off';
|
||||
const barVal = param.status === 'fixed' ? param.val : value;
|
||||
const midi = param.midi ?? defaultMidiSpec(param.engineIndex ?? 0);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -93,6 +98,25 @@ export function OutputControlRow({
|
|||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{showMidi ? (
|
||||
<input
|
||||
aria-label={`MIDI name for ${param.name}`}
|
||||
value={midi.name}
|
||||
onChange={(e) => onChange({ midi: { ...midi, name: e.target.value } })}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
width: 0,
|
||||
border: 0,
|
||||
borderBottom: '1px solid var(--line)',
|
||||
background: 'transparent',
|
||||
color: 'var(--fg)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 'var(--fs-xs)',
|
||||
padding: '1px 0',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
|
|
@ -106,6 +130,7 @@ export function OutputControlRow({
|
|||
>
|
||||
{param.name}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>{param.group}</span>
|
||||
{onDelete && (
|
||||
<button
|
||||
|
|
@ -145,6 +170,59 @@ export function OutputControlRow({
|
|||
/>
|
||||
</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 }}>
|
||||
{/* tri-state segmented */}
|
||||
<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
|
||||
* 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.
|
||||
* - 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
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import type { ConsoleCtx } from '../console/types';
|
||||
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 {
|
||||
applyPreset,
|
||||
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
|
||||
* ones to control, and "Apply" — this fills the per-output CC table (CC#, channel,
|
||||
* name) from the device template and sets the CC count. Pure local state + the
|
||||
* ones to control, and "Apply" — this fills the per-output card fields (CC#,
|
||||
* 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
|
||||
* 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>
|
||||
<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 style={{ fontSize: 9, color: statusColor }}>{s.message}</span>
|
||||
</div>
|
||||
|
||||
<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