refactor: folders/files to group dsp logic

This commit is contained in:
2026-08-11 23:17:47 +02:00
parent d91bcaa20f
commit 34fad6f905
35 changed files with 194 additions and 337 deletions
@@ -0,0 +1,504 @@
// InputSection.tsx
import VerticalSlider from '../../../../components/VerticalSlider/VerticalSlider';
// ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping
// input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values
// if the actual enum differs.
import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter } from '../../types';
import { DSP408 as DSP } from '../../../../types';
import { CrossoverFilter } from '../../types';
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
import { useRef, useState } from 'react';
import './InputSection.css';
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
import Button from '../../../../components/Button/Button';
import MeterBar from '../../../../components/MeterBar/MeterBar';
const FREQ_MIN = 20;
const FREQ_MAX = 20000;
const FREQ_STEP = 1;
const FILTER_TYPE_OPTIONS: { value: CrossoverFilter; label: string }[] = [
{ value: CrossoverFilter.Bw6, label: 'BW-6' },
{ value: CrossoverFilter.Bw12, label: 'BW-12' },
{ value: CrossoverFilter.Bw18, label: 'BW-18' },
{ value: CrossoverFilter.Bw24, label: 'BW-24' },
{ value: CrossoverFilter.Bw30, label: 'BW-30' },
{ value: CrossoverFilter.Bw36, label: 'BW-36' },
{ value: CrossoverFilter.Bw42, label: 'BW-42' },
{ value: CrossoverFilter.Bw48, label: 'BW-48' },
{ value: CrossoverFilter.Lk12, label: 'LR-12' },
{ value: CrossoverFilter.Lk24, label: 'LR-24' },
{ value: CrossoverFilter.Lk36, label: 'LR-36' },
{ value: CrossoverFilter.Lk48, label: 'LR-48' },
];
const PEQ_SLIDER_PARAMS = [
{ key: 'frequency', label: 'Frequency', min: 19.7, max: 20160, step: 0.5, unit: ' Hz' },
{ key: 'q', label: 'Q', min: 1, max: 999, step: 1, unit: '' },
{ key: 'gain', label: 'Gain', min: -12, max: +12, step: 0.1, unit: ' dB' },
] as const;
type SetPEQFn = (
channel: Channel,
band: PEQBand,
gainDb: number,
freqHz: number,
q: number,
filterType: PEQFilter,
bypass: boolean
) => Promise<boolean>;
type PeqParamKey = 'frequency' | 'q' | 'gain';
type PEQValues = {
frequency: number;
q: number;
gain: number;
filter_type: PEQFilter;
bypass: boolean;
};
function PeqPanelSliders({
channel,
dsp,
band,
setPEQ,
// onLiveValue,
}: {
channel: InputChannel;
dsp: DSP;
band: PEQBand;
setPEQ: SetPEQFn;
// onLiveValue: (channel: InputChannel, key: PeqParamKey, value: number) => void;
}) {
const inputState = dsp.state.current_config.input_states[channel];
const peq = inputState.peq_chain.bands[band] as PEQ;
const commit = (overrides: Partial<PEQValues>) => {
const next = { ...peq, ...overrides };
return setPEQ(
{ Input: channel },
band,
next.gain,
next.frequency,
next.q,
next.filter_type,
next.bypass
);
};
return (
<div className="input-panel">
<div className="input-panel__title">{'PEQ Parameters'}</div>
<div className="input-panel__grid">
{PEQ_SLIDER_PARAMS.map((param) => (
<div className={`input-panel__cell input-panel__cell--${param.key}`} key={param.key}>
<VerticalSlider
min={param.min}
max={param.max}
step={() => param.step}
unit={param.unit}
value={peq[param.key]}
// onInput={(value) => onLiveValue(channel, param.key, value)}
onChange={(value) => commit({ [param.key]: value } as Partial<PEQValues>)}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<div className="input-panel__cell-label">{param.label}</div>
</div>
))}
</div>
</div>
);
}
function CrossoverPanel({
channel,
dsp,
title,
setCrossover,
}: {
channel: Channel;
dsp: DSP;
title: string;
setCrossover: (
channel: Channel,
frequency: number,
filter_type: CrossoverFilter,
bypass: boolean
) => Promise<boolean>;
}) {
const channelState =
'Input' in channel
? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output];
const crossover = channelState?.crossover;
const frequency = crossover.high_pass.frequency;
const filter_type = crossover.high_pass.slope;
const bypass = false;
const commit = (overrides: {
frequency?: number;
filter_type?: CrossoverFilter;
bypass?: boolean;
}) => {
const next = { frequency, filter_type, bypass, ...overrides };
return setCrossover(channel, next.frequency, next.filter_type, next.bypass);
};
return (
<div className="crossover-panel">
<div className="crossover-panel__title">{title}</div>
<VerticalSlider
min={20}
max={20000}
step={() => 1}
unit=" Hz"
value={frequency}
onChange={(value) => commit({ frequency: value })}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<select
className="crossover-panel__select"
value={filter_type}
onChange={(e) => commit({ filter_type: e.target.value as CrossoverFilter })}
>
{FILTER_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<Button
variant="toggle"
className={bypass ? 'active-warning' : ''}
onClick={() => commit({ bypass: !bypass })}
style={{
width: '85%',
paddingLeft: 0,
paddingRight: 0,
fontSize: '4cqh',
paddingTop: '2cqh',
paddingBottom: '2cqh',
minHeight: 0,
}}
>
{bypass ? 'Bypassed' : 'Bypass'}
</Button>
</div>
);
}
function GainMeterPanel({
dsp,
channel,
title,
meter,
setGain,
setMute,
setInverse,
}: {
dsp: DSP;
channel: Channel;
title: string;
meter: Meter;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) {
const channelState =
'Input' in channel
? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output];
const muted = channelState?.mute ?? false;
const inverted = channelState?.phase_inverted ?? false;
const gain = channelState?.gain ?? 0;
const toggleMute = async () => {
await setMute(channel, !muted);
};
const toggleInverse = async () => {
await setInverse(channel, !inverted);
};
return (
<div className="gain-meter-panel">
<div className="gain-meter-panel__title">{title}</div>
<div className="gain-meter-panel__grid">
<div className="gain-meter-panel__cell gain-meter-panel__cell--slider">
<VerticalSlider
min={-60}
max={12}
step={(v) => (v >= -20 ? 0.1 : 0.5)}
unit=" dB"
value={gain}
onChange={(value) => setGain(channel, value)}
style={{ width: '100%', height: '100%' }}
/>
</div>
<div className="gain-meter-panel__cell gain-meter-panel__cell--meter">
<MeterBar label="" meter={meter} />
</div>
<div className="gain-meter-panel__cell gain-meter-panel__cell--btn">
<Button
variant="toggle"
className={inverted ? 'active-warning' : ''}
onClick={toggleInverse}
style={{
width: '100%',
paddingLeft: 0,
paddingRight: 0,
fontSize: '3.5cqh',
paddingTop: '1cqh',
paddingBottom: '1cqh',
minHeight: 0,
}}
>
{inverted ? 'Inverted' : 'Invert'}
</Button>
</div>
<div className="gain-meter-panel__cell gain-meter-panel__cell--btn">
<Button
variant="toggle"
className={muted ? 'active-danger' : ''}
onClick={toggleMute}
style={{
width: '100%',
paddingLeft: 0,
paddingRight: 0,
fontSize: '3.5cqh',
paddingTop: '1cqh',
paddingBottom: '1cqh',
minHeight: 0,
}}
>
{muted ? 'Muted' : 'Mute'}
</Button>
</div>
</div>
</div>
);
}
type SetFilterFn = (
channel: Channel,
freq: number,
filterType: CrossoverFilter
) => Promise<boolean>;
type setPeqBandFn = (
channel: Channel,
band: PEQBand,
gainDb: number,
freqHz: number,
q: number,
filterType: PEQFilter,
bypass: boolean
) => Promise<boolean>;
function InputSection({
dsp,
channel,
set_high_pass: setHighPass,
set_low_pass,
setPeq,
// setPeqBypass,
setGain,
setMute,
setInverse,
}: {
dsp: DSP;
channel: InputChannel;
set_high_pass: SetFilterFn;
set_low_pass: SetFilterFn;
setPeq: setPeqBandFn;
// setPeqBypass;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) {
const [activeBand, setActiveBand] = useState<PEQBand>(PEQBand.B1);
const [liveHpFreq, setLiveHpFreq] = useState<number>(19.7);
const [liveLpFreq, setLiveLpFreq] = useState<number>(20.16);
const [livePeq, setLivePeq] = useState<Partial<Record<PEQBand, PEQValues>>>({});
const inputState = dsp.state.current_config.input_states[channel];
const hp = inputState.crossover.high_pass;
const lp = inputState.crossover.low_pass;
const activePeq = livePeq[activeBand] ?? (inputState.peq_chain.bands[activeBand] as PEQValues);
const bypassed = hp.slope === CrossoverFilter.Bypass;
const activeFilterType = bypassed && lp.slope !== CrossoverFilter.Bypass ? lp.slope : hp.slope;
// Remembers the last non-bypass slope so the Bypass button can restore it.
// const lastNonBypassType = useRef<CrossoverFilter>(
// activeFilterType === CrossoverFilter.Bypass ? DEFAULT_FILTER_TYPE : activeFilterType
// );
// if (activeFilterType !== CrossoverFilter.Bypass) {
// lastNonBypassType.current = activeFilterType;
// }
const hpFreq = liveHpFreq ?? hp.frequency;
const lpFreq = liveLpFreq ?? lp.frequency;
// const commitHighPass = async (freq: number, filterType: CrossoverFilter) => {
// const success = await set_high_pass({ Input: channel }, freq, filterType);
// if (success) setLiveHpFreq(null);
// return success;
// };
// const commitLowPass = async (freq: number, filterType: CrossoverFilter) => {
// const success = await set_low_pass({ Input: channel }, freq, filterType);
// if (success) setLiveLpFreq(null);
// return success;
// };
// const handleFilterTypeChange = (filterType: CrossoverFilter) => {
// // Applying a slope change always takes the crossover out of bypass.
// void commitHighPass(hpFreq, filterType);
// void commitLowPass(lpFreq, filterType);
// };
// const handleBypassToggle = () => {
// if (bypassed) {
// const restore = lastNonBypassType.current;
// void commitHighPass(hpFreq, restore);
// void commitLowPass(lpFreq, restore);
// } else {
// void commitHighPass(hpFreq, CrossoverFilter.Bypass);
// void commitLowPass(lpFreq, CrossoverFilter.Bypass);
// }
// };
return (
<>
<div className="input-graph-section">
<div className="input-graph-panel">
<div className="input-graph__title">{inputState.name} - Filters</div>
{/* TODO: draw HP/LP transfer curves here once filter math is defined,
same pattern as LimiterSection's `draw` prop. */}
<LinearGraph
xMin={20}
xMax={20000}
yMin={-30}
yMax={6}
xLabel="Hz"
yLabel="dB"
formatX={(v) => Math.round(v).toString()}
formatY={(v) => Math.round(v).toString()}
draw={() => {}}
/>
</div>
</div>
<div className="input-control-section">
<div className="input-control-panel">
<ChannelGroup title="PEQ">
<PeqPanelSliders channel={channel} dsp={dsp} band={PEQBand.B1} setPEQ={setPeq} />
</ChannelGroup>
<ChannelGroup title="CrossOver">
<CrossoverPanel
channel={{ Input: channel }}
dsp={dsp}
title="LowPass"
setCrossover={setHighPass}
/>
<CrossoverPanel
channel={{ Input: channel }}
dsp={dsp}
title="HighPass"
setCrossover={setHighPass}
/>
</ChannelGroup>
<ChannelGroup title="Gain">
<GainMeterPanel
dsp={dsp}
channel={{ Input: channel }}
title={dsp.state.current_config.input_states.InA.name}
meter={dsp.meters.channels[0]}
setGain={setGain}
setMute={setMute}
setInverse={setInverse}
/>
</ChannelGroup>
{/* <div className="input-control-panel__sliders">
<div className="input-control-panel__cell input-control-panel__cell--high-pass">
<VerticalSlider
min={FREQ_MIN}
max={FREQ_MAX}
step={FREQ_STEP}
unit=" Hz"
value={hpFreq}
// disabled={bypassed}
onInput={(value) => setLiveHpFreq(value)}
onChange={(value) => commitHighPass(value, activeFilterType)}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<div className="input-control-panel__cell-label">High Pass</div>
</div>
<div className="input-control-panel__cell input-control-panel__cell--low-pass">
<VerticalSlider
min={FREQ_MIN}
max={FREQ_MAX}
step={FREQ_STEP}
unit=" Hz"
value={lpFreq}
// disabled={bypassed}
onInput={(value) => setLiveLpFreq(value)}
onChange={(value) => commitLowPass(value, activeFilterType)}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<div className="input-control-panel__cell-label">Low Pass</div>
</div>
</div>
<div className="input-control-panel__side">
<label className="input-control-panel__select">
<span className="input-control-panel__select-label">Filter Type</span>
<select
value={activeFilterType}
disabled={bypassed}
onChange={(e) => handleFilterTypeChange(e.target.value as CrossoverFilter)}
>
{FILTER_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
<button
type="button"
className={`input-control-panel__bypass-btn${
bypassed ? ' input-control-panel__bypass-btn--active' : ''
}`}
onClick={handleBypassToggle}
>
{bypassed ? 'Bypassed' : 'Bypass'}
</button>
</div> */}
</div>
</div>
</>
);
}
export default InputSection;