feat: add demo mode
This commit is contained in:
+30
-17
@@ -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<DSP[]>([]);
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const [page, setPage] = useState<AppPage>('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 (
|
||||
<div className="app">
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
<main className="stage">
|
||||
{page === 'credits' ? (
|
||||
<CreditsPage />
|
||||
) : page === 'add-dsp' ? (
|
||||
<AddDSPForm onCancel={() => setPage('home')} onAdd={dsp408.addDSP} />
|
||||
) : activeDsp ? (
|
||||
<DSPPage dsp={activeDsp} dsp408={dsp408.forDSP(activeDsp)} />
|
||||
<AddDSPForm
|
||||
onCancel={() => setPage('home')}
|
||||
onAdd={(params) => addDSP(setDsps, setSelected, setPage, notify, params)}
|
||||
/>
|
||||
) : activeInstance ? (
|
||||
<DSPPage instance={activeInstance} />
|
||||
) : (
|
||||
<HomePage
|
||||
onAddDSP={() => setPage('add-dsp')}
|
||||
|
||||
+127
-22
@@ -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<LinearGraphHandle, LinearGraphProps>(function LinearGraph(
|
||||
{
|
||||
xMin,
|
||||
@@ -80,6 +117,8 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
|
||||
yMin,
|
||||
yMax,
|
||||
yStep,
|
||||
xScale = 'linear',
|
||||
yScale = 'linear',
|
||||
draw,
|
||||
width,
|
||||
height,
|
||||
@@ -138,6 +177,18 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(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<LinearGraphHandle, LinearGraphProps>(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<LinearGraphHandle, LinearGraphProps>(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<LinearGraphHandle, LinearGraphProps>(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<LinearGraphHandle, LinearGraphProps>(funct
|
||||
metrics,
|
||||
xMin,
|
||||
xMax,
|
||||
effXStep,
|
||||
xLogValid,
|
||||
yMin,
|
||||
yMax,
|
||||
effYStep,
|
||||
yLogValid,
|
||||
xTicks,
|
||||
yTicks,
|
||||
showGrid,
|
||||
showAxes,
|
||||
showTicks,
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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({
|
||||
<div className="gain-graph-panel">
|
||||
<div className="gain-graph-title">Frequency Response</div>
|
||||
<LinearGraph
|
||||
xMin={20}
|
||||
xMax={20000}
|
||||
yMin={-24}
|
||||
yMax={24}
|
||||
yStep={6}
|
||||
xMin={10}
|
||||
xMax={30000}
|
||||
xScale="log"
|
||||
yMin={-18}
|
||||
yMax={18}
|
||||
yStep={3}
|
||||
xLabel="Hz"
|
||||
yLabel="dB"
|
||||
draw={({ ctx, toPx }) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
+1719
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -67,3 +67,8 @@
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.add-dsp-form__checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -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<DSP, 'status' | 'id' | 'state' | 'meters' | 'pollingPaused'>) => void;
|
||||
}) {
|
||||
const [form, setForm] = useState({
|
||||
const [form, setForm] = useState<DSPForm>({
|
||||
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) => (
|
||||
<option key={dsp.name} value={dsp.name}>
|
||||
{dsp.name}
|
||||
{Object.keys(DSP_DEFAULT_PARAMETERS).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -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 })}
|
||||
/>
|
||||
</div>
|
||||
@@ -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) })}
|
||||
/>
|
||||
</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">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
|
||||
@@ -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;
|
||||
@@ -1,260 +1,11 @@
|
||||
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 { 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';
|
||||
import { DSPInstance } from '../../dsp/dspApi';
|
||||
import DSP408Panel from '../DSP408Panel/DSP408Panel';
|
||||
|
||||
function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
|
||||
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 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>
|
||||
);
|
||||
function DSPPage({ instance }: { instance: DSPInstance }) {
|
||||
if (instance.dsp.type === 'DSP408') {
|
||||
return <DSP408Panel dsp={instance.dsp} api={instance.api} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default DSPPage;
|
||||
|
||||
@@ -379,8 +379,8 @@ export const defaultDSPState: DSPState = {
|
||||
|
||||
presets: {
|
||||
current_index: 0,
|
||||
names: ['Default'],
|
||||
modified: [false],
|
||||
names: Array(20).fill('Default'),
|
||||
modified: Array(20).fill(false),
|
||||
},
|
||||
|
||||
current_config: {
|
||||
+41
-26
@@ -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 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 Notification = {
|
||||
id: number;
|
||||
message: string;
|
||||
@@ -36,3 +11,43 @@ export type Notification = {
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user