feat: add delay tab

This commit is contained in:
2026-08-06 20:51:38 +02:00
parent e802067056
commit 470b95ace3
10 changed files with 662 additions and 47 deletions
@@ -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;