feat: dont moove slider cursor if tcp command fails

This commit is contained in:
2026-07-28 13:53:57 +02:00
parent 54fda168cc
commit eab62e151e
6 changed files with 376 additions and 353 deletions

View File

@ -18,14 +18,7 @@ function App() {
const activeDsp = dsps.find((d) => d.id === selected) ?? null; 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( const dsp408 = useDSP408(dsps, setDsps, selected, setSelected, setPage, notify);
dsps,
setDsps,
selected,
setSelected,
setPage,
notify
);
return ( return (
<div className="app"> <div className="app">
@ -60,10 +53,7 @@ function App() {
) : page === 'add-dsp' ? ( ) : page === 'add-dsp' ? (
<AddDSPForm onCancel={() => setPage('home')} onAdd={dsp408.addDSP} /> <AddDSPForm onCancel={() => setPage('home')} onAdd={dsp408.addDSP} />
) : activeDsp ? ( ) : activeDsp ? (
<DSPPage <DSPPage dsp={activeDsp} dsp408={dsp408.forDSP(activeDsp)} />
dsp={activeDsp}
dsp408={dsp408.forDSP(activeDsp)}
/>
) : ( ) : (
<HomePage <HomePage
onAddDSP={() => setPage('add-dsp')} onAddDSP={() => setPage('add-dsp')}

View File

@ -1,8 +1,6 @@
import VerticalSlider from '../Slider/VerticalSlider'; import VerticalSlider from '../Slider/VerticalSlider';
import { invoke } from '@tauri-apps/api/core';
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
import { DSP } from '../../../types/types'; import { DSP } from '../../../types/types';
import { NotificationType } 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';
@ -12,109 +10,62 @@ import './GainSection.css';
const PANEL_DESIGN_WIDTH = 90; const PANEL_DESIGN_WIDTH = 90;
const PANEL_DESIGN_HEIGHT = 400; const PANEL_DESIGN_HEIGHT = 400;
function ChannelGroupScaled({ title, children }: { title: string; children: React.ReactNode }) { // function ChannelGroupScaled({ title, children }: { title: string; children: React.ReactNode }) {
const wrapperRef = useRef<HTMLDivElement>(null); // const wrapperRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1); // const [scale, setScale] = useState(1);
useEffect(() => { // useEffect(() => {
const el = wrapperRef.current; // const el = wrapperRef.current;
if (!el) return; // if (!el) return;
const observer = new ResizeObserver((entries) => { // const observer = new ResizeObserver((entries) => {
const { height } = entries[0].contentRect; // const { height } = entries[0].contentRect;
if (height === 0) return; // if (height === 0) return;
setScale(height / PANEL_DESIGN_HEIGHT); // height is the only free axis here // setScale(height / PANEL_DESIGN_HEIGHT); // height is the only free axis here
}); // });
observer.observe(el); // observer.observe(el);
return () => observer.disconnect(); // return () => observer.disconnect();
}, []); // }, []);
return ( // return (
<div ref={wrapperRef} style={{ height: '100%', '--panel-scale': scale } as React.CSSProperties}> // <div ref={wrapperRef} style={{ height: '100%', '--panel-scale': scale } as React.CSSProperties}>
<ChannelGroup title={title}>{children}</ChannelGroup> // <ChannelGroup title={title}>{children}</ChannelGroup>
</div> // </div>
); // );
} // }
function GainPanel({ function GainPanel({
dsp, dsp,
notify,
channel, channel,
title, title,
setGain,
setMute,
setInverse,
}: { }: {
dsp: DSP; dsp: DSP;
notify: (message: string, type?: NotificationType, dspName?: string) => void;
channel: Channel; channel: Channel;
title: string; title: string;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) { }) {
const setGain = async (value: number) => { const channelState =
try { 'Input' in channel
const success = await invoke<boolean>('set_channel_gain', { ? dsp.state?.current_config.input_states[channel.Input]
id: dsp.id, : dsp.state?.current_config.output_states[channel.Output];
channel,
db: value,
});
if (success) { const muted = channelState?.mute ?? false;
notify(`Input A gain set to ${value} dB`, 'success', dsp.name); const inverted = channelState?.phase_inverted ?? false;
} else { const gain = channelState?.gain ?? 0;
notify('Failed to set input gain', 'error', dsp.name);
}
} catch (err) {
notify(`Gain update failed: ${String(err)}`, 'error', dsp.name);
}
};
const setMute = async (muted: boolean) => {
try {
const success = await invoke<boolean>('set_mute', {
id: dsp.id,
channel,
muted,
});
if (success) {
notify(`Input A ${muted ? 'muted' : 'unmuted'}`, 'success', dsp.name);
} else {
notify('Failed to change mute state', 'error', dsp.name);
}
} catch (err) {
notify(`Mute update failed: ${String(err)}`, 'error', dsp.name);
}
};
const setInverse = async (inverted: boolean) => {
try {
const success = await invoke<boolean>('set_channel_inverse_gain', {
id: dsp.id,
channel,
inverted,
});
if (success) {
notify(`Input A phase ${inverted ? 'inverted' : 'normal'}`, 'success', dsp.name);
} else {
notify('Failed to change phase', 'error', dsp.name);
}
} catch (err) {
notify(`Phase update failed: ${String(err)}`, 'error', dsp.name);
}
};
const [muted, setMuted] = useState(false);
const [inverted, setInverted] = useState(false);
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
const toggleMute = async () => { const toggleMute = async () => {
const next = !muted; await setMute(channel, !muted);
setMuted(next);
await setMute(next);
}; };
const toggleInverse = async () => { const toggleInverse = async () => {
const next = !inverted; await setInverse(channel, !inverted);
setInverted(next);
await setInverse(next);
}; };
useEffect(() => { useEffect(() => {
@ -148,7 +99,8 @@ function GainPanel({
switchStepValue={-10} switchStepValue={-10}
switchStep={0.1} switchStep={0.1}
unit=" dB" unit=" dB"
onChange={setGain} value={gain}
onChange={(value) => setGain(channel, value)}
style={{ width: 70, height: 260, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink style={{ width: 70, height: 260, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink
/> />
@ -169,10 +121,14 @@ function GainPanel({
function GainSection({ function GainSection({
dsp, dsp,
notify, setGain,
setMute,
setInverse,
}: { }: {
dsp: DSP; dsp: DSP;
notify: (message: string, type?: NotificationType, dspName?: string) => void; setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) { }) {
const inputs: { channel: Channel; title: string }[] = [ const inputs: { channel: Channel; title: string }[] = [
{ channel: { Input: InputChannel.InA }, title: 'In A' }, { channel: { Input: InputChannel.InA }, title: 'In A' },
@ -223,13 +179,29 @@ function GainSection({
<div className="gain-panels"> <div className="gain-panels">
<ChannelGroup title="Input"> <ChannelGroup title="Input">
{inputs.map(({ channel, title }) => ( {inputs.map(({ channel, title }) => (
<GainPanel key={title} dsp={dsp} notify={notify} channel={channel} title={title} /> <GainPanel
key={title}
dsp={dsp}
channel={channel}
title={title}
setGain={setGain}
setMute={setMute}
setInverse={setInverse}
/>
))} ))}
</ChannelGroup> </ChannelGroup>
<ChannelGroup title="Output"> <ChannelGroup title="Output">
{outputs.map(({ channel, title }) => ( {outputs.map(({ channel, title }) => (
<GainPanel key={title} dsp={dsp} notify={notify} channel={channel} title={title} /> <GainPanel
key={title}
dsp={dsp}
channel={channel}
title={title}
setGain={setGain}
setMute={setMute}
setInverse={setInverse}
/>
))} ))}
</ChannelGroup> </ChannelGroup>
</div> </div>

View File

@ -1,35 +1,35 @@
import VerticalSlider from '../Slider/VerticalSlider'; // import VerticalSlider from '../Slider/VerticalSlider';
import { invoke } from '@tauri-apps/api/core'; // import { invoke } from '@tauri-apps/api/core';
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; // import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
import { DSP } from '../../../types/types'; // import { DSP } from '../../../types/types';
import { NotificationType } from '../../../types/types'; // import { NotificationType } from '../../../types/types';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; // import ChannelGroup from '../ChannelGroup/ChannelGroup';
import './GateSection.css'; // import './GateSection.css';
function GateSection({ // function GateSection({
dsp, // dsp,
notify, // notify,
}: { // }: {
dsp: DSP; // dsp: DSP;
notify: (message: string, type?: NotificationType, dspName?: string) => void; // notify: (message: string, type?: NotificationType, dspName?: string) => void;
}) { // }) {
console.log(dsp, notify); // console.log(dsp, notify);
return ( // return (
<div className="rerere"> // <div className="rerere">
<VerticalSlider // <VerticalSlider
min={-60} // min={-60}
max={12} // max={12}
step={0.5} // step={0.5}
switchStepValue={-10} // switchStepValue={-10}
switchStep={0.1} // switchStep={0.1}
unit=" dB" // unit=" dB"
onChange={(_f: number) => { // onChange={(_f: number) => {
return; // return;
}} // }}
/> // />
</div> // </div>
); // );
} // }
export default GateSection; // export default GateSection;

View File

@ -13,8 +13,8 @@ type VerticalSliderProps = {
switchStepValue?: number; switchStepValue?: number;
switchStep?: number; switchStep?: number;
value?: number; value: number;
onChange?: (value: number) => void; onChange?: (value: number) => Promise<boolean>;
unit?: string; unit?: string;
@ -34,23 +34,18 @@ export default function VerticalSlider({
step, step,
switchStepValue, switchStepValue,
switchStep, switchStep,
value: controlledValue, value,
onChange, onChange,
unit = '', unit = '',
className, className,
style, style,
}: VerticalSliderProps) { }: VerticalSliderProps) {
const [internalValue, setInternalValue] = useState(min);
const value = controlledValue ?? internalValue;
const [editingText, setEditingText] = useState<string | null>(null); const [editingText, setEditingText] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const sliderRef = useRef<HTMLDivElement>(null); const sliderRef = useRef<HTMLDivElement>(null);
const lastValue = useRef(value);
const keyboardValue = useRef<number | null>(null); const keyboardValue = useRef<number | null>(null);
const [displayValue, setDisplayValue] = useState(value); const [displayValue, setDisplayValue] = useState(value);
const dragValue = useRef(value); const dragValue = useRef(value);
const rafRef = useRef<number | null>(null);
const dragging = useRef(false); const dragging = useRef(false);
const thumbRef = useRef<HTMLDivElement>(null); const thumbRef = useRef<HTMLDivElement>(null);
const keyboardEditing = useRef(false); const keyboardEditing = useRef(false);
@ -74,16 +69,20 @@ export default function VerticalSlider({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (dragging.current) return;
if (keyboardEditing.current) return; if (keyboardEditing.current) return;
setDisplayValue(value); setDisplayValue(value);
dragValue.current = value; dragValue.current = value;
}, [value]); }, [value]);
const currentStep = function getStep(value: number) {
switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) {
? switchStep return switchStep;
: step; }
return step;
}
const decimals = useMemo( const decimals = useMemo(
() => Math.max(getDecimals(step), switchStep !== undefined ? getDecimals(switchStep) : 0), () => Math.max(getDecimals(step), switchStep !== undefined ? getDecimals(switchStep) : 0),
@ -92,30 +91,25 @@ export default function VerticalSlider({
function normalizeValue(input: number) { function normalizeValue(input: number) {
const clamped = Math.min(max, Math.max(min, input)); const clamped = Math.min(max, Math.max(min, input));
const snapped = Math.round(clamped / currentStep) * currentStep;
return Number(snapped.toFixed(6)); const currentStep = getStep(clamped);
const snapped = min + Math.round((clamped - min) / currentStep) * currentStep;
return Number(Math.min(max, Math.max(min, snapped)).toFixed(6));
} }
function updateValue(v: number) { function updateValue(input: number) {
const next = normalizeValue(v); const next = normalizeValue(input);
dragValue.current = next; dragValue.current = next;
lastValue.current = next; setDisplayValue(next);
if (thumbRef.current) {
const percent = (next - min) / (max - min);
thumbRef.current.style.bottom = `${percent * 100}%`;
}
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(() => {
setDisplayValue(dragValue.current);
rafRef.current = null;
});
}
} }
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return; if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
return;
}
keyboardEditing.current = true; keyboardEditing.current = true;
@ -123,33 +117,39 @@ export default function VerticalSlider({
const base = keyboardValue.current ?? value; const base = keyboardValue.current ?? value;
const currentStep = getStep(base);
let next = base; let next = base;
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') next += currentStep; if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
next += currentStep;
}
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') next -= currentStep; if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
next -= currentStep;
}
next = normalizeValue(next); next = normalizeValue(next);
keyboardValue.current = next; keyboardValue.current = next;
updateValue(next); // updates displayValue only updateValue(next);
} }
function handleKeyUp() { async function handleKeyUp() {
if (keyboardValue.current == null) return; if (keyboardValue.current == null) return;
keyboardEditing.current = false;
const next = keyboardValue.current; const next = keyboardValue.current;
if (controlledValue === undefined) { const success = await onChange?.(next);
setInternalValue(next);
}
onChange?.(next);
keyboardEditing.current = false;
keyboardValue.current = null; keyboardValue.current = null;
if (success === false) {
setDisplayValue(value);
dragValue.current = value;
}
} }
function valueFromPointer(clientY: number) { function valueFromPointer(clientY: number) {
@ -172,28 +172,47 @@ export default function VerticalSlider({
valueFromPointer(e.clientY); valueFromPointer(e.clientY);
} }
function endDrag(e: React.PointerEvent<HTMLDivElement>) { async function endDrag(e: React.PointerEvent<HTMLDivElement>) {
dragging.current = false; if (!dragging.current) return;
const finalValue = dragValue.current; const finalValue = dragValue.current;
if (controlledValue === undefined) setInternalValue(finalValue); const success = await onChange?.(finalValue);
setDisplayValue(finalValue);
onChange?.(finalValue); dragging.current = false;
if (success === false) {
setDisplayValue(value);
dragValue.current = value;
}
if (e.currentTarget.hasPointerCapture(e.pointerId)) { if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId); e.currentTarget.releasePointerCapture(e.pointerId);
} }
} }
function commitEditing() { async function commitEditing() {
if (editingText !== null && editingText !== '' && editingText !== '-') { if (editingText !== null && editingText !== '' && editingText !== '-') {
const parsed = Number(editingText); const parsed = Number(editingText);
if (!Number.isNaN(parsed)) updateValue(parsed);
if (!Number.isNaN(parsed)) {
const next = normalizeValue(parsed);
setDisplayValue(next);
const success = await onChange?.(next);
if (success === false) {
setDisplayValue(value);
dragValue.current = value;
}
}
} }
setEditingText(null); setEditingText(null);
} }
const percent = (value - min) / (max - min); const percent = (displayValue - min) / (max - min);
return ( return (
<div <div

View File

@ -3,7 +3,8 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { DSP, DSPStatus, AppPage } from '../types/types'; import { DSP, AppPage } from '../types/types';
import { Channel } from '../types/dsp408State';
import { import {
createDSP408, createDSP408,
@ -16,11 +17,7 @@ import {
getDSP408MeterLevels, getDSP408MeterLevels,
} from '../api/dsp408'; } from '../api/dsp408';
type NotifyFunction = ( type NotifyFunction = (message: string, type: 'success' | 'error', dspName?: string) => void;
message: string,
type: 'success' | 'error',
dspName?: string
) => void;
type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>; type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>;
@ -28,14 +25,8 @@ type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>;
// Helpers // Helpers
// ============================================================ // ============================================================
function updateDSP( function updateDSP(setDsps: SetDsps, dspId: number, updater: (dsp: DSP) => DSP) {
setDsps: SetDsps, setDsps((prev) => prev.map((dsp) => (dsp.id === dspId ? updater(dsp) : dsp)));
dspId: number,
updater: (dsp: DSP) => DSP
) {
setDsps((prev) =>
prev.map((dsp) => (dsp.id === dspId ? updater(dsp) : dsp))
);
} }
export type DSP408Device = { export type DSP408Device = {
@ -43,6 +34,9 @@ export type DSP408Device = {
disconnect: () => Promise<void>; disconnect: () => Promise<void>;
recallPreset: (index: number) => Promise<boolean>; recallPreset: (index: number) => Promise<boolean>;
setCurrentPresetName: (name: string) => Promise<boolean>; setCurrentPresetName: (name: string) => Promise<boolean>;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}; };
// ============================================================ // ============================================================
@ -173,10 +167,7 @@ export function useDSP408(
// Generic operation wrapper // Generic operation wrapper
// ---------------------------------------------------------- // ----------------------------------------------------------
async function withPollingPaused<T>( async function withPollingPaused<T>(dsp: DSP, operation: () => Promise<T>): Promise<T> {
dsp: DSP,
operation: () => Promise<T>
): Promise<T> {
setPollingPaused(dsp.id, true); setPollingPaused(dsp.id, true);
try { try {
@ -216,11 +207,7 @@ export function useDSP408(
status: 'disconnected', status: 'disconnected',
})); }));
notify( notify(`Connection failed: ${String(err)}`, 'error', dsp.name);
`Connection failed: ${String(err)}`,
'error',
dsp.name
);
} finally { } finally {
setPollingPaused(dsp.id, false); setPollingPaused(dsp.id, false);
} }
@ -242,11 +229,7 @@ export function useDSP408(
// If disconnect failed, it is probably still connected. // If disconnect failed, it is probably still connected.
setPollingPaused(dsp.id, false); setPollingPaused(dsp.id, false);
notify( notify(`Disconnect failed: ${String(err)}`, 'error', dsp.name);
`Disconnect failed: ${String(err)}`,
'error',
dsp.name
);
} }
} }
@ -254,18 +237,9 @@ export function useDSP408(
// Add / Remove // Add / Remove
// ========================================================== // ==========================================================
async function addDSP( async function addDSP(dsp: Omit<DSP, 'id' | 'status' | 'state' | 'meters' | 'pollingPaused'>) {
dsp: Omit<
DSP,
'id' | 'status' | 'state' | 'meters' | 'pollingPaused'
>
) {
try { try {
const id = await createDSP408( const id = await createDSP408(dsp.ip, dsp.port, dsp.deviceId);
dsp.ip,
dsp.port,
dsp.deviceId
);
const newDsp: DSP = { const newDsp: DSP = {
id, id,
@ -282,11 +256,7 @@ export function useDSP408(
notify('Added successfully', 'success', dsp.name); notify('Added successfully', 'success', dsp.name);
} catch (err) { } catch (err) {
notify( notify(`Connection failed: ${String(err)}`, 'error', dsp.name);
`Connection failed: ${String(err)}`,
'error',
dsp.name
);
} }
} }
@ -297,32 +267,20 @@ export function useDSP408(
await removeDSP408(dsp.id); await removeDSP408(dsp.id);
setDsps((prev) => { setDsps((prev) => {
const next = prev.filter( const next = prev.filter((d) => d.id !== dsp.id);
(d) => d.id !== dsp.id
);
if (selected === dsp.id) { if (selected === dsp.id) {
setSelected( setSelected(next.length ? next[0].id : null);
next.length ? next[0].id : null
);
} }
return next; return next;
}); });
notify( notify('Removed successfully', 'success', dsp.name);
'Removed successfully',
'success',
dsp.name
);
} catch (err) { } catch (err) {
setPollingPaused(dsp.id, false); setPollingPaused(dsp.id, false);
notify( notify(`Remove failed: ${String(err)}`, 'error', dsp.name);
`Remove failed: ${String(err)}`,
'error',
dsp.name
);
} }
} }
@ -330,23 +288,13 @@ export function useDSP408(
// Presets // Presets
// ========================================================== // ==========================================================
async function recallPreset( async function recallPreset(dsp: DSP, presetIndex: number) {
dsp: DSP,
presetIndex: number
) {
return withPollingPaused(dsp, async () => { return withPollingPaused(dsp, async () => {
try { try {
const success = await recallPresetDSP408( const success = await recallPresetDSP408(dsp.id, presetIndex);
dsp.id,
presetIndex
);
if (!success) { if (!success) {
notify( notify('Failed to recall preset', 'error', dsp.name);
'Failed to recall preset',
'error',
dsp.name
);
return false; return false;
} }
@ -366,47 +314,26 @@ export function useDSP408(
}; };
}); });
notify( notify(`Preset ${presetIndex} recalled`, 'success', dsp.name);
`Preset ${presetIndex} recalled`,
'success',
dsp.name
);
return true; return true;
} catch (err) { } catch (err) {
notify( notify(`Preset recall failed: ${String(err)}`, 'error', dsp.name);
`Preset recall failed: ${String(err)}`,
'error',
dsp.name
);
return false; return false;
} }
}); });
} }
async function setCurrentPresetName( async function setCurrentPresetName(dsp: DSP, name: string) {
dsp: DSP,
name: string
) {
return withPollingPaused(dsp, async () => { return withPollingPaused(dsp, async () => {
try { try {
const paddedName = name const paddedName = name.slice(0, 14).padEnd(14, ' ');
.slice(0, 14)
.padEnd(14, ' ');
const success = const success = await DSP408SetCurrentPresetName(dsp.id, paddedName);
await DSP408SetCurrentPresetName(
dsp.id,
paddedName
);
if (!success) { if (!success) {
notify( notify('Failed to rename preset', 'error', dsp.name);
'Failed to rename preset',
'error',
dsp.name
);
return false; return false;
} }
@ -423,36 +350,20 @@ export function useDSP408(
...d.state, ...d.state,
presets: { presets: {
...presets, ...presets,
names: presets.names.map( names: presets.names.map((presetName, i) =>
(presetName, i) => i === index ? paddedName : presetName
i === index
? paddedName
: presetName
),
modified: presets.modified.map(
(modified, i) =>
i === index
? true
: modified
), ),
modified: presets.modified.map((modified, i) => (i === index ? true : modified)),
}, },
}, },
}; };
}); });
notify( notify('Preset renamed', 'success', dsp.name);
'Preset renamed',
'success',
dsp.name
);
return true; return true;
} catch (err) { } catch (err) {
notify( notify(`Preset rename failed: ${String(err)}`, 'error', dsp.name);
`Preset rename failed: ${String(err)}`,
'error',
dsp.name
);
return false; return false;
} }
@ -463,28 +374,54 @@ export function useDSP408(
// Gain // Gain
// ========================================================== // ==========================================================
async function setGain( async function setGain(dsp: DSP, channel: Channel, gain: number): Promise<boolean> {
dsp: DSP,
channel: number,
gain: number
) {
return withPollingPaused(dsp, async () => { return withPollingPaused(dsp, async () => {
try { try {
const success = await invoke<boolean>( const success = await invoke<boolean>('set_channel_gain', {
'set_gain', id: dsp.id,
{ channel,
id: dsp.id, db: gain,
channel, });
gain,
}
);
if (!success) { if (!success) {
notify( notify('Failed to set gain', 'error', dsp.name);
'Failed to set gain',
'error', return false;
dsp.name }
);
const state = await getDSP408State(dsp.id);
updateDSP(setDsps, dsp.id, (d) => ({
...d,
state,
}));
notify('Gain updated', 'success', dsp.name);
return true;
} catch (err) {
notify(`Gain update failed: ${String(err)}`, 'error', dsp.name);
return false;
}
});
}
// ==========================================================
// Mute
// ==========================================================
async function setMute(dsp: DSP, channel: Channel, muted: boolean): Promise<boolean> {
return withPollingPaused(dsp, async () => {
try {
const success = await invoke<boolean>('set_mute', {
id: dsp.id,
channel,
muted,
});
if (!success) {
notify('Failed to change mute state', 'error', dsp.name);
return false; return false;
} }
@ -492,28 +429,130 @@ export function useDSP408(
updateDSP(setDsps, dsp.id, (d) => { updateDSP(setDsps, dsp.id, (d) => {
if (!d.state) return d; if (!d.state) return d;
const currentConfig = d.state.current_config;
if ('Input' in channel) {
const inputChannel = channel.Input;
return {
...d,
state: {
...d.state,
current_config: {
...currentConfig,
input_states: {
...currentConfig.input_states,
[inputChannel]: {
...currentConfig.input_states[inputChannel],
mute: muted,
},
},
},
},
};
}
const outputChannel = channel.Output;
return { return {
...d, ...d,
state: { state: {
...d.state, ...d.state,
// Update gain state here current_config: {
...currentConfig,
output_states: {
...currentConfig.output_states,
[outputChannel]: {
...currentConfig.output_states[outputChannel],
mute: muted,
},
},
},
}, },
}; };
}); });
notify( notify(muted ? 'Muted' : 'Unmuted', 'success', dsp.name);
'Gain updated',
'success',
dsp.name
);
return true; return true;
} catch (err) { } catch (err) {
notify( notify(`Mute update failed: ${String(err)}`, 'error', dsp.name);
`Gain update failed: ${String(err)}`,
'error', return false;
dsp.name }
); });
}
// ==========================================================
// Inverse
// ==========================================================
async function setInverse(dsp: DSP, channel: Channel, inverted: boolean): Promise<boolean> {
return withPollingPaused(dsp, async () => {
try {
const success = await invoke<boolean>('set_channel_inverse_gain', {
id: dsp.id,
channel,
inverted,
});
if (!success) {
notify('Failed to change phase', 'error', dsp.name);
return false;
}
updateDSP(setDsps, dsp.id, (d) => {
if (!d.state) return d;
const currentConfig = d.state.current_config;
if ('Input' in channel) {
const inputChannel = channel.Input;
return {
...d,
state: {
...d.state,
current_config: {
...currentConfig,
input_states: {
...currentConfig.input_states,
[inputChannel]: {
...currentConfig.input_states[inputChannel],
phase_inverted: inverted,
},
},
},
},
};
}
const outputChannel = channel.Output;
return {
...d,
state: {
...d.state,
current_config: {
...currentConfig,
output_states: {
...currentConfig.output_states,
[outputChannel]: {
...currentConfig.output_states[outputChannel],
phase_inverted: inverted,
},
},
},
},
};
});
notify(inverted ? 'Phase inverted' : 'Phase normal', 'success', dsp.name);
return true;
} catch (err) {
notify(`Phase update failed: ${String(err)}`, 'error', dsp.name);
return false; return false;
} }
@ -526,15 +565,15 @@ export function useDSP408(
function forDSP(dsp: DSP): DSP408Device { function forDSP(dsp: DSP): DSP408Device {
return { return {
connect: () => connectDSP(dsp), connect: () => connectDSP(dsp),
disconnect: () => disconnectDSP(dsp), disconnect: () => disconnectDSP(dsp),
recallPreset: (index: number) => recallPreset(dsp, index), recallPreset: (index: number) => recallPreset(dsp, index),
setCurrentPresetName: (name: string) => setCurrentPresetName: (name: string) => setCurrentPresetName(dsp, name),
setCurrentPresetName(dsp, name), setGain: (channel: Channel, gain: number) => setGain(dsp, channel, gain),
// setGain: (channel, gain) => setMute: (channel: Channel, muted: boolean) => setMute(dsp, channel, muted),
// setGain(dsp, channel, gain), setInverse: (channel: Channel, inverted: boolean) => setInverse(dsp, channel, inverted),
}; };
} }
return { return {
forDSP, forDSP,

View File

@ -6,17 +6,11 @@ import ConnectionPanel from '../../components/dsp408/ConnectionSection/Connectio
import GainSection from '../../components/dsp408/GainSection/GainSection'; import GainSection from '../../components/dsp408/GainSection/GainSection';
import { DSP408Device } from '../../hooks/useDSP408'; import { DSP408Device } from '../../hooks/useDSP408';
import './DspPage.css'; import './DspPage.css';
import GateSection from '../../components/dsp408/GateSection/GateSection'; // import GateSection from '../../components/dsp408/GateSection/GateSection';
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay'; import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
import { InputChannel, OutputChannel } from '../../types/dsp408State'; import { InputChannel, OutputChannel } from '../../types/dsp408State';
function DSPPage({ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
dsp,
dsp408,
}: {
dsp: DSP;
dsp408: DSP408Device;
}) {
const [activeSidebar, setActiveSidebar] = useState('overview'); const [activeSidebar, setActiveSidebar] = useState('overview');
const [sidebarExpanded, setSidebarExpanded] = useState(false); const [sidebarExpanded, setSidebarExpanded] = useState(false);
const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0); const [selectedPreset, setSelectedPreset] = useState(dsp.state?.presets.current_index ?? 0);
@ -170,10 +164,19 @@ function DSPPage({
const renderTab = () => { const renderTab = () => {
switch (activeSidebar) { switch (activeSidebar) {
case 'overview': case 'overview':
return <ConnectionPanel dsp={dsp} onConnect={dsp408.connect} onDisconnect={dsp408.disconnect} />; return (
<ConnectionPanel dsp={dsp} onConnect={dsp408.connect} onDisconnect={dsp408.disconnect} />
);
// case 'gain': case 'gain':
// return <GainSection dsp={dsp} notify={notify} />; return (
<GainSection
dsp={dsp}
setGain={dsp408.setGain}
setMute={dsp408.setMute}
setInverse={dsp408.setInverse}
/>
);
// case 'gate': // case 'gate':
// return <GateSection dsp={dsp} notify={notify} />; // return <GateSection dsp={dsp} notify={notify} />;