// 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;