diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 5b6834a..3ff9ad3 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -461,3 +461,106 @@ pub async fn set_peq_band( .map_err(|e| e.to_string()) }) } + +#[tauri::command] +pub async fn store_current_configuration( + state: State<'_, AppState>, + id: u64, + preset_index: usize, + force: bool, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .store_current_configuration(preset_index, force) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn factory_reset( + state: State<'_, AppState>, + id: u64, + force: bool, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .factory_reset(force) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn copy_channel( + state: State<'_, AppState>, + id: u64, + channel_1: types::Channel, + channel_2: types::Channel, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .copy_channel(channel_1, channel_2) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn set_input_source( + state: State<'_, AppState>, + id: u64, + input_source: types::InputSource, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_input_source(input_source) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn link_channels( + state: State<'_, AppState>, + id: u64, + source: types::Channel, + destinations: Vec, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .link_channels(source, destinations) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn set_bypass_peq( + state: State<'_, AppState>, + id: u64, + channel: types::Channel, + bypass: bool, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_bypass_peq(channel, bypass) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn set_channel_name( + state: State<'_, AppState>, + id: u64, + channel: types::Channel, + name: String, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_channel_name(channel, &name) + .map_err(|e| e.to_string()) + }) +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0fb7299..207eaa9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,28 +10,39 @@ pub fn run() { .manage(AppState::new()) .invoke_handler(tauri::generate_handler![ commands::create_dsp408, - commands::connect_dsp408, commands::remove_dsp408, - commands::disconnect_dsp408, commands::get_dsp_state, - commands::set_channel_gain, - commands::set_mute, - commands::set_channel_inverse_gain, - commands::recall_preset, - commands::get_meter_levels, - commands::set_current_preset_name, - commands::set_gate, - commands::set_compressor, - commands::set_limiter, - commands::set_delay_unit, - commands::set_delay, - commands::set_matrix_routing, - commands::set_matrix_gain, - commands::set_geq_band, - commands::set_geq_bypass, - commands::set_low_pass, - commands::set_high_pass, - commands::set_peq_band, + + commands::connect_dsp408, // 0x10 + commands::disconnect_dsp408, // 0x11 + commands::set_delay_unit, // 0x15 + + commands::recall_preset, // 0x20 + commands::store_current_configuration, // 0x21 + commands::factory_reset, // 0x23 + commands::set_current_preset_name, // 0x26 + commands::copy_channel, // 0x2A + + commands::set_compressor, // 0x30 + commands::set_low_pass, // 0x31 + commands::set_high_pass, // 0x32 + commands::set_peq_band, // 0x33 + commands::set_channel_gain, // 0x34 + commands::set_mute, // 0x35 + commands::set_channel_inverse_gain, // 0x36 + commands::set_delay, // 0x38 + commands::set_input_source, // 0x39 + commands::set_matrix_routing, // 0x3A + commands::link_channels, // 0x3B + commands::set_bypass_peq, // 0x3C + commands::set_channel_name, // 0x3D + commands::set_gate, // 0x3E + commands::set_limiter, // 0x3F + + commands::get_meter_levels, // 0x40 + commands::set_matrix_gain, // 0x41 + commands::set_geq_band, // 0x48 + commands::set_geq_bypass, // 0x49 ] ) .run(tauri::generate_context!()) diff --git a/src/App.tsx b/src/App.tsx index 7dd16c7..d1a61c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; import './App.css'; import { DSP, AppPage } from './types/types'; import TopBar from './components/Topbar/Topbar'; @@ -8,17 +8,33 @@ import { useNotifications } from './hooks/useNotifications'; import Notifications from './components/Notifications/Notifications'; import CreditsPage from './pages/CreditsPage/CreditsPage'; import HomePage from './pages/HomePage/HomePage'; -import { useDSP408 } from './hooks/useDSP408'; +import { addDSP, createDSPInstance } from './dsp/dspApi'; +import { removeDSP } from './dsp/dspShared'; +import { stopAllPolling } from './dsp/pollRegistry'; function App() { const [dsps, setDsps] = useState([]); const [selected, setSelected] = useState(null); const [page, setPage] = useState('home'); const { notifications, notify, removeNotification, hoverNotification } = useNotifications(); - const activeDsp = dsps.find((d) => d.id === selected) ?? null; const connectedDspCount = dsps.filter((dsp) => dsp.status === 'connected').length; - const dsp408 = useDSP408(dsps, setDsps, selected, setSelected, setPage, notify); + const activeDsp = dsps.find((d) => d.id === selected) ?? null; + const activeInstance = activeDsp ? createDSPInstance(activeDsp, setDsps, notify) : null; + + const dspsRef = useRef(dsps); + dspsRef.current = dsps; + useEffect(() => { + return () => stopAllPolling(dspsRef.current); + }, []); + + useEffect(() => { + if (activeDsp == null) return; + const instance = createDSPInstance(activeDsp, setDsps, notify); + instance.api.setActive(true); + return () => instance.api.setActive(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeDsp?.id]); return (
@@ -34,29 +50,26 @@ function App() { page={page} setPage={setPage} setSelect={setSelected} - onSelect={(id) => { setSelected(id); setPage('view-dsp'); }} - - onRemove={dsp408.removeDSP} - - onAddClick={() => { - setPage('add-dsp'); - }} - - onConnect={dsp408.connectDSP} - onDisconnect={dsp408.disconnectDSP} + onRemove={(dsp) => removeDSP(setDsps, selected, setSelected, dsp.id, dsp.type)} + onAddClick={() => setPage('add-dsp')} + onConnect={(dsp) => createDSPInstance(dsp, setDsps, notify).api.connect()} + onDisconnect={(dsp) => createDSPInstance(dsp, setDsps, notify).api.disconnect()} />
{page === 'credits' ? ( ) : page === 'add-dsp' ? ( - setPage('home')} onAdd={dsp408.addDSP} /> - ) : activeDsp ? ( - + setPage('home')} + onAdd={(params) => addDSP(setDsps, setSelected, setPage, notify, params)} + /> + ) : activeInstance ? ( + ) : ( setPage('add-dsp')} diff --git a/src/components/dsp408/ChannelGroup/ChannelGroup.css b/src/components/ChannelGroup/ChannelGroup.css similarity index 100% rename from src/components/dsp408/ChannelGroup/ChannelGroup.css rename to src/components/ChannelGroup/ChannelGroup.css diff --git a/src/components/dsp408/ChannelGroup/ChannelGroup.tsx b/src/components/ChannelGroup/ChannelGroup.tsx similarity index 100% rename from src/components/dsp408/ChannelGroup/ChannelGroup.tsx rename to src/components/ChannelGroup/ChannelGroup.tsx diff --git a/src/components/dsp408/LinearGraph/LinearGraph.tsx b/src/components/LinearGraph/LinearGraph.tsx similarity index 63% rename from src/components/dsp408/LinearGraph/LinearGraph.tsx rename to src/components/LinearGraph/LinearGraph.tsx index e189fe5..9c877b7 100644 --- a/src/components/dsp408/LinearGraph/LinearGraph.tsx +++ b/src/components/LinearGraph/LinearGraph.tsx @@ -24,6 +24,8 @@ export interface LinearGraphHandle { getCanvas: () => HTMLCanvasElement | null; } +export type AxisScale = 'linear' | 'log'; + export interface LinearGraphProps { xMin: number; xMax: number; @@ -31,6 +33,10 @@ export interface LinearGraphProps { yMin: number; yMax: number; yStep?: number; + /** 'log' requires xMin > 0. Ticks are drawn at 1/2/5-per-decade mantissas, with decade (power-of-10) lines highlighted. */ + xScale?: AxisScale; + /** 'log' requires yMin > 0. */ + yScale?: AxisScale; draw?: DrawFn; width?: number; height?: number; @@ -72,6 +78,37 @@ function range(min: number, max: number, step: number): number[] { return out; } +interface Tick { + value: number; + /** Decade lines (powers of 10) get major=true for a bolder/brighter grid line. */ + major: boolean; +} + +// Linear ticks: every step is equally weighted, so mark every Nth (default 5th) as "major" +// purely for label emphasis — grid highlighting logic only really matters for log mode. +function linearTicks(min: number, max: number, step: number): Tick[] { + return range(min, max, step).map((value) => ({ value, major: false })); +} + +// Log ticks: standard 1/2/3/4/5/6/7/8/9 mantissas within each decade between min and max. +// Mantissa === 1 (i.e. the decade itself: ...0.1, 1, 10, 100, 1000...) is flagged major +// so it can be drawn as a highlighted line distinct from the minor in-decade ticks. +function logTicks(min: number, max: number): Tick[] { + if (min <= 0 || max <= 0 || max <= min) return []; + const startDecade = Math.floor(Math.log10(min)); + const endDecade = Math.ceil(Math.log10(max)); + const out: Tick[] = []; + for (let d = startDecade; d <= endDecade; d++) { + const decadeValue = Math.pow(10, d); + for (const mantissa of [1, 2, 3, 4, 5, 6, 7, 8, 9]) { + const value = decadeValue * mantissa; + if (value < min - 1e-9 || value > max + 1e-9) continue; + out.push({ value, major: mantissa === 1 }); + } + } + return out; +} + export const LinearGraph = forwardRef(function LinearGraph( { xMin, @@ -80,6 +117,8 @@ export const LinearGraph = forwardRef(funct yMin, yMax, yStep, + xScale = 'linear', + yScale = 'linear', draw, width, height, @@ -138,6 +177,18 @@ export const LinearGraph = forwardRef(funct const effXStep = xStep ?? niceStep(xMin, xMax); const effYStep = yStep ?? niceStep(yMin, yMax); + const xLogValid = xScale === 'log' && xMin > 0 && xMax > xMin; + const yLogValid = yScale === 'log' && yMin > 0 && yMax > yMin; + + const xTicks = useMemo( + () => (xLogValid ? logTicks(xMin, xMax) : linearTicks(xMin, xMax, effXStep)), + [xLogValid, xMin, xMax, effXStep] + ); + const yTicks = useMemo( + () => (yLogValid ? logTicks(yMin, yMax) : linearTicks(yMin, yMax, effYStep)), + [yLogValid, yMin, yMax, effYStep] + ); + useEffect(() => { if (width && height) return; const el = containerRef.current; @@ -176,17 +227,41 @@ export const LinearGraph = forwardRef(funct height: Math.max(1, cssH - pad.top - pad.bottom), }; - const toPxX = (x: number) => plot.x + ((x - xMin) / (xMax - xMin || 1)) * plot.width; - const toPxY = (y: number) => - plot.y + plot.height - ((y - yMin) / (yMax - yMin || 1)) * plot.height; + const logXMin = xLogValid ? Math.log10(xMin) : 0; + const logXMax = xLogValid ? Math.log10(xMax) : 0; + const logYMin = yLogValid ? Math.log10(yMin) : 0; + const logYMax = yLogValid ? Math.log10(yMax) : 0; + + const toPxX = (x: number) => { + if (xLogValid) { + const lx = Math.log10(Math.max(x, 1e-9)); + return plot.x + ((lx - logXMin) / (logXMax - logXMin || 1)) * plot.width; + } + return plot.x + ((x - xMin) / (xMax - xMin || 1)) * plot.width; + }; + const toPxY = (y: number) => { + if (yLogValid) { + const ly = Math.log10(Math.max(y, 1e-9)); + return plot.y + plot.height - ((ly - logYMin) / (logYMax - logYMin || 1)) * plot.height; + } + return plot.y + plot.height - ((y - yMin) / (yMax - yMin || 1)) * plot.height; + }; const toPx = (x: number, y: number): [number, number] => [toPxX(x), toPxY(y)]; - const toData = (px: number, py: number): [number, number] => [ - xMin + ((px - plot.x) / plot.width) * (xMax - xMin), - yMin + (1 - (py - plot.y) / plot.height) * (yMax - yMin), - ]; + const toData = (px: number, py: number): [number, number] => { + const xRatio = (px - plot.x) / plot.width; + const yRatio = 1 - (py - plot.y) / plot.height; + const x = xLogValid + ? Math.pow(10, logXMin + xRatio * (logXMax - logXMin)) + : xMin + xRatio * (xMax - xMin); + const y = yLogValid + ? Math.pow(10, logYMin + yRatio * (logYMax - logYMin)) + : yMin + yRatio * (yMax - yMin); + return [x, y]; + }; const css = getComputedStyle(canvas); const gridColor = css.getPropertyValue('--graph-grid').trim() || '#2a2f3a'; + const gridMajorColor = css.getPropertyValue('--graph-grid-major').trim() || '#4b5563'; const axisColor = css.getPropertyValue('--graph-axis').trim() || '#6b7280'; const textColor = css.getPropertyValue('--graph-text').trim() || '#9ca3af'; const bgColor = css.getPropertyValue('--graph-bg').trim(); @@ -217,32 +292,55 @@ export const LinearGraph = forwardRef(funct ); if (showGrid) { + // minor grid lines first (thin), then major/decade lines drawn on top so + // they aren't obscured and read clearly as the "multiples of 10" markers. ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); - for (const gx of range(xMin, xMax, effXStep)) { - const px = Math.round(toPxX(gx)) + 0.5; + for (const t of xTicks) { + if (t.major) continue; + const px = Math.round(toPxX(t.value)) + 0.5; ctx.moveTo(px, plot.y); ctx.lineTo(px, plot.y + plot.height); } - for (const gy of range(yMin, yMax, effYStep)) { - const py = Math.round(toPxY(gy)) + 0.5; + for (const t of yTicks) { + if (t.major) continue; + const py = Math.round(toPxY(t.value)) + 0.5; ctx.moveTo(plot.x, py); ctx.lineTo(plot.x + plot.width, py); } ctx.stroke(); + + const majorX = xTicks.filter((t) => t.major); + const majorY = yTicks.filter((t) => t.major); + if (majorX.length || majorY.length) { + ctx.strokeStyle = gridMajorColor; + ctx.lineWidth = Math.max(1, metrics.axisLineWidth * 0.6); + ctx.beginPath(); + for (const t of majorX) { + const px = Math.round(toPxX(t.value)) + 0.5; + ctx.moveTo(px, plot.y); + ctx.lineTo(px, plot.y + plot.height); + } + for (const t of majorY) { + const py = Math.round(toPxY(t.value)) + 0.5; + ctx.moveTo(plot.x, py); + ctx.lineTo(plot.x + plot.width, py); + } + ctx.stroke(); + } } if (showAxes) { ctx.strokeStyle = axisColor; ctx.lineWidth = metrics.axisLineWidth; ctx.beginPath(); - if (yMin <= 0 && yMax >= 0) { + if (!yLogValid && yMin <= 0 && yMax >= 0) { const py = Math.round(toPxY(0)) + 0.5; ctx.moveTo(plot.x, py); ctx.lineTo(plot.x + plot.width, py); } - if (xMin <= 0 && xMax >= 0) { + if (!xLogValid && xMin <= 0 && xMax >= 0) { const px = Math.round(toPxX(0)) + 0.5; ctx.moveTo(px, plot.y); ctx.lineTo(px, plot.y + plot.height); @@ -264,29 +362,34 @@ export const LinearGraph = forwardRef(funct ); if (showTicks) { - ctx.fillStyle = textColor; ctx.font = `${metrics.tickFont}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; - for (const gx of range(xMin, xMax, effXStep)) { - const px = toPxX(gx); + for (const t of xTicks) { + // In log mode, only label the decade (major) ticks — labeling every + // 2,3,4...9 mantissa gets unreadably dense. + if (xLogValid && !t.major) continue; + const px = toPxX(t.value); ctx.beginPath(); ctx.moveTo(px, plot.y + plot.height); ctx.lineTo(px, plot.y + plot.height + metrics.tickLen); ctx.strokeStyle = axisColor; ctx.stroke(); - ctx.fillText(formatX(gx), px, plot.y + plot.height + metrics.tickLen + 2); + ctx.fillStyle = t.major && xLogValid ? textColor : textColor; + ctx.fillText(formatX(t.value), px, plot.y + plot.height + metrics.tickLen + 2); } ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; - for (const gy of range(yMin, yMax, effYStep)) { - const py = toPxY(gy); + for (const t of yTicks) { + if (yLogValid && !t.major) continue; + const py = toPxY(t.value); ctx.beginPath(); ctx.moveTo(plot.x - metrics.tickLen, py); ctx.lineTo(plot.x, py); ctx.strokeStyle = axisColor; ctx.stroke(); - ctx.fillText(formatY(gy), plot.x - metrics.tickLen - 2, py); + ctx.fillStyle = textColor; + ctx.fillText(formatY(t.value), plot.x - metrics.tickLen - 2, py); } } @@ -314,10 +417,12 @@ export const LinearGraph = forwardRef(funct metrics, xMin, xMax, - effXStep, + xLogValid, yMin, yMax, - effYStep, + yLogValid, + xTicks, + yTicks, showGrid, showAxes, showTicks, diff --git a/src/components/dsp408/VerticalSlider/VerticalSlider.css b/src/components/VerticalSlider/VerticalSlider.css similarity index 100% rename from src/components/dsp408/VerticalSlider/VerticalSlider.css rename to src/components/VerticalSlider/VerticalSlider.css diff --git a/src/components/dsp408/VerticalSlider/VerticalSlider.tsx b/src/components/VerticalSlider/VerticalSlider.tsx similarity index 100% rename from src/components/dsp408/VerticalSlider/VerticalSlider.tsx rename to src/components/VerticalSlider/VerticalSlider.tsx diff --git a/src/components/dsp408/CompressorSection/CompressorSection.tsx b/src/components/dsp408/CompressorSection/CompressorSection.tsx index a6c09fb..0ccc5be 100644 --- a/src/components/dsp408/CompressorSection/CompressorSection.tsx +++ b/src/components/dsp408/CompressorSection/CompressorSection.tsx @@ -1,10 +1,10 @@ // CompressorSection.tsx -import VerticalSlider from '../VerticalSlider/VerticalSlider'; -import { OutputChannel, Ratio } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; +import { OutputChannel, Ratio } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; import Button from '../../Button/Button'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import MeterBar from '../../MeterBar/MeterBar'; import { useMemo, useState, useEffect } from 'react'; import { TriangleUpIcon, TriangleDownIcon } from '../../../assets/icons/Triangle'; diff --git a/src/components/dsp408/DelaySection/DelaySection.tsx b/src/components/dsp408/DelaySection/DelaySection.tsx index 332f650..d32585a 100644 --- a/src/components/dsp408/DelaySection/DelaySection.tsx +++ b/src/components/dsp408/DelaySection/DelaySection.tsx @@ -1,9 +1,9 @@ -import VerticalSlider from '../VerticalSlider/VerticalSlider'; -import { InputChannel, Channel, OutputChannel, DelayUnit } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; +import { InputChannel, Channel, OutputChannel, DelayUnit } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; import Button from '../../Button/Button'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import { useMemo, useState } from 'react'; import './DelaySection.css'; diff --git a/src/components/dsp408/GainSection/GainSection.tsx b/src/components/dsp408/GainSection/GainSection.tsx index 5a5694a..ccd0133 100644 --- a/src/components/dsp408/GainSection/GainSection.tsx +++ b/src/components/dsp408/GainSection/GainSection.tsx @@ -1,9 +1,9 @@ -import VerticalSlider from '../VerticalSlider/VerticalSlider'; -import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; +import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; import Button from '../../Button/Button'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import './GainSection.css'; function GainPanel({ @@ -26,9 +26,9 @@ function GainPanel({ ? dsp.state?.current_config.input_states[channel.Input] : dsp.state?.current_config.output_states[channel.Output]; - const muted = channelState?.mute ?? false; - const inverted = channelState?.phase_inverted ?? false; - const gain = channelState?.gain ?? 0; + const muted = channelState.mute; + const inverted = channelState.phase_inverted; + const gain = channelState.gain; const toggleMute = async () => { await setMute(channel, !muted); @@ -122,11 +122,12 @@ function GainSection({
Frequency Response
{ diff --git a/src/components/dsp408/GateSection/GateSection.tsx b/src/components/dsp408/GateSection/GateSection.tsx index dca75d5..8f0cfa6 100644 --- a/src/components/dsp408/GateSection/GateSection.tsx +++ b/src/components/dsp408/GateSection/GateSection.tsx @@ -1,9 +1,9 @@ // GateSection.tsx -import VerticalSlider from '../VerticalSlider/VerticalSlider'; -import { InputChannel } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; +import { InputChannel } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import MeterBar from '../../MeterBar/MeterBar'; import { useMemo, useState } from 'react'; import './GateSection.css'; diff --git a/src/components/dsp408/GeqSection/GeqSection.tsx b/src/components/dsp408/GeqSection/GeqSection.tsx index 2331b1f..d34be83 100644 --- a/src/components/dsp408/GeqSection/GeqSection.tsx +++ b/src/components/dsp408/GeqSection/GeqSection.tsx @@ -1,9 +1,9 @@ -import VerticalSlider from '../VerticalSlider/VerticalSlider'; -import { InputChannel, DiscreteFrequency } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; +import { InputChannel, DiscreteFrequency } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; import Button from '../../Button/Button'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import { useMemo, useState } from 'react'; import './GeqSection.css'; diff --git a/src/components/dsp408/InputSection/InputSection.tsx b/src/components/dsp408/InputSection/InputSection.tsx index 70177f7..0b0c1e1 100644 --- a/src/components/dsp408/InputSection/InputSection.tsx +++ b/src/components/dsp408/InputSection/InputSection.tsx @@ -1,15 +1,15 @@ // InputSection.tsx -import VerticalSlider from '../VerticalSlider/VerticalSlider'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; // ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping // input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values // if the actual enum differs. -import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; -import { CrossoverFilter } from '../../../types/dsp408State'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; +import { CrossoverFilter } from '../../../types/dsp408'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import { useRef, useState } from 'react'; import './InputSection.css'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; import Button from '../../Button/Button'; import MeterBar from '../../MeterBar/MeterBar'; diff --git a/src/components/dsp408/LimiterSection/LimiterSection.tsx b/src/components/dsp408/LimiterSection/LimiterSection.tsx index 625430c..3c003fd 100644 --- a/src/components/dsp408/LimiterSection/LimiterSection.tsx +++ b/src/components/dsp408/LimiterSection/LimiterSection.tsx @@ -1,9 +1,9 @@ // LimiterSection.tsx -import VerticalSlider from '../VerticalSlider/VerticalSlider'; -import { OutputChannel } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; -import ChannelGroup from '../ChannelGroup/ChannelGroup'; -import { LinearGraph } from '../LinearGraph/LinearGraph'; +import VerticalSlider from '../../VerticalSlider/VerticalSlider'; +import { OutputChannel } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; +import ChannelGroup from '../../ChannelGroup/ChannelGroup'; +import { LinearGraph } from '../../LinearGraph/LinearGraph'; import MeterBar from '../../MeterBar/MeterBar'; import { useMemo, useState } from 'react'; import './LimiterSection.css'; diff --git a/src/components/dsp408/Matrixsection/Matrixsection.tsx b/src/components/dsp408/Matrixsection/Matrixsection.tsx index 61373c0..b447af1 100644 --- a/src/components/dsp408/Matrixsection/Matrixsection.tsx +++ b/src/components/dsp408/Matrixsection/Matrixsection.tsx @@ -1,7 +1,7 @@ // MatrixSection.tsx import { Fragment, useRef, useState } from 'react'; -import { InputChannel, OutputChannel } from '../../../types/dsp408State'; -import { DSP } from '../../../types/types'; +import { InputChannel, OutputChannel } from '../../../types/dsp408'; +import { DSP408 as DSP } from '../../../types/types'; import './MatrixSection.css'; // ASSUMPTION: an InputChannel enum exists alongside OutputChannel in diff --git a/src/dsp/dsp408.ts b/src/dsp/dsp408.ts new file mode 100644 index 0000000..ef31dd3 --- /dev/null +++ b/src/dsp/dsp408.ts @@ -0,0 +1,1719 @@ +import { invoke } from '@tauri-apps/api/core'; +import { DSP } from '../types/types'; +import { + Channel, + InputChannel, + OutputChannel, + Ratio, + DelayUnit, + DiscreteFrequency, + CrossoverFilter, + PEQFilter, + PEQBand, + InputSource, +} from '../types/dsp408'; +import { SetDsps, NotifyFn, updateDSP, setPollingPaused } from './dspShared'; +import { registerPollStopper } from './pollRegistry'; +import { defaultDSPState, defaultMeters } from '../types/dsp408'; + +type DSP408 = Extract; + +export type DSP408Api = ReturnType; + +// --- selection + polling state, private to this module ------------------- + +const selectedIds = new Set(); +const pollIntervals = new Map>(); + +const ACTIVE_POLL_INTERVAL_MS = 1000; +const BACKGROUND_POLL_INTERVAL_MS = 5000; + +function pollIntervalFor(id: number): number { + return selectedIds.has(id) ? ACTIVE_POLL_INTERVAL_MS : BACKGROUND_POLL_INTERVAL_MS; +} + +async function poll(dsp: DSP408, setDsps: SetDsps) { + try { + const meters = await invoke('get_meter_levels', { id: dsp.id }); + + updateDSP(setDsps, dsp.id, (d) => + d.pollingPaused + ? d + : ({ + ...d, + meters, + } as DSP408) + ); + } catch { + // transient poll failures — don't notify + } +} + +function startPolling(dsp: DSP408, setDsps: SetDsps) { + stopPolling(dsp.id); + + void poll(dsp, setDsps); + + const interval = setInterval(() => poll(dsp, setDsps), pollIntervalFor(dsp.id)); + + pollIntervals.set(dsp.id, interval); +} + +function restartPollingIfActive(dsp: DSP408, setDsps: SetDsps) { + // only reschedule if we're actually polling this dsp right now + if (pollIntervals.has(dsp.id)) startPolling(dsp, setDsps); +} + +export function stopPolling(id: number) { + const interval = pollIntervals.get(id); + if (interval) { + clearInterval(interval); + pollIntervals.delete(id); + } +} + +registerPollStopper('DSP408', stopPolling); + +// --- pause helper ---------------------------------------------------------- + +async function withPollingPaused( + dsp: DSP408, + setDsps: SetDsps, + operation: () => Promise +): Promise { + setPollingPaused(setDsps, dsp.id, true); + try { + return await operation(); + } finally { + setPollingPaused(setDsps, dsp.id, false); + } +} + +// --- factory ----------------------------------------------------------- + +export async function create(params: { + name: string; + ip: string; + port: number; + deviceId: number; + demo: boolean; +}): Promise { + const id = await invoke('create_dsp408', { + ip: params.ip, + port: params.port, + deviceId: params.deviceId, + }); + + return { + id, + type: 'DSP408', + name: params.name, + ip: params.ip, + port: params.port, + deviceId: params.deviceId, + demo: params.demo, + status: 'disconnected', + pollingPaused: true, + state: defaultDSPState, + meters: defaultMeters, + }; +} + +export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { + async function updateDSPState(): Promise { + try { + const state = await invoke('get_dsp_state', { id: dsp.id }); + updateDSP(setDsps, dsp.id, (d) => ({ ...d, state }) as DSP408); + } catch (err) { + notify(`Failed to fetch state: ${String(err)}`, 'error', dsp.name); + throw err; + } + } + + return { + type: 'DSP408' as const, + + setDSPState(updater: (dsp: DSP) => DSP) { + updateDSP(setDsps, dsp.id, updater); + }, + + setActive(active: boolean) { + if (active) { + selectedIds.add(dsp.id); + } else { + selectedIds.delete(dsp.id); + } + restartPollingIfActive(dsp, setDsps); + }, + + updateDSPState, + + async connect() { + if (dsp.demo) { + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connected', pollingPaused: false })); + return; + } + + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connecting' })); + try { + await invoke('connect_dsp408', { id: dsp.id }); + + await updateDSPState(); + + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'connected', pollingPaused: false })); + + notify('Connected', 'success', dsp.name); + + startPolling(dsp, setDsps); + } catch (err) { + updateDSP(setDsps, dsp.id, (d) => ({ ...d, status: 'disconnected' })); + notify(`Connection failed: ${String(err)}`, 'error', dsp.name); + } + }, + + async disconnect() { + if (dsp.demo) { + updateDSP(setDsps, dsp.id, (d) => ({ + ...d, + status: 'disconnected', + pollingPaused: true, + })); + + return; + } + + stopPolling(dsp.id); + + try { + await withPollingPaused(dsp, setDsps, async () => { + await invoke('disconnect_dsp408', { id: dsp.id }); + }); + + updateDSP(setDsps, dsp.id, (d) => ({ + ...d, + status: 'disconnected', + pollingPaused: true, + })); + + notify('Disconnected', 'success', dsp.name); + } catch (err) { + notify(`Disconnection failed: ${String(err)}`, 'error', dsp.name); + } + }, + + async recallPreset(presetIndex: number): Promise { + if (dsp.demo) { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + return { + ...d, + state: { + ...d.state, + presets: { + ...d.state.presets, + current_index: presetIndex - 1, + }, + }, + }; + }); + + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('recall_preset', { + id: dsp.id, + presetIndex, + }); + + if (!success) { + notify('Failed to recall preset', 'error', dsp.name); + return false; + } + + await updateDSPState(); + + notify(`Preset ${presetIndex} recalled`, 'success', dsp.name); + return true; + } catch (err) { + notify(`Preset recall failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setCurrentPresetName(name: string): Promise { + const paddedName = name.slice(0, 14).padEnd(14, ' '); + + const updatePresetName = () => { + 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)), + }, + }, + }; + }); + }; + + if (dsp.demo) { + updatePresetName(); + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + 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; + } + + await updateDSPState(); + + notify('Preset renamed', 'success', dsp.name); + return true; + } catch (err) { + notify(`Preset rename failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setGain(channel: Channel, gain: number): Promise { + const updateGain = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...cfg.input_states[channel.Input], + gain, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...cfg.output_states[channel.Output], + gain, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateGain(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Gain updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Gain update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setMute(channel: Channel, muted: boolean): Promise { + const updateMute = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + if ('Input' in channel) { + const inputChannel = channel.Input; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [inputChannel]: { + ...cfg.input_states[inputChannel], + mute: muted, + }, + }, + }, + }, + }; + } + + const outputChannel = channel.Output; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [outputChannel]: { + ...cfg.output_states[outputChannel], + mute: muted, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateMute(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify(muted ? 'Muted' : 'Unmuted', 'success', dsp.name); + return true; + } catch (err) { + notify(`Mute update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setInverse(channel: Channel, inverted: boolean): Promise { + const updatePhase = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + if ('Input' in channel) { + const inputChannel = channel.Input; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [inputChannel]: { + ...cfg.input_states[inputChannel], + phase_inverted: inverted, + }, + }, + }, + }, + }; + } + + const outputChannel = channel.Output; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [outputChannel]: { + ...cfg.output_states[outputChannel], + phase_inverted: inverted, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updatePhase(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify(inverted ? 'Phase inverted' : 'Phase normal', 'success', dsp.name); + return true; + } catch (err) { + notify(`Phase update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setGate( + inputChannel: InputChannel, + threshold: number, + attack: number, + hold: number, + release: number + ): Promise { + const updateGate = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [inputChannel]: { + ...cfg.input_states[inputChannel], + gate: { + threshold, + attack, + hold, + release, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateGate(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Gate updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Gate update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setCompressor( + outputChannel: OutputChannel, + ratio: Ratio, + attack: number, + release: number, + knee: number, + threshold: number + ): Promise { + const updateCompressor = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [outputChannel]: { + ...cfg.output_states[outputChannel], + compressor: { + ratio, + attack, + release, + knee, + threshold, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateCompressor(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Compressor updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Compressor update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setLimiter( + outputChannel: OutputChannel, + threshold: number, + attack: number, + release: number + ): Promise { + const updateLimiter = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [outputChannel]: { + ...cfg.output_states[outputChannel], + limiter: { + threshold, + attack, + release, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateLimiter(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Limiter updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Limiter update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setDelayUnit(unit: DelayUnit): Promise { + const updateDelayUnit = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...d.state.current_config, + delay_unit: unit, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateDelayUnit(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Delay unit updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Delay unit update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setDelay(channel: Channel, delay: number): Promise { + const updateDelay = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...cfg.input_states[channel.Input], + delay, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...cfg.output_states[channel.Output], + delay, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateDelay(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Channel delay updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Channel delay update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setMatrixRouting(output: OutputChannel, inputs: InputChannel[]): Promise { + const updateRouting = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [output]: { + ...cfg.output_states[output], + matrix_routes: { + ...cfg.output_states[output].matrix_routes, + connected: inputs, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateRouting(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Matrix routing updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Matrix routing update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setMatrixGain( + inputChannel: InputChannel, + outputChannel: OutputChannel, + gain: number + ): Promise { + const updateMatrixGain = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + const outputState = cfg.output_states[outputChannel]; + const inputIndex = Object.values(InputChannel).indexOf(inputChannel); + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [outputChannel]: { + ...outputState, + matrix_routes: { + ...outputState.matrix_routes, + gains: outputState.matrix_routes.gains.map((g, i) => + i === inputIndex ? gain : g + ), + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateMatrixGain(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('Matrix gain updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Matrix gain update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setGeqBand( + inputChannel: InputChannel, + frequencyBand: DiscreteFrequency, + gain: number + ): Promise { + const updateGeqBand = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + const inputState = cfg.input_states[inputChannel]; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [inputChannel]: { + ...inputState, + geq: { + ...inputState.geq, + gains: { + ...inputState.geq.gains, + [frequencyBand]: gain, + }, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateGeqBand(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('GEQ band updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`GEQ band update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setGeqBypass(inputChannel: InputChannel, bypass: boolean): Promise { + const updateGeqBypass = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + const inputState = cfg.input_states[inputChannel]; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [inputChannel]: { + ...inputState, + geq: { + ...inputState.geq, + bypass, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateGeqBypass(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + 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 setLowPass( + channel: Channel, + freq: number, + filterType: CrossoverFilter + ): Promise { + const updateLowPass = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + const lowPass = { + frequency: freq, + filter_type: filterType, + }; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...cfg.input_states[channel.Input], + low_pass: lowPass, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...cfg.output_states[channel.Output], + low_pass: lowPass, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateLowPass(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + 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 setHighPass( + channel: Channel, + freq: number, + filterType: CrossoverFilter + ): Promise { + const updateHighPass = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + const highPass = { + frequency: freq, + filter_type: filterType, + }; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...cfg.input_states[channel.Input], + high_pass: highPass, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...cfg.output_states[channel.Output], + high_pass: highPass, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateHighPass(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + 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 setPeqBand( + channel: Channel, + band: PEQBand, + gainDb: number, + freqHz: number, + q: number, + filterType: PEQFilter, + bypass: boolean + ): Promise { + const updatePeqBand = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + const peqBand = { + gain: gainDb, + frequency: freqHz, + q, + filter_type: filterType, + bypass, + }; + + if ('Input' in channel) { + const inputState = cfg.input_states[channel.Input]; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...inputState, + peq_chain: { + ...inputState.peq_chain, + bands: { + ...inputState.peq_chain.bands, + [band]: peqBand, + }, + }, + }, + }, + }, + }, + }; + } + + const outputState = cfg.output_states[channel.Output]; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...outputState, + peq_chain: { + ...outputState.peq_chain, + bands: { + ...outputState.peq_chain.bands, + [band]: peqBand, + }, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updatePeqBand(); + return true; + } + + return withPollingPaused(dsp, setDsps, 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; + } + + await updateDSPState(); + + notify('PEQ band updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`PEQ update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setPEQBypass(channel: Channel, bypass: boolean): Promise { + const updateBypass = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...cfg.input_states[channel.Input], + peq_chain: { + ...cfg.input_states[channel.Input].peq_chain, + bypass, + }, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...cfg.output_states[channel.Output], + peq_chain: { + ...cfg.output_states[channel.Output].peq_chain, + bypass, + }, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateBypass(); + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('set_bypass_peq', { + id: dsp.id, + channel, + bypass, + }); + + if (!success) { + notify('Failed to set PEQ bypass', 'error', dsp.name); + return false; + } + + await updateDSPState(); + + notify(bypass ? 'PEQ bypass enabled' : 'PEQ bypass disabled', 'success', dsp.name); + + return true; + } catch (err) { + notify(`PEQ bypass failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async storeCurrentConfiguration(presetIndex: number, force: boolean): Promise { + if (dsp.demo) { + if (presetIndex === 0 && !force) { + return false; + } + + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const currentIndex = d.state.presets.current_index; + + return { + ...d, + state: { + ...d.state, + presets: { + ...d.state.presets, + modified: d.state.presets.modified.map((modified, index) => + index === currentIndex ? true : modified + ), + }, + }, + }; + }); + + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('store_current_configuration', { + id: dsp.id, + presetIndex, + force, + }); + + if (!success) { + return false; + } + + await updateDSPState(); + + return true; + } catch (err) { + notify(`Store configuration failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async factoryReset(force: boolean): Promise { + if (!force) { + return false; + } + + if (dsp.demo) { + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('factory_reset', { + id: dsp.id, + force, + }); + + if (!success) { + return false; + } + + await updateDSPState(); + + return true; + } catch (err) { + notify(`Factory reset failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async copyChannel(channel1: Channel, channel2: Channel): Promise { + if (dsp.demo) { + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('copy_channel', { + id: dsp.id, + channel1, + channel2, + }); + + if (!success) { + return false; + } + + await updateDSPState(); + + return true; + } catch (err) { + notify(`Channel copy failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setInputSource(inputSource: InputSource): Promise { + const updateInputSource = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...d.state.current_config, + input_source: inputSource, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateInputSource(); + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('set_input_source', { + id: dsp.id, + inputSource, + }); + + if (!success) { + return false; + } + + await updateDSPState(); + + return true; + } catch (err) { + notify(`Input source update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async linkChannels(source: Channel, destinations: Channel[]): Promise { + if (dsp.demo) { + // TODO: update local channel link state + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('link_channels', { + id: dsp.id, + source, + destinations, + }); + + if (!success) { + return false; + } + + await updateDSPState(); + + return true; + } catch (err) { + notify(`Channel linking failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + + async setChannelName(channel: Channel, name: string): Promise { + const updateChannelName = () => { + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const cfg = d.state.current_config; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + input_states: { + ...cfg.input_states, + [channel.Input]: { + ...cfg.input_states[channel.Input], + name, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...cfg, + output_states: { + ...cfg.output_states, + [channel.Output]: { + ...cfg.output_states[channel.Output], + name, + }, + }, + }, + }, + }; + }); + }; + + if (dsp.demo) { + updateChannelName(); + return true; + } + + return withPollingPaused(dsp, setDsps, async () => { + try { + const success = await invoke('set_channel_name', { + id: dsp.id, + channel, + name, + }); + + if (!success) { + return false; + } + + await updateDSPState(); + + return true; + } catch (err) { + notify(`Channel name update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + }, + }; +} diff --git a/src/dsp/dspApi.ts b/src/dsp/dspApi.ts new file mode 100644 index 0000000..67eea53 --- /dev/null +++ b/src/dsp/dspApi.ts @@ -0,0 +1,45 @@ +import { DSP, DSPTypes, AppPage } from '../types/types'; +import { SetDsps, NotifyFn } from './dspShared'; +import { forDSP408, create as createDSP408 } from './dsp408'; +import { DSP408Api } from './dsp408'; +// import { forDSP204 } from './dsp204'; + +export type DSPInstance = { dsp: Extract; api: DSP408Api }; +// | { dsp: Extract; api: DSP408Api }; + +export function createDSPInstance(dsp: DSP, setDsps: SetDsps, notify: NotifyFn): DSPInstance { + switch (dsp.type) { + case 'DSP408': + return { dsp, api: forDSP408(dsp, setDsps, notify) }; + case 'DSP204': + throw new Error('DSP204 not yet implemented'); + } +} + +export async function addDSP( + setDsps: SetDsps, + setSelected: React.Dispatch>, + setPage: React.Dispatch>, + notify: NotifyFn, + params: { + name: string; + type: DSPTypes; + ip: string; + port: number; + deviceId: number; + demo: boolean; + } +) { + try { + const newDsp: DSP = + params.type === 'DSP408' + ? await createDSP408(params) + : await Promise.reject('DSP204 not implemented'); + + setDsps((prev) => [...prev, newDsp]); + setSelected(newDsp.id); + setPage('view-dsp'); + } catch (err) { + notify(`Failed to add DSP: ${String(err)}`, 'error', params.name); + } +} diff --git a/src/dsp/dspShared.ts b/src/dsp/dspShared.ts new file mode 100644 index 0000000..ba32c84 --- /dev/null +++ b/src/dsp/dspShared.ts @@ -0,0 +1,25 @@ +import { DSP, DSPTypes } from '../types/types'; +import { stopPollingFor } from './pollRegistry'; + +export type SetDsps = React.Dispatch>; +export type NotifyFn = (message: string, type: 'success' | 'error', dspName?: string) => void; + +export function updateDSP(setDsps: SetDsps, id: number, updater: (dsp: DSP) => DSP) { + setDsps((prev) => prev.map((d) => (d.id === id ? updater(d) : d))); +} + +export function setPollingPaused(setDsps: SetDsps, id: number, paused: boolean) { + updateDSP(setDsps, id, (d) => ({ ...d, pollingPaused: paused })); +} + +export function removeDSP( + setDsps: SetDsps, + selected: number | null, + setSelected: React.Dispatch>, + id: number, + type: DSPTypes +) { + stopPollingFor(type, id); + setDsps((prev) => prev.filter((d) => d.id !== id)); + if (selected === id) setSelected(null); +} diff --git a/src/dsp/pollRegistry.ts b/src/dsp/pollRegistry.ts new file mode 100644 index 0000000..67b701e --- /dev/null +++ b/src/dsp/pollRegistry.ts @@ -0,0 +1,15 @@ +import { DSPTypes } from '../types/types'; + +const stoppers = new Map void>(); + +export function registerPollStopper(type: DSPTypes, stop: (id: number) => void) { + stoppers.set(type, stop); +} + +export function stopPollingFor(type: DSPTypes, id: number) { + stoppers.get(type)?.(id); +} + +export function stopAllPolling(dsps: { id: number; type: DSPTypes }[]) { + for (const dsp of dsps) stopPollingFor(dsp.type, dsp.id); +} diff --git a/src/hooks/useDSP408.ts b/src/hooks/useDSP408.ts deleted file mode 100644 index e0389f0..0000000 --- a/src/hooks/useDSP408.ts +++ /dev/null @@ -1,1474 +0,0 @@ -// hooks/useDSP408.ts - -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/dsp408State'; - -type NotifyFunction = (message: string, type: 'success' | 'error', dspName?: string) => void; - -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: NotifyFunction -) { - // 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, - }; -} diff --git a/src/hooks/useDSPController.ts b/src/hooks/useDSPController.ts new file mode 100644 index 0000000..0a49808 --- /dev/null +++ b/src/hooks/useDSPController.ts @@ -0,0 +1,1469 @@ +// 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, +// }; +// } diff --git a/src/pages/AddDspPage/AddDspPage.css b/src/pages/AddDspPage/AddDspPage.css index 9d60079..6c78719 100644 --- a/src/pages/AddDspPage/AddDspPage.css +++ b/src/pages/AddDspPage/AddDspPage.css @@ -67,3 +67,8 @@ gap: var(--space-3); margin-top: var(--space-2); } + +.add-dsp-form__checkbox-label { + display: flex; + align-items: center; +} diff --git a/src/pages/AddDspPage/AddDspPage.tsx b/src/pages/AddDspPage/AddDspPage.tsx index bc4a0ba..bf2fe52 100644 --- a/src/pages/AddDspPage/AddDspPage.tsx +++ b/src/pages/AddDspPage/AddDspPage.tsx @@ -1,10 +1,19 @@ import { useState } from 'react'; -import { DSP, DSP_TYPES } from '../../types/types'; +import { DSP, DSP_DEFAULT_PARAMETERS, DSPTypes } from '../../types/types'; import Button from '../../components/Button/Button'; import Card from '../../components/Card/Card'; import './AddDspPage.css'; +type DSPForm = { + name: string; + type: DSPTypes; + ip: string; + port: number; + deviceId: number; + demo: boolean; +}; + function AddDSPForm({ onCancel, onAdd, @@ -12,17 +21,18 @@ function AddDSPForm({ onCancel: () => void; onAdd: (d: Omit) => void; }) { - const [form, setForm] = useState({ + const [form, setForm] = useState({ name: '', - type: DSP_TYPES[0].name, - ip: '192.168.49.30', - port: DSP_TYPES[0].defaultPort, - deviceId: 1, + type: 'DSP408', + ip: DSP_DEFAULT_PARAMETERS['DSP408'].ip, + port: DSP_DEFAULT_PARAMETERS['DSP408'].defaultPort, + deviceId: DSP_DEFAULT_PARAMETERS['DSP408'].device_id, + demo: false, }); const canSubmit = form.name.trim().length > 0 && - form.ip.trim().length > 0 && + (form.demo || form.ip.trim().length > 0) && Number.isInteger(form.deviceId) && form.deviceId >= 1 && form.deviceId <= 255; @@ -58,18 +68,17 @@ function AddDSPForm({ id="f-type" value={form.type} onChange={(e) => { - const selected = DSP_TYPES.find((d) => d.name === e.target.value); + const type = e.target.value as DSPTypes; setForm({ ...form, - type: e.target.value, - port: selected?.defaultPort ?? form.port, + type, }); }} > - {DSP_TYPES.map((dsp) => ( - ))} @@ -98,6 +107,7 @@ function AddDSPForm({ id="f-ip" className="mono" value={form.ip} + disabled={form.demo} onChange={(e) => setForm({ ...form, ip: e.target.value })} />
@@ -109,11 +119,24 @@ function AddDSPForm({ className="mono" type="number" value={form.port} + disabled={form.demo} onChange={(e) => setForm({ ...form, port: Number(e.target.value) })} />
+
+ +
+