feat: add demo mode

This commit is contained in:
2026-08-11 22:02:46 +02:00
parent b572e1c794
commit d91bcaa20f
28 changed files with 3904 additions and 1876 deletions
+103
View File
@@ -461,3 +461,106 @@ pub async fn set_peq_band(
.map_err(|e| e.to_string()) .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<bool, String> {
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<bool, String> {
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<bool, String> {
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<bool, String> {
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<types::Channel>,
) -> Result<bool, String> {
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<bool, String> {
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<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_channel_name(channel, &name)
.map_err(|e| e.to_string())
})
}
+31 -20
View File
@@ -10,28 +10,39 @@ pub fn run() {
.manage(AppState::new()) .manage(AppState::new())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::create_dsp408, commands::create_dsp408,
commands::connect_dsp408,
commands::remove_dsp408, commands::remove_dsp408,
commands::disconnect_dsp408,
commands::get_dsp_state, commands::get_dsp_state,
commands::set_channel_gain,
commands::set_mute, commands::connect_dsp408, // 0x10
commands::set_channel_inverse_gain, commands::disconnect_dsp408, // 0x11
commands::recall_preset, commands::set_delay_unit, // 0x15
commands::get_meter_levels,
commands::set_current_preset_name, commands::recall_preset, // 0x20
commands::set_gate, commands::store_current_configuration, // 0x21
commands::set_compressor, commands::factory_reset, // 0x23
commands::set_limiter, commands::set_current_preset_name, // 0x26
commands::set_delay_unit, commands::copy_channel, // 0x2A
commands::set_delay,
commands::set_matrix_routing, commands::set_compressor, // 0x30
commands::set_matrix_gain, commands::set_low_pass, // 0x31
commands::set_geq_band, commands::set_high_pass, // 0x32
commands::set_geq_bypass, commands::set_peq_band, // 0x33
commands::set_low_pass, commands::set_channel_gain, // 0x34
commands::set_high_pass, commands::set_mute, // 0x35
commands::set_peq_band, 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!()) .run(tauri::generate_context!())
+30 -17
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect, useRef, useMemo } from 'react';
import './App.css'; import './App.css';
import { DSP, AppPage } from './types/types'; import { DSP, AppPage } from './types/types';
import TopBar from './components/Topbar/Topbar'; import TopBar from './components/Topbar/Topbar';
@@ -8,17 +8,33 @@ import { useNotifications } from './hooks/useNotifications';
import Notifications from './components/Notifications/Notifications'; import Notifications from './components/Notifications/Notifications';
import CreditsPage from './pages/CreditsPage/CreditsPage'; import CreditsPage from './pages/CreditsPage/CreditsPage';
import HomePage from './pages/HomePage/HomePage'; 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() { function App() {
const [dsps, setDsps] = useState<DSP[]>([]); const [dsps, setDsps] = useState<DSP[]>([]);
const [selected, setSelected] = useState<number | null>(null); const [selected, setSelected] = useState<number | null>(null);
const [page, setPage] = useState<AppPage>('home'); const [page, setPage] = useState<AppPage>('home');
const { notifications, notify, removeNotification, hoverNotification } = useNotifications(); 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 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 ( return (
<div className="app"> <div className="app">
@@ -34,29 +50,26 @@ function App() {
page={page} page={page}
setPage={setPage} setPage={setPage}
setSelect={setSelected} setSelect={setSelected}
onSelect={(id) => { onSelect={(id) => {
setSelected(id); setSelected(id);
setPage('view-dsp'); setPage('view-dsp');
}} }}
onRemove={(dsp) => removeDSP(setDsps, selected, setSelected, dsp.id, dsp.type)}
onRemove={dsp408.removeDSP} onAddClick={() => setPage('add-dsp')}
onConnect={(dsp) => createDSPInstance(dsp, setDsps, notify).api.connect()}
onAddClick={() => { onDisconnect={(dsp) => createDSPInstance(dsp, setDsps, notify).api.disconnect()}
setPage('add-dsp');
}}
onConnect={dsp408.connectDSP}
onDisconnect={dsp408.disconnectDSP}
/> />
<main className="stage"> <main className="stage">
{page === 'credits' ? ( {page === 'credits' ? (
<CreditsPage /> <CreditsPage />
) : page === 'add-dsp' ? ( ) : page === 'add-dsp' ? (
<AddDSPForm onCancel={() => setPage('home')} onAdd={dsp408.addDSP} /> <AddDSPForm
) : activeDsp ? ( onCancel={() => setPage('home')}
<DSPPage dsp={activeDsp} dsp408={dsp408.forDSP(activeDsp)} /> onAdd={(params) => addDSP(setDsps, setSelected, setPage, notify, params)}
/>
) : activeInstance ? (
<DSPPage instance={activeInstance} />
) : ( ) : (
<HomePage <HomePage
onAddDSP={() => setPage('add-dsp')} onAddDSP={() => setPage('add-dsp')}
@@ -24,6 +24,8 @@ export interface LinearGraphHandle {
getCanvas: () => HTMLCanvasElement | null; getCanvas: () => HTMLCanvasElement | null;
} }
export type AxisScale = 'linear' | 'log';
export interface LinearGraphProps { export interface LinearGraphProps {
xMin: number; xMin: number;
xMax: number; xMax: number;
@@ -31,6 +33,10 @@ export interface LinearGraphProps {
yMin: number; yMin: number;
yMax: number; yMax: number;
yStep?: 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; draw?: DrawFn;
width?: number; width?: number;
height?: number; height?: number;
@@ -72,6 +78,37 @@ function range(min: number, max: number, step: number): number[] {
return out; 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<LinearGraphHandle, LinearGraphProps>(function LinearGraph( export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(function LinearGraph(
{ {
xMin, xMin,
@@ -80,6 +117,8 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
yMin, yMin,
yMax, yMax,
yStep, yStep,
xScale = 'linear',
yScale = 'linear',
draw, draw,
width, width,
height, height,
@@ -138,6 +177,18 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
const effXStep = xStep ?? niceStep(xMin, xMax); const effXStep = xStep ?? niceStep(xMin, xMax);
const effYStep = yStep ?? niceStep(yMin, yMax); 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(() => { useEffect(() => {
if (width && height) return; if (width && height) return;
const el = containerRef.current; const el = containerRef.current;
@@ -176,17 +227,41 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
height: Math.max(1, cssH - pad.top - pad.bottom), height: Math.max(1, cssH - pad.top - pad.bottom),
}; };
const toPxX = (x: number) => plot.x + ((x - xMin) / (xMax - xMin || 1)) * plot.width; const logXMin = xLogValid ? Math.log10(xMin) : 0;
const toPxY = (y: number) => const logXMax = xLogValid ? Math.log10(xMax) : 0;
plot.y + plot.height - ((y - yMin) / (yMax - yMin || 1)) * plot.height; 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 toPx = (x: number, y: number): [number, number] => [toPxX(x), toPxY(y)];
const toData = (px: number, py: number): [number, number] => [ const toData = (px: number, py: number): [number, number] => {
xMin + ((px - plot.x) / plot.width) * (xMax - xMin), const xRatio = (px - plot.x) / plot.width;
yMin + (1 - (py - plot.y) / plot.height) * (yMax - yMin), 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 css = getComputedStyle(canvas);
const gridColor = css.getPropertyValue('--graph-grid').trim() || '#2a2f3a'; 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 axisColor = css.getPropertyValue('--graph-axis').trim() || '#6b7280';
const textColor = css.getPropertyValue('--graph-text').trim() || '#9ca3af'; const textColor = css.getPropertyValue('--graph-text').trim() || '#9ca3af';
const bgColor = css.getPropertyValue('--graph-bg').trim(); const bgColor = css.getPropertyValue('--graph-bg').trim();
@@ -217,32 +292,55 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
); );
if (showGrid) { 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.strokeStyle = gridColor;
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
for (const gx of range(xMin, xMax, effXStep)) { for (const t of xTicks) {
const px = Math.round(toPxX(gx)) + 0.5; if (t.major) continue;
const px = Math.round(toPxX(t.value)) + 0.5;
ctx.moveTo(px, plot.y); ctx.moveTo(px, plot.y);
ctx.lineTo(px, plot.y + plot.height); ctx.lineTo(px, plot.y + plot.height);
} }
for (const gy of range(yMin, yMax, effYStep)) { for (const t of yTicks) {
const py = Math.round(toPxY(gy)) + 0.5; if (t.major) continue;
const py = Math.round(toPxY(t.value)) + 0.5;
ctx.moveTo(plot.x, py); ctx.moveTo(plot.x, py);
ctx.lineTo(plot.x + plot.width, py); ctx.lineTo(plot.x + plot.width, py);
} }
ctx.stroke(); 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) { if (showAxes) {
ctx.strokeStyle = axisColor; ctx.strokeStyle = axisColor;
ctx.lineWidth = metrics.axisLineWidth; ctx.lineWidth = metrics.axisLineWidth;
ctx.beginPath(); ctx.beginPath();
if (yMin <= 0 && yMax >= 0) { if (!yLogValid && yMin <= 0 && yMax >= 0) {
const py = Math.round(toPxY(0)) + 0.5; const py = Math.round(toPxY(0)) + 0.5;
ctx.moveTo(plot.x, py); ctx.moveTo(plot.x, py);
ctx.lineTo(plot.x + plot.width, 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; const px = Math.round(toPxX(0)) + 0.5;
ctx.moveTo(px, plot.y); ctx.moveTo(px, plot.y);
ctx.lineTo(px, plot.y + plot.height); ctx.lineTo(px, plot.y + plot.height);
@@ -264,29 +362,34 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
); );
if (showTicks) { if (showTicks) {
ctx.fillStyle = textColor;
ctx.font = `${metrics.tickFont}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`; ctx.font = `${metrics.tickFont}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
for (const gx of range(xMin, xMax, effXStep)) { for (const t of xTicks) {
const px = toPxX(gx); // 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.beginPath();
ctx.moveTo(px, plot.y + plot.height); ctx.moveTo(px, plot.y + plot.height);
ctx.lineTo(px, plot.y + plot.height + metrics.tickLen); ctx.lineTo(px, plot.y + plot.height + metrics.tickLen);
ctx.strokeStyle = axisColor; ctx.strokeStyle = axisColor;
ctx.stroke(); 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.textAlign = 'right';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
for (const gy of range(yMin, yMax, effYStep)) { for (const t of yTicks) {
const py = toPxY(gy); if (yLogValid && !t.major) continue;
const py = toPxY(t.value);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(plot.x - metrics.tickLen, py); ctx.moveTo(plot.x - metrics.tickLen, py);
ctx.lineTo(plot.x, py); ctx.lineTo(plot.x, py);
ctx.strokeStyle = axisColor; ctx.strokeStyle = axisColor;
ctx.stroke(); 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<LinearGraphHandle, LinearGraphProps>(funct
metrics, metrics,
xMin, xMin,
xMax, xMax,
effXStep, xLogValid,
yMin, yMin,
yMax, yMax,
effYStep, yLogValid,
xTicks,
yTicks,
showGrid, showGrid,
showAxes, showAxes,
showTicks, showTicks,
@@ -1,10 +1,10 @@
// CompressorSection.tsx // CompressorSection.tsx
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
import { OutputChannel, Ratio } from '../../../types/dsp408State'; import { OutputChannel, Ratio } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import Button from '../../Button/Button'; import Button from '../../Button/Button';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import MeterBar from '../../MeterBar/MeterBar'; import MeterBar from '../../MeterBar/MeterBar';
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState, useEffect } from 'react';
import { TriangleUpIcon, TriangleDownIcon } from '../../../assets/icons/Triangle'; import { TriangleUpIcon, TriangleDownIcon } from '../../../assets/icons/Triangle';
@@ -1,9 +1,9 @@
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
import { InputChannel, Channel, OutputChannel, DelayUnit } from '../../../types/dsp408State'; import { InputChannel, Channel, OutputChannel, DelayUnit } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import Button from '../../Button/Button'; import Button from '../../Button/Button';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import './DelaySection.css'; import './DelaySection.css';
@@ -1,9 +1,9 @@
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import Button from '../../Button/Button'; import Button from '../../Button/Button';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import './GainSection.css'; import './GainSection.css';
function GainPanel({ function GainPanel({
@@ -26,9 +26,9 @@ function GainPanel({
? dsp.state?.current_config.input_states[channel.Input] ? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output]; : dsp.state?.current_config.output_states[channel.Output];
const muted = channelState?.mute ?? false; const muted = channelState.mute;
const inverted = channelState?.phase_inverted ?? false; const inverted = channelState.phase_inverted;
const gain = channelState?.gain ?? 0; const gain = channelState.gain;
const toggleMute = async () => { const toggleMute = async () => {
await setMute(channel, !muted); await setMute(channel, !muted);
@@ -122,11 +122,12 @@ function GainSection({
<div className="gain-graph-panel"> <div className="gain-graph-panel">
<div className="gain-graph-title">Frequency Response</div> <div className="gain-graph-title">Frequency Response</div>
<LinearGraph <LinearGraph
xMin={20} xMin={10}
xMax={20000} xMax={30000}
yMin={-24} xScale="log"
yMax={24} yMin={-18}
yStep={6} yMax={18}
yStep={3}
xLabel="Hz" xLabel="Hz"
yLabel="dB" yLabel="dB"
draw={({ ctx, toPx }) => { draw={({ ctx, toPx }) => {
@@ -1,9 +1,9 @@
// GateSection.tsx // GateSection.tsx
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
import { InputChannel } from '../../../types/dsp408State'; import { InputChannel } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import MeterBar from '../../MeterBar/MeterBar'; import MeterBar from '../../MeterBar/MeterBar';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import './GateSection.css'; import './GateSection.css';
@@ -1,9 +1,9 @@
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
import { InputChannel, DiscreteFrequency } from '../../../types/dsp408State'; import { InputChannel, DiscreteFrequency } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import Button from '../../Button/Button'; import Button from '../../Button/Button';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import './GeqSection.css'; import './GeqSection.css';
@@ -1,15 +1,15 @@
// InputSection.tsx // InputSection.tsx
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
// ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping // ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping
// input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values // input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values
// if the actual enum differs. // if the actual enum differs.
import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter } from '../../../types/dsp408State'; import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import { CrossoverFilter } from '../../../types/dsp408State'; import { CrossoverFilter } from '../../../types/dsp408';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import './InputSection.css'; import './InputSection.css';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import Button from '../../Button/Button'; import Button from '../../Button/Button';
import MeterBar from '../../MeterBar/MeterBar'; import MeterBar from '../../MeterBar/MeterBar';
@@ -1,9 +1,9 @@
// LimiterSection.tsx // LimiterSection.tsx
import VerticalSlider from '../VerticalSlider/VerticalSlider'; import VerticalSlider from '../../VerticalSlider/VerticalSlider';
import { OutputChannel } from '../../../types/dsp408State'; import { OutputChannel } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../../LinearGraph/LinearGraph';
import MeterBar from '../../MeterBar/MeterBar'; import MeterBar from '../../MeterBar/MeterBar';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import './LimiterSection.css'; import './LimiterSection.css';
@@ -1,7 +1,7 @@
// MatrixSection.tsx // MatrixSection.tsx
import { Fragment, useRef, useState } from 'react'; import { Fragment, useRef, useState } from 'react';
import { InputChannel, OutputChannel } from '../../../types/dsp408State'; import { InputChannel, OutputChannel } from '../../../types/dsp408';
import { DSP } from '../../../types/types'; import { DSP408 as DSP } from '../../../types/types';
import './MatrixSection.css'; import './MatrixSection.css';
// ASSUMPTION: an InputChannel enum exists alongside OutputChannel in // ASSUMPTION: an InputChannel enum exists alongside OutputChannel in
+1719
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -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<DSP, { type: 'DSP408' }>; api: DSP408Api };
// | { dsp: Extract<DSP, { type: 'DSP204' }>; 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<React.SetStateAction<number | null>>,
setPage: React.Dispatch<React.SetStateAction<AppPage>>,
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);
}
}
+25
View File
@@ -0,0 +1,25 @@
import { DSP, DSPTypes } from '../types/types';
import { stopPollingFor } from './pollRegistry';
export type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>;
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<React.SetStateAction<number | null>>,
id: number,
type: DSPTypes
) {
stopPollingFor(type, id);
setDsps((prev) => prev.filter((d) => d.id !== id));
if (selected === id) setSelected(null);
}
+15
View File
@@ -0,0 +1,15 @@
import { DSPTypes } from '../types/types';
const stoppers = new Map<DSPTypes, (id: number) => 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);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -67,3 +67,8 @@
gap: var(--space-3); gap: var(--space-3);
margin-top: var(--space-2); margin-top: var(--space-2);
} }
.add-dsp-form__checkbox-label {
display: flex;
align-items: center;
}
+36 -13
View File
@@ -1,10 +1,19 @@
import { useState } from 'react'; 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 Button from '../../components/Button/Button';
import Card from '../../components/Card/Card'; import Card from '../../components/Card/Card';
import './AddDspPage.css'; import './AddDspPage.css';
type DSPForm = {
name: string;
type: DSPTypes;
ip: string;
port: number;
deviceId: number;
demo: boolean;
};
function AddDSPForm({ function AddDSPForm({
onCancel, onCancel,
onAdd, onAdd,
@@ -12,17 +21,18 @@ function AddDSPForm({
onCancel: () => void; onCancel: () => void;
onAdd: (d: Omit<DSP, 'status' | 'id' | 'state' | 'meters' | 'pollingPaused'>) => void; onAdd: (d: Omit<DSP, 'status' | 'id' | 'state' | 'meters' | 'pollingPaused'>) => void;
}) { }) {
const [form, setForm] = useState({ const [form, setForm] = useState<DSPForm>({
name: '', name: '',
type: DSP_TYPES[0].name, type: 'DSP408',
ip: '192.168.49.30', ip: DSP_DEFAULT_PARAMETERS['DSP408'].ip,
port: DSP_TYPES[0].defaultPort, port: DSP_DEFAULT_PARAMETERS['DSP408'].defaultPort,
deviceId: 1, deviceId: DSP_DEFAULT_PARAMETERS['DSP408'].device_id,
demo: false,
}); });
const canSubmit = const canSubmit =
form.name.trim().length > 0 && form.name.trim().length > 0 &&
form.ip.trim().length > 0 && (form.demo || form.ip.trim().length > 0) &&
Number.isInteger(form.deviceId) && Number.isInteger(form.deviceId) &&
form.deviceId >= 1 && form.deviceId >= 1 &&
form.deviceId <= 255; form.deviceId <= 255;
@@ -58,18 +68,17 @@ function AddDSPForm({
id="f-type" id="f-type"
value={form.type} value={form.type}
onChange={(e) => { onChange={(e) => {
const selected = DSP_TYPES.find((d) => d.name === e.target.value); const type = e.target.value as DSPTypes;
setForm({ setForm({
...form, ...form,
type: e.target.value, type,
port: selected?.defaultPort ?? form.port,
}); });
}} }}
> >
{DSP_TYPES.map((dsp) => ( {Object.keys(DSP_DEFAULT_PARAMETERS).map((type) => (
<option key={dsp.name} value={dsp.name}> <option key={type} value={type}>
{dsp.name} {type}
</option> </option>
))} ))}
</select> </select>
@@ -98,6 +107,7 @@ function AddDSPForm({
id="f-ip" id="f-ip"
className="mono" className="mono"
value={form.ip} value={form.ip}
disabled={form.demo}
onChange={(e) => setForm({ ...form, ip: e.target.value })} onChange={(e) => setForm({ ...form, ip: e.target.value })}
/> />
</div> </div>
@@ -109,11 +119,24 @@ function AddDSPForm({
className="mono" className="mono"
type="number" type="number"
value={form.port} value={form.port}
disabled={form.demo}
onChange={(e) => setForm({ ...form, port: Number(e.target.value) })} onChange={(e) => setForm({ ...form, port: Number(e.target.value) })}
/> />
</div> </div>
</div> </div>
<div className="add-dsp-form__field add-dsp-form__field--checkbox">
<label htmlFor="f-demo" className="add-dsp-form__checkbox-label">
<input
id="f-demo"
type="checkbox"
checked={form.demo}
onChange={(e) => setForm({ ...form, demo: e.target.checked })}
/>
Demo mode (simulate device, no network connection)
</label>
</div>
<div className="add-dsp-form__actions"> <div className="add-dsp-form__actions">
<Button variant="ghost" onClick={onCancel}> <Button variant="ghost" onClick={onCancel}>
Cancel Cancel
+202
View File
@@ -0,0 +1,202 @@
import { useState } from 'react';
import { DSP } from '../../types/types';
import Sidebar from '../../components/Sidebar/Sidebar';
import { InfoIcon, KnobGainIcon, TextIcon } from '../../assets/icons/AudioIcons';
import ConnectionPanel from '../../components/dsp408/ConnectionSection/ConnectionSection';
import GainSection from '../../components/dsp408/GainSection/GainSection';
import CompressorSection from '../../components/dsp408/CompressorSection/CompressorSection';
import GateSection from '../../components/dsp408/GateSection/GateSection';
import LimiterSection from '../../components/dsp408/LimiterSection/LimiterSection';
import DelaySection from '../../components/dsp408/DelaySection/DelaySection';
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
import { InputChannel, OutputChannel } from '../../types/dsp408';
import MatrixSection from '../../components/dsp408/Matrixsection/Matrixsection';
import GeqSection from '../../components/dsp408/GeqSection/GeqSection';
import InputSection from '../../components/dsp408/InputSection/InputSection';
import { DSP408Api } from '../../dsp/dsp408';
import '../DspPage/DspPage.css';
type DSP408 = Extract<DSP, { type: 'DSP408' }>;
function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
const [activeSidebar, setActiveSidebar] = useState('overview');
const [sidebarExpanded, setSidebarExpanded] = useState(false);
const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0);
const [presetLoading, setPresetLoading] = useState(false);
const inputStates = dsp.state?.current_config.input_states;
const outputStates = dsp.state?.current_config.output_states;
const sidebarItems = [
{ id: 'overview', label: 'Overview', icon: <InfoIcon /> },
{ id: 'gain', label: 'Gain', icon: <KnobGainIcon /> },
{ id: 'gate', label: 'Gate', icon: <KnobGainIcon /> },
{ id: 'compressor', label: 'Compressor', icon: <KnobGainIcon /> },
{ id: 'limiter', label: 'Limiter', icon: <KnobGainIcon /> },
{ id: 'delay', label: 'Delay', icon: <KnobGainIcon /> },
{ id: 'matrix', label: 'Matrix', icon: <KnobGainIcon /> },
{ id: 'geq', label: 'Graphic Equalizer', icon: <KnobGainIcon /> },
{ id: 'ina', label: `${inputStates[InputChannel.InA].name}`, icon: <TextIcon>In A</TextIcon> },
{ id: 'inb', label: `${inputStates[InputChannel.InB].name}`, icon: <TextIcon>In B</TextIcon> },
{ id: 'inc', label: `${inputStates[InputChannel.InC].name}`, icon: <TextIcon>In C</TextIcon> },
{ id: 'ind', label: `${inputStates[InputChannel.InD].name}`, icon: <TextIcon>In D</TextIcon> },
{
id: 'out1',
label: `${outputStates[OutputChannel.Out1].name}`,
icon: <TextIcon>Out 1</TextIcon>,
},
{
id: 'out2',
label: `${outputStates[OutputChannel.Out2].name}`,
icon: <TextIcon>Out 2</TextIcon>,
},
{
id: 'out3',
label: `${outputStates[OutputChannel.Out3].name}`,
icon: <TextIcon>Out 3</TextIcon>,
},
{
id: 'out4',
label: `${outputStates[OutputChannel.Out4].name}`,
icon: <TextIcon>Out 4</TextIcon>,
},
{
id: 'out5',
label: `${outputStates[OutputChannel.Out5].name}`,
icon: <TextIcon>Out 5</TextIcon>,
},
{
id: 'out6',
label: `${outputStates[OutputChannel.Out6].name}`,
icon: <TextIcon>Out 6</TextIcon>,
},
{
id: 'out7',
label: `${outputStates[OutputChannel.Out7].name}`,
icon: <TextIcon>Out 7</TextIcon>,
},
{
id: 'out8',
label: `${outputStates[OutputChannel.Out8].name}`,
icon: <TextIcon>Out 8</TextIcon>,
},
{ id: 'miscellaneous', label: 'Miscellaneous', icon: <TextIcon>Out 1</TextIcon> },
];
const presets = dsp.state?.presets.names
? [
{ id: 'f00', name: 'Factory Preset' },
...dsp.state.presets.names.map((name, index) => ({
id: `u${String(index + 1).padStart(2, '0')}`,
name,
})),
]
: [
{ id: 'f00', name: 'Factory Preset' },
...Array.from({ length: 20 }, (_, index) => ({
id: `u${String(index + 1).padStart(2, '0')}`,
name: 'Default Preset',
})),
];
const onAppChange = async (id: string) => {
const presetIndex = presets.findIndex((preset) => preset.id === id);
if (presetIndex === -1) return;
setPresetLoading(true);
const success = await api.recallPreset(presetIndex);
if (success) setSelectedPreset(presetIndex);
setPresetLoading(false);
};
const renderTab = () => {
switch (activeSidebar) {
case 'overview':
return <ConnectionPanel dsp={dsp} onConnect={api.connect} onDisconnect={api.disconnect} />;
case 'gain':
return (
<GainSection
dsp={dsp}
setGain={api.setGain}
setMute={api.setMute}
setInverse={api.setInverse}
/>
);
case 'gate':
return <GateSection dsp={dsp} set_gate={api.setGate} />;
case 'compressor':
return <CompressorSection dsp={dsp} set_compressor={api.setCompressor} />;
case 'limiter':
return <LimiterSection dsp={dsp} set_limiter={api.setLimiter} />;
// case 'delay':
// return (
// <DelaySection
// dsp={dsp}
// setDelay={api.setDelay}
// setDelayUnit={api.setDelayUnit}
// setDSPState={api.setDSPState}
// />
// );
case 'ina':
return (
<InputSection
dsp={dsp}
channel={InputChannel.InA}
set_low_pass={api.setLowPass}
set_high_pass={api.setHighPass}
setPeq={api.setPeqBand}
setGain={api.setGain}
setMute={api.setMute}
setInverse={api.setInverse}
/>
);
case 'matrix':
return (
<MatrixSection
dsp={dsp}
setMatrixRouting={api.setMatrixRouting}
setMatrixGain={api.setMatrixGain}
/>
);
case 'geq':
return <GeqSection dsp={dsp} setGeqBand={api.setGeqBand} setGeqBypass={api.setGeqBypass} />;
default:
return null;
}
};
return (
<div className={`app-root ${sidebarExpanded ? 'app-root--sidebar-expanded' : ''}`}>
<Sidebar
items={sidebarItems}
activeId={activeSidebar}
onSelect={setActiveSidebar}
appName={dsp.name}
appOptions={presets}
selectedAppId={presets[selectedPreset]?.id}
onAppChange={onAppChange}
onAppRename={api.setCurrentPresetName}
expanded={sidebarExpanded}
onToggle={() => setSidebarExpanded((v) => !v)}
/>
<LoadingOverlay
active={presetLoading}
label="Recalling preset…"
overlayClassName="dsp-loading-overlay"
>
<div className="dsp-content">{renderTab()}</div>
</LoadingOverlay>
</div>
);
}
export default DSP408Panel;
+7 -256
View File
@@ -1,260 +1,11 @@
import { useState } from 'react'; import { DSPInstance } from '../../dsp/dspApi';
import { DSP } from '../../types/types'; import DSP408Panel from '../DSP408Panel/DSP408Panel';
import Sidebar from '../../components/Sidebar/Sidebar';
import { InfoIcon, KnobGainIcon, TextIcon } from '../../assets/icons/AudioIcons';
import ConnectionPanel from '../../components/dsp408/ConnectionSection/ConnectionSection';
import GainSection from '../../components/dsp408/GainSection/GainSection';
import CompressorSection from '../../components/dsp408/CompressorSection/CompressorSection';
import { DSP408Device } from '../../hooks/useDSP408';
import './DspPage.css';
import GateSection from '../../components/dsp408/GateSection/GateSection';
import LimiterSection from '../../components/dsp408/LimiterSection/LimiterSection';
import DelaySection from '../../components/dsp408/DelaySection/DelaySection';
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
import { InputChannel, OutputChannel } from '../../types/dsp408State';
import MatrixSection from '../../components/dsp408/Matrixsection/Matrixsection';
import GeqSection from '../../components/dsp408/GeqSection/GeqSection';
import InputSection from '../../components/dsp408/InputSection/InputSection';
function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) { function DSPPage({ instance }: { instance: DSPInstance }) {
const [activeSidebar, setActiveSidebar] = useState('overview'); if (instance.dsp.type === 'DSP408') {
const [sidebarExpanded, setSidebarExpanded] = useState(false); return <DSP408Panel dsp={instance.dsp} api={instance.api} />;
const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0); }
const [presetLoading, setPresetLoading] = useState(false); return null;
const inputStates = dsp.state?.current_config.input_states;
const outputStates = dsp.state?.current_config.output_states;
const sidebarItems = [
{
id: 'overview',
label: 'Overview',
icon: <InfoIcon />,
},
{
id: 'gain',
label: 'Gain',
icon: <KnobGainIcon />,
},
{
id: 'gate',
label: 'Gate',
icon: <KnobGainIcon />,
},
{
id: 'compressor',
label: 'Compressor',
icon: <KnobGainIcon />,
},
{
id: 'limiter',
label: 'Limiter',
icon: <KnobGainIcon />,
},
{
id: 'delay',
label: 'Delay',
icon: <KnobGainIcon />,
},
{
id: 'matrix',
label: 'Matrix',
icon: <KnobGainIcon />,
},
{
id: 'geq',
label: 'Graphic Equalizer',
icon: <KnobGainIcon />,
},
{
id: 'ina',
label: `${inputStates?.[InputChannel.InA]?.name}`,
icon: <TextIcon>In A</TextIcon>,
},
{
id: 'inb',
label: `${inputStates?.[InputChannel.InB]?.name}`,
icon: <TextIcon>In B</TextIcon>,
},
{
id: 'inc',
label: `${inputStates?.[InputChannel.InC]?.name}`,
icon: <TextIcon>In C</TextIcon>,
},
{
id: 'ind',
label: `${inputStates?.[InputChannel.InD]?.name}`,
icon: <TextIcon>In D</TextIcon>,
},
{
id: 'out1',
label: `${outputStates?.[OutputChannel.Out1]?.name}`,
icon: <TextIcon>Out 1</TextIcon>,
},
{
id: 'out2',
label: `${outputStates?.[OutputChannel.Out2]?.name}`,
icon: <TextIcon>Out 2</TextIcon>,
},
{
id: 'out3',
label: `${outputStates?.[OutputChannel.Out3]?.name}`,
icon: <TextIcon>Out 3</TextIcon>,
},
{
id: 'out4',
label: `${outputStates?.[OutputChannel.Out4]?.name}`,
icon: <TextIcon>Out 4</TextIcon>,
},
{
id: 'out5',
label: `${outputStates?.[OutputChannel.Out5]?.name}`,
icon: <TextIcon>Out 5</TextIcon>,
},
{
id: 'out6',
label: `${outputStates?.[OutputChannel.Out6]?.name}`,
icon: <TextIcon>Out 6</TextIcon>,
},
{
id: 'out7',
label: `${outputStates?.[OutputChannel.Out7]?.name}`,
icon: <TextIcon>Out 7</TextIcon>,
},
{
id: 'out8',
label: `${outputStates?.[OutputChannel.Out8]?.name}`,
icon: <TextIcon>Out 8</TextIcon>,
},
{
id: 'miscellaneous',
label: 'Miscellaneous',
icon: <TextIcon>Out 1</TextIcon>,
},
];
const presets = dsp.state?.presets.names
? [
{
id: 'f00',
name: 'Factory Preset',
},
...dsp.state.presets.names.map((name, index) => ({
id: `u${String(index + 1).padStart(2, '0')}`,
name,
})),
]
: [
{
id: 'f00',
name: 'Factory Preset',
},
...Array.from({ length: 20 }, (_, index) => ({
id: `u${String(index + 1).padStart(2, '0')}`,
name: 'Default Preset',
})),
];
const onAppChange = async (id: string) => {
const presetIndex = presets.findIndex((preset) => preset.id === id);
if (presetIndex === -1) return;
setPresetLoading(true);
const success = await dsp408.recallPreset(presetIndex);
if (success) {
setSelectedPreset(presetIndex);
}
setPresetLoading(false);
};
const renderTab = () => {
switch (activeSidebar) {
case 'overview':
return (
<ConnectionPanel dsp={dsp} onConnect={dsp408.connect} onDisconnect={dsp408.disconnect} />
);
case 'gain':
return (
<GainSection
dsp={dsp}
setGain={dsp408.setGain}
setMute={dsp408.setMute}
setInverse={dsp408.setInverse}
/>
);
case 'gate':
return <GateSection dsp={dsp} set_gate={dsp408.setGate} />;
case 'compressor':
return <CompressorSection dsp={dsp} set_compressor={dsp408.setCompressor} />;
case 'limiter':
return <LimiterSection dsp={dsp} set_limiter={dsp408.setLimiter} />;
case 'delay':
return (
<DelaySection
dsp={dsp}
setDelay={dsp408.setDelay}
setDelayUnit={dsp408.setDelayUnit}
setDSPState={dsp408.setDSPState}
/>
);
case 'ina':
return (
<InputSection
dsp={dsp}
channel={InputChannel.InA}
set_low_pass={dsp408.setLowPass}
set_high_pass={dsp408.setHighPass}
setPeq={dsp408.setPeqBand}
/>
);
case 'matrix':
return (
<MatrixSection
dsp={dsp}
setMatrixRouting={dsp408.setMatrixRouting}
setMatrixGain={dsp408.setMatrixGain}
/>
);
case 'geq':
return (
<GeqSection dsp={dsp} setGeqBand={dsp408.setGeqBand} setGeqBypass={dsp408.setGeqBypass} />
);
default:
return null;
}
};
return (
<div className={`app-root ${sidebarExpanded ? 'app-root--sidebar-expanded' : ''}`}>
<Sidebar
items={sidebarItems}
activeId={activeSidebar}
onSelect={setActiveSidebar}
appName={dsp.name}
appOptions={presets}
selectedAppId={presets[selectedPreset]?.id}
onAppChange={onAppChange}
onAppRename={dsp408.setCurrentPresetName}
expanded={sidebarExpanded}
onToggle={() => setSidebarExpanded((v) => !v)}
/>
<LoadingOverlay
active={presetLoading}
label="Recalling preset…"
overlayClassName="dsp-loading-overlay"
>
<div className="dsp-content">{renderTab()}</div>
</LoadingOverlay>
</div>
);
} }
export default DSPPage; export default DSPPage;
@@ -379,8 +379,8 @@ export const defaultDSPState: DSPState = {
presets: { presets: {
current_index: 0, current_index: 0,
names: ['Default'], names: Array(20).fill('Default'),
modified: [false], modified: Array(20).fill(false),
}, },
current_config: { current_config: {
+41 -26
View File
@@ -1,33 +1,8 @@
import { DSPState, Meters } from './dsp408State'; import { DSPState as DSP408State, Meters as DSP408Meters } from './dsp408';
export type DSPStatus = 'connected' | 'connecting' | 'disconnected'; export type DSPStatus = 'connected' | 'connecting' | 'disconnected';
export type DSP = {
id: number;
name: string;
type: string;
ip: string;
port: number;
deviceId: number;
status: DSPStatus;
state: DSPState;
meters: Meters;
pollingPaused: boolean;
};
export const DSP_TYPES = [
{
name: 'DSP408',
defaultPort: 9761,
},
{
name: 'DSP204',
defaultPort: 94,
},
];
export type NotificationType = 'success' | 'error' | 'warning'; export type NotificationType = 'success' | 'error' | 'warning';
export type Notification = { export type Notification = {
id: number; id: number;
message: string; message: string;
@@ -36,3 +11,43 @@ export type Notification = {
}; };
export type AppPage = 'home' | 'add-dsp' | 'view-dsp' | 'credits'; export type AppPage = 'home' | 'add-dsp' | 'view-dsp' | 'credits';
type DSPDefaultParameters = {
ip: string;
defaultPort: number;
device_id: number;
};
export type DSPTypes = 'DSP408' | 'DSP204';
export const DSP_DEFAULT_PARAMETERS: Record<DSPTypes, DSPDefaultParameters> = {
DSP408: { ip: '192.168.49.30', defaultPort: 9761, device_id: 1 },
DSP204: { ip: '192.168.49.30', defaultPort: 94, device_id: 1 },
};
interface DSPBase {
id: number;
name: string;
ip: string;
port: number;
deviceId: number;
demo: boolean;
status: DSPStatus;
pollingPaused: boolean;
}
export type DSP408 = DSPBase & {
type: 'DSP408';
state: DSP408State;
meters: DSP408Meters;
};
export type DSP204 = DSPBase & {
type: 'DSP204';
state: null;
meters: null;
};
export type DSP = DSP408 | DSP204;