feat: add delay tab
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use dsp_thomann::dsp408::types::DelayUnit;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::state::{AppState, DSPDevice};
|
use crate::state::{AppState, DSPDevice};
|
||||||
use dsp_thomann::dsp408::{DSP408, types};
|
use dsp_thomann::dsp408::{DSP408, types};
|
||||||
@@ -307,4 +308,34 @@ pub async fn set_limiter(
|
|||||||
.set_limiter(channel, threshold, attack, release)
|
.set_limiter(channel, threshold, attack, release)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_delay_unit(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
id: u64,
|
||||||
|
unit: types::DelayUnit,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
state.with_device_mut(id, |device| {
|
||||||
|
device
|
||||||
|
.dsp
|
||||||
|
.set_delay_unit(unit)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_delay(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
id: u64,
|
||||||
|
channel: types::Channel,
|
||||||
|
delay: f32,
|
||||||
|
unit: DelayUnit
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
state.with_device_mut(id, |device| {
|
||||||
|
device
|
||||||
|
.dsp
|
||||||
|
.set_delay(channel, delay, unit)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,8 @@ pub fn run() {
|
|||||||
commands::set_gate,
|
commands::set_gate,
|
||||||
commands::set_compressor,
|
commands::set_compressor,
|
||||||
commands::set_limiter,
|
commands::set_limiter,
|
||||||
|
commands::set_delay_unit,
|
||||||
|
commands::set_delay,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -45,6 +45,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Active toggle states */
|
/* Active toggle states */
|
||||||
|
.btn-toggle.active-info {
|
||||||
|
background: var(--accent-brand);
|
||||||
|
border-color: var(--accent-brand);
|
||||||
|
color: var(--bg-void);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-toggle.active-danger {
|
.btn-toggle.active-danger {
|
||||||
background: var(--accent-down);
|
background: var(--accent-down);
|
||||||
border-color: var(--accent-down);
|
border-color: var(--accent-down);
|
||||||
@@ -58,6 +64,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Optional: active state hover */
|
/* Optional: active state hover */
|
||||||
|
.btn-toggle.active-info:hover:not(:disabled) {
|
||||||
|
background: var(--accent-brand);
|
||||||
|
border-color: var(--accent-brand);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-toggle.active-danger:hover:not(:disabled) {
|
.btn-toggle.active-danger:hover:not(:disabled) {
|
||||||
background: var(--accent-down);
|
background: var(--accent-down);
|
||||||
border-color: var(--accent-down);
|
border-color: var(--accent-down);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
|
|
||||||
.compressor-panel {
|
.compressor-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 10;
|
|
||||||
|
|
||||||
aspect-ratio: 0.5;
|
aspect-ratio: 0.5;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ function CompressorPanel({
|
|||||||
onLiveValue: (channel: OutputChannel, key: CompressorParamKey, value: number | Ratio) => void;
|
onLiveValue: (channel: OutputChannel, key: CompressorParamKey, value: number | Ratio) => void;
|
||||||
}) {
|
}) {
|
||||||
const outputState = dsp.state.current_config.output_states[channel];
|
const outputState = dsp.state.current_config.output_states[channel];
|
||||||
// ASSUMPTION: compressor settings live at output_states[channel].compressor
|
|
||||||
const comp = outputState.compressor as CompressorValues;
|
const comp = outputState.compressor as CompressorValues;
|
||||||
|
|
||||||
const commit = (overrides: Partial<CompressorValues>) => {
|
const commit = (overrides: Partial<CompressorValues>) => {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
.delay-section {
|
||||||
|
container-type: size;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
height: var(--dsp-control-heigth);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-panels {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-panel {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1 / 4;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
border: var(--border-width) solid var(--border-hairline);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 2cqh;
|
||||||
|
background: var(--bg-void);
|
||||||
|
padding-bottom: 2cqh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-panel__title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 4cqh;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
padding-top: 2cqh;
|
||||||
|
padding-bottom: 2cqh;
|
||||||
|
border-bottom: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-unit-selector {
|
||||||
|
height: 100%;
|
||||||
|
width: 20cqh;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5cqh;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-linear-graph-section {
|
||||||
|
flex: 0 0 calc(100% - var(--dsp-control-heigth));
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1cqh;
|
||||||
|
padding: 0.5cqh;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-graph-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
border: var(--border-width) solid var(--border-hairline);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-graph-title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
padding: 1cqh;
|
||||||
|
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 2.5cqh;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-graph-panel > .container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
import VerticalSlider from '../Slider/VerticalSlider';
|
||||||
|
import { InputChannel, Channel, OutputChannel, DelayUnit } from '../../../types/dsp408State';
|
||||||
|
import { DSP } from '../../../types/types';
|
||||||
|
import Button from '../../Button/Button';
|
||||||
|
import ChannelGroup from '../ChannelGroup/ChannelGroup';
|
||||||
|
import { LinearGraph } from '../LinearGraph/LinearGraph';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import './DelaySection.css';
|
||||||
|
|
||||||
|
const DELAY_RANGES: Record<DelayUnit, [number, number]> = {
|
||||||
|
[DelayUnit.Millisecond]: [0, 680.0],
|
||||||
|
[DelayUnit.Meter]: [0, 233.58],
|
||||||
|
[DelayUnit.Feet]: [0, 766.329],
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNIT_LABEL: Record<DelayUnit, string> = {
|
||||||
|
[DelayUnit.Millisecond]: 'ms',
|
||||||
|
[DelayUnit.Meter]: 'm',
|
||||||
|
[DelayUnit.Feet]: 'ft',
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNIT_ORDER: DelayUnit[] = [DelayUnit.Millisecond, DelayUnit.Meter, DelayUnit.Feet];
|
||||||
|
|
||||||
|
const DELAY_STEPS = 255;
|
||||||
|
const DELAY_STEP: Record<DelayUnit, number> = {
|
||||||
|
[DelayUnit.Millisecond]: (680.0 - 0) / (DELAY_STEPS - 1),
|
||||||
|
[DelayUnit.Meter]: (233.58 - 0) / (DELAY_STEPS - 1),
|
||||||
|
[DelayUnit.Feet]: (766.329 - 0) / (DELAY_STEPS - 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetDelayFn = (channel: Channel, delay: number) => Promise<boolean>;
|
||||||
|
|
||||||
|
function channelKey(channel: Channel): string {
|
||||||
|
return 'Input' in channel ? `Input:${channel.Input}` : `Output:${channel.Output}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayPanel({
|
||||||
|
channel,
|
||||||
|
dsp,
|
||||||
|
setDelay,
|
||||||
|
onLiveValue,
|
||||||
|
}: {
|
||||||
|
channel: Channel;
|
||||||
|
dsp: DSP;
|
||||||
|
setDelay: SetDelayFn;
|
||||||
|
onLiveValue: (channel: Channel, value: number) => void;
|
||||||
|
}) {
|
||||||
|
function getChannelState(channel: Channel) {
|
||||||
|
if ('Input' in channel) {
|
||||||
|
return dsp.state.current_config.input_states[channel.Input];
|
||||||
|
} else {
|
||||||
|
return dsp.state.current_config.output_states[channel.Output];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getChannelState(channel);
|
||||||
|
const unit = dsp.state.current_config.delay_unit;
|
||||||
|
const [min, max] = DELAY_RANGES[unit];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="delay-panel">
|
||||||
|
<div className="delay-panel__title">{state.name}</div>
|
||||||
|
|
||||||
|
<VerticalSlider
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={DELAY_STEP[unit]}
|
||||||
|
unit={' ' + UNIT_LABEL[unit]}
|
||||||
|
value={state.delay}
|
||||||
|
onInput={(value) => onLiveValue(channel, value)}
|
||||||
|
onChange={(value) => setDelay(channel, value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayGraph({
|
||||||
|
title,
|
||||||
|
rows,
|
||||||
|
unit,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
rows: { label: string; delay: number }[];
|
||||||
|
unit: DelayUnit;
|
||||||
|
}) {
|
||||||
|
const [min, max] = DELAY_RANGES[unit];
|
||||||
|
const labelByX = useMemo(() => {
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
rows.forEach((row, i) => map.set(i + 1, row.label));
|
||||||
|
return map;
|
||||||
|
}, [rows]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="delay-graph-panel">
|
||||||
|
<div className="delay-graph-title">{title}</div>
|
||||||
|
<LinearGraph
|
||||||
|
xMin={0.5}
|
||||||
|
xMax={rows.length + 0.5}
|
||||||
|
xStep={1}
|
||||||
|
yMin={min}
|
||||||
|
yMax={max}
|
||||||
|
xLabel="Channel"
|
||||||
|
yLabel={UNIT_LABEL[unit]}
|
||||||
|
showGrid={true}
|
||||||
|
formatX={(v) => labelByX.get(Math.round(v)) ?? ''}
|
||||||
|
formatY={(v) => v.toFixed(0)}
|
||||||
|
draw={({ ctx, toPx, plot }) => {
|
||||||
|
const barWidth = (plot.width / rows.length) * 0.55;
|
||||||
|
|
||||||
|
rows.forEach((row, i) => {
|
||||||
|
const x = i + 1;
|
||||||
|
const [xPx, baseY] = toPx(x, min);
|
||||||
|
const [, topY] = toPx(x, row.delay);
|
||||||
|
|
||||||
|
ctx.fillStyle = '#4ade80';
|
||||||
|
ctx.fillRect(xPx - barWidth / 2, topY, barWidth, baseY - topY);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayUnitSelector({
|
||||||
|
unit,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
unit: DelayUnit;
|
||||||
|
onSelect: (unit: DelayUnit) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="delay-unit-selector">
|
||||||
|
{UNIT_ORDER.map((u) => (
|
||||||
|
<Button
|
||||||
|
key={u}
|
||||||
|
variant="toggle"
|
||||||
|
className={u === unit ? 'active-info' : ''}
|
||||||
|
onClick={() => onSelect(u)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '3.5cqh',
|
||||||
|
paddingTop: '2cqh',
|
||||||
|
paddingBottom: '2cqh',
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{UNIT_LABEL[u]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelaySection({
|
||||||
|
dsp,
|
||||||
|
setDelay,
|
||||||
|
setDelayUnit,
|
||||||
|
setDSPState,
|
||||||
|
}: {
|
||||||
|
dsp: DSP;
|
||||||
|
setDelay: SetDelayFn;
|
||||||
|
setDelayUnit: (unit: DelayUnit) => Promise<boolean>;
|
||||||
|
setDSPState: (updater: (dsp: DSP) => DSP) => void;
|
||||||
|
}) {
|
||||||
|
const unit = dsp.state?.current_config.delay_unit ?? DelayUnit.Millisecond;
|
||||||
|
|
||||||
|
const inputs: InputChannel[] = [
|
||||||
|
InputChannel.InA,
|
||||||
|
InputChannel.InB,
|
||||||
|
InputChannel.InC,
|
||||||
|
InputChannel.InD,
|
||||||
|
];
|
||||||
|
|
||||||
|
const outputs: { channel: Channel; title: string }[] = [
|
||||||
|
{ channel: { Output: OutputChannel.Out1 }, title: 'Out 1' },
|
||||||
|
{ channel: { Output: OutputChannel.Out2 }, title: 'Out 2' },
|
||||||
|
{ channel: { Output: OutputChannel.Out3 }, title: 'Out 3' },
|
||||||
|
{ channel: { Output: OutputChannel.Out4 }, title: 'Out 4' },
|
||||||
|
{ channel: { Output: OutputChannel.Out5 }, title: 'Out 5' },
|
||||||
|
{ channel: { Output: OutputChannel.Out6 }, title: 'Out 6' },
|
||||||
|
{ channel: { Output: OutputChannel.Out7 }, title: 'Out 7' },
|
||||||
|
{ channel: { Output: OutputChannel.Out8 }, title: 'Out 8' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [liveDelay, setLiveDelay] = useState<Partial<Record<string, number>>>({});
|
||||||
|
|
||||||
|
const handleLiveValue = (channel: Channel, value: number) => {
|
||||||
|
setLiveDelay((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[channelKey(channel)]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetDelay: SetDelayFn = async (channel, delay) => {
|
||||||
|
const success = await setDelay(channel, delay);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const key = channelKey(channel);
|
||||||
|
|
||||||
|
setLiveDelay((prev) => {
|
||||||
|
if (!(key in prev)) return prev;
|
||||||
|
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const committedDelay = (channel: Channel): number =>
|
||||||
|
'Input' in channel
|
||||||
|
? dsp.state.current_config.input_states[channel.Input].delay
|
||||||
|
: dsp.state.current_config.output_states[channel.Output].delay;
|
||||||
|
|
||||||
|
const getDelay = (channel: Channel): number => {
|
||||||
|
const key = channelKey(channel);
|
||||||
|
|
||||||
|
if (key in liveDelay) {
|
||||||
|
return liveDelay[key]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return committedDelay(channel);
|
||||||
|
};
|
||||||
|
|
||||||
|
function convertDelayUnit(value: number, from: DelayUnit, to: DelayUnit): number {
|
||||||
|
const [, fromMax] = DELAY_RANGES[from];
|
||||||
|
const [, toMax] = DELAY_RANGES[to];
|
||||||
|
|
||||||
|
if (fromMax === 0) return 0;
|
||||||
|
|
||||||
|
const ratio = value / fromMax;
|
||||||
|
|
||||||
|
return ratio * toMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnitChange = async (newUnit: DelayUnit) => {
|
||||||
|
if (newUnit === unit) return;
|
||||||
|
|
||||||
|
const success = await setDelayUnit(newUnit);
|
||||||
|
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
setDSPState((d) => {
|
||||||
|
if (!d.state) return d;
|
||||||
|
|
||||||
|
const currentConfig = d.state.current_config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
state: {
|
||||||
|
...d.state,
|
||||||
|
current_config: {
|
||||||
|
...currentConfig,
|
||||||
|
delay_unit: newUnit,
|
||||||
|
|
||||||
|
input_states: {
|
||||||
|
...currentConfig.input_states,
|
||||||
|
[InputChannel.InA]: {
|
||||||
|
...currentConfig.input_states[InputChannel.InA],
|
||||||
|
delay: convertDelayUnit(
|
||||||
|
currentConfig.input_states[InputChannel.InA].delay,
|
||||||
|
unit,
|
||||||
|
newUnit
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[InputChannel.InB]: {
|
||||||
|
...currentConfig.input_states[InputChannel.InB],
|
||||||
|
delay: convertDelayUnit(
|
||||||
|
currentConfig.input_states[InputChannel.InB].delay,
|
||||||
|
unit,
|
||||||
|
newUnit
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[InputChannel.InC]: {
|
||||||
|
...currentConfig.input_states[InputChannel.InC],
|
||||||
|
delay: convertDelayUnit(
|
||||||
|
currentConfig.input_states[InputChannel.InC].delay,
|
||||||
|
unit,
|
||||||
|
newUnit
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[InputChannel.InD]: {
|
||||||
|
...currentConfig.input_states[InputChannel.InD],
|
||||||
|
delay: convertDelayUnit(
|
||||||
|
currentConfig.input_states[InputChannel.InD].delay,
|
||||||
|
unit,
|
||||||
|
newUnit
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
output_states: {
|
||||||
|
...currentConfig.output_states,
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.values(OutputChannel).map((channel) => [
|
||||||
|
channel,
|
||||||
|
{
|
||||||
|
...currentConfig.output_states[channel],
|
||||||
|
delay: convertDelayUnit(
|
||||||
|
currentConfig.output_states[channel].delay,
|
||||||
|
unit,
|
||||||
|
newUnit
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputRows = inputs.map((channel) => ({
|
||||||
|
label: dsp.state.current_config.input_states[channel].name,
|
||||||
|
delay: getDelay({ Input: channel }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const outputRows = outputs.map(({ channel, title }) => ({
|
||||||
|
label: title,
|
||||||
|
delay: getDelay(channel),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="delay-linear-graph-section">
|
||||||
|
<DelayGraph title="Input Delay" rows={inputRows} unit={unit} />
|
||||||
|
<DelayGraph title="Output Delay" rows={outputRows} unit={unit} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="delay-section">
|
||||||
|
<div className="delay-panels">
|
||||||
|
<ChannelGroup title="Input">
|
||||||
|
{inputs.map((channel) => (
|
||||||
|
<DelayPanel
|
||||||
|
key={channel}
|
||||||
|
channel={{ Input: channel }}
|
||||||
|
dsp={dsp}
|
||||||
|
setDelay={handleSetDelay}
|
||||||
|
onLiveValue={handleLiveValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ChannelGroup>
|
||||||
|
|
||||||
|
<ChannelGroup title="Output">
|
||||||
|
{outputs.map(({ channel, title }) => (
|
||||||
|
<DelayPanel
|
||||||
|
key={title}
|
||||||
|
channel={channel}
|
||||||
|
dsp={dsp}
|
||||||
|
setDelay={handleSetDelay}
|
||||||
|
onLiveValue={handleLiveValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ChannelGroup>
|
||||||
|
|
||||||
|
<ChannelGroup title="Unit">
|
||||||
|
<DelayUnitSelector unit={unit} onSelect={handleUnitChange} />
|
||||||
|
</ChannelGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DelaySection;
|
||||||
@@ -91,7 +91,11 @@ export default function VerticalSlider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decimals = useMemo(
|
const decimals = useMemo(
|
||||||
() => Math.max(getDecimals(step), switchStep !== undefined ? getDecimals(switchStep) : 0),
|
() =>
|
||||||
|
Math.min(
|
||||||
|
3,
|
||||||
|
Math.max(getDecimals(step), switchStep !== undefined ? getDecimals(switchStep) : 0)
|
||||||
|
),
|
||||||
[step, switchStep]
|
[step, switchStep]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+117
-44
@@ -11,6 +11,7 @@ import {
|
|||||||
defaultMeters,
|
defaultMeters,
|
||||||
OutputChannel,
|
OutputChannel,
|
||||||
Ratio,
|
Ratio,
|
||||||
|
DelayUnit,
|
||||||
} from '../types/dsp408State';
|
} from '../types/dsp408State';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,15 +29,13 @@ type NotifyFunction = (message: string, type: 'success' | 'error', dspName?: str
|
|||||||
|
|
||||||
type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>;
|
type SetDsps = React.Dispatch<React.SetStateAction<DSP[]>>;
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Helpers
|
// Helpers
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function updateDSP(setDsps: SetDsps, dspId: number, updater: (dsp: DSP) => DSP) {
|
function updateDSP(setDsps: SetDsps, dspId: number, updater: (dsp: DSP) => DSP) {
|
||||||
setDsps((prev) => prev.map((dsp) => (dsp.id === dspId ? updater(dsp) : dsp)));
|
setDsps((prev) => prev.map((dsp) => (dsp.id === dspId ? updater(dsp) : dsp)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DSP408Device = {
|
export type DSP408Device = {
|
||||||
|
setDSPState: (updater: (dsp: DSP) => DSP) => void;
|
||||||
connect: () => Promise<void>;
|
connect: () => Promise<void>;
|
||||||
disconnect: () => Promise<void>;
|
disconnect: () => Promise<void>;
|
||||||
recallPreset: (index: number) => Promise<boolean>;
|
recallPreset: (index: number) => Promise<boolean>;
|
||||||
@@ -65,12 +64,11 @@ export type DSP408Device = {
|
|||||||
attack: number,
|
attack: number,
|
||||||
release: number
|
release: number
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
setDelayUnit: (unit: DelayUnit) => Promise<boolean>;
|
||||||
|
setDelay: (channel: Channel, delay: number) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Polling
|
// Polling
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function useDSPPolling(
|
function useDSPPolling(
|
||||||
dsps: DSP[],
|
dsps: DSP[],
|
||||||
selected: number | null,
|
selected: number | null,
|
||||||
@@ -165,10 +163,6 @@ function useDSPPolling(
|
|||||||
}, [setDsps]);
|
}, [setDsps]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Main hook
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export function useDSP408(
|
export function useDSP408(
|
||||||
dsps: DSP[],
|
dsps: DSP[],
|
||||||
setDsps: SetDsps,
|
setDsps: SetDsps,
|
||||||
@@ -177,10 +171,7 @@ export function useDSP408(
|
|||||||
setPage: React.Dispatch<React.SetStateAction<AppPage>>,
|
setPage: React.Dispatch<React.SetStateAction<AppPage>>,
|
||||||
notify: NotifyFunction
|
notify: NotifyFunction
|
||||||
) {
|
) {
|
||||||
// ----------------------------------------------------------
|
|
||||||
// Polling control
|
// Polling control
|
||||||
// ----------------------------------------------------------
|
|
||||||
|
|
||||||
const setPollingPaused = (id: number, paused: boolean) => {
|
const setPollingPaused = (id: number, paused: boolean) => {
|
||||||
updateDSP(setDsps, id, (dsp) => ({
|
updateDSP(setDsps, id, (dsp) => ({
|
||||||
...dsp,
|
...dsp,
|
||||||
@@ -188,13 +179,14 @@ export function useDSP408(
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function setDSPState(dsp: DSP, updater: (dsp: DSP) => DSP) {
|
||||||
|
updateDSP(setDsps, dsp.id, updater);
|
||||||
|
}
|
||||||
|
|
||||||
// Polling is part of the hook.
|
// Polling is part of the hook.
|
||||||
useDSPPolling(dsps, selected, setDsps);
|
useDSPPolling(dsps, selected, setDsps);
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// Generic operation wrapper
|
// Generic operation wrapper
|
||||||
// ----------------------------------------------------------
|
|
||||||
|
|
||||||
async function withPollingPaused<T>(dsp: DSP, operation: () => Promise<T>): Promise<T> {
|
async function withPollingPaused<T>(dsp: DSP, operation: () => Promise<T>): Promise<T> {
|
||||||
setPollingPaused(dsp.id, true);
|
setPollingPaused(dsp.id, true);
|
||||||
|
|
||||||
@@ -205,10 +197,7 @@ export function useDSP408(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Connection
|
// Connection
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function connectDSP(dsp: DSP) {
|
async function connectDSP(dsp: DSP) {
|
||||||
setPollingPaused(dsp.id, true);
|
setPollingPaused(dsp.id, true);
|
||||||
|
|
||||||
@@ -261,10 +250,7 @@ export function useDSP408(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Add / Remove
|
// Add / Remove
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function addDSP(dsp: Omit<DSP, 'id' | 'status' | 'state' | 'meters' | 'pollingPaused'>) {
|
async function addDSP(dsp: Omit<DSP, 'id' | 'status' | 'state' | 'meters' | 'pollingPaused'>) {
|
||||||
try {
|
try {
|
||||||
const id = await createDSP408(dsp.ip, dsp.port, dsp.deviceId);
|
const id = await createDSP408(dsp.ip, dsp.port, dsp.deviceId);
|
||||||
@@ -312,10 +298,7 @@ export function useDSP408(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Presets
|
// Presets
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function recallPreset(dsp: DSP, presetIndex: number) {
|
async function recallPreset(dsp: DSP, presetIndex: number) {
|
||||||
return withPollingPaused(dsp, async () => {
|
return withPollingPaused(dsp, async () => {
|
||||||
try {
|
try {
|
||||||
@@ -398,10 +381,7 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Gain
|
// Gain
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function setGain(dsp: DSP, channel: Channel, gain: number): Promise<boolean> {
|
async function setGain(dsp: DSP, channel: Channel, gain: number): Promise<boolean> {
|
||||||
return withPollingPaused(dsp, async () => {
|
return withPollingPaused(dsp, async () => {
|
||||||
try {
|
try {
|
||||||
@@ -435,10 +415,7 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Mute
|
// Mute
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function setMute(dsp: DSP, channel: Channel, muted: boolean): Promise<boolean> {
|
async function setMute(dsp: DSP, channel: Channel, muted: boolean): Promise<boolean> {
|
||||||
return withPollingPaused(dsp, async () => {
|
return withPollingPaused(dsp, async () => {
|
||||||
try {
|
try {
|
||||||
@@ -511,10 +488,7 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Inverse
|
// Inverse
|
||||||
// ==========================================================
|
|
||||||
|
|
||||||
async function setInverse(dsp: DSP, channel: Channel, inverted: boolean): Promise<boolean> {
|
async function setInverse(dsp: DSP, channel: Channel, inverted: boolean): Promise<boolean> {
|
||||||
return withPollingPaused(dsp, async () => {
|
return withPollingPaused(dsp, async () => {
|
||||||
try {
|
try {
|
||||||
@@ -587,9 +561,7 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Gate
|
// Gate
|
||||||
// ==========================================================
|
|
||||||
async function setGate(
|
async function setGate(
|
||||||
dsp: DSP,
|
dsp: DSP,
|
||||||
inputChannel: InputChannel,
|
inputChannel: InputChannel,
|
||||||
@@ -641,9 +613,7 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Compressor
|
// Compressor
|
||||||
// ==========================================================
|
|
||||||
async function setCompressor(
|
async function setCompressor(
|
||||||
dsp: DSP,
|
dsp: DSP,
|
||||||
outputChannel: OutputChannel,
|
outputChannel: OutputChannel,
|
||||||
@@ -708,9 +678,7 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
|
||||||
// Limiter
|
// Limiter
|
||||||
// ==========================================================
|
|
||||||
async function setLimiter(
|
async function setLimiter(
|
||||||
dsp: DSP,
|
dsp: DSP,
|
||||||
outputChannel: OutputChannel,
|
outputChannel: OutputChannel,
|
||||||
@@ -769,12 +737,115 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// Delay Unit
|
||||||
// Return API
|
async function setDelayUnit(dsp: DSP, unit: DelayUnit): Promise<boolean> {
|
||||||
// ==========================================================
|
return withPollingPaused(dsp, async () => {
|
||||||
|
try {
|
||||||
|
const success = await invoke<boolean>('set_delay_unit', {
|
||||||
|
id: dsp.id,
|
||||||
|
unit,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
notify('Failed to change delay unit', 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDSP(setDsps, dsp.id, (d) => {
|
||||||
|
if (!d.state) return d;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
state: {
|
||||||
|
...d.state,
|
||||||
|
current_config: {
|
||||||
|
...d.state.current_config,
|
||||||
|
delay_unit: unit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
notify('Delay unit updated', 'success', dsp.name);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
notify(`Delay unit update failed: ${String(err)}`, 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel Delay
|
||||||
|
async function setDelay(dsp: DSP, channel: Channel, delay: number): Promise<boolean> {
|
||||||
|
return withPollingPaused(dsp, async () => {
|
||||||
|
try {
|
||||||
|
const success = await invoke<boolean>('set_delay', {
|
||||||
|
id: dsp.id,
|
||||||
|
channel,
|
||||||
|
delay,
|
||||||
|
unit: dsp.state.current_config.delay_unit,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
notify('Failed to change channel delay', 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDSP(setDsps, dsp.id, (d) => {
|
||||||
|
if (!d.state) return d;
|
||||||
|
|
||||||
|
const currentConfig = d.state.current_config;
|
||||||
|
|
||||||
|
if ('Input' in channel) {
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
state: {
|
||||||
|
...d.state,
|
||||||
|
current_config: {
|
||||||
|
...currentConfig,
|
||||||
|
input_states: {
|
||||||
|
...currentConfig.input_states,
|
||||||
|
[channel.Input]: {
|
||||||
|
...currentConfig.input_states[channel.Input],
|
||||||
|
delay,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
state: {
|
||||||
|
...d.state,
|
||||||
|
current_config: {
|
||||||
|
...currentConfig,
|
||||||
|
output_states: {
|
||||||
|
...currentConfig.output_states,
|
||||||
|
[channel.Output]: {
|
||||||
|
...currentConfig.output_states[channel.Output],
|
||||||
|
delay,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
notify('Channel delay updated', 'success', dsp.name);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
notify(`Channel delay update failed: ${String(err)}`, 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return API
|
||||||
function forDSP(dsp: DSP): DSP408Device {
|
function forDSP(dsp: DSP): DSP408Device {
|
||||||
return {
|
return {
|
||||||
|
setDSPState: (updater: (dsp: DSP) => DSP) => setDSPState(dsp, updater),
|
||||||
connect: () => connectDSP(dsp),
|
connect: () => connectDSP(dsp),
|
||||||
disconnect: () => disconnectDSP(dsp),
|
disconnect: () => disconnectDSP(dsp),
|
||||||
recallPreset: (index: number) => recallPreset(dsp, index),
|
recallPreset: (index: number) => recallPreset(dsp, index),
|
||||||
@@ -803,6 +874,8 @@ export function useDSP408(
|
|||||||
attack: number,
|
attack: number,
|
||||||
release: number
|
release: number
|
||||||
) => setLimiter(dsp, outputChannel, threshold, attack, release),
|
) => setLimiter(dsp, outputChannel, threshold, attack, release),
|
||||||
|
setDelayUnit: (unit: DelayUnit) => setDelayUnit(dsp, unit),
|
||||||
|
setDelay: (channel: Channel, delay: number) => setDelay(dsp, channel, delay),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,6 +891,6 @@ export function useDSP408(
|
|||||||
recallPreset,
|
recallPreset,
|
||||||
setCurrentPresetName,
|
setCurrentPresetName,
|
||||||
|
|
||||||
setGain,
|
setDSPState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 LimiterSection from '../../components/dsp408/LimiterSection/LimiterSection';
|
import LimiterSection from '../../components/dsp408/LimiterSection/LimiterSection';
|
||||||
|
import DelaySection from '../../components/dsp408/DelaySection/DelaySection';
|
||||||
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
|
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
|
||||||
import { InputChannel, OutputChannel } from '../../types/dsp408State';
|
import { InputChannel, OutputChannel } from '../../types/dsp408State';
|
||||||
|
|
||||||
@@ -187,6 +188,16 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
|
|||||||
case 'limiter':
|
case 'limiter':
|
||||||
return <LimiterSection dsp={dsp} set_limiter={dsp408.setLimiter} />;
|
return <LimiterSection dsp={dsp} set_limiter={dsp408.setLimiter} />;
|
||||||
|
|
||||||
|
case 'delay':
|
||||||
|
return (
|
||||||
|
<DelaySection
|
||||||
|
dsp={dsp}
|
||||||
|
setDelay={dsp408.setDelay}
|
||||||
|
setDelayUnit={dsp408.setDelayUnit}
|
||||||
|
setDSPState={dsp408.setDSPState}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user