// InputSection.tsx import VerticalSlider, { makeLogStep, makeLogQuantize, } from '../../../../components/VerticalSlider/VerticalSlider'; import { CROSSOVER_FILTER_LABEL, PEQ_BANDS_ORDER, PEQ_FILTERS_LABEL, PEQ_Q_RANGES, PEAK_Q_VALUES, } from '../../constants'; import { InputChannel, 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 } from 'react'; import './InputSection.css'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import Button from '../../../../components/Button/Button'; // import { buildTotalCurveDraw, buildTotalPhaseCurveDraw } from '../../utils/totalCurve'; 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'; 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 InputActions({ 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 commit = (overrides: Partial) => { const next = { ...peq, ...overrides }; return setPEQ(next.gain, next.frequency, next.q, 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={peq.q} onChange={(value) => commit({ q: value })} onInput={(value) => onLiveValue({ ...peq, q: value })} 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 })} 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' }} />
); } function PeqBandList({ bands, liveBands, activeBand, setActiveBand, setPEQ, }: { bands: Record; liveBands: Partial>; activeBand: PEQBand; setActiveBand: (band: PEQBand) => void; setPEQ: ( band: PEQBand, gainDb: number, freqHz: number, q: number, filterType: PEQFilter, bypass: boolean ) => Promise; }) { const commit = (band: PEQBand, overrides: Partial) => { const peq = liveBands[band] ?? bands[band]; const next = { ...peq, ...overrides }; return setPEQ(band, next.gain, next.frequency, next.q, next.filter_type, next.bypass); }; return (
Band
Freq
Q
Gain
Type
Bypass
{PEQ_BANDS_ORDER.map((band, i) => { const peq = liveBands[band] ?? bands[band]; const isActive = band === activeBand; return (
{/* Frequency */} = 1000 ? (peq.frequency / 1000).toFixed(2) : peq.frequency.toFixed(1) } onChange={(e) => { const value = Number(e.target.value); if (!Number.isNaN(value)) { commit(band, { frequency: peq.frequency >= 1000 ? value * 1000 : value, }); } }} /> commit(band, { q: Number(e.target.value) })} /> commit(band, { gain: Number(e.target.value) })} />
); })}
); } type SetFilterFn = ( channel: Channel, freq: number, filterType: CrossoverFilter ) => Promise; /** Small DOM-overlay markers positioned on top of the magnitude graph via its exposed toPxX/toPxY. */ // function GraphMarkers({ // graphRef, // bands, // activeBand, // setActiveBand, // hpFreq, // hpBypassed, // lpFreq, // lpBypassed, // }: { // graphRef: React.RefObject; // bands: Record; // activeBand: PEQBand; // setActiveBand: (band: PEQBand) => void; // hpFreq: number; // hpBypassed: boolean; // lpFreq: number; // lpBypassed: boolean; // }) { // const g = graphRef.current; // if (!g) return null; // return ( // <> // {PEQ_BANDS_ORDER.map((band, i) => { // const peq = bands[band]; // const left = g.toPxX(peq.frequency); // const top = g.toPxY(peq.gain); // const isActive = band === activeBand; // return ( // // ); // })} //
// HP //
//
// LP //
// // ); // } function InputSection({ dsp, channel, setHighPass, setLowPass, setPeq, setPeqBypass, }: { dsp: DSP; channel: InputChannel; 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); // 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 activeInputState = dsp.state.current_config.input_states[channel]; const ActiveHighPassFilter = activeInputState.crossover.high_pass; const ActiveLowPassFilter = activeInputState.crossover.low_pass; const ActivePeq = activeInputState.peq_chain; const handleResetPeq = async () => { if (resetting) return; setResetting(true); for (const band of PEQ_BANDS_ORDER) { const current = displayPeq[band]; if (current.gain === 0 || current.filter_type === PEQFilter.Peak) continue; // skip bands already flat await commitPEQ(band, 0, current.frequency, current.q, PEQFilter.Peak, current.bypass); } setResetting(false); }; const handleBypassClick = () => { setPeqBypass({ Input: 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({ Input: channel }, freq, filterType); if (success) { setLiveHpFreq(freq); } return success; }; const commitLowPass = async (freq: number, filterType: CrossoverFilter) => { const success = await setLowPass({ Input: 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({ Input: 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 ( <>
{activeInputState.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 }, }, ], }); }} /> )}
commitPEQ(activeBand, gain, frequency, q, filter_type, bypass) } onLiveValue={(value) => handleLivePEQValue(activeBand, value)} />
); } export default InputSection;