diff --git a/manifold/src/dock/OutputsBackendConfig.tsx b/manifold/src/dock/OutputsBackendConfig.tsx index 0644127..1bc0943 100644 --- a/manifold/src/dock/OutputsBackendConfig.tsx +++ b/manifold/src/dock/OutputsBackendConfig.tsx @@ -28,6 +28,7 @@ import { savePreset, type OutputPreset, } from '../backends/presets'; +import { MIDI_DEVICES, MIDI_DEVICES_BY_ID, type MidiDeviceParam } from '../midi-devices'; function num(s: string, fallback: number): number { const v = parseFloat(s); @@ -205,6 +206,129 @@ function PresetBar({ ctx, backend }: { ctx: ConsoleCtx; backend: BackendId }) { ); } +// ---- Device template picker (synth-midi-cc templates) ---------------------- + +/** + * 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 + * 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. + */ +function DeviceTemplatePicker({ ctx }: { ctx: ConsoleCtx }) { + const [deviceId, setDeviceId] = useState(''); + const [selected, setSelected] = useState>(new Set()); + const device = deviceId ? MIDI_DEVICES_BY_ID[deviceId] : undefined; + const limit = ctx.params.length; // engine output count caps how many CCs we can drive + + const pickDevice = (id: string) => { + setDeviceId(id); + setSelected(new Set()); + }; + const toggle = (id: string) => + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + const selectFirst = (n: number) => { + if (!device) return; + setSelected(new Set(device.params.slice(0, n).map((p) => p.id))); + }; + + const apply = () => { + if (!device) return; + const chosen = device.params.filter((p) => selected.has(p.id)).slice(0, limit); + if (chosen.length === 0) return; + ctx.setMidiCcCount(chosen.length); + chosen.forEach((p, i) => + ctx.setParam(i, { + name: p.label, + midi: { cc: p.cc, channel: device.default_channel, name: p.label, value: 0 }, + }), + ); + }; + + // Stable, first-seen-order grouping for the checklist. + const groups: { group: string; params: MidiDeviceParam[] }[] = []; + if (device) { + const byGroup = new Map(); + for (const p of device.params) { + if (!byGroup.has(p.group)) { + const arr: MidiDeviceParam[] = []; + byGroup.set(p.group, arr); + groups.push({ group: p.group, params: arr }); + } + byGroup.get(p.group)!.push(p); + } + } + + const willApply = device ? Math.min(selected.size, limit) : 0; + const overflow = selected.size > limit; + + return ( + <> + Device template +
+ + {device && ( + <> + + + + + )} +
+ {device && ( + <> +

+ {device.manufacturer} · default ch {device.default_channel} · {selected.size} selected + {overflow ? ` (only the first ${limit} will map — engine has ${limit} outputs)` : ''}. Tick the parameters + the model should drive, then Apply. You can still tweak CC#/channel below. +

+
+ {groups.map(({ group, params }) => ( +
+
+ {group} +
+ {params.map((p) => ( + + ))} +
+ ))} +
+ + )} + + ); +} + // ---- MIDI config (backends-spec §2.3 / §4.1) ------------------------------- function MidiConfig({ ctx }: { ctx: ConsoleCtx }) { @@ -244,6 +368,8 @@ function MidiConfig({ ctx }: { ctx: ConsoleCtx }) { {s.message} + + Per-output CC · name · channel
diff --git a/manifold/src/midi-devices/index.ts b/manifold/src/midi-devices/index.ts new file mode 100644 index 0000000..68999b0 --- /dev/null +++ b/manifold/src/midi-devices/index.ts @@ -0,0 +1,9 @@ +/** + * External MIDI synth device templates. + * + * Re-exports the codegen output (generated from schemas/midi_devices/ via + * `bun run codegen/generate-midi-devices.ts`). Import from here, not from + * ./generated directly, so the path stays stable if non-generated helpers are + * added later. + */ +export * from './generated';