From 00adc4df53f9f801910fbad2c2b8b829630571ed Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Wed, 12 Aug 2026 18:45:57 +0200 Subject: [PATCH] feat: add matrix section --- src/components/Button/Button.css | 2 + src/components/Button/Button.tsx | 3 - .../ChoiceStepper/ChoiceStepper.css | 80 + .../ChoiceStepper/ChoiceStepper.tsx | 90 + src/components/Sidebar/Sidebar.tsx | 16 +- .../SignalFlowMatrix/SignalFlowMatrix.css | 33 + .../SignalFlowMatrix/SignalFlowMatrix.tsx | 259 +++ .../CompressorSection/CompressorSection.css | 81 - .../CompressorSection/CompressorSection.tsx | 84 +- .../components/DSP408Panel/DSP408Panel.tsx | 12 +- .../Matrixsection/Matrixsection.css | 168 +- .../Matrixsection/Matrixsection.tsx | 284 +--- src/features/dsp408/constants.ts | 42 +- src/features/dsp408/dsp408.ts | 9 +- src/features/dsp408/types.ts | 11 +- src/hooks/useDSPController.ts | 1469 ----------------- 16 files changed, 657 insertions(+), 1986 deletions(-) create mode 100644 src/components/ChoiceStepper/ChoiceStepper.css create mode 100644 src/components/ChoiceStepper/ChoiceStepper.tsx create mode 100644 src/components/SignalFlowMatrix/SignalFlowMatrix.css create mode 100644 src/components/SignalFlowMatrix/SignalFlowMatrix.tsx delete mode 100644 src/hooks/useDSPController.ts diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css index da21d40..73110de 100644 --- a/src/components/Button/Button.css +++ b/src/components/Button/Button.css @@ -17,6 +17,8 @@ font-weight: var(--font-weight-medium); cursor: pointer; + + font-size: 13px; } .btn:disabled { diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 765c775..a9d9a38 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -7,7 +7,6 @@ interface ButtonProps extends ButtonHTMLAttributes { variant?: ButtonVariant; children: ReactNode; className?: string; - fontSize?: number; } export default function Button({ @@ -15,7 +14,6 @@ export default function Button({ children, className = '', disabled, - fontSize = 13, type = 'button', style, ...props @@ -26,7 +24,6 @@ export default function Button({ className={`btn btn-${variant} ${className}`} disabled={disabled} style={{ - fontSize: `${fontSize}px`, ...style, }} {...props} diff --git a/src/components/ChoiceStepper/ChoiceStepper.css b/src/components/ChoiceStepper/ChoiceStepper.css new file mode 100644 index 0000000..b65c30a --- /dev/null +++ b/src/components/ChoiceStepper/ChoiceStepper.css @@ -0,0 +1,80 @@ +.choice-stepper { + width: 100%; + flex: 1 1 auto; + + position: relative; + display: flex; + align-items: stretch; + justify-content: center; + + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-sm); + background: transparent; +} + +.choice-stepper__input { + flex: 1 1 auto; + min-width: 0; + width: 100%; + + font-family: var(--font-ui); + font-size: 3cqh; + font-weight: var(--font-weight-semibold); + text-align: center; + + color: var(--accent-brand); + + background: transparent; + border: none; + border-radius: var(--radius-sm); + padding: 0.5ch; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.choice-stepper__input:focus { + outline: none; +} + +.choice-stepper__arrows { + flex: 0 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.5cqh; + + height: 100%; + box-sizing: border-box; + padding: 1cqh; + padding-left: 0; +} + +.choice-stepper__arrow-btn { + aspect-ratio: 1; + height: 4.5cqh; + min-height: 0; + + display: flex; + align-items: center; + justify-content: center; + + padding: 0; + color: var(--accent-brand); + + background: transparent; + border-radius: 1cqh; + + cursor: pointer; +} + +.choice-stepper__arrow-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.choice-stepper__arrow-btn svg { + width: 100%; + height: 100%; +} diff --git a/src/components/ChoiceStepper/ChoiceStepper.tsx b/src/components/ChoiceStepper/ChoiceStepper.tsx new file mode 100644 index 0000000..1fae96f --- /dev/null +++ b/src/components/ChoiceStepper/ChoiceStepper.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react'; +import Button from '../Button/Button'; +import { TriangleUpIcon, TriangleDownIcon } from '../../assets/icons/Triangle'; +import './ChoiceStepper.css'; + +type ChoiceStepperProps = { + value: T; + values: readonly T[]; + formatLabel: (value: T) => string; + onChange: (value: T) => void; +}; + +function ChoiceStepper({ value, values, formatLabel, onChange }: ChoiceStepperProps) { + const index = values.indexOf(value); + + const canUp = index === -1 || index < values.length - 1; + const canDown = index > 0; + + const [text, setText] = useState(formatLabel(value)); + + useEffect(() => { + setText(formatLabel(value)); + }, [value, formatLabel]); + + const step = (direction: 1 | -1) => { + const currentIndex = index === -1 ? 0 : index; + + const nextIndex = Math.min(values.length - 1, Math.max(0, currentIndex + direction)); + + if (nextIndex !== currentIndex) { + onChange(values[nextIndex]); + } + }; + + const commitText = () => { + const match = values.find( + (v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase() + ); + + if (match !== undefined) { + onChange(match); + } else { + setText(formatLabel(value)); + } + }; + + return ( +
+ setText(e.target.value)} + onBlur={commitText} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.currentTarget.blur(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + step(1); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + step(-1); + } + }} + /> + +
+ + + +
+
+ ); +} + +export default ChoiceStepper; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 7c1746e..941c782 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from 'react'; import './Sidebar.css'; import { ChevronLeftIcon } from '../../assets/icons/ChevronLeftIcon'; -export type SidebarItem = { - id: string; +export type SidebarItem = { + id: T; label: string; icon: React.ReactNode; }; @@ -13,12 +13,12 @@ export type SidebarAppOption = { name: string; }; -function SidebarButton({ +function SidebarButton({ item, active, onSelect, }: { - item: SidebarItem; + item: SidebarItem; active: boolean; onSelect: () => void; }) { @@ -218,7 +218,7 @@ function AppCombobox({ ); } -export default function Sidebar({ +export function Sidebar({ items, activeId, onSelect, @@ -230,9 +230,9 @@ export default function Sidebar({ expanded, onToggle, }: { - items: SidebarItem[]; - activeId: string | null; - onSelect: (id: string) => void; + items: SidebarItem[]; + activeId: T | null; + onSelect: (id: T) => void; appName: string; appOptions?: SidebarAppOption[]; selectedAppId?: string; diff --git a/src/components/SignalFlowMatrix/SignalFlowMatrix.css b/src/components/SignalFlowMatrix/SignalFlowMatrix.css new file mode 100644 index 0000000..1aeb044 --- /dev/null +++ b/src/components/SignalFlowMatrix/SignalFlowMatrix.css @@ -0,0 +1,33 @@ +.signal-flow { + width: 100%; + height: 100%; +} + +.signal-flow__svg { + width: 100%; + height: 100%; + display: block; +} + +.signal-flow__chain-line { + stroke: var(--border-hairline, #333); + stroke-width: 1; +} + +.signal-flow__stage { + fill: var(--bg-void); + stroke: var(--border-hairline); + stroke-width: 1; + transition: fill 0.1s ease; +} + +.signal-flow__stage:hover { + fill: var(--bg-panel); +} + +.signal-flow__stage-label { + font-family: var(--font-ui); + font-size: 13px; + fill: var(--text-primary, #e5e7eb); + pointer-events: none; +} diff --git a/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx b/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx new file mode 100644 index 0000000..faf2439 --- /dev/null +++ b/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx @@ -0,0 +1,259 @@ +import { useMemo } from 'react'; +import { InputChannel, OutputChannel } from '../../features/dsp408/types'; +import { DSP408 } from '../../types'; +import { + INPUT_ORDER, + OUTPUT_ORDER, + MATRIX_GAIN_MIN, + MATRIX_GAIN_MAX, + MATRIX_GAIN_STEP, +} from '../../features/dsp408/constants'; +import { SidebarId } from '../../features/dsp408/constants'; +import './SignalFlowMatrix.css'; + +const INPUT_SIDEBAR: Record = { + InA: 'ina', + InB: 'inb', + InC: 'inc', + InD: 'ind', +}; + +const OUTPUT_SIDEBAR: Record = { + Out1: 'out1', + Out2: 'out2', + Out3: 'out3', + Out4: 'out4', + Out5: 'out5', + Out6: 'out6', + Out7: 'out7', + Out8: 'out8', +}; + +const INPUT_STAGES: { + key: string; + label: string; + sidebar: SidebarId | 'input'; +}[] = [ + { key: 'gain', label: 'GAIN', sidebar: 'gain' }, + { key: 'gate', label: 'GATE', sidebar: 'gate' }, + { key: 'geq', label: 'GEQ', sidebar: 'geq' }, + { key: 'peq', label: 'PEQ', sidebar: 'input' }, + { key: 'hplp', label: 'HP/LP', sidebar: 'input' }, + { key: 'delay', label: 'DELAY', sidebar: 'delay' }, +]; + +const OUTPUT_STAGES: { + key: string; + label: string; + sidebar: SidebarId | 'output'; +}[] = [ + { key: 'xover', label: 'XOVER', sidebar: 'output' }, + { key: 'peq', label: 'PEQ', sidebar: 'output' }, + { key: 'gain', label: 'GAIN', sidebar: 'gain' }, + { key: 'compressor', label: 'COMP', sidebar: 'compressor' }, + { key: 'limiter', label: 'LIMIT', sidebar: 'limiter' }, + { key: 'delay', label: 'DELAY', sidebar: 'delay' }, + { key: 'mute', label: 'MUTE', sidebar: 'output' }, +]; + +const INPUT_COLORS = [ + 'var(--accent-a, #60a5fa)', + 'var(--accent-b, #34d399)', + 'var(--accent-c, #f59e0b)', + 'var(--accent-d, #f472b6)', +]; + +const W = 1800; +const H = 520; +const MARGIN = 36; +const LEFT_CHAIN_W = 560; +const RIGHT_CHAIN_W = 620; + +function getRoutes(dsp: DSP408, output: OutputChannel) { + return dsp.state.current_config.output_states[output].matrix_routes; +} +function isRouted(dsp: DSP408, output: OutputChannel, input: InputChannel) { + return getRoutes(dsp, output).connected.includes(input); +} +function getGain(dsp: DSP408, output: OutputChannel, input: InputChannel) { + return getRoutes(dsp, output).gains[input]; +} + +function SignalFlowMatrix({ + dsp, + setMatrixRouting, + setMatrixGain, + setActiveSidebar, +}: { + dsp: DSP408; + setMatrixRouting: (output: OutputChannel, inputs: InputChannel[]) => Promise; + setMatrixGain: (input: InputChannel, output: OutputChannel, gain: number) => Promise; + setActiveSidebar: React.Dispatch>; +}) { + const nInputs = INPUT_ORDER.length; + const nOutputs = OUTPUT_ORDER.length; + + const usableH = H - MARGIN * 2; + const inputRowH = usableH / nInputs; + const outputRowH = usableH / nOutputs; + + const inputStageW = LEFT_CHAIN_W / (INPUT_STAGES.length + 1); + const outputStageW = RIGHT_CHAIN_W / (OUTPUT_STAGES.length + 1); + + const leftStart = MARGIN; + const rightStart = W - MARGIN - RIGHT_CHAIN_W; + + const inputCenterY = (i: number) => MARGIN + inputRowH * (i + 0.5); + const outputCenterY = (j: number) => MARGIN + outputRowH * (j + 0.5); + + const inputNodeX = leftStart + LEFT_CHAIN_W; + const outputNodeX = rightStart; + + const handleToggle = (output: OutputChannel, input: InputChannel) => { + const routes = getRoutes(dsp, output); + const next = routes.connected.includes(input) + ? routes.connected.filter((c) => c !== input) + : [...routes.connected, input]; + void setMatrixRouting(output, next); + }; + + const handleWheelGain = (e: React.WheelEvent, output: OutputChannel, input: InputChannel) => { + e.preventDefault(); + const current = getGain(dsp, output, input); + const delta = e.deltaY < 0 ? MATRIX_GAIN_STEP : -MATRIX_GAIN_STEP; + const next = + Math.round(Math.min(MATRIX_GAIN_MAX, Math.max(MATRIX_GAIN_MIN, current + delta)) * 10) / 10; + void setMatrixGain(input, output, next); + }; + + const routePairs = useMemo(() => { + const pairs: { input: InputChannel; output: OutputChannel; i: number; j: number }[] = []; + OUTPUT_ORDER.forEach((output, j) => { + INPUT_ORDER.forEach((input, i) => pairs.push({ input, output, i, j })); + }); + return pairs; + }, []); + + return ( +
+ + {routePairs.map(({ input, output, i, j }) => { + const connected = isRouted(dsp, output, input); + const y1 = inputCenterY(i); + const y2 = outputCenterY(j); + const cx1 = inputNodeX + (outputNodeX - inputNodeX) * 0.4; + const cx2 = inputNodeX + (outputNodeX - inputNodeX) * 0.6; + const path = `M ${inputNodeX} ${y1} C ${cx1} ${y1}, ${cx2} ${y2}, ${outputNodeX} ${y2}`; + const color = INPUT_COLORS[i % INPUT_COLORS.length]; + + return ( + + handleToggle(output, input)} + onWheel={connected ? (e) => handleWheelGain(e, output, input) : undefined} + /> + + ); + })} + + {INPUT_ORDER.map((input, i) => { + const y = inputCenterY(i); + const boxH = Math.min(inputRowH * 0.55, 40); + const name = dsp.state.current_config.input_states[input].name; + const boxes = [{ key: 'name', label: name, sidebar: 'input' as const }, ...INPUT_STAGES]; + return ( + + {boxes.map((stage, si) => { + const x = leftStart + si * inputStageW; + const sidebarId = stage.sidebar === 'input' ? INPUT_SIDEBAR[input] : stage.sidebar; + return ( + setActiveSidebar(sidebarId as SidebarId)} + > + + + {si === 0 ? name : stage.label} + + + ); + })} + + ); + })} + + {OUTPUT_ORDER.map((output, j) => { + const y = outputCenterY(j); + const boxH = Math.min(outputRowH * 0.55, 32); + const name = dsp.state.current_config.output_states[output].name; + const boxes = [ + ...OUTPUT_STAGES, + { key: 'name', label: name, sidebar: 'output' as const }, + ]; + return ( + + {boxes.map((stage, si) => { + const x = rightStart + si * outputStageW; + const sidebarId = + stage.sidebar === 'output' ? OUTPUT_SIDEBAR[output] : stage.sidebar; + const isLast = si === boxes.length - 1; + return ( + setActiveSidebar(sidebarId as SidebarId)} + > + + + {isLast ? name : stage.label} + + + ); + })} + + ); + })} + +
+ ); +} + +export default SignalFlowMatrix; diff --git a/src/features/dsp408/components/CompressorSection/CompressorSection.css b/src/features/dsp408/components/CompressorSection/CompressorSection.css index c9a5051..c080e52 100644 --- a/src/features/dsp408/components/CompressorSection/CompressorSection.css +++ b/src/features/dsp408/components/CompressorSection/CompressorSection.css @@ -126,87 +126,6 @@ white-space: nowrap; } -.choice-stepper { - width: 100%; - flex: 1 1 auto; - - position: relative; - display: flex; - align-items: stretch; - justify-content: center; - - border: var(--border-width) solid var(--border-hairline); - border-radius: var(--radius-sm); - background: transparent; -} - -.choice-stepper__input { - flex: 1 1 auto; - min-width: 0; - width: 100%; - - font-family: var(--font-ui); - font-size: 3cqh; - font-weight: var(--font-weight-semibold); - text-align: center; - - color: var(--accent-brand); - - background: transparent; - border: none; - border-radius: var(--radius-sm); - padding: 0.5ch; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.choice-stepper__input:focus { - outline: none; -} - -.choice-stepper__arrows { - flex: 0 0 auto; - display: flex; - flex-direction: column; - justify-content: center; - gap: 0.5cqh; - - height: 100%; - box-sizing: border-box; - padding: 1cqh; - padding-left: 0; -} - -.choice-stepper__arrow-btn { - aspect-ratio: 1; - height: 4.5cqh; - min-height: 0; - - display: flex; - align-items: center; - justify-content: center; - - padding: 0; - color: var(--accent-brand); - - background: transparent; - border-radius: 1cqh; - - cursor: pointer; -} - -.choice-stepper__arrow-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.choice-stepper__arrow-btn svg { - width: 100%; - height: 100%; -} - .compressor-graph__channel { font-size: var(--font-xs); color: var(--text-muted); diff --git a/src/features/dsp408/components/CompressorSection/CompressorSection.tsx b/src/features/dsp408/components/CompressorSection/CompressorSection.tsx index a4d4668..9c4597e 100644 --- a/src/features/dsp408/components/CompressorSection/CompressorSection.tsx +++ b/src/features/dsp408/components/CompressorSection/CompressorSection.tsx @@ -2,12 +2,11 @@ import VerticalSlider from '../../../../components/VerticalSlider/VerticalSlider'; import { OutputChannel, Ratio } from '../../types'; import { DSP408 as DSP } from '../../../../types'; -import Button from '../../../../components/Button/Button'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph'; import MeterBar from '../../../../components/MeterBar/MeterBar'; -import { useMemo, useState, useEffect } from 'react'; -import { TriangleUpIcon, TriangleDownIcon } from '../../../../assets/icons/Triangle'; +import { useMemo, useState } from 'react'; +import ChoiceStepper from '../../../../components/ChoiceStepper/ChoiceStepper'; import './CompressorSection.css'; // Enum order defines stepper order (1:1 -> Limit). @@ -109,85 +108,6 @@ type SetCompressorFn = ( threshold: number ) => Promise; -function ChoiceStepper({ - value, - values, - formatLabel, - onChange, -}: { - value: T; - values: readonly T[]; - formatLabel: (value: T) => string; - onChange: (value: T) => void; -}) { - const index = values.indexOf(value); - const canUp = index === -1 || index < values.length - 1; - const canDown = index > 0; - - const [text, setText] = useState(formatLabel(value)); - - useEffect(() => { - setText(formatLabel(value)); - }, [value, formatLabel]); - - const step = (dir: 1 | -1) => { - const currentIndex = index === -1 ? 0 : index; - const nextIndex = Math.min(values.length - 1, Math.max(0, currentIndex + dir)); - if (nextIndex !== currentIndex) onChange(values[nextIndex]); - }; - - const commitText = () => { - const match = values.find( - (v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase() - ); - if (match !== undefined) { - onChange(match); - } else { - setText(formatLabel(value)); - } - }; - - return ( -
- setText(e.target.value)} - onBlur={commitText} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.currentTarget.blur(); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - step(1); - } else if (e.key === 'ArrowDown') { - e.preventDefault(); - step(-1); - } - }} - /> -
- - -
-
- ); -} - function CompressorPanel({ channel, dsp, diff --git a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx index 8fd6bf0..2c601ec 100644 --- a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx +++ b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; -import { DSP } from '../../../../types'; -import Sidebar from '../../../../components/Sidebar/Sidebar'; +import { Sidebar, SidebarItem } from '../../../../components/Sidebar/Sidebar'; import { InfoIcon, KnobGainIcon, TextIcon } from '../../../../assets/icons/AudioIcons'; import ConnectionPanel from '../ConnectionSection/ConnectionSection'; import GainSection from '../GainSection/GainSection'; @@ -14,19 +13,19 @@ import MatrixSection from '../Matrixsection/Matrixsection'; import GeqSection from '../GeqSection/GeqSection'; import InputSection from '../InputSection/InputSection'; import { DSP408Api } from '../../dsp408'; +import { DSP408 } from '../../../../types'; +import { SidebarId } from '../../constants'; import './DSP408Panel.css'; -type DSP408 = Extract; - function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) { - const [activeSidebar, setActiveSidebar] = useState('overview'); + const [activeSidebar, setActiveSidebar] = useState('overview'); const [sidebarExpanded, setSidebarExpanded] = useState(false); const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0); const [presetLoading, setPresetLoading] = useState(false); const inputStates = dsp.state?.current_config.input_states; const outputStates = dsp.state?.current_config.output_states; - const sidebarItems = [ + const sidebarItems: SidebarItem[] = [ { id: 'overview', label: 'Overview', icon: }, { id: 'gain', label: 'Gain', icon: }, { id: 'gate', label: 'Gate', icon: }, @@ -155,6 +154,7 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) { dsp={dsp} setMatrixRouting={api.setMatrixRouting} setMatrixGain={api.setMatrixGain} + setActiveSidebar={setActiveSidebar} /> ); diff --git a/src/features/dsp408/components/Matrixsection/Matrixsection.css b/src/features/dsp408/components/Matrixsection/Matrixsection.css index 1a400b6..9e564c3 100644 --- a/src/features/dsp408/components/Matrixsection/Matrixsection.css +++ b/src/features/dsp408/components/Matrixsection/Matrixsection.css @@ -1,3 +1,4 @@ +/* MatrixSection.css */ .matrix-section { container-type: size; position: absolute; @@ -8,7 +9,7 @@ height: var(--dsp-control-heigth); display: flex; - align-items: stretch; + align-items: flex-end; justify-content: center; box-sizing: border-box; @@ -20,152 +21,73 @@ border-top: var(--border-width) solid var(--border-hairline); } -/* ---------- Placeholder graph area (top) ---------- */ -/* Intentionally minimal for now — see chat for ideas on what belongs here. */ - -.matrix-graph-section { - flex: 0 0 calc(100% - var(--dsp-control-heigth)); - min-height: 0; - +.matrix-panels { + height: 100%; display: flex; - flex-direction: row; - gap: 1cqh; - padding: 0.5cqh; - - box-sizing: border-box; - background: var(--bg-panel); + gap: var(--space-2); + overflow-x: auto; } -.matrix-graph-panel { - flex: 1 1 auto; - min-width: 0; - min-height: 0; +.matrix-panel { + position: relative; - display: flex; - align-items: center; - justify-content: center; - - border: var(--border-width) dashed var(--border-hairline); - border-radius: var(--radius-md); - - font-family: var(--font-ui); - font-size: var(--font-xs); - color: var(--text-muted); -} - -/* ---------- Matrix grid (bottom) ---------- */ - -.matrix-grid-panel { - flex: 1 1 auto; - min-width: 0; + aspect-ratio: 0.6; + flex: 0 0 auto; display: flex; flex-direction: column; - - padding: var(--space-2); - box-sizing: border-box; - - overflow: auto; -} - -.matrix-grid { - display: grid; - /* 1 corner cell + 1 column per input */ - grid-template-columns: minmax(8ch, 12cqh) repeat(var(--matrix-input-count, 4), minmax(9cqh, 1fr)); - /* 1 header row + 1 row per output */ - grid-auto-rows: minmax(6cqh, 1fr); - - gap: var(--border-width); - background: var(--border-hairline); + align-items: stretch; + justify-content: flex-start; border: var(--border-width) solid var(--border-hairline); border-radius: var(--radius-md); - overflow: hidden; + box-sizing: border-box; + background: var(--bg-void); } -.matrix-grid__corner, -.matrix-grid__col-header, -.matrix-grid__row-header { - background: var(--bg-void); - font-family: var(--font-ui); - font-weight: var(--font-weight-semibold); - color: var(--text-muted); - - display: flex; - align-items: center; - justify-content: center; +.matrix-panel__title { + flex: 0 0 auto; + width: 100%; text-align: center; - - font-size: 2.5cqh; - padding: 0.5cqh; + font-family: var(--font-ui); + font-size: 3cqh; + font-weight: var(--font-weight-semibold); + padding-top: 1.5cqh; + padding-bottom: 1.5cqh; + border-bottom: var(--border-width) solid var(--border-hairline); min-width: 0; - min-height: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.matrix-grid__row-header { - justify-content: flex-start; - padding-left: 1cqh; -} - -.matrix-cell { - position: relative; - - background: var(--bg-panel); - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.5cqh; - - min-width: 0; +.matrix-panel__rows { + flex: 1 1 auto; min-height: 0; - border: none; - cursor: pointer; - -webkit-user-select: none; - user-select: none; - touch-action: none; + display: grid; + grid-template-rows: repeat(4, 1fr); + gap: 5cqh; + + padding-block: 5cqh; + box-sizing: border-box; } -.matrix-cell:focus-visible { - outline: 2px solid var(--accent-brand); - outline-offset: -2px; +.matrix-panel__row { + min-height: 0; + min-width: 0; + + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3cqh; + padding-inline: 3cqh; } -.matrix-cell--off { - color: var(--text-muted); -} - -.matrix-cell--off .matrix-cell__dot { - width: 0.8cqh; - height: 0.8cqh; - border-radius: 50%; - background: currentColor; - opacity: 0.35; -} - -.matrix-cell--on { - background: color-mix(in srgb, var(--accent-brand) 14%, var(--bg-panel)); - color: var(--accent-brand); -} - -.matrix-cell--dragging { - background: color-mix(in srgb, var(--accent-brand) 26%, var(--bg-panel)); -} - -.matrix-cell__gain { - font-size: 2.6cqh; - font-weight: var(--font-weight-semibold); - font-family: var(--font-ui); - white-space: nowrap; -} - -.matrix-cell__link-icon { - width: 2.2cqh; - height: 2.2cqh; +.matrix-flow { + flex: 0 0 calc(100% - var(--dsp-control-heigth)); + min-height: 0; + box-sizing: border-box; + padding: var(--space-2); + border-bottom: var(--border-width) solid var(--border-hairline); } diff --git a/src/features/dsp408/components/Matrixsection/Matrixsection.tsx b/src/features/dsp408/components/Matrixsection/Matrixsection.tsx index fdb518f..683e99e 100644 --- a/src/features/dsp408/components/Matrixsection/Matrixsection.tsx +++ b/src/features/dsp408/components/Matrixsection/Matrixsection.tsx @@ -1,183 +1,49 @@ // MatrixSection.tsx -import { Fragment, useRef, useState } from 'react'; -import { InputChannel, OutputChannel } from '../../types'; +import { InputChannel, OutputChannel, MatrixRoutes } from '../../types'; import { DSP408 as DSP } from '../../../../types'; +import Button from '../../../../components/Button/Button'; +import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; +import ChoiceStepper from '../../../../components/ChoiceStepper/ChoiceStepper'; +import { + INPUT_ORDER, + OUTPUT_ORDER, + MATRIX_GAIN_MIN, + MATRIX_GAIN_MAX, + MATRIX_GAIN_STEP, +} from '../../constants'; +import { SidebarId } from '../../constants'; +import SignalFlowMatrix from '../../../../components/SignalFlowMatrix/SignalFlowMatrix'; import './MatrixSection.css'; -// ASSUMPTION: an InputChannel enum exists alongside OutputChannel in -// dsp408State, e.g. In1..In4. Adjust the import/labels below if the real -// enum differs. -const INPUTS: InputChannel[] = [ - InputChannel.InA, - InputChannel.InB, - InputChannel.InC, - InputChannel.InD, -]; +const GAIN_VALUES: number[] = (() => { + const values: number[] = []; + for (let v = MATRIX_GAIN_MIN; v <= MATRIX_GAIN_MAX + 1e-9; v += MATRIX_GAIN_STEP) { + values.push(Math.round(v * 10) / 10); + } + return values; +})(); -const OUTPUTS: OutputChannel[] = [ - OutputChannel.Out1, - OutputChannel.Out2, - OutputChannel.Out3, - OutputChannel.Out4, - OutputChannel.Out5, - OutputChannel.Out6, - OutputChannel.Out7, - OutputChannel.Out8, -]; - -const INPUT_LABEL: Record = { - [InputChannel.InA]: 'In 1', - [InputChannel.InB]: 'In 2', - [InputChannel.InC]: 'In 3', - [InputChannel.InD]: 'In 4', -}; - -const GAIN_MIN = -60; -const GAIN_MAX = 12; -const GAIN_DEFAULT = 0; // gain a link is set to the moment it's first routed -const GAIN_STEP = 0.5; // wheel / arrow-key nudge -const DRAG_PX_PER_DB = 4; // vertical drag sensitivity - -function clampGain(v: number) { - return Math.min(GAIN_MAX, Math.max(GAIN_MIN, v)); +function getMatrixRoutes(dsp: DSP, output: OutputChannel): MatrixRoutes { + return dsp.state.current_config.output_states[output].matrix_routes; } -function formatGain(v: number) { - return `${v > 0 ? '+' : ''}${v.toFixed(1)}`; -} - -// ASSUMPTION: each output tracks which inputs are currently routed to it -// (output_states[out].matrix_inputs: InputChannel[]) and the last-known gain -// per input (output_states[out].matrix_gains: Partial>, in dB, kept around even if the link is later removed so it can -// be restored). Adjust these two accessors if the real shape differs — -// everything else only depends on the two functions below. -type MatrixOutputState = { - matrix_inputs?: InputChannel[]; - matrix_gains?: Partial>; -}; - function isRouted(dsp: DSP, output: OutputChannel, input: InputChannel): boolean { - const state = dsp.state.current_config.output_states[output] as unknown as MatrixOutputState; - return state.matrix_inputs?.includes(input) ?? false; + return getMatrixRoutes(dsp, output).connected.includes(input); } function getLinkGain(dsp: DSP, output: OutputChannel, input: InputChannel): number { - const state = dsp.state.current_config.output_states[output] as unknown as MatrixOutputState; - return state.matrix_gains?.[input] ?? GAIN_DEFAULT; + return getMatrixRoutes(dsp, output).gains[input]; } function getRoutedInputs(dsp: DSP, output: OutputChannel): InputChannel[] { - const state = dsp.state.current_config.output_states[output] as unknown as MatrixOutputState; - return state.matrix_inputs ?? []; -} - -function MatrixCell({ - output, - input, - connected, - gain, - onToggle, - onGainChange, -}: { - output: OutputChannel; - input: InputChannel; - connected: boolean; - gain: number; - onToggle: () => void; - onGainChange: (gain: number) => void; -}) { - const [liveGain, setLiveGain] = useState(null); - const [dragging, setDragging] = useState(false); - const dragStart = useRef<{ y: number; base: number } | null>(null); - - const displayGain = liveGain ?? gain; - - const handlePointerDown = (e: React.PointerEvent) => { - if (!connected) return; // plain click on an off cell just routes it (see onClick) - e.currentTarget.setPointerCapture(e.pointerId); - dragStart.current = { y: e.clientY, base: gain }; - setDragging(true); - }; - - const handlePointerMove = (e: React.PointerEvent) => { - if (!dragStart.current) return; - const deltaPx = dragStart.current.y - e.clientY; // up = louder - const next = clampGain(dragStart.current.base + deltaPx / DRAG_PX_PER_DB); - setLiveGain(next); - }; - - const endDrag = () => { - if (dragStart.current && liveGain !== null) { - onGainChange(liveGain); - } - dragStart.current = null; - setDragging(false); - setLiveGain(null); - }; - - const handleWheel = (e: React.WheelEvent) => { - if (!connected) return; - e.preventDefault(); - onGainChange(clampGain(gain + (e.deltaY < 0 ? GAIN_STEP : -GAIN_STEP))); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onToggle(); - } else if (connected && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) { - e.preventDefault(); - const dir = e.key === 'ArrowUp' ? 1 : -1; - onGainChange(clampGain(gain + dir * GAIN_STEP)); - } else if (connected && (e.key === 'Backspace' || e.key === 'Delete')) { - e.preventDefault(); - onToggle(); - } - }; - - return ( -
- {connected ? ( - {formatGain(displayGain)} - ) : ( - - )} -
- ); + return getMatrixRoutes(dsp, output).connected; } function MatrixSection({ dsp, setMatrixRouting, setMatrixGain, + setActiveSidebar, }: { dsp: DSP; setMatrixRouting: (output: OutputChannel, inputs: InputChannel[]) => Promise; @@ -186,21 +52,16 @@ function MatrixSection({ outputChannel: OutputChannel, gain: number ) => Promise; + setActiveSidebar: React.Dispatch>; }) { - // Toggling a cell sends the *whole* next input list for that output — - // setMatrixRouting replaces, it doesn't add/remove a single link. const handleToggle = (output: OutputChannel, input: InputChannel) => { const current = getRoutedInputs(dsp, output); - const nowConnecting = !current.includes(input); - const next = nowConnecting ? [...current, input] : current.filter((i) => i !== input); - void setMatrixRouting(output, next).then((ok) => { - // Give a freshly-connected link a defined starting gain rather than - // leaving it at whatever stale value the backend last had for it. - if (ok && nowConnecting) { - void setMatrixGain(input, output, GAIN_DEFAULT); - } - }); + const isConnected = current.includes(input); + + const next = isConnected ? current.filter((channel) => channel !== input) : [...current, input]; + + void setMatrixRouting(output, next); }; const handleGainChange = (output: OutputChannel, input: InputChannel, gain: number) => { @@ -209,45 +70,58 @@ function MatrixSection({ return ( <> -
-
Routing overview — see chat for ideas
+
+
-
-
-
-
- {INPUTS.map((input) => ( -
- {INPUT_LABEL[input]} -
- ))} - - {OUTPUTS.map((output) => ( - -
+
+ + {OUTPUT_ORDER.map((output) => ( +
+
{dsp.state.current_config.output_states[output].name}
- {INPUTS.map((input) => { - const connected = isRouted(dsp, output, input); - return ( - handleToggle(output, input)} - onGainChange={(gain) => handleGainChange(output, input, gain)} - /> - ); - })} - + +
+ {INPUT_ORDER.map((input) => { + const connected = isRouted(dsp, output, input); + const gain = getLinkGain(dsp, output, input); + + return ( +
+ + + `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`} + onChange={(g) => handleGainChange(output, input, g)} + /> +
+ ); + })} +
+
))} -
+
diff --git a/src/features/dsp408/constants.ts b/src/features/dsp408/constants.ts index 1eedcc2..718e1c4 100644 --- a/src/features/dsp408/constants.ts +++ b/src/features/dsp408/constants.ts @@ -1,4 +1,4 @@ -import { DiscreteFrequency, InputChannel } from './types'; +import { DiscreteFrequency, InputChannel, OutputChannel } from './types'; export const INPUT_ORDER: InputChannel[] = [ InputChannel.InA, @@ -7,6 +7,17 @@ export const INPUT_ORDER: InputChannel[] = [ InputChannel.InD, ]; +export const OUTPUT_ORDER: OutputChannel[] = [ + OutputChannel.Out1, + OutputChannel.Out2, + OutputChannel.Out3, + OutputChannel.Out4, + OutputChannel.Out5, + OutputChannel.Out6, + OutputChannel.Out7, + OutputChannel.Out8, +]; + export const DISCRETE_FREQUENCIES: Record = { [DiscreteFrequency.F20]: 20, [DiscreteFrequency.F25]: 25, @@ -82,8 +93,37 @@ export const GEQ_GAIN_MIN = -12; export const GEQ_GAIN_MAX = 12; export const GEQ_GAIN_STEP = 0.1; +// ------ Matrix constants ------ +export const MATRIX_GAIN_MIN = -60; +export const MATRIX_GAIN_MAX = 0; +export const MATRIX_GAIN_STEP = 0.1; + // ------ Graph constants ------ export const GRAPH_X_MIN = 10; export const GRAPH_X_MAX = 30000; export const GRAPH_Y_MIN = -21; export const GRAPH_Y_MAX = 21; + +// ------ App ------ +export type SidebarId = + | 'overview' + | 'gain' + | 'gate' + | 'compressor' + | 'limiter' + | 'delay' + | 'matrix' + | 'geq' + | 'ina' + | 'inb' + | 'inc' + | 'ind' + | 'out1' + | 'out2' + | 'out3' + | 'out4' + | 'out5' + | 'out6' + | 'out7' + | 'out8' + | 'miscellaneous'; diff --git a/src/features/dsp408/dsp408.ts b/src/features/dsp408/dsp408.ts index 5cc4085..661cfea 100644 --- a/src/features/dsp408/dsp408.ts +++ b/src/features/dsp408/dsp408.ts @@ -952,7 +952,6 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { const cfg = d.state.current_config; const outputState = cfg.output_states[outputChannel]; - const inputIndex = Object.values(InputChannel).indexOf(inputChannel); return { ...d, @@ -966,9 +965,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { ...outputState, matrix_routes: { ...outputState.matrix_routes, - gains: outputState.matrix_routes.gains.map((g, i) => - i === inputIndex ? gain : g - ), + gains: { + ...outputState.matrix_routes.gains, + [inputChannel]: gain, + }, }, }, }, @@ -977,7 +977,6 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { }; }); }; - if (dsp.demo) { updateMatrixGain(); return true; diff --git a/src/features/dsp408/types.ts b/src/features/dsp408/types.ts index da252ed..9e5074c 100644 --- a/src/features/dsp408/types.ts +++ b/src/features/dsp408/types.ts @@ -165,7 +165,7 @@ export type CrossoverFilters = { export type MatrixRoutes = { connected: InputChannel[]; - gains: number[]; + gains: Record; }; export type PEQChain = { @@ -341,8 +341,13 @@ const createDefaultOutputChannelState = (name: string): OutputChannelState => ({ name, matrix_routes: { - connected: [], - gains: [], + connected: [InputChannel.InA], + gains: { + [InputChannel.InA]: 0, + [InputChannel.InB]: 0, + [InputChannel.InC]: 0, + [InputChannel.InD]: 0, + }, }, crossover: createDefaultCrossoverFilters(), diff --git a/src/hooks/useDSPController.ts b/src/hooks/useDSPController.ts deleted file mode 100644 index 0a49808..0000000 --- a/src/hooks/useDSPController.ts +++ /dev/null @@ -1,1469 +0,0 @@ -// import { useEffect, useRef } from 'react'; -// import { invoke } from '@tauri-apps/api/core'; -// import { DSP, AppPage } from '../types/types'; -// import { -// Channel, -// InputChannel, -// defaultDSPState, -// defaultMeters, -// OutputChannel, -// Ratio, -// DelayUnit, -// Meters, -// DSPState, -// DiscreteFrequency, -// CrossoverFilter, -// PEQFilter, -// PEQBand, -// } from '../types/dsp408'; - -// type SetDsps = React.Dispatch>; - -// // Helpers -// function updateDSP(setDsps: SetDsps, dspId: number, updater: (dsp: DSP) => DSP) { -// setDsps((prev) => prev.map((dsp) => (dsp.id === dspId ? updater(dsp) : dsp))); -// } - -// export type DSP408Device = { -// setDSPState: (updater: (dsp: DSP) => DSP) => void; -// connect: () => Promise; -// disconnect: () => Promise; -// recallPreset: (index: number) => Promise; -// setCurrentPresetName: (name: string) => Promise; -// setGain: (channel: Channel, gain: number) => Promise; -// setMute: (channel: Channel, muted: boolean) => Promise; -// setInverse: (channel: Channel, inverted: boolean) => Promise; -// setGate: ( -// inputChannel: InputChannel, -// threshold: number, -// attack: number, -// hold: number, -// release: number -// ) => Promise; -// setCompressor: ( -// outputChannel: OutputChannel, -// ratio: Ratio, -// attack: number, -// release: number, -// knee: number, -// threshold: number -// ) => Promise; -// setLimiter: ( -// outputChannel: OutputChannel, -// threshold: number, -// attack: number, -// release: number -// ) => Promise; -// setDelayUnit: (unit: DelayUnit) => Promise; -// setDelay: (channel: Channel, delay: number) => Promise; -// setMatrixRouting: (output: OutputChannel, inputs: InputChannel[]) => Promise; -// setMatrixGain: ( -// inputChannel: InputChannel, -// outputChannel: OutputChannel, -// gain: number -// ) => Promise; -// setGeqBand: ( -// inputChannel: InputChannel, -// frequencyBand: DiscreteFrequency, -// gain: number -// ) => Promise; -// setGeqBypass: (inputChannel: InputChannel, bypass: boolean) => Promise; -// setLowPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => Promise; -// setHighPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => Promise; -// setPeqBand: ( -// channel: Channel, -// band: PEQBand, -// gainDb: number, -// freqHz: number, -// q: number, -// filterType: PEQFilter, -// bypass: boolean -// ) => Promise; -// }; - -// // Polling -// function useDSPPolling( -// dsps: DSP[], -// selected: number | null, -// setDsps: React.Dispatch> -// ) { -// const dspsRef = useRef(dsps); -// const selectedRef = useRef(selected); - -// useEffect(() => { -// dspsRef.current = dsps; -// }, [dsps]); - -// useEffect(() => { -// selectedRef.current = selected; -// }, [selected]); - -// useEffect(() => { -// let stopped = false; -// let activeTimeout: ReturnType | undefined; -// let inactiveTimeout: ReturnType | undefined; - -// const pollDSP = async (dsp: DSP) => { -// // Don't poll paused DSPs -// if (dsp.pollingPaused) { -// return; -// } - -// try { -// const meters = await invoke('get_meter_levels', { -// id: dsp.id, -// }); - -// if (stopped) return; - -// setDsps((prev) => prev.map((d) => (d.id === dsp.id ? { ...d, meters } : d))); -// } catch (err) { -// if (!stopped) { -// console.error(`Meter polling failed for ${dsp.name}:`, err); -// } -// } -// }; - -// const activeLoop = async () => { -// if (stopped) return; - -// const activeId = selectedRef.current; - -// if (activeId !== null) { -// const dsp = dspsRef.current.find( -// (d) => d.id === activeId && d.status === 'connected' && !d.pollingPaused -// ); - -// if (dsp) { -// await pollDSP(dsp); -// } -// } - -// if (!stopped) { -// activeTimeout = setTimeout(activeLoop, 500); -// } -// }; - -// const inactiveLoop = async () => { -// if (stopped) return; - -// const activeId = selectedRef.current; - -// const inactiveDsps = dspsRef.current.filter( -// (dsp) => dsp.status === 'connected' && dsp.id !== activeId && !dsp.pollingPaused -// ); - -// await Promise.all(inactiveDsps.map((dsp) => pollDSP(dsp))); - -// if (!stopped) { -// inactiveTimeout = setTimeout(inactiveLoop, 5000); -// } -// }; - -// // Start polling -// activeLoop(); -// inactiveLoop(); - -// return () => { -// stopped = true; - -// if (activeTimeout !== undefined) { -// clearTimeout(activeTimeout); -// } - -// if (inactiveTimeout !== undefined) { -// clearTimeout(inactiveTimeout); -// } -// }; -// }, [setDsps]); -// } - -// export function useDSP408( -// dsps: DSP[], -// setDsps: SetDsps, -// selected: number | null, -// setSelected: React.Dispatch>, -// setPage: React.Dispatch>, -// notify: (message: string, type: 'success' | 'error', dspName?: string) => void, -// ) { -// // Polling control -// const setPollingPaused = (id: number, paused: boolean) => { -// updateDSP(setDsps, id, (dsp) => ({ -// ...dsp, -// pollingPaused: paused, -// })); -// }; - -// function setDSPState(dsp: DSP, updater: (dsp: DSP) => DSP) { -// updateDSP(setDsps, dsp.id, updater); -// } - -// // Polling is part of the hook. -// useDSPPolling(dsps, selected, setDsps); - -// // Generic operation wrapper -// async function withPollingPaused(dsp: DSP, operation: () => Promise): Promise { -// setPollingPaused(dsp.id, true); - -// try { -// return await operation(); -// } finally { -// setPollingPaused(dsp.id, false); -// } -// } - -// // Connection -// async function connectDSP(dsp: DSP) { -// setPollingPaused(dsp.id, true); - -// updateDSP(setDsps, dsp.id, (d) => ({ -// ...d, -// status: 'connecting', -// })); - -// try { -// await invoke('connect_dsp408', { id: dsp.id }); - -// const state = await invoke('get_dsp_state', { -// id: dsp.id, -// }); - -// updateDSP(setDsps, dsp.id, (d) => ({ -// ...d, -// status: 'connected', -// state, -// })); - -// notify('Connected', 'success', dsp.name); -// } catch (err) { -// updateDSP(setDsps, dsp.id, (d) => ({ -// ...d, -// status: 'disconnected', -// })); - -// notify(`Connection failed: ${String(err)}`, 'error', dsp.name); -// } finally { -// setPollingPaused(dsp.id, false); -// } -// } - -// async function disconnectDSP(dsp: DSP) { -// setPollingPaused(dsp.id, true); - -// try { -// await invoke('disconnect_dsp408', { id: dsp.id }); - -// updateDSP(setDsps, dsp.id, (d) => ({ -// ...d, -// status: 'disconnected', -// })); - -// notify('Disconnected', 'success', dsp.name); -// } catch (err) { -// // If disconnect failed, it is probably still connected. -// setPollingPaused(dsp.id, false); - -// notify(`Disconnect failed: ${String(err)}`, 'error', dsp.name); -// } -// } - -// // Add / Remove -// async function addDSP(dsp: Omit) { -// try { -// const id = await invoke('create_dsp408', { -// ip: dsp.ip, -// port: dsp.port, -// deviceId: dsp.deviceId, -// }); - -// const newDsp: DSP = { -// id, -// ...dsp, -// status: 'disconnected', -// state: defaultDSPState, -// meters: defaultMeters, -// pollingPaused: true, -// }; - -// setDsps((prev) => [...prev, newDsp]); -// setSelected(id); -// setPage('view-dsp'); - -// notify('Added successfully', 'success', dsp.name); -// } catch (err) { -// notify(`Connection failed: ${String(err)}`, 'error', dsp.name); -// } -// } - -// async function removeDSP(dsp: DSP) { -// setPollingPaused(dsp.id, true); - -// try { -// await invoke('remove_dsp408', { -// id: dsp.id, -// }); - -// setDsps((prev) => { -// const next = prev.filter((d) => d.id !== dsp.id); - -// if (selected === dsp.id) { -// setSelected(next.length ? next[0].id : null); -// } - -// return next; -// }); - -// notify('Removed successfully', 'success', dsp.name); -// } catch (err) { -// setPollingPaused(dsp.id, false); - -// notify(`Remove failed: ${String(err)}`, 'error', dsp.name); -// } -// } - -// // Presets -// async function recallPreset(dsp: DSP, presetIndex: number) { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('recall_preset', { -// id: dsp.id, -// presetIndex, -// }); - -// if (!success) { -// notify('Failed to recall preset', 'error', dsp.name); - -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// return { -// ...d, -// state: { -// ...d.state, -// presets: { -// ...d.state.presets, -// current_index: presetIndex - 1, -// }, -// }, -// }; -// }); - -// notify(`Preset ${presetIndex} recalled`, 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`Preset recall failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// async function setCurrentPresetName(dsp: DSP, name: string) { -// return withPollingPaused(dsp, async () => { -// try { -// const paddedName = name.slice(0, 14).padEnd(14, ' '); - -// const success = await invoke('set_current_preset_name', { -// id: dsp.id, -// name: paddedName, -// }); - -// if (!success) { -// notify('Failed to rename preset', 'error', dsp.name); - -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const presets = d.state.presets; -// const index = presets.current_index; - -// return { -// ...d, -// state: { -// ...d.state, -// presets: { -// ...presets, -// names: presets.names.map((presetName, i) => -// i === index ? paddedName : presetName -// ), -// modified: presets.modified.map((modified, i) => (i === index ? true : modified)), -// }, -// }, -// }; -// }); - -// notify('Preset renamed', 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`Preset rename failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// // Gain -// async function setGain(dsp: DSP, channel: Channel, gain: number): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_channel_gain', { -// id: dsp.id, -// channel, -// db: gain, -// }); - -// if (!success) { -// notify('Failed to set gain', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// if ('Input' in channel) { -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [channel.Input]: { -// ...currentConfig.input_states[channel.Input], -// gain, -// }, -// }, -// }, -// }, -// }; -// } - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [channel.Output]: { -// ...currentConfig.output_states[channel.Output], -// gain, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Gain updated', 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`Gain update failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// // Mute -// async function setMute(dsp: DSP, channel: Channel, muted: boolean): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_mute', { -// id: dsp.id, -// channel, -// muted, -// }); - -// if (!success) { -// notify('Failed to change mute state', 'error', dsp.name); - -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// if ('Input' in channel) { -// const inputChannel = channel.Input; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [inputChannel]: { -// ...currentConfig.input_states[inputChannel], -// mute: muted, -// }, -// }, -// }, -// }, -// }; -// } - -// const outputChannel = channel.Output; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [outputChannel]: { -// ...currentConfig.output_states[outputChannel], -// mute: muted, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify(muted ? 'Muted' : 'Unmuted', 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`Mute update failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// // Inverse -// async function setInverse(dsp: DSP, channel: Channel, inverted: boolean): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_channel_inverse_gain', { -// id: dsp.id, -// channel, -// inverted, -// }); - -// if (!success) { -// notify('Failed to change phase', 'error', dsp.name); - -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// if ('Input' in channel) { -// const inputChannel = channel.Input; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [inputChannel]: { -// ...currentConfig.input_states[inputChannel], -// phase_inverted: inverted, -// }, -// }, -// }, -// }, -// }; -// } - -// const outputChannel = channel.Output; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [outputChannel]: { -// ...currentConfig.output_states[outputChannel], -// phase_inverted: inverted, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify(inverted ? 'Phase inverted' : 'Phase normal', 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`Phase update failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// // Gate -// async function setGate( -// dsp: DSP, -// inputChannel: InputChannel, -// threshold: number, -// attack: number, -// hold: number, -// release: number -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_gate', { -// id: dsp.id, -// channel: inputChannel, -// threshold, -// attack, -// hold, -// release, -// }); -// if (!success) { -// notify('Failed to change gate settings', 'error', dsp.name); -// return false; -// } -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; -// const currentConfig = d.state.current_config; -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [inputChannel]: { -// ...currentConfig.input_states[inputChannel], -// gate: { threshold, attack, hold, release }, -// }, -// }, -// }, -// }, -// }; -// }); -// notify('Gate updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Gate update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // Compressor -// async function setCompressor( -// dsp: DSP, -// outputChannel: OutputChannel, -// ratio: Ratio, -// attack: number, -// release: number, -// knee: number, -// threshold: number -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_compressor', { -// id: dsp.id, -// channel: outputChannel, -// ratio, -// attack, -// release, -// knee, -// threshold, -// }); - -// if (!success) { -// notify('Failed to change compressor settings', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [outputChannel]: { -// ...currentConfig.output_states[outputChannel], -// compressor: { -// ratio, -// attack, -// release, -// knee, -// threshold, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Compressor updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Compressor update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // Limiter -// async function setLimiter( -// dsp: DSP, -// outputChannel: OutputChannel, -// threshold: number, -// attack: number, -// release: number -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_limiter', { -// id: dsp.id, -// channel: outputChannel, -// threshold, -// attack, -// release, -// }); - -// if (!success) { -// notify('Failed to change limiter settings', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [outputChannel]: { -// ...currentConfig.output_states[outputChannel], -// limiter: { -// threshold, -// attack, -// release, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Limiter updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Limiter update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // Delay Unit -// async function setDelayUnit(dsp: DSP, unit: DelayUnit): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_delay_unit', { -// id: dsp.id, -// unit, -// }); - -// if (!success) { -// notify('Failed to change delay unit', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...d.state.current_config, -// delay_unit: unit, -// }, -// }, -// }; -// }); - -// notify('Delay unit updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Delay unit update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // Channel Delay -// async function setDelay(dsp: DSP, channel: Channel, delay: number): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_delay', { -// id: dsp.id, -// channel, -// delay, -// unit: dsp.state.current_config.delay_unit, -// }); - -// if (!success) { -// notify('Failed to change channel delay', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// if ('Input' in channel) { -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [channel.Input]: { -// ...currentConfig.input_states[channel.Input], -// delay, -// }, -// }, -// }, -// }, -// }; -// } - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [channel.Output]: { -// ...currentConfig.output_states[channel.Output], -// delay, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Channel delay updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Channel delay update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // Matrix Routing -// async function setMatrixRouting( -// dsp: DSP, -// output: OutputChannel, -// inputs: InputChannel[] -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_matrix_routing', { -// id: dsp.id, -// output, -// inputs, -// }); - -// if (!success) { -// notify('Failed to change matrix routing', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [output]: { -// ...currentConfig.output_states[output], -// matrix_routes: { -// ...currentConfig.output_states[output].matrix_routes, -// connected: inputs, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Matrix routing updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Matrix routing update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // Matrix Gain -// async function setMatrixGain( -// dsp: DSP, -// inputChannel: InputChannel, -// outputChannel: OutputChannel, -// gain: number -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_matrix_gain', { -// id: dsp.id, -// inputChannel, -// outputChannel, -// gain, -// }); - -// if (!success) { -// notify('Failed to change matrix gain', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// const outputState = currentConfig.output_states[outputChannel]; - -// const inputIndex = Object.values(InputChannel).indexOf(inputChannel); - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [outputChannel]: { -// ...outputState, -// matrix_routes: { -// ...outputState.matrix_routes, -// gains: outputState.matrix_routes.gains.map((g, i) => -// i === inputIndex ? gain : g -// ), -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Matrix gain updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Matrix gain update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // GEQ Band -// async function setGeqBand( -// dsp: DSP, -// inputChannel: InputChannel, -// frequencyBand: DiscreteFrequency, -// gain: number -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_geq_band', { -// id: dsp.id, -// inputChannel, -// frequencyBand, -// gain, -// }); - -// if (!success) { -// notify('Failed to change GEQ band', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; -// const inputState = currentConfig.input_states[inputChannel]; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [inputChannel]: { -// ...inputState, -// geq: { -// ...inputState.geq, -// gains: { -// ...inputState.geq.gains, -// [frequencyBand]: gain, -// }, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('GEQ band updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`GEQ band update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// // GEQ Bypass -// async function setGeqBypass( -// dsp: DSP, -// inputChannel: InputChannel, -// bypass: boolean -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_geq_bypass', { -// id: dsp.id, -// inputChannel, -// bypass, -// }); - -// if (!success) { -// notify('Failed to change GEQ bypass', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; -// const inputState = currentConfig.input_states[inputChannel]; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [inputChannel]: { -// ...inputState, -// geq: { -// ...inputState.geq, -// bypass, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify(bypass ? 'GEQ bypass enabled' : 'GEQ bypass disabled', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`GEQ bypass update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// async function setLowPass( -// dsp: DSP, -// channel: Channel, -// freq: number, -// filterType: CrossoverFilter -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_low_pass', { -// id: dsp.id, -// channel, -// freq, -// filterType, -// }); - -// if (!success) { -// notify('Failed to set low-pass filter', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// if ('Input' in channel) { -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [channel.Input]: { -// ...currentConfig.input_states[channel.Input], -// low_pass: { -// frequency: freq, -// filter_type: filterType, -// }, -// }, -// }, -// }, -// }, -// }; -// } - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [channel.Output]: { -// ...currentConfig.output_states[channel.Output], -// low_pass: { -// frequency: freq, -// filter_type: filterType, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('Low-pass filter updated', 'success', dsp.name); -// return true; -// } catch (err) { -// notify(`Low-pass update failed: ${String(err)}`, 'error', dsp.name); -// return false; -// } -// }); -// } - -// async function setHighPass( -// dsp: DSP, -// channel: Channel, -// freq: number, -// filterType: CrossoverFilter -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_high_pass', { -// id: dsp.id, -// channel, -// freq, -// filterType, -// }); - -// if (!success) { -// notify('Failed to set high-pass filter', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// if ('Input' in channel) { -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [channel.Input]: { -// ...currentConfig.input_states[channel.Input], -// high_pass: { -// frequency: freq, -// filter_type: filterType, -// }, -// }, -// }, -// }, -// }, -// }; -// } - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [channel.Output]: { -// ...currentConfig.output_states[channel.Output], -// high_pass: { -// frequency: freq, -// filter_type: filterType, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('High-pass filter updated', 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`High-pass update failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// async function setPeqBand( -// dsp: DSP, -// channel: Channel, -// band: PEQBand, -// gainDb: number, -// freqHz: number, -// q: number, -// filterType: PEQFilter, -// bypass: boolean -// ): Promise { -// return withPollingPaused(dsp, async () => { -// try { -// const success = await invoke('set_peq_band', { -// id: dsp.id, -// channel, -// band, -// gainDb, -// freqHz, -// q, -// filterType, -// bypass, -// }); - -// if (!success) { -// notify('Failed to set PEQ band', 'error', dsp.name); -// return false; -// } - -// updateDSP(setDsps, dsp.id, (d) => { -// if (!d.state) return d; - -// const currentConfig = d.state.current_config; - -// const peqBand = { -// gain: gainDb, -// frequency: freqHz, -// q, -// filter_type: filterType, -// bypass, -// }; - -// if ('Input' in channel) { -// const inputState = currentConfig.input_states[channel.Input]; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// input_states: { -// ...currentConfig.input_states, -// [channel.Input]: { -// ...inputState, -// peq_chain: { -// ...inputState.peq_chain, -// bands: { -// ...inputState.peq_chain.bands, -// [band]: peqBand, -// }, -// }, -// }, -// }, -// }, -// }, -// }; -// } - -// const outputState = currentConfig.output_states[channel.Output]; - -// return { -// ...d, -// state: { -// ...d.state, -// current_config: { -// ...currentConfig, -// output_states: { -// ...currentConfig.output_states, -// [channel.Output]: { -// ...outputState, -// peq_chain: { -// ...outputState.peq_chain, -// bands: { -// ...outputState.peq_chain.bands, -// [band]: peqBand, -// }, -// }, -// }, -// }, -// }, -// }, -// }; -// }); - -// notify('PEQ band updated', 'success', dsp.name); - -// return true; -// } catch (err) { -// notify(`PEQ update failed: ${String(err)}`, 'error', dsp.name); - -// return false; -// } -// }); -// } - -// // Return API -// function forDSP(dsp: DSP): DSP408Device { -// return { -// setDSPState: (updater: (dsp: DSP) => DSP) => setDSPState(dsp, updater), -// connect: () => connectDSP(dsp), -// disconnect: () => disconnectDSP(dsp), -// recallPreset: (index: number) => recallPreset(dsp, index), -// setCurrentPresetName: (name: string) => setCurrentPresetName(dsp, name), -// setGain: (channel: Channel, gain: number) => setGain(dsp, channel, gain), -// setMute: (channel: Channel, muted: boolean) => setMute(dsp, channel, muted), -// setInverse: (channel: Channel, inverted: boolean) => setInverse(dsp, channel, inverted), -// setGate: ( -// inputChannel: InputChannel, -// threshold: number, -// attack: number, -// hold: number, -// release: number -// ) => setGate(dsp, inputChannel, threshold, attack, hold, release), -// setCompressor: ( -// outputChannel: OutputChannel, -// ratio: Ratio, -// attack: number, -// release: number, -// knee: number, -// threshold: number -// ) => setCompressor(dsp, outputChannel, ratio, attack, release, knee, threshold), -// setLimiter: ( -// outputChannel: OutputChannel, -// threshold: number, -// attack: number, -// release: number -// ) => setLimiter(dsp, outputChannel, threshold, attack, release), -// setDelayUnit: (unit: DelayUnit) => setDelayUnit(dsp, unit), -// setDelay: (channel: Channel, delay: number) => setDelay(dsp, channel, delay), -// setMatrixRouting: (output: OutputChannel, inputs: InputChannel[]) => -// setMatrixRouting(dsp, output, inputs), -// setMatrixGain: (inputChannel: InputChannel, outputChannel: OutputChannel, gain: number) => -// setMatrixGain(dsp, inputChannel, outputChannel, gain), -// setGeqBand: (inputChannel: InputChannel, frequencyBand: DiscreteFrequency, gain: number) => -// setGeqBand(dsp, inputChannel, frequencyBand, gain), -// setGeqBypass: (inputChannel: InputChannel, bypass: boolean) => -// setGeqBypass(dsp, inputChannel, bypass), -// setLowPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => -// setLowPass(dsp, channel, freq, filterType), -// setHighPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => -// setHighPass(dsp, channel, freq, filterType), -// setPeqBand: ( -// channel: Channel, -// band: PEQBand, -// gainDb: number, -// freqHz: number, -// q: number, -// filterType: PEQFilter, -// bypass: boolean -// ) => setPeqBand(dsp, channel, band, gainDb, freqHz, q, filterType, bypass), -// }; -// } - -// return { -// forDSP, - -// addDSP, -// removeDSP, - -// connectDSP, -// disconnectDSP, - -// setDSPState, -// }; -// }