feat: add behaviour to hp/lp filters

This commit is contained in:
2026-08-13 11:32:06 +02:00
parent a28dd890dd
commit a0c9fbb674
7 changed files with 167 additions and 174 deletions
+2 -2
View File
@@ -13,8 +13,8 @@
"windows": [
{
"title": "thomann-dsp-app",
"width": 1200,
"height": 800,
"width": 1600,
"height": 950,
"minWidth": 1200,
"minHeight": 600
}
@@ -26,7 +26,7 @@ type VerticalSliderProps = {
onChange?: (value: number) => Promise<boolean>;
onInput?: (value: number) => void;
unit?: string;
unit?: string | ((value: number) => string);
formatValue?: (value: number, decimals: number) => string;
showTicks?: boolean;
@@ -198,6 +198,8 @@ export default function VerticalSlider({
const formattedValue = (formatValue ?? formatValueDefault)(displayValue, decimals);
const displayUnit = typeof unit === 'function' ? unit(displayValue) : unit;
function normalizeValue(input: number) {
const clamped = Math.min(max, Math.max(min, input));
@@ -434,7 +436,7 @@ export default function VerticalSlider({
}
}}
/>
{unit && <span className="value-unit">{unit}</span>}
{displayUnit && <span className="value-unit">{' ' + displayUnit}</span>}
</div>
</div>
);
@@ -208,7 +208,7 @@ function GeqSection({
}) {
const [activeChannel, setActiveChannel] = useState<InputChannel>(INPUT_ORDER[0]);
const [liveGeq, setLiveGeq] = useState<Partial<Record<InputChannel, LiveBands>>>({});
const [resetting, setResetting] = useState(false); // NEW
const [resetting, setResetting] = useState(false);
const handleLiveValue = (channel: InputChannel, band: DiscreteFrequency, value: number) => {
setLiveGeq((prev) => ({
@@ -231,7 +231,11 @@ function GeqSection({
return success;
};
// NEW: sequential per-band reset, since hardware has no bulk reset command.
const handleBypassClick = () => {
setGeqBypass(activeChannel, !activeGeq.bypass);
};
// Sequential per-band reset, since hardware has no bulk reset command.
const handleReset = async (channel: InputChannel) => {
if (resetting) return;
setResetting(true);
@@ -251,10 +255,6 @@ function GeqSection({
const activeGeq = activeInputState.geq;
const activeLiveBands = liveGeq[activeChannel];
const handleBypassClick = () => {
setGeqBypass(activeChannel, !activeGeq.bypass);
};
const channelOptions = INPUT_ORDER.map((channel) => ({
channel,
label: dsp.state.current_config.input_states[channel].name,
@@ -228,7 +228,7 @@
.crossover-panel {
position: relative;
height: 100%;
aspect-ratio: 0.28;
aspect-ratio: 0.3;
flex: 0 0 auto;
display: flex;
@@ -6,7 +6,7 @@ import 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 { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter, Crossover } from '../../types';
import { DSP408 as DSP } from '../../../../types';
import { CrossoverFilter } from '../../types';
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
@@ -15,6 +15,15 @@ import './InputSection.css';
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
import Button from '../../../../components/Button/Button';
import MeterBar from '../../../../components/MeterBar/MeterBar';
import {
FREQUENCY_MAX,
FREQUENCY_MIN,
FREQUENCY_QUANTIZATION,
GRAPH_X_MAX,
GRAPH_X_MIN,
GRAPH_Y_MAX,
GRAPH_Y_MIN,
} from '../../constants';
const PEQ_BANDS: PEQBand[] = [
PEQBand.B1,
@@ -67,7 +76,7 @@ type SetPEQFn = (
type PeqParamKey = 'frequency' | 'q' | 'gain';
type PEQValues = {
type PEQValue = {
frequency: number;
q: number;
gain: number;
@@ -91,7 +100,7 @@ function PeqPanelSliders({
const inputState = dsp.state.current_config.input_states[channel];
const peq = inputState.peq_chain.bands[band] as PEQ;
const commit = (overrides: Partial<PEQValues>) => {
const commit = (overrides: Partial<PEQValue>) => {
const next = { ...peq, ...overrides };
return setPEQ(
{ Input: channel },
@@ -118,7 +127,7 @@ function PeqPanelSliders({
unit={param.unit}
value={peq[param.key]}
// onInput={(value) => onLiveValue(channel, param.key, value)}
onChange={(value) => commit({ [param.key]: value } as Partial<PEQValues>)}
onChange={(value) => commit({ [param.key]: value } as Partial<PEQValue>)}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<div className="input-panel__cell-label">{param.label}</div>
@@ -129,45 +138,49 @@ function PeqPanelSliders({
);
}
const FREQ_MIN = 19.7;
const FREQ_MAX = 20160;
const FREQ_POINTS = 300;
const FREQ_STEP = makeLogStep(FREQ_MIN, FREQ_MAX, FREQ_POINTS);
const FREQ_QUANTIZE = makeLogQuantize(FREQ_MIN, FREQ_MAX, FREQ_POINTS);
function CrossoverPanel({
channel,
dsp,
crossover,
title,
setCrossover,
onLiveValue,
}: {
channel: Channel;
dsp: DSP;
crossover: Crossover;
title: string;
setCrossover: (
channel: Channel,
frequency: number,
filter_type: CrossoverFilter
) => Promise<boolean>;
setCrossover: (frequency: number, filter_type: CrossoverFilter) => Promise<boolean>;
onLiveValue: (value: number) => void;
}) {
const channelState =
'Input' in channel
? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output];
const frequency = crossover.frequency;
const filter_type = crossover.slope;
const bypass = filter_type === CrossoverFilter.Bypass;
const crossover = channelState?.crossover;
const lastFilterType = useRef<CrossoverFilter>(
filter_type !== CrossoverFilter.Bypass ? filter_type : CrossoverFilter.Lk24
);
const frequency = crossover.high_pass.frequency;
const filter_type = crossover.high_pass.slope;
const bypass = false;
const commit = (overrides: {
const commit = async (overrides: {
frequency?: number;
filter_type?: CrossoverFilter;
bypass?: boolean;
}) => {
const next = { frequency, filter_type, bypass, ...overrides };
return setCrossover(channel, next.frequency, next.filter_type);
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 (
@@ -175,20 +188,24 @@ function CrossoverPanel({
<div className="crossover-panel__title">{title}</div>
<VerticalSlider
min={FREQ_MIN}
max={FREQ_MAX}
min={FREQUENCY_MIN}
max={FREQUENCY_MAX}
scale="log"
step={FREQ_STEP}
quantize={FREQ_QUANTIZE}
unit=" Hz"
step={makeLogStep(FREQUENCY_MIN, FREQUENCY_MAX, FREQUENCY_QUANTIZATION)}
quantize={makeLogQuantize(FREQUENCY_MIN, FREQUENCY_MAX, FREQUENCY_QUANTIZATION)}
unit={(value) => (value >= 1000 ? 'kHz' : 'Hz')}
value={frequency}
formatValue={(value) =>
value >= 1000 ? `${(value / 1000).toFixed(2)}` : `${Math.round(value)}`
}
onChange={(value) => commit({ frequency: value })}
onInput={(value) => onLiveValue(value)}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<select
className="crossover-panel__select"
value={filter_type}
value={bypass ? lastFilterType.current : filter_type}
onChange={(e) => commit({ filter_type: e.target.value as CrossoverFilter })}
>
{FILTER_TYPE_OPTIONS.map((opt) => (
@@ -233,7 +250,7 @@ function PeqBandList({
}) {
const inputState = dsp.state.current_config.input_states[channel];
const commit = (band: PEQBand, overrides: Partial<PEQValues>) => {
const commit = (band: PEQBand, overrides: Partial<PEQValue>) => {
const peq = inputState.peq_chain.bands[band] as PEQ;
const next = { ...peq, ...overrides };
return setPEQ(
@@ -364,76 +381,88 @@ function InputSection({
setPeq: setPeqBandFn;
setPeqBypass: (channel: Channel, bypass: boolean) => Promise<boolean>;
}) {
// UI variables
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>>>({});
// Live values
const [liveHpFreq, setLiveHpFreq] = useState<number>(FREQUENCY_MIN);
const [liveLpFreq, setLiveLpFreq] = useState<number>(FREQUENCY_MAX);
const [livePeq, setLivePeq] = useState<Partial<Record<PEQBand, PEQValue>>>({});
const inputState = dsp.state.current_config.input_states[channel];
// 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 hp = inputState.crossover.high_pass;
const lp = inputState.crossover.low_pass;
const activePeq = livePeq[activeBand] ?? (inputState.peq_chain.bands[activeBand] as PEQValues);
// Live handlers
const handleLiveHighPassValue = (value: number) => {
setLiveHpFreq(value);
};
const bypassed = hp.slope === CrossoverFilter.Bypass;
const activeFilterType = bypassed && lp.slope !== CrossoverFilter.Bypass ? lp.slope : hp.slope;
const handleLiveLowPassValue = (value: number) => {
setLiveLpFreq(value);
};
// 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 handleLivePEQValue = (band: PEQBand, value: PEQValue) => {
setLivePeq((prev) => ({
...prev,
[band]: value,
}));
};
const hpFreq = liveHpFreq ?? hp.frequency;
const lpFreq = liveLpFreq ?? lp.frequency;
// 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 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 setLowPass({ Input: channel }, freq, filterType);
if (success) {
setLiveLpFreq(freq);
}
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);
// }
// };
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 channelLive = prev[band];
if (!channelLive || !(band in channelLive)) return prev;
const next = { ...channelLive };
return { ...prev, [channel]: next };
});
}
return success;
};
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. */}
<div className="input-graph__title">{activeInputState.name} - Filters</div>
<LinearGraph
xMin={20}
xMax={20000}
yMin={-30}
yMax={6}
xMin={GRAPH_X_MIN}
xMax={GRAPH_X_MAX}
xScale="log"
yMin={GRAPH_Y_MIN}
yMax={GRAPH_Y_MAX}
yStep={3}
xLabel="Hz"
yLabel="dB"
showGrid={true}
formatX={(v) => Math.round(v).toString()}
formatY={(v) => Math.round(v).toString()}
draw={() => {}}
@@ -456,76 +485,18 @@ function InputSection({
<ChannelGroup title="CrossOver">
<CrossoverPanel
channel={{ Input: channel }}
dsp={dsp}
crossover={ActiveLowPassFilter}
title="LowPass"
setCrossover={setHighPass}
setCrossover={commitLowPass}
onLiveValue={handleLiveLowPassValue}
/>
<CrossoverPanel
channel={{ Input: channel }}
dsp={dsp}
crossover={ActiveHighPassFilter}
title="HighPass"
setCrossover={setHighPass}
setCrossover={commitHighPass}
onLiveValue={handleLiveHighPassValue}
/>
</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>
</>
+5
View File
@@ -93,6 +93,11 @@ export const GEQ_GAIN_MIN = -12;
export const GEQ_GAIN_MAX = 12;
export const GEQ_GAIN_STEP = 0.1;
// ------ Filter constants ------
export const FREQUENCY_MIN = 19.7;
export const FREQUENCY_MAX = 20160;
export const FREQUENCY_QUANTIZATION = 300;
// ------ Matrix constants ------
export const MATRIX_GAIN_MIN = -60;
export const MATRIX_GAIN_MAX = 0;
+23 -8
View File
@@ -11,6 +11,7 @@ import {
PEQFilter,
PEQBand,
InputSource,
Crossover,
} from './types';
import { SetDsps, NotifyFn, updateDSP, setPollingPaused } from '../../dsp/dspShared';
import { registerPollStopper } from '../../dsp/pollRegistry';
@@ -1144,9 +1145,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
if (!d.state) return d;
const cfg = d.state.current_config;
const lowPass = {
const lowPass: Crossover = {
frequency: freq,
filter_type: filterType,
slope: filterType,
};
if ('Input' in channel) {
@@ -1160,7 +1162,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
...cfg.input_states,
[channel.Input]: {
...cfg.input_states[channel.Input],
low_pass: lowPass,
crossover: {
...cfg.input_states[channel.Input].crossover,
low_pass: lowPass,
},
},
},
},
@@ -1178,7 +1183,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
...cfg.output_states,
[channel.Output]: {
...cfg.output_states[channel.Output],
low_pass: lowPass,
crossover: {
...cfg.output_states[channel.Output].crossover,
low_pass: lowPass,
},
},
},
},
@@ -1227,9 +1235,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
if (!d.state) return d;
const cfg = d.state.current_config;
const highPass = {
const highPass: Crossover = {
frequency: freq,
filter_type: filterType,
slope: filterType,
};
if ('Input' in channel) {
@@ -1243,7 +1252,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
...cfg.input_states,
[channel.Input]: {
...cfg.input_states[channel.Input],
high_pass: highPass,
crossover: {
...cfg.input_states[channel.Input].crossover,
high_pass: highPass,
},
},
},
},
@@ -1261,7 +1273,10 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
...cfg.output_states,
[channel.Output]: {
...cfg.output_states[channel.Output],
high_pass: highPass,
crossover: {
...cfg.output_states[channel.Output].crossover,
high_pass: highPass,
},
},
},
},