refactor: folders/files to group dsp logic
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user