/** * OutputsBackendConfig — the editable, per-backend specialisation of the Outputs * panel (backends-spec §4) plus the named-preset bar (§5). * * 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. * - 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 * module over the same Deno bridge. * - Synth/Particle/Editor → handled by ModeConfig in Drawers (no extra config here). * * Everything is editable inline; writes go through the shared MFParam store * (ctx.setParam) — never a second data path. The full-depth modal reuses the * same sections via BackendAdvanced. */ import { useEffect, useState } from 'react'; import type { ConsoleCtx } from '../console/types'; import type { BackendId } from './output-state'; import { defaultMidiSpec, defaultOscSpec, defaultVcvSpec } from './output-state'; import { applyPreset, deletePreset, getPreset, listPresets, renamePreset, savePreset, type OutputPreset, } from '../backends/presets'; function num(s: string, fallback: number): number { const v = parseFloat(s); return Number.isFinite(v) ? v : fallback; } const cellInput: React.CSSProperties = { width: '100%', background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 'var(--r-1)', color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: 'var(--fs-xs)', padding: '3px 6px', boxSizing: 'border-box', }; const btn = (color: string): React.CSSProperties => ({ fontSize: 'var(--fs-xs)', fontFamily: 'var(--font-mono)', padding: '3px 9px', cursor: 'pointer', borderRadius: 'var(--r-pill)', border: `1px solid ${color}`, background: 'transparent', color, }); function SectionLabel({ children }: { children: React.ReactNode }) { return (
{children}
); } function Th({ children }: { children: React.ReactNode }) { return ( {children} ); } // ---- Named-preset bar (backends-spec §5) ----------------------------------- function PresetBar({ ctx, backend }: { ctx: ConsoleCtx; backend: BackendId }) { const [presets, setPresets] = useState([]); const [name, setName] = useState(''); const [selected, setSelected] = useState(''); const refresh = () => setPresets(listPresets(backend)); useEffect(() => { refresh(); setSelected(''); setName(''); // eslint-disable-next-line react-hooks/exhaustive-deps }, [backend]); const backendSettings = (): Record => { if (backend === 'midi') return { outputId: ctx.midiOutputId, ccCount: ctx.midiCcCount }; if (backend === 'osc') return { url: ctx.oscUrl, sendRaw: ctx.oscSendRaw }; if (backend === 'vcv') return { url: ctx.vcvUrl, sendRaw: ctx.vcvSendRaw }; return {}; }; const applySettings = (s?: Record) => { if (!s) return; if (backend === 'midi') { if ('outputId' in s) ctx.setMidiOutputId((s.outputId as string | null) ?? null); if ('ccCount' in s) ctx.setMidiCcCount(Number(s.ccCount) || ctx.midiCcCount); } else if (backend === 'osc') { if ('url' in s) ctx.setOscUrl(String(s.url)); if ('sendRaw' in s) ctx.setOscSendRaw(Boolean(s.sendRaw)); } else if (backend === 'vcv') { if ('url' in s) ctx.setVcvUrl(String(s.url)); if ('sendRaw' in s) ctx.setVcvSendRaw(Boolean(s.sendRaw)); } }; const doSave = () => { const n = name.trim(); if (!n) return; savePreset(backend, n, ctx.params, backendSettings()); refresh(); setSelected(n); }; const doRestore = (n: string) => { const p = getPreset(backend, n); if (!p) return; ctx.setParams(applyPreset(ctx.params, p)); applySettings(p.settings); }; const doDelete = () => { if (!selected) return; deletePreset(backend, selected); refresh(); setSelected(''); }; const doRename = () => { const to = name.trim(); if (!selected || !to) return; if (renamePreset(backend, selected, to)) { refresh(); setSelected(to); } }; return (
Presets · {backend} setName(e.target.value)} />
); } // ---- MIDI config (backends-spec §2.3 / §4.1) ------------------------------- function MidiConfig({ ctx }: { ctx: ConsoleCtx }) { const s = ctx.backendStatus; const statusColor = s.state === 'ready' ? 'var(--good)' : s.state === 'error' || s.state === 'unavailable' ? 'var(--danger)' : 'var(--warn)'; return ( <> MIDI output
{s.message}
Per-output CC · name · channel
{ctx.params.slice(0, ctx.midiCcCount).map((p, i) => { const m = p.midi ?? defaultMidiSpec(i); return ( ); })}
Name CC# Ch State
ctx.setParam(i, { midi: { ...m, name: e.target.value } })} /> ctx.setParam(i, { midi: { ...m, cc: Math.max(0, Math.min(127, num(e.target.value, m.cc))) } }) } /> ctx.setParam(i, { midi: { ...m, channel: Math.max(1, Math.min(16, num(e.target.value, m.channel))) }, }) } /> {p.status} {p.muted ? ' · muted' : ''}
); } // ---- OSC config (backends-spec §2.4 / §4.2) -------------------------------- function OscConfig({ ctx }: { ctx: ConsoleCtx }) { const s = ctx.backendStatus; const statusColor = s.state === 'ready' ? 'var(--good)' : s.state === 'connecting' ? 'var(--warn)' : 'var(--danger)'; const [draftUrl, setDraftUrl] = useState(ctx.oscUrl); useEffect(() => setDraftUrl(ctx.oscUrl), [ctx.oscUrl]); return ( <> OSC bridge
setDraftUrl(e.target.value)} onBlur={() => ctx.setOscUrl(draftUrl)} placeholder="ws://localhost:8765" /> {s.message}

The Deno OSC bridge process must be running locally (see manifold/osc-bridge). The browser sends over WebSocket; the bridge encodes OSC and forwards over UDP.

Per-output address · physical range
{ctx.params.map((p, i) => { const o = p.osc ?? defaultOscSpec(p.name); return ( ); })}
OSC path range min range max
ctx.setParam(i, { osc: { ...o, path: e.target.value } })} /> ctx.setParam(i, { osc: { ...o, rangeMin: num(e.target.value, o.rangeMin) } })} /> ctx.setParam(i, { osc: { ...o, rangeMax: num(e.target.value, o.rangeMax) } })} />
); } // ---- VCV / CV config (backends-spec §2.6 / §4.3) --------------------------- function VcvConfig({ ctx }: { ctx: ConsoleCtx }) { const s = ctx.backendStatus; const statusColor = s.state === 'ready' ? 'var(--good)' : s.state === 'connecting' ? 'var(--warn)' : 'var(--danger)'; const [draftUrl, setDraftUrl] = useState(ctx.vcvUrl); useEffect(() => setDraftUrl(ctx.vcvUrl), [ctx.vcvUrl]); return ( <> VCV bridge
setDraftUrl(e.target.value)} onBlur={() => ctx.setVcvUrl(draftUrl)} placeholder="ws://localhost:8765" /> {s.message}

The VCV Rack NISPS module AND the Deno OSC bridge (manifold/osc-bridge) must both be running. The browser drives the module's inputs and forwards the verdict loop (thumbs up/down, explore-and-place) over the bridge, so the module's embedded net trains in lock-step. Default module UDP port 7001.

Per-output polarity
{ctx.params.map((p, i) => { const v = p.vcv ?? defaultVcvSpec(); return ( ); })}
output range
{p.name}
); } // ---- Public entry ---------------------------------------------------------- export interface OutputsBackendConfigProps { ctx: ConsoleCtx; backend: BackendId; } /** The specialised, editable per-backend config + preset bar for the Outputs panel. */ export function OutputsBackendConfig({ ctx, backend }: OutputsBackendConfigProps) { // MIDI / OSC / VCV carry a config + preset surface here; synth/particle/editor // config is rendered by ModeConfig in Drawers. The full-depth BackendAdvanced // modal reuses the same per-channel sections. if (backend !== 'midi' && backend !== 'osc' && backend !== 'vcv') return null; return (
{backend === 'midi' ? : backend === 'osc' ? : }
); } export { PresetBar as OutputPresetBar }; /** Tiny status pill for the Outputs drawer header. */ export function BackendStatusChip({ ctx }: { ctx: ConsoleCtx }) { const s = ctx.backendStatus; if (s.state === 'idle') return null; const color = s.state === 'ready' ? 'var(--good)' : s.state === 'error' || s.state === 'unavailable' ? 'var(--danger)' : 'var(--warn)'; return ( {s.message} ); }