feat: remove state and let rust handle

This commit is contained in:
2026-09-12 19:37:59 +02:00
parent 2717dfe99a
commit 1e00ca3a7c
17 changed files with 221 additions and 1426 deletions
+28 -2
View File
@@ -41,16 +41,28 @@ impl AppState {
} }
} }
#[derive(serde::Serialize)]
pub struct DSP408Snapshot {
pub state: types::DSPState,
pub meters: types::Meters,
pub mode: types::DSPMode,
}
#[tauri::command] #[tauri::command]
pub fn create_dsp408( pub fn create_dsp408(
state: State<AppState>, state: State<AppState>,
ip: String, ip: String,
port: u16, port: u16,
device_id: u8, device_id: u8,
demo: bool,
) -> Result<u64, String> { ) -> Result<u64, String> {
let ip = IpAddr::from_str(&ip).map_err(|e| e.to_string())?; let ip = IpAddr::from_str(&ip).map_err(|e| e.to_string())?;
let dsp = DSP408::new(ip, port, device_id).map_err(|e| e.to_string())?; let dsp = if demo {
DSP408::new_demo()
} else {
DSP408::new(ip, port, device_id).map_err(|e| e.to_string())?
};
let mut next_id = state.next_id.lock().map_err(|e| e.to_string())?; let mut next_id = state.next_id.lock().map_err(|e| e.to_string())?;
@@ -81,6 +93,20 @@ pub async fn remove_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), St
Ok(()) Ok(())
} }
#[tauri::command]
pub async fn get_dsp_snapshot(
state: State<'_, AppState>,
id: u64,
) -> Result<DSP408Snapshot, String> {
state.with_device_mut(id, |device| {
Ok(DSP408Snapshot {
state: device.dsp.state().clone(),
meters: device.dsp.meters().clone(),
mode: device.dsp.mode(),
})
})
}
#[tauri::command] #[tauri::command]
pub async fn connect_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), String> { pub async fn connect_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), String> {
state.with_device_mut(id, |device| device.dsp.connect().map_err(|e| e.to_string())) state.with_device_mut(id, |device| device.dsp.connect().map_err(|e| e.to_string()))
@@ -99,7 +125,7 @@ pub async fn get_dsp_state(
id: u64, id: u64,
) -> Result<dsp_thomann::dsp408::types::DSPState, String> { ) -> Result<dsp_thomann::dsp408::types::DSPState, String> {
state.with_device_mut(id, |device| { state.with_device_mut(id, |device| {
let dsp_state = device.dsp.state().map_err(|e| e.to_string())?; let dsp_state = device.dsp.state();
Ok(dsp_state.clone()) Ok(dsp_state.clone())
}) })
+1
View File
@@ -11,6 +11,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::create_dsp408, commands::create_dsp408,
commands::remove_dsp408, commands::remove_dsp408,
commands::get_dsp_snapshot,
commands::get_dsp_state, commands::get_dsp_state,
commands::connect_dsp408, // 0x10 commands::connect_dsp408, // 0x10
commands::disconnect_dsp408, // 0x11 commands::disconnect_dsp408, // 0x11
+3 -13
View File
@@ -1,11 +1,6 @@
export function TriangleUpIcon(props: React.SVGProps<SVGSVGElement>) { export function TriangleUpIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M8 3L2 12H14L8 3Z" d="M8 3L2 12H14L8 3Z"
stroke="currentColor" stroke="currentColor"
@@ -19,12 +14,7 @@ export function TriangleUpIcon(props: React.SVGProps<SVGSVGElement>) {
export function TriangleDownIcon(props: React.SVGProps<SVGSVGElement>) { export function TriangleDownIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M8 13L2 4H14L8 13Z" d="M8 13L2 4H14L8 13Z"
stroke="currentColor" stroke="currentColor"
@@ -34,4 +24,4 @@ export function TriangleDownIcon(props: React.SVGProps<SVGSVGElement>) {
/> />
</svg> </svg>
); );
} }
+11 -2
View File
@@ -9,6 +9,8 @@ type ChoiceSelectProps<T extends string> = {
onChange: (value: T) => void; onChange: (value: T) => void;
id?: string; id?: string;
disabled?: boolean; disabled?: boolean;
className?: string;
style?: React.CSSProperties;
}; };
function ChoiceSelect<T extends string>({ function ChoiceSelect<T extends string>({
@@ -18,15 +20,21 @@ function ChoiceSelect<T extends string>({
onChange, onChange,
id, id,
disabled, disabled,
className = '',
style,
}: ChoiceSelectProps<T>) { }: ChoiceSelectProps<T>) {
return ( return (
<div className="choice-select" data-disabled={disabled || undefined}> <div className={`choice-select ${className}`} data-disabled={disabled || undefined}>
<select <select
id={id} id={id}
className="choice-select__input" className="choice-select__input"
style={style}
value={value} value={value}
disabled={disabled} disabled={disabled}
onChange={(e) => onChange(e.target.value as T)} onChange={(e) => {
onChange(e.target.value as T);
e.currentTarget.blur();
}}
> >
{options.map((option) => ( {options.map((option) => (
<option key={option} value={option}> <option key={option} value={option}>
@@ -34,6 +42,7 @@ function ChoiceSelect<T extends string>({
</option> </option>
))} ))}
</select> </select>
<span className="choice-select__chevron" aria-hidden="true"> <span className="choice-select__chevron" aria-hidden="true">
<TriangleDownIcon /> <TriangleDownIcon />
</span> </span>
+33 -8
View File
@@ -67,9 +67,15 @@ function ChoiceStepperList<T>({
useEffect(() => setText(formatLabel(value)), [value, formatLabel]); useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
const step = (direction: 1 | -1) => { const step = (direction: 1 | -1) => {
const currentIndex = index === -1 ? 0 : index; const currentIndex = values.indexOf(value);
const nextIndex = Math.min(values.length - 1, Math.max(0, currentIndex + direction));
if (nextIndex !== currentIndex) onChange(values[nextIndex]); if (currentIndex === -1) return;
const nextIndex = currentIndex + direction;
if (nextIndex >= 0 && nextIndex < values.length) {
onChange(values[nextIndex]);
}
}; };
const commitText = () => { const commitText = () => {
@@ -89,6 +95,15 @@ function ChoiceStepperList<T>({
disabled={disabled} disabled={disabled}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onBlur={commitText} onBlur={commitText}
onWheel={(e) => {
e.preventDefault();
if (e.deltaY < 0 && canUp) {
step(1);
} else if (e.deltaY > 0 && canDown) {
step(-1);
}
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Enter') e.currentTarget.blur();
else if (e.key === 'ArrowUp') { else if (e.key === 'ArrowUp') {
@@ -164,14 +179,24 @@ function ChoiceStepperRange({
disabled={disabled} disabled={disabled}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onBlur={commitText} onBlur={commitText}
onKeyDown={(e) => { onWheel={(e) => {
if (e.key === 'Enter') e.currentTarget.blur(); e.preventDefault();
else if (e.key === 'ArrowUp') {
e.preventDefault(); if (e.deltaY < 0 && canUp) {
step(1); step(1);
} else if (e.deltaY > 0 && canDown) {
step(-1);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (canUp) step(1);
} else if (e.key === 'ArrowDown') { } else if (e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
step(-1); if (canDown) step(-1);
} }
}} }}
/> />
@@ -351,4 +351,4 @@ function SignalFlowMatrix({
); );
} }
export default SignalFlowMatrix; export default SignalFlowMatrix;
@@ -10,6 +10,9 @@
box-sizing: border-box; box-sizing: border-box;
background: var(--bg-panel); background: var(--bg-panel);
user-select: none;
-webkit-user-select: none;
} }
.input-graph-panel { .input-graph-panel {
@@ -40,6 +43,9 @@
min-height: 0; min-height: 0;
} }
/* Bottom control bar — same pattern as .compressor-section: absolute,
bottom-anchored, panels flow in a row and scroll horizontally instead
of squeezing into a fixed max-width. */
.input-control-section { .input-control-section {
container-type: size; container-type: size;
position: absolute; position: absolute;
@@ -50,7 +56,7 @@
height: var(--dsp-control-heigth); height: var(--dsp-control-heigth);
display: flex; display: flex;
align-items: center; align-items: flex-end;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
@@ -60,104 +66,23 @@
z-index: 20; z-index: 20;
border-top: var(--border-width) solid var(--border-hairline); border-top: var(--border-width) solid var(--border-hairline);
user-select: none;
-webkit-user-select: none;
} }
.input-control-panel { .input-control-panel {
display: grid;
grid-auto-flow: column;
height: 100%; height: 100%;
width: 100%; gap: var(--space-2);
max-width: 60cqw; overflow-x: auto;
justify-content: center;
display: flex;
align-items: stretch;
justify-content: center; /* ← add this */
gap: var(--space-4);
box-sizing: border-box; box-sizing: border-box;
padding: 2cqh; padding: 2cqh;
} }
.input-control-panel__sliders {
flex: 1 1 auto;
min-width: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4cqh;
}
.input-control-panel__cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
min-height: 0;
min-width: 0;
gap: 1cqh;
}
.input-control-panel__cell-label {
flex: 0 0 auto;
font-family: var(--font-ui);
font-size: 2.5cqh;
color: var(--text-muted);
white-space: nowrap;
}
.input-control-panel__side {
flex: 0 0 auto;
width: 16cqh;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 2cqh;
}
.input-control-panel__select {
display: flex;
flex-direction: column;
gap: 0.5cqh;
}
.input-control-panel__select-label {
font-family: var(--font-ui);
font-size: 2.2cqh;
color: var(--text-muted);
}
.input-control-panel__select select {
font-family: var(--font-ui);
font-size: 2.5cqh;
padding: 0.8cqh;
background: var(--bg-void);
color: var(--text-primary);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-md);
}
.input-control-panel__select select:disabled {
opacity: 0.5;
}
.input-control-panel__bypass-btn {
font-family: var(--font-ui);
font-size: 2.5cqh;
font-weight: var(--font-weight-semibold);
padding: 1cqh;
background: var(--bg-void);
color: var(--text-primary);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-md);
cursor: pointer;
}
.input-control-panel__bypass-btn--active {
background: var(--accent, #f87171);
color: var(--bg-void);
border-color: var(--accent, #f87171);
}
/* Peq Sliders */ /* Peq Sliders */
.input-panel { .input-panel {
position: relative; position: relative;
@@ -260,16 +185,26 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* Now the root of a ChoiceSelect rather than a native <select>. */
.crossover-panel__select { .crossover-panel__select {
flex: 0 0 auto; flex: 0 0 auto;
width: 85%; width: 85%;
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 3cqh; }
padding: 1cqh;
background: var(--bg-panel); .crossover-panel__select .choice-select__input,
color: inherit; .crossover-panel__select .choice-select__input option {
border: var(--border-width) solid var(--border-hairline); font-size: 4cqh;
border-radius: var(--radius-sm); color: var(--accent-brand);
/* padding-left: 1cqh; */
}
.crossover-panel__select {
font-size: 4cqh;
}
.crossover-panel__select .choice-select__input {
font-size: 4cqh;
} }
.peq-band-list { .peq-band-list {
@@ -298,7 +233,7 @@
min-width: 0; min-width: 0;
display: grid; display: grid;
grid-template-columns: 1fr 1.2fr 1fr 1fr 1.4fr 1.2fr; grid-template-columns: 1fr 1.2fr 1fr 1fr 1.6fr 1.2fr;
align-items: stretch; align-items: stretch;
gap: 1cqh; gap: 1cqh;
@@ -340,8 +275,7 @@
text-align: center; text-align: center;
} }
.peq-band-list__input, .peq-band-list__input {
.peq-band-list__select {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-width: 0; min-width: 0;
@@ -362,6 +296,17 @@
box-sizing: border-box; box-sizing: border-box;
} }
.peq-band-list__stepper,
.peq-band-list__select {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
font-family: var(--font-ui);
font-size: 4cqh;
}
.input-graph-panel__magnitude-wrap { .input-graph-panel__magnitude-wrap {
flex: 1 1 0; flex: 1 1 0;
min-height: 0; min-height: 0;
@@ -405,3 +350,15 @@
background: var(--accent, #4dd0e1); background: var(--accent, #4dd0e1);
color: var(--bg-void); color: var(--bg-void);
} }
.peq-band-list__stepper .choice-stepper__input,
.peq-band-list__select .choice-select__input {
height: 100%;
min-height: 0;
font-family: var(--font-mono);
font-size: 4cqh;
color: var(--accent-brand);
background: transparent;
}
@@ -3,6 +3,8 @@ import VerticalSlider, {
makeLogStep, makeLogStep,
makeLogQuantize, makeLogQuantize,
} from '../../../../components/VerticalSlider/VerticalSlider'; } from '../../../../components/VerticalSlider/VerticalSlider';
import ChoiceStepper from '../../../../components/ChoiceStepper/ChoiceStepper';
import ChoiceSelect from '../../../../components/ChoiceSelect/ChoiceSelect';
import { import {
CROSSOVER_FILTER_LABEL, CROSSOVER_FILTER_LABEL,
PEQ_BANDS_ORDER, PEQ_BANDS_ORDER,
@@ -18,7 +20,6 @@ import { useRef, useState } from 'react';
import './InputSection.css'; import './InputSection.css';
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
import Button from '../../../../components/Button/Button'; import Button from '../../../../components/Button/Button';
// import { buildTotalCurveDraw, buildTotalPhaseCurveDraw } from '../../utils/totalCurve';
import { import {
FREQUENCY_MAX, FREQUENCY_MAX,
FREQUENCY_MIN, FREQUENCY_MIN,
@@ -258,17 +259,15 @@ function CrossoverPanel({
style={{ width: '100%', flex: '1 1 auto' }} style={{ width: '100%', flex: '1 1 auto' }}
/> />
<select <ChoiceSelect
className="crossover-panel__select" className="crossover-panel__select"
value={bypass ? lastFilterType.current : filter_type} value={bypass ? lastFilterType.current : filter_type}
onChange={(e) => commit({ filter_type: e.target.value as CrossoverFilter })} options={CROSSOVER_FILTER_LABEL.map((opt) => opt.value)}
> formatLabel={(v) =>
{CROSSOVER_FILTER_LABEL.map((opt) => ( CROSSOVER_FILTER_LABEL.find((opt) => opt.value === v)?.label ?? String(v)
<option key={opt.value} value={opt.value}> }
{opt.label} onChange={(value) => commit({ filter_type: value })}
</option> />
))}
</select>
<Button <Button
variant="toggle" variant="toggle"
@@ -331,6 +330,9 @@ function PeqBandList({
const peq = liveBands[band] ?? bands[band]; const peq = liveBands[band] ?? bands[band];
const isActive = band === activeBand; const isActive = band === activeBand;
const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type];
const qValues = PEAK_Q_VALUES.filter((q) => q >= qMin && q <= qMax);
return ( return (
<div <div
className={`peq-band-list__row${isActive ? ' peq-band-list__row--active' : ''}`} className={`peq-band-list__row${isActive ? ' peq-band-list__row--active' : ''}`}
@@ -338,62 +340,56 @@ function PeqBandList({
> >
<Button <Button
variant="toggle" variant="toggle"
className={isActive ? 'active' : ''} className={isActive ? 'active-info' : ''}
onClick={() => setActiveBand(band)} onClick={() => setActiveBand(band)}
style={{ minHeight: 0 }} style={{
minHeight: 0,
fontSize: '4cqh',
}}
> >
{`B${i + 1}`} {`B${i + 1}`}
</Button> </Button>
{/* Frequency */} {/* Frequency */}
<input <ChoiceStepper
className="peq-band-list__input" className="peq-band-list__stepper"
type="text" value={peq.frequency}
value={ min={FREQUENCY_MIN}
peq.frequency >= 1000 ? (peq.frequency / 1000).toFixed(2) : peq.frequency.toFixed(1) max={FREQUENCY_MAX}
step={FREQUENCY_QUANTIZATION}
formatLabel={(v) =>
v >= 1000 ? `${(v / 1000).toFixed(2)} kHz` : `${v.toFixed(1)} Hz`
} }
onChange={(e) => { onChange={(value) => commit(band, { frequency: value })}
const value = Number(e.target.value);
if (!Number.isNaN(value)) {
commit(band, {
frequency: peq.frequency >= 1000 ? value * 1000 : value,
});
}
}}
/> />
<input <ChoiceStepper
className="peq-band-list__input" className="peq-band-list__stepper"
type="number"
step={1}
min={1}
max={999}
value={peq.q} value={peq.q}
onChange={(e) => commit(band, { q: Number(e.target.value) })} values={qValues}
formatLabel={(v) => v.toFixed(2)}
onChange={(value) => commit(band, { q: value })}
/> />
<input <ChoiceStepper
className="peq-band-list__input" className="peq-band-list__stepper"
type="number" value={peq.gain}
step={0.1}
min={-12} min={-12}
max={12} max={12}
value={peq.gain} step={0.1}
onChange={(e) => commit(band, { gain: Number(e.target.value) })} formatLabel={(v) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
onChange={(value) => commit(band, { gain: value })}
/> />
<select <ChoiceSelect<PEQFilter>
className="peq-band-list__select" className="peq-band-list__select"
value={peq.filter_type} value={peq.filter_type}
onChange={(e) => commit(band, { filter_type: e.target.value as PEQFilter, q: 3.0 })} options={PEQ_FILTERS_LABEL.map((opt) => opt.value)}
> formatLabel={(v) =>
{PEQ_FILTERS_LABEL.map((opt) => ( PEQ_FILTERS_LABEL.find((opt) => opt.value === v)?.label ?? String(v)
<option key={opt.value} value={opt.value}> }
{opt.label} onChange={(value) => commit(band, { filter_type: value, q: 3.0 })}
</option> />
))}
</select>
<Button <Button
variant="toggle" variant="toggle"
@@ -416,75 +412,6 @@ type SetFilterFn = (
filterType: CrossoverFilter filterType: CrossoverFilter
) => Promise<boolean>; ) => Promise<boolean>;
/** Small DOM-overlay markers positioned on top of the magnitude graph via its exposed toPxX/toPxY. */
// function GraphMarkers({
// graphRef,
// bands,
// activeBand,
// setActiveBand,
// hpFreq,
// hpBypassed,
// lpFreq,
// lpBypassed,
// }: {
// graphRef: React.RefObject<LinearGraphHandle>;
// bands: Record<PEQBand, PEQ>;
// activeBand: PEQBand;
// setActiveBand: (band: PEQBand) => void;
// hpFreq: number;
// hpBypassed: boolean;
// lpFreq: number;
// lpBypassed: boolean;
// }) {
// const g = graphRef.current;
// if (!g) return null;
// return (
// <>
// {PEQ_BANDS_ORDER.map((band, i) => {
// const peq = bands[band];
// const left = g.toPxX(peq.frequency);
// const top = g.toPxY(peq.gain);
// const isActive = band === activeBand;
// return (
// <button
// key={band}
// type="button"
// className={`graph-marker graph-marker--peq${isActive ? ' graph-marker--active' : ''}${
// peq.bypass ? ' graph-marker--bypassed' : ''
// }`}
// style={{ left, top }}
// onClick={() => setActiveBand(band)}
// title={`B${i + 1} · ${Math.round(peq.frequency)} Hz · ${peq.gain.toFixed(1)} dB`}
// >
// {`B${i + 1}`}
// </button>
// );
// })}
// <div
// className={`graph-marker graph-marker--crossover graph-marker--hp${
// hpBypassed ? ' graph-marker--bypassed' : ''
// }`}
// style={{ left: g.toPxX(hpFreq), top: g.toPxY(0) }}
// title={`HPF · ${Math.round(hpFreq)} Hz`}
// >
// HP
// </div>
// <div
// className={`graph-marker graph-marker--crossover graph-marker--lp${
// lpBypassed ? ' graph-marker--bypassed' : ''
// }`}
// style={{ left: g.toPxX(lpFreq), top: g.toPxY(0) }}
// title={`LPF · ${Math.round(lpFreq)} Hz`}
// >
// LP
// </div>
// </>
// );
// }
function InputSection({ function InputSection({
dsp, dsp,
channel, channel,
@@ -626,8 +553,8 @@ function InputSection({
yMin={GRAPH_Y_MIN} yMin={GRAPH_Y_MIN}
yMax={GRAPH_Y_MAX} yMax={GRAPH_Y_MAX}
yStep={3} yStep={3}
xLabel="Hz" xLabel="Frequency - Hz"
yLabel="dB" yLabel="Magnituded - dB"
showGrid={true} showGrid={true}
formatX={(v) => Math.round(v).toString()} formatX={(v) => Math.round(v).toString()}
formatY={(v) => Math.round(v).toString()} formatY={(v) => Math.round(v).toString()}
@@ -635,7 +562,6 @@ function InputSection({
drawCurves({ drawCurves({
ctx, ctx,
toPx, toPx,
xMin: GRAPH_X_MIN, xMin: GRAPH_X_MIN,
xMax: GRAPH_X_MAX, xMax: GRAPH_X_MAX,
yMin: GRAPH_Y_MIN, yMin: GRAPH_Y_MIN,
@@ -667,8 +593,8 @@ function InputSection({
yMin={-180} yMin={-180}
yMax={180} yMax={180}
yStep={45} yStep={45}
xLabel="Hz" xLabel="Frequency - Hz"
yLabel="°" yLabel="Phase - Deg"
showGrid={true} showGrid={true}
formatX={(v) => Math.round(v).toString()} formatX={(v) => Math.round(v).toString()}
formatY={(v) => Math.round(v).toString()} formatY={(v) => Math.round(v).toString()}
@@ -676,7 +602,6 @@ function InputSection({
drawCurves({ drawCurves({
ctx, ctx,
toPx, toPx,
xMin: GRAPH_X_MIN, xMin: GRAPH_X_MIN,
xMax: GRAPH_X_MAX, xMax: GRAPH_X_MAX,
yMin: GRAPH_Y_MIN, yMin: GRAPH_Y_MIN,
@@ -109,4 +109,4 @@
.matrix-gain-stepper .choice-stepper__input { .matrix-gain-stepper .choice-stepper__input {
font-size: 2.8cqh; font-size: 2.8cqh;
} }
+1
View File
@@ -127,6 +127,7 @@ export const PEQ_BANDS_ORDER: PEQBand[] = [
PEQBand.B6, PEQBand.B6,
PEQBand.B7, PEQBand.B7,
PEQBand.B8, PEQBand.B8,
PEQBand.B9,
]; ];
export const PEQ_Q_RANGES: Record<PEQFilter, readonly [number, number]> = { export const PEQ_Q_RANGES: Record<PEQFilter, readonly [number, number]> = {
-196
View File
@@ -1,196 +0,0 @@
import {
Gate,
GraphicEQ,
DiscreteFrequency,
PEQ,
PEQFilter,
PEQBand,
PEQChain,
Crossover,
CrossoverFilter,
CrossoverFilters,
InputChannelState,
OutputChannelState,
InputChannel,
DSPState,
Meters,
Meter,
Ratio,
OutputChannel,
DelayUnit,
} from './types';
import { FREQUENCY_MIN, FREQUENCY_MAX } from './constants';
const createDefaultGate = (): Gate => ({
attack: 10,
release: 10,
hold: 10,
threshold: 0,
});
const createDefaultGraphicEQ = (): GraphicEQ => ({
gains: {
[DiscreteFrequency.F20]: 0,
[DiscreteFrequency.F25]: 0,
[DiscreteFrequency.F31_5]: 0,
[DiscreteFrequency.F40]: 0,
[DiscreteFrequency.F50]: 0,
[DiscreteFrequency.F63]: 0,
[DiscreteFrequency.F80]: 0,
[DiscreteFrequency.F100]: 0,
[DiscreteFrequency.F125]: 0,
[DiscreteFrequency.F160]: 0,
[DiscreteFrequency.F200]: 0,
[DiscreteFrequency.F250]: 0,
[DiscreteFrequency.F315]: 0,
[DiscreteFrequency.F400]: 0,
[DiscreteFrequency.F500]: 0,
[DiscreteFrequency.F630]: 0,
[DiscreteFrequency.F800]: 0,
[DiscreteFrequency.F1000]: 0,
[DiscreteFrequency.F1250]: 0,
[DiscreteFrequency.F1600]: 0,
[DiscreteFrequency.F2000]: 0,
[DiscreteFrequency.F2500]: 0,
[DiscreteFrequency.F3150]: 0,
[DiscreteFrequency.F4000]: 0,
[DiscreteFrequency.F5000]: 0,
[DiscreteFrequency.F6300]: 0,
[DiscreteFrequency.F8000]: 0,
[DiscreteFrequency.F10000]: 0,
[DiscreteFrequency.F12500]: 0,
[DiscreteFrequency.F16000]: 0,
[DiscreteFrequency.F20000]: 0,
},
bypass: false,
});
const createDefaultPEQ = (): PEQ => ({
gain: 0,
frequency: 1000,
q: 1,
filter_type: PEQFilter.Peak,
bypass: false,
});
const createDefaultPEQChain = (): PEQChain => ({
bands: Object.fromEntries(
Object.values(PEQBand).map((band) => [band, createDefaultPEQ()])
) as Record<PEQBand, PEQ>,
bypass: false,
});
const createDefaultCrossover = (frequency: number): Crossover => ({
frequency: frequency,
slope: CrossoverFilter.Bypass,
});
const createDefaultCrossoverFilters = (): CrossoverFilters => ({
high_pass: createDefaultCrossover(FREQUENCY_MIN),
low_pass: createDefaultCrossover(FREQUENCY_MAX),
});
const createDefaultInputChannelState = (name: string): InputChannelState => ({
name,
gain: 0,
phase_inverted: false,
delay: 0,
gate: createDefaultGate(),
geq: createDefaultGraphicEQ(),
peq_chain: createDefaultPEQChain(),
crossover: createDefaultCrossoverFilters(),
linked_channels: [],
mute: false,
});
const createDefaultOutputChannelState = (name: string): OutputChannelState => ({
name,
matrix_routes: {
connected: [InputChannel.InA],
gains: {
[InputChannel.InA]: 0,
[InputChannel.InB]: 0,
[InputChannel.InC]: 0,
[InputChannel.InD]: 0,
},
},
crossover: createDefaultCrossoverFilters(),
peq_chain: createDefaultPEQChain(),
compressor: {
threshold: -90,
ratio: Ratio.Ratio1_1,
attack: 1,
release: 10,
knee: 0,
},
limiter: {
threshold: -90,
attack: 1,
release: 10,
},
gain: 0,
phase_inverted: false,
delay: 0,
linked_channels: [],
mute: false,
});
export const defaultDSPState: DSPState = {
name: 'Default',
flags: {
is_locked: false,
},
presets: {
current_index: 0,
names: Array(20).fill('Default'),
modified: Array(20).fill(false),
},
current_config: {
input_source: {
type: 'Analog',
frequency: '48kHz',
},
delay_unit: DelayUnit.Millisecond,
input_states: {
[InputChannel.InA]: createDefaultInputChannelState('InInInIn'),
[InputChannel.InB]: createDefaultInputChannelState('In B'),
[InputChannel.InC]: createDefaultInputChannelState('In C'),
[InputChannel.InD]: createDefaultInputChannelState('In D'),
},
output_states: {
[OutputChannel.Out1]: createDefaultOutputChannelState('Out 1'),
[OutputChannel.Out2]: createDefaultOutputChannelState('Out 2'),
[OutputChannel.Out3]: createDefaultOutputChannelState('Out 3'),
[OutputChannel.Out4]: createDefaultOutputChannelState('Out 4'),
[OutputChannel.Out5]: createDefaultOutputChannelState('Out 5'),
[OutputChannel.Out6]: createDefaultOutputChannelState('Out 6'),
[OutputChannel.Out7]: createDefaultOutputChannelState('Out 7'),
[OutputChannel.Out8]: createDefaultOutputChannelState('Out 8'),
},
},
};
const createDefaultMeter = (): Meter => ({
rms: -60,
peak: -60,
clip: false,
});
export const defaultMeters: Meters = {
channels: Array.from({ length: 12 }, createDefaultMeter),
};
File diff suppressed because it is too large Load Diff
+8
View File
@@ -238,6 +238,14 @@ export type DSPState = {
current_config: DSPConfigState; current_config: DSPConfigState;
}; };
export type DSPMode = 'demo' | 'disconnected' | 'connected';
export type DSP408Snapshot = {
state: DSPState;
meters: Meters;
mode: DSPMode;
};
export interface Meter { export interface Meter {
rms: number; rms: number;
peak: number; peak: number;
+1 -1
View File
@@ -172,4 +172,4 @@
.add-dsp-form select { .add-dsp-form select {
user-select: text; user-select: text;
-webkit-user-select: text; -webkit-user-select: text;
} }
+19 -20
View File
@@ -1,5 +1,5 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { DSP, DSP_DEFAULT_PARAMETERS, DSPTypes } from '../../types'; import { DSP_DEFAULT_PARAMETERS, DSPTypes } from '../../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 ChoiceStepper from '../../components/ChoiceStepper/ChoiceStepper'; import ChoiceStepper from '../../components/ChoiceStepper/ChoiceStepper';
@@ -37,7 +37,7 @@ function AddDSPForm({
onAdd, onAdd,
}: { }: {
onCancel: () => void; onCancel: () => void;
onAdd: (dsp: Omit<DSP, 'status' | 'id' | 'state' | 'meters' | 'pollingPaused'>) => void; onAdd: (params: DSPForm) => void;
}) { }) {
const [form, setForm] = useState<DSPForm>({ const [form, setForm] = useState<DSPForm>({
name: '', name: '',
@@ -60,39 +60,37 @@ function AddDSPForm({
form.deviceId <= 255; form.deviceId <= 255;
function validateIp() { function validateIp() {
if (form.demo) return; if (form.demo) return;
const ip = form.ip.trim(); const ip = form.ip.trim();
if (isValidIPv4(ip)) { if (isValidIPv4(ip)) {
lastValidIp.current = ip; lastValidIp.current = ip;
if (ip !== form.ip) { if (ip !== form.ip) {
setForm((prev) => ({ ...prev, ip })); setForm((prev) => ({ ...prev, ip }));
}
return;
} }
return; setForm((prev) => ({
...prev,
ip: lastValidIp.current,
}));
} }
setForm((prev) => ({
...prev,
ip: lastValidIp.current,
}));
}
function submit() { function submit() {
if (!canSubmit) return; if (!canSubmit) return;
onAdd({ ...form }); onAdd(form);
} }
return ( return (
<Card className="add-dsp-form"> <Card className="add-dsp-form">
<div className="add-dsp-form__header"> <div className="add-dsp-form__header">
<h1 className="add-dsp-form__title">Add DSP</h1> <h1 className="add-dsp-form__title">Add DSP</h1>
<p className="add-dsp-form__hint"> <p className="add-dsp-form__hint">Register a new device to control from this app.</p>
Register a new device to control from this app.
</p>
</div> </div>
<div className="add-dsp-form__field"> <div className="add-dsp-form__field">
@@ -109,6 +107,7 @@ function AddDSPForm({
<div className="add-dsp-form__field"> <div className="add-dsp-form__field">
<label htmlFor="f-type">Model</label> <label htmlFor="f-type">Model</label>
<ChoiceSelect <ChoiceSelect
className=""
id="f-type" id="f-type"
value={form.type} value={form.type}
options={DSP_TYPES} options={DSP_TYPES}
@@ -200,4 +199,4 @@ function AddDSPForm({
); );
} }
export default AddDSPForm; export default AddDSPForm;
+1 -1
View File
@@ -35,4 +35,4 @@
.credits-page textarea { .credits-page textarea {
user-select: text; user-select: text;
-webkit-user-select: text; -webkit-user-select: text;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
import { DSPState as DSP408State, Meters as DSP408Meters } from './features/dsp408/types'; import { DSPState as DSP408State, Meters as DSP408Meters, DSPMode } from './features/dsp408/types';
export type DSPStatus = 'connected' | 'connecting' | 'disconnected'; export type DSPStatus = 'connected' | 'connecting' | 'disconnected';
@@ -31,7 +31,7 @@ interface DSPBase {
ip: string; ip: string;
port: number; port: number;
deviceId: number; deviceId: number;
demo: boolean; mode: DSPMode;
status: DSPStatus; status: DSPStatus;