// ChannelSection.tsx import VerticalSlider, { makeLogStep, makeLogQuantize, } from '../../../../components/VerticalSlider/VerticalSlider'; import ChoiceStepper from '../../../../components/ChoiceStepper/ChoiceStepper'; import ChoiceSelect from '../../../../components/ChoiceSelect/ChoiceSelect'; import { CROSSOVER_FILTER_LABEL, PEQ_BANDS_ORDER, PEQ_FILTERS_LABEL, PEQ_Q_RANGES, PEAK_Q_VALUES, PEQ_Q_UNUSED, PEQ_GAIN_USED, } from '../../constants'; import { Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types'; import { DSP408 as DSP } from '../../../../types'; import { CrossoverFilter } from '../../types'; import { LinearGraph, LinearGraphHandle } from '../../../../components/LinearGraph/LinearGraph'; import { useRef, useState, useEffect } from 'react'; import './ChannelSection.css'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import Button from '../../../../components/Button/Button'; import { FREQUENCY_MAX, FREQUENCY_MIN, FREQUENCY_QUANTIZATION, GRAPH_X_MAX, GRAPH_X_MIN, GRAPH_Y_MAX, GRAPH_Y_MIN, } from '../../constants'; import { drawCurves } from '../../utils/graph/draw'; import { getPeqCurvePoints, getPeqPhasePoints } from '../../utils/graph/peq'; import { logSpace, sumCurvePoints, sumPhaseCurvePoints } from '../../utils/graph/curve'; import { getCrossoverCurvePoints, getCrossoverPhasePoints } from '../../utils/graph/crossover'; function nearestQValue(q: number, values: readonly number[] = PEAK_Q_VALUES): number { return values.reduce((nearest, current) => Math.abs(current - q) < Math.abs(nearest - q) ? current : nearest ); } type GraphView = 'both' | 'magnitude' | 'phase'; function GraphViewToggle({ view, onChange, }: { view: GraphView; onChange: (v: GraphView) => void; }) { const options: { value: GraphView; label: string }[] = [ { value: 'both', label: 'Both' }, { value: 'magnitude', label: 'Mag' }, { value: 'phase', label: 'Phase' }, ]; return (
{options.map((opt) => ( ))}
); } function ChannelActions({ bypass, resetting, onBypass, onReset, }: { bypass: boolean; resetting: boolean; onBypass: () => void; onReset: () => void; }) { return (
); } function PeqPanelSliders({ peq, setPEQ, onLiveValue, }: { peq: PEQ; setPEQ: ( gainDb: number, freqHz: number, q: number, filterType: PEQFilter, bypass: boolean ) => Promise; onLiveValue: (value: PEQ) => void; }) { const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type]; const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type); const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type); const nearestQ = nearestQValue(peq.q); const commit = (overrides: Partial) => { const next = { ...peq, ...overrides }; return setPEQ(next.gain, next.frequency, overrides.q ?? nearestQ, next.filter_type, next.bypass); }; return (
PEQ Parameters
(value >= 1000 ? 'kHz' : 'Hz')} formatValue={(value) => value >= 1000 ? `${(value / 1000).toFixed(2)}` : `${value.toFixed(1)}` } value={peq.frequency} onChange={(value) => commit({ frequency: value })} onInput={(value) => onLiveValue({ ...peq, frequency: value })} style={{ width: '100%', flex: '1 1 auto' }} />
Frequency
v.toFixed(2)} value={nearestQ} onChange={(value) => commit({ q: value })} onInput={(value) => onLiveValue({ ...peq, q: nearestQValue(value) })} disabled={qDisabled} style={{ width: '100%', flex: '1 1 auto' }} />
Q
0.1} unit="dB" value={peq.gain} formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))} onChange={(value) => commit({ gain: value })} onInput={(value) => onLiveValue({ ...peq, gain: value })} disabled={gainDisabled} style={{ width: '100%', flex: '1 1 auto' }} />
Gain
); } function CrossoverPanel({ crossover, title, setCrossover, onLiveValue, }: { crossover: Crossover; title: string; setCrossover: (frequency: number, filter_type: CrossoverFilter) => Promise; onLiveValue: (value: number) => void; }) { const frequency = crossover.frequency; const filter_type = crossover.slope; const bypass = filter_type === CrossoverFilter.Bypass; const lastFilterType = useRef( filter_type !== CrossoverFilter.Bypass ? filter_type : CrossoverFilter.Lk48 ); const commit = async (overrides: { frequency?: number; filter_type?: CrossoverFilter; bypass?: boolean; }) => { const nextFrequency = overrides.frequency ?? frequency; const selectedFilterType = overrides.filter_type !== undefined ? overrides.filter_type : bypass ? lastFilterType.current : filter_type; // Always remember the latest non-bypass selection. if (selectedFilterType !== CrossoverFilter.Bypass) { lastFilterType.current = selectedFilterType; } const nextBypass = overrides.bypass ?? bypass; const nextFilterType = nextBypass ? CrossoverFilter.Bypass : selectedFilterType; return setCrossover(nextFrequency, nextFilterType); }; return (
{title}
(value >= 1000 ? 'kHz' : 'Hz')} value={frequency} formatValue={(value) => value >= 1000 ? `${(value / 1000).toFixed(2)}` : `${value.toFixed(1)}` } onChange={(value) => commit({ frequency: value })} onInput={(value) => onLiveValue(value)} style={{ width: '100%', flex: '1 1 auto' }} /> opt.value)} formatLabel={(v) => CROSSOVER_FILTER_LABEL.find((opt) => opt.value === v)?.label ?? String(v) } onChange={(value) => commit({ filter_type: value })} />
); } function PeqBandList({ bands, liveBands, activeBand, setActiveBand, setPEQ, bandsOrder, }: { bands: Record; liveBands: Partial>; activeBand: PEQBand; setActiveBand: (band: PEQBand) => void; setPEQ: ( band: PEQBand, gainDb: number, freqHz: number, q: number, filterType: PEQFilter, bypass: boolean ) => Promise; bandsOrder: PEQBand[]; }) { const commit = (band: PEQBand, overrides: Partial) => { const live = liveBands[band] ?? bands[band]; const next = { ...live, ...overrides }; return setPEQ( band, next.gain, next.frequency, nearestQValue(next.q), next.filter_type, next.bypass ); }; return (
Band
Freq
Q
Gain
Type
Bypass
{bandsOrder.map((band, i) => { const peq = liveBands[band] ?? bands[band]; const isActive = band === activeBand; const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type]; const qValues = PEAK_Q_VALUES.filter((q) => q >= qMin && q <= qMax); const qValue = nearestQValue(peq.q, qValues.length ? qValues : PEAK_Q_VALUES); const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type); const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type); return (
{/* Frequency */} v >= 1000 ? `${(v / 1000).toFixed(2)} kHz` : `${v.toFixed(1)} Hz` } onChange={(value) => commit(band, { frequency: value })} /> v.toFixed(2)} disabled={qDisabled} onChange={(value) => commit(band, { q: value })} /> `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`} disabled={gainDisabled} onChange={(value) => commit(band, { gain: value })} /> className="peq-band-list__select" value={peq.filter_type} options={PEQ_FILTERS_LABEL.map((opt) => opt.value)} formatLabel={(v) => PEQ_FILTERS_LABEL.find((opt) => opt.value === v)?.label ?? String(v) } onChange={(value) => { const [min, max] = PEQ_Q_RANGES[value]; const allowed = PEAK_Q_VALUES.filter((q) => q >= min && q <= max); commit(band, { filter_type: value, q: nearestQValue(3.0, allowed.length ? allowed : PEAK_Q_VALUES), }); }} />
); })}
); } type SetFilterFn = ( channel: Channel, freq: number, filterType: CrossoverFilter ) => Promise; function ChannelSection({ dsp, channel, setHighPass, setLowPass, setPeq, setPeqBypass, }: { dsp: DSP; channel: Channel; setHighPass: SetFilterFn; setLowPass: SetFilterFn; setPeq: ( channel: Channel, band: PEQBand, gainDb: number, freqHz: number, q: number, filterType: PEQFilter, bypass: boolean ) => Promise; setPeqBypass: (channel: Channel, bypass: boolean) => Promise; }) { // UI variables const [activeBand, setActiveBand] = useState(PEQBand.B1); const [graphView, setGraphView] = useState('both'); const [resetting, setResetting] = useState(false); const [isResetConfirmOpen, setIsResetConfirmOpen] = useState(false); // Live values const [liveHpFreq, setLiveHpFreq] = useState(FREQUENCY_MIN); const [liveLpFreq, setLiveLpFreq] = useState(FREQUENCY_MAX); const [livePeq, setLivePeq] = useState>>({}); const magnitudeGraphRef = useRef(null); // DSP values const isInput = 'Input' in channel; const activeChannelState = isInput ? dsp.state.current_config.input_states[channel.Input] : dsp.state.current_config.output_states[channel.Output]; const ActiveHighPassFilter = activeChannelState.crossover.high_pass; const ActiveLowPassFilter = activeChannelState.crossover.low_pass; const ActivePeq = activeChannelState.peq_chain; const bandsOrder = isInput ? PEQ_BANDS_ORDER.slice(0, -1) : PEQ_BANDS_ORDER; useEffect(() => { if (!isResetConfirmOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { setIsResetConfirmOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isResetConfirmOpen]); const handleResetClick = () => { setIsResetConfirmOpen(true); }; const handleResetPeq = async () => { if (resetting) return; setResetting(true); for (const band of bandsOrder) { const current = displayPeq[band]; // Skip only if already flat — nothing to change. if (current.gain === 0 && current.filter_type === PEQFilter.Peak) continue; await commitPEQ(band, 0, current.frequency, nearestQValue(3.0, PEAK_Q_VALUES), PEQFilter.Peak, current.bypass); } setResetting(false); }; const confirmResetPeq = async () => { setIsResetConfirmOpen(false); await handleResetPeq(); }; const handleBypassClick = () => { setPeqBypass(channel, !ActivePeq.bypass); }; // Live handlers const handleLiveHighPassValue = (value: number) => { setLiveHpFreq(value); }; const handleLiveLowPassValue = (value: number) => { setLiveLpFreq(value); }; const handleLivePEQValue = (band: PEQBand, value: PEQ) => { setLivePeq((prev) => ({ ...prev, [band]: value, })); }; // Real dsp value handlers const commitHighPass = async (freq: number, filterType: CrossoverFilter) => { const success = await setHighPass(channel, freq, filterType); if (success) { setLiveHpFreq(freq); } return success; }; const commitLowPass = async (freq: number, filterType: CrossoverFilter) => { const success = await setLowPass(channel, freq, filterType); if (success) { setLiveLpFreq(freq); } return success; }; const commitPEQ = async ( band: PEQBand, gain: number, frequency: number, q: number, filter_type: PEQFilter, bypass: boolean ) => { const success = await setPeq(channel, band, gain, frequency, q, filter_type, bypass); if (success) { setLivePeq((prev) => { const next = { ...prev }; delete next[band]; return next; }); } return success; }; // Diplay value (merge live and real) const displayPeq: Record = { ...ActivePeq.bands, ...livePeq, }; const highPassSpec = { frequency: liveHpFreq, filter_type: ActiveHighPassFilter.slope }; const lowPassSpec = { frequency: liveLpFreq, filter_type: ActiveLowPassFilter.slope }; const freqs = logSpace(GRAPH_X_MIN, GRAPH_X_MAX, 1000); return ( <>
{activeChannelState.name} - Filters
{(graphView === 'both' || graphView === 'magnitude') && (
Math.round(v).toString()} formatY={(v) => Math.round(v).toString()} draw={({ ctx, toPx }) => { drawCurves({ ctx, toPx, xMin: GRAPH_X_MIN, xMax: GRAPH_X_MAX, yMin: GRAPH_Y_MIN, yMax: GRAPH_Y_MAX, curves: [ { points: sumCurvePoints([ getPeqCurvePoints({ freqs, bands: displayPeq }), getCrossoverCurvePoints({ freqs, highPass: highPassSpec, lowPass: lowPassSpec, }), ]), style: { strokeColor: '#e5e7eb', dimmed: ActivePeq.bypass }, }, ], }); }} />
)} {(graphView === 'both' || graphView === 'phase') && ( Math.round(v).toString()} formatY={(v) => Math.round(v).toString()} draw={({ ctx, toPx }) => { drawCurves({ ctx, toPx, xMin: GRAPH_X_MIN, xMax: GRAPH_X_MAX, yMin: GRAPH_Y_MIN, yMax: GRAPH_Y_MAX, curves: [ { points: sumPhaseCurvePoints([ getPeqPhasePoints({ freqs, bands: displayPeq }), getCrossoverPhasePoints({ freqs, highPass: highPassSpec, lowPass: lowPassSpec, }), ]), style: { strokeColor: '#e5e7eb', dimmed: ActivePeq.bypass }, }, ], }); }} /> )} {ActivePeq.bypass && (
BYPASSED
)}
commitPEQ(activeBand, gain, frequency, q, filter_type, bypass) } onLiveValue={(value) => handleLivePEQValue(activeBand, value)} />
{isResetConfirmOpen && (
setIsResetConfirmOpen(false)}>
e.stopPropagation()}>

Reset {activeChannelState.name} PEQ?

This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB, Peak). This action cannot be undone.

)} ); } export default ChannelSection;