feat: disable inputs if peq dont use them

This commit is contained in:
2026-09-23 20:03:39 +02:00
parent e1d6398d57
commit d48155683b
12 changed files with 174 additions and 110 deletions
+7 -43
View File
@@ -113,25 +113,14 @@ export function BoltIcon(props: React.SVGProps<SVGSVGElement>) {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<circle <circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" />
cx="12"
cy="12"
r="4"
stroke="currentColor"
strokeWidth="2"
/>
</svg> </svg>
); );
} }
export function GainIcon(props: React.SVGProps<SVGSVGElement>) { export function GainIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M3 12H7M17 12H21" d="M3 12H7M17 12H21"
stroke="currentColor" stroke="currentColor"
@@ -152,12 +141,7 @@ export function GainIcon(props: React.SVGProps<SVGSVGElement>) {
export function GateIcon(props: React.SVGProps<SVGSVGElement>) { export function GateIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M3 3V21H21" d="M3 3V21H21"
stroke="currentColor" stroke="currentColor"
@@ -179,12 +163,7 @@ export function GateIcon(props: React.SVGProps<SVGSVGElement>) {
export function CompressorIcon(props: React.SVGProps<SVGSVGElement>) { export function CompressorIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M3 3V21H21" d="M3 3V21H21"
stroke="currentColor" stroke="currentColor"
@@ -206,12 +185,7 @@ export function CompressorIcon(props: React.SVGProps<SVGSVGElement>) {
export function LimiterIcon(props: React.SVGProps<SVGSVGElement>) { export function LimiterIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M3 3V21H21" d="M3 3V21H21"
stroke="currentColor" stroke="currentColor"
@@ -233,12 +207,7 @@ export function LimiterIcon(props: React.SVGProps<SVGSVGElement>) {
export function DelayIcon(props: React.SVGProps<SVGSVGElement>) { export function DelayIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path <path
d="M4 5V19M12 8.5V15.5M20 11V13" d="M4 5V19M12 8.5V15.5M20 11V13"
stroke="currentColor" stroke="currentColor"
@@ -252,12 +221,7 @@ export function DelayIcon(props: React.SVGProps<SVGSVGElement>) {
export function MatrixIcon(props: React.SVGProps<SVGSVGElement>) { export function MatrixIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M3 7C11 7 11 4 21 4" /> <path d="M3 7C11 7 11 4 21 4" />
<path d="M3 7C11 7 11 20 21 20" /> <path d="M3 7C11 7 11 20 21 20" />
<path d="M3 17C11 17 11 12 21 12" /> <path d="M3 17C11 17 11 12 21 12" />
+42 -42
View File
@@ -40,35 +40,35 @@ function ChoiceSelect<T extends string>({
const listboxRef = useRef<HTMLUListElement>(null); const listboxRef = useRef<HTMLUListElement>(null);
const suppressNextClick = useRef(false); const suppressNextClick = useRef(false);
useEffect(() => { useEffect(() => {
function handleMouseDown(e: MouseEvent) { function handleMouseDown(e: MouseEvent) {
const target = e.target as Node; const target = e.target as Node;
const clickedOutside = const clickedOutside =
rootRef.current && rootRef.current &&
!rootRef.current.contains(target) && !rootRef.current.contains(target) &&
!listboxRef.current?.contains(target); !listboxRef.current?.contains(target);
if (clickedOutside && open) { if (clickedOutside && open) {
setOpen(false); setOpen(false);
suppressNextClick.current = true; suppressNextClick.current = true;
}
} }
}
function handleClickCapture(e: MouseEvent) { function handleClickCapture(e: MouseEvent) {
if (suppressNextClick.current) { if (suppressNextClick.current) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
suppressNextClick.current = false; suppressNextClick.current = false;
}
} }
}
document.addEventListener('mousedown', handleMouseDown); document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('click', handleClickCapture, true); // capture phase document.addEventListener('click', handleClickCapture, true); // capture phase
return () => { return () => {
document.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('click', handleClickCapture, true); document.removeEventListener('click', handleClickCapture, true);
}; };
}, [open]); }, [open]);
// Re-check placement/position on scroll/resize while open — the trigger's // Re-check placement/position on scroll/resize while open — the trigger's
// position can change (e.g. inside a scrollable panel), and since the // position can change (e.g. inside a scrollable panel), and since the
@@ -93,27 +93,27 @@ function ChoiceSelect<T extends string>({
}; };
function updatePlacement() { function updatePlacement() {
const el = triggerRef.current; const el = triggerRef.current;
if (!el) return; if (!el) return;
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom; const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top; const spaceAbove = rect.top;
const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow; const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow;
setDropUp(needsFlip); setDropUp(needsFlip);
// Capture the trigger's resolved font-size (its cqh has already resolved // Capture the trigger's resolved font-size (its cqh has already resolved
// correctly here, since it's still inside the real container tree) and // correctly here, since it's still inside the real container tree) and
// carry it as a plain px value into the portal, where cqh would break. // carry it as a plain px value into the portal, where cqh would break.
const fontSize = parseFloat(getComputedStyle(el).fontSize); const fontSize = parseFloat(getComputedStyle(el).fontSize);
setCoords({ setCoords({
top: needsFlip ? rect.top - LISTBOX_GAP : rect.bottom + LISTBOX_GAP, top: needsFlip ? rect.top - LISTBOX_GAP : rect.bottom + LISTBOX_GAP,
left: rect.left, left: rect.left,
width: rect.width, width: rect.width,
fontSize, fontSize,
}); });
} }
function openList() { function openList() {
if (disabled) return; if (disabled) return;
@@ -78,9 +78,15 @@
} }
.choice-stepper[data-disabled] { .choice-stepper[data-disabled] {
opacity: 0.5; opacity: 0.45;
filter: grayscale(1);
} }
.choice-stepper__input:disabled { .choice-stepper__input:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.choice-stepper[data-disabled] .choice-stepper__input,
.choice-stepper[data-disabled] .choice-stepper__arrow-btn {
color: var(--border-hairline);
}
@@ -146,3 +146,26 @@
font-family: inherit; font-family: inherit;
color: inherit; color: inherit;
} }
.gauge-viewport.is-disabled {
opacity: 0.45;
filter: grayscale(1);
}
.gauge-viewport.is-disabled .slider {
cursor: not-allowed;
}
.gauge-viewport.is-disabled .thumb {
border-color: var(--border-hairline);
box-shadow: none;
}
.gauge-viewport.is-disabled .value {
color: var(--border-hairline);
box-shadow: none;
}
.value input:disabled {
cursor: not-allowed;
}
@@ -38,6 +38,9 @@ type VerticalSliderProps = {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
/** Greys out the slider and blocks all interaction. */
disabled?: boolean;
}; };
const defaultStep: StepFn = () => 0.1; const defaultStep: StepFn = () => 0.1;
@@ -174,6 +177,7 @@ export default function VerticalSlider({
formatValue, formatValue,
showTicks = true, showTicks = true,
scale = 'linear', scale = 'linear',
disabled = false,
className, className,
style, style,
}: VerticalSliderProps) { }: VerticalSliderProps) {
@@ -240,6 +244,26 @@ export default function VerticalSlider({
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
useEffect(() => {
if (!disabled) return;
dragging.current = false;
keyboardEditing.current = false;
keyboardValue.current = null;
wheelEditing.current = false;
wheelValue.current = null;
if (wheelCommitTimeout.current) {
clearTimeout(wheelCommitTimeout.current);
wheelCommitTimeout.current = null;
}
setEditingText(null);
setDisplayValue(value);
dragValue.current = value;
onInput?.(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disabled]);
const decimals = useMemo(() => { const decimals = useMemo(() => {
if (listValues) return Math.min(3, getDecimals(displayValue)); if (listValues) return Math.min(3, getDecimals(displayValue));
return Math.min(3, getDecimals(step(displayValue))); return Math.min(3, getDecimals(step(displayValue)));
@@ -273,6 +297,8 @@ export default function VerticalSlider({
} }
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
if (disabled) return;
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
return; return;
} }
@@ -307,6 +333,8 @@ export default function VerticalSlider({
} }
async function handleKeyUp() { async function handleKeyUp() {
if (disabled) return;
if (keyboardValue.current == null) return; if (keyboardValue.current == null) return;
const next = keyboardValue.current; const next = keyboardValue.current;
@@ -323,6 +351,7 @@ export default function VerticalSlider({
} }
function handleWheel(e: WheelEvent) { function handleWheel(e: WheelEvent) {
if (disabled) return;
e.preventDefault(); e.preventDefault();
wheelEditing.current = true; wheelEditing.current = true;
@@ -393,6 +422,8 @@ export default function VerticalSlider({
} }
function startDrag(e: React.PointerEvent<HTMLDivElement>) { function startDrag(e: React.PointerEvent<HTMLDivElement>) {
if (disabled) return;
const target = e.currentTarget; const target = e.currentTarget;
dragging.current = true; dragging.current = true;
target.setPointerCapture(e.pointerId); target.setPointerCapture(e.pointerId);
@@ -427,6 +458,11 @@ export default function VerticalSlider({
} }
async function commitEditing() { async function commitEditing() {
if (disabled) {
setEditingText(null);
return;
}
if (editingText !== null && editingText !== '' && editingText !== '-') { if (editingText !== null && editingText !== '' && editingText !== '-') {
const parsed = Number(editingText); const parsed = Number(editingText);
@@ -452,7 +488,10 @@ export default function VerticalSlider({
const percent = valueToPercent(displayValue, min, max, scale, listValues); const percent = valueToPercent(displayValue, min, max, scale, listValues);
return ( return (
<div className={['gauge-viewport', className].filter(Boolean).join(' ')} style={style}> <div
className={['gauge-viewport', disabled && 'is-disabled', className].filter(Boolean).join(' ')}
style={style}
>
<div <div
className={['gauge-track', !showTicks && 'no-ticks'].filter(Boolean).join(' ')} className={['gauge-track', !showTicks && 'no-ticks'].filter(Boolean).join(' ')}
ref={trackRef} ref={trackRef}
@@ -468,7 +507,12 @@ export default function VerticalSlider({
<div <div
className="slider" className="slider"
ref={sliderRef} ref={sliderRef}
tabIndex={0} role="slider"
aria-disabled={disabled}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={displayValue}
tabIndex={disabled ? -1 : 0}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
onPointerDown={startDrag} onPointerDown={startDrag}
@@ -494,6 +538,7 @@ export default function VerticalSlider({
ref={inputRef} ref={inputRef}
type="text" type="text"
inputMode="decimal" inputMode="decimal"
disabled={disabled}
value={editingText ?? formattedValue} value={editingText ?? formattedValue}
style={{ width: '100%' }} style={{ width: '100%' }}
onFocus={() => setEditingText(value.toFixed(decimals))} // plain number while editing onFocus={() => setEditingText(value.toFixed(decimals))} // plain number while editing
@@ -11,6 +11,8 @@ import {
PEQ_FILTERS_LABEL, PEQ_FILTERS_LABEL,
PEQ_Q_RANGES, PEQ_Q_RANGES,
PEAK_Q_VALUES, PEAK_Q_VALUES,
PEQ_Q_UNUSED,
PEQ_GAIN_USED,
} from '../../constants'; } from '../../constants';
import { Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types'; import { Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types';
import { DSP408 as DSP } from '../../../../types'; import { DSP408 as DSP } from '../../../../types';
@@ -128,6 +130,8 @@ function PeqPanelSliders({
onLiveValue: (value: PEQ) => void; onLiveValue: (value: PEQ) => void;
}) { }) {
const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type]; 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 commit = (overrides: Partial<PEQ>) => { const commit = (overrides: Partial<PEQ>) => {
const next = { ...peq, ...overrides }; const next = { ...peq, ...overrides };
@@ -168,6 +172,7 @@ function PeqPanelSliders({
value={peq.q} value={peq.q}
onChange={(value) => commit({ q: value })} onChange={(value) => commit({ q: value })}
onInput={(value) => onLiveValue({ ...peq, q: value })} onInput={(value) => onLiveValue({ ...peq, q: value })}
disabled={qDisabled}
style={{ width: '100%', flex: '1 1 auto' }} style={{ width: '100%', flex: '1 1 auto' }}
/> />
@@ -184,6 +189,7 @@ function PeqPanelSliders({
formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))} formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))}
onChange={(value) => commit({ gain: value })} onChange={(value) => commit({ gain: value })}
onInput={(value) => onLiveValue({ ...peq, gain: value })} onInput={(value) => onLiveValue({ ...peq, gain: value })}
disabled={gainDisabled}
style={{ width: '100%', flex: '1 1 auto' }} style={{ width: '100%', flex: '1 1 auto' }}
/> />
@@ -335,6 +341,9 @@ function PeqBandList({
const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type]; const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type];
const qValues = PEAK_Q_VALUES.filter((q) => q >= qMin && q <= qMax); const qValues = PEAK_Q_VALUES.filter((q) => q >= qMin && q <= qMax);
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
return ( return (
<div <div
className={`peq-band-list__row${isActive ? ' peq-band-list__row--active' : ''}`} className={`peq-band-list__row${isActive ? ' peq-band-list__row--active' : ''}`}
@@ -370,6 +379,7 @@ function PeqBandList({
value={peq.q} value={peq.q}
values={qValues} values={qValues}
formatLabel={(v) => v.toFixed(2)} formatLabel={(v) => v.toFixed(2)}
disabled={qDisabled}
onChange={(value) => commit(band, { q: value })} onChange={(value) => commit(band, { q: value })}
/> />
@@ -380,6 +390,7 @@ function PeqBandList({
max={12} max={12}
step={0.1} step={0.1}
formatLabel={(v) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`} formatLabel={(v) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
disabled={gainDisabled}
onChange={(value) => commit(band, { gain: value })} onChange={(value) => commit(band, { gain: value })}
/> />
@@ -715,8 +726,8 @@ function ChannelSection({
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}> <div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
<h3>Reset {activeChannelState.name} PEQ?</h3> <h3>Reset {activeChannelState.name} PEQ?</h3>
<p> <p>
This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB, This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB, Peak).
Peak). This action cannot be undone. This action cannot be undone.
</p> </p>
<div className="confirm-modal__actions"> <div className="confirm-modal__actions">
@@ -1,6 +1,17 @@
import { useState } from 'react'; import { useState } from 'react';
import { Sidebar, SidebarItem } from '../../../../components/Sidebar/Sidebar'; import { Sidebar, SidebarItem } from '../../../../components/Sidebar/Sidebar';
import { BoltIcon, CompressorIcon, DelayIcon, GainIcon, GateIcon, InfoIcon, LimiterIcon, MatrixIcon, SlidersVerticalIcon, TextIcon } from '../../../../assets/icons/AudioIcons'; import {
BoltIcon,
CompressorIcon,
DelayIcon,
GainIcon,
GateIcon,
InfoIcon,
LimiterIcon,
MatrixIcon,
SlidersVerticalIcon,
TextIcon,
} from '../../../../assets/icons/AudioIcons';
import ConnectionPanel from '../ConnectionSection/ConnectionSection'; import ConnectionPanel from '../ConnectionSection/ConnectionSection';
import GainSection from '../GainSection/GainSection'; import GainSection from '../GainSection/GainSection';
import CompressorSection from '../CompressorSection/CompressorSection'; import CompressorSection from '../CompressorSection/CompressorSection';
@@ -97,9 +97,7 @@ function getInputChannelCurves(
): ChannelCurves { ): ChannelCurves {
const gain = liveGain ?? state.gain; const gain = liveGain ?? state.gain;
const geqValues = GEQ_BANDS_LABEL.map( const geqValues = GEQ_BANDS_LABEL.map(({ frequency }) => state.geq.gains[frequency] ?? 0);
({ frequency }) => state.geq.gains[frequency] ?? 0
);
const geq = state.geq.bypass const geq = state.geq.bypass
? [] ? []
@@ -109,17 +107,9 @@ function getInputChannelCurves(
q: GEQ_CURVE_Q, q: GEQ_CURVE_Q,
}); });
const peqCrossover = getPeqCrossoverPoints( const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover);
freqs,
state.peq_chain,
state.crossover
);
const total = sumCurvePoints([ const total = sumCurvePoints([geq, peqCrossover, getGainCurvePoints(freqs, gain)]);
geq,
peqCrossover,
getGainCurvePoints(freqs, gain),
]);
return { geq, peqCrossover, total }; return { geq, peqCrossover, total };
} }
@@ -112,7 +112,7 @@ function MiscellaneousPanel({
value={inputSource.type} value={inputSource.type}
options={INPUT_TYPES} options={INPUT_TYPES}
onChange={handleInputTypeChange} onChange={handleInputTypeChange}
style={{color: 'var(--font-primary)'}} style={{ color: 'var(--font-primary)' }}
/> />
</div> </div>
@@ -132,7 +132,7 @@ function MiscellaneousPanel({
return `${frequency.slice(1)} Hz`; return `${frequency.slice(1)} Hz`;
}} }}
onChange={handleFrequencyChange} onChange={handleFrequencyChange}
style={{color: 'var(--font-primary)'}} style={{ color: 'var(--font-primary)' }}
/> />
</div> </div>
)} )}
+14
View File
@@ -225,6 +225,20 @@ export const PEQ_FILTERS_LABEL: { value: PEQFilter; label: string }[] = [
{ value: PEQFilter.AllPass2, label: 'All Pass 2' }, { value: PEQFilter.AllPass2, label: 'All Pass 2' },
]; ];
/** Filter types that ignore Q (first-order filters). */
export const PEQ_Q_UNUSED = new Set<PEQFilter>([
PEQFilter.Lp6Db,
PEQFilter.Hp6Db,
PEQFilter.AllPass1,
]);
/** Filter types that ignore gain (everything except Peak and the shelves). */
export const PEQ_GAIN_USED = new Set<PEQFilter>([
PEQFilter.Peak,
PEQFilter.LowShelf,
PEQFilter.HighShelf,
]);
export const FREQUENCY_MIN = 19.7; export const FREQUENCY_MIN = 19.7;
export const FREQUENCY_MAX = 20160; export const FREQUENCY_MAX = 20160;
export const FREQUENCY_QUANTIZATION = 300; export const FREQUENCY_QUANTIZATION = 300;
+1 -1
View File
@@ -773,7 +773,7 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
await updateDSPState(); await updateDSPState();
notify('Channels copied successfully', 'success', dsp.name) notify('Channels copied successfully', 'success', dsp.name);
return true; return true;
} catch (err) { } catch (err) {