Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e151ab4f26 | ||
|
|
e28f61447f | ||
|
|
388552f1c4 |
@@ -7,11 +7,25 @@ This template should help get you started developing with Tauri, React and Types
|
|||||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
|
||||||
- Check render to improve perfoormances
|
|
||||||
- Check in meter if clip bool is limit instead
|
|
||||||
- On close disconenct all ds^p
|
|
||||||
- Ma-trix gain block sometimes
|
|
||||||
- Q out of range
|
|
||||||
|
|
||||||
- Check end to end behavioour
|
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
||||||
|
|
||||||
|
- Check render to improve perfoormances
|
||||||
|
|
||||||
|
- Check in meter if clip bool is limit instead
|
||||||
|
|
||||||
|
- On close disconenct all ds^p
|
||||||
|
|
||||||
|
- Check end to end behavioour
|
||||||
|
|
||||||
|
- Roll back on scroll when 2 scroll
|
||||||
|
|
||||||
|
- test en dto end connection
|
||||||
|
|
||||||
|
Data: 10020001052f 31313131 10032a // lock
|
||||||
|
|
||||||
|
Data: 10020001052d 31313131 100328 // unlock fail
|
||||||
|
Data: 10020100032d 3100 10031f
|
||||||
|
|
||||||
|
Data: 10020001062d 0031313131 10032b // unlock success
|
||||||
|
Data: 10020100032d 0001 10032f
|
||||||
@@ -12,6 +12,23 @@ const PENDING_FALLBACK_MS = 1000;
|
|||||||
|
|
||||||
type CommitResult = Promise<boolean | void> | boolean | void;
|
type CommitResult = Promise<boolean | void> | boolean | void;
|
||||||
|
|
||||||
|
function useNonPassiveWheel(
|
||||||
|
ref: React.RefObject<HTMLElement | null>,
|
||||||
|
handler: (e: WheelEvent) => void
|
||||||
|
) {
|
||||||
|
const handlerRef = useRef(handler);
|
||||||
|
handlerRef.current = handler;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const listener = (e: WheelEvent) => handlerRef.current(e);
|
||||||
|
el.addEventListener('wheel', listener, { passive: false });
|
||||||
|
return () => el.removeEventListener('wheel', listener);
|
||||||
|
}, [ref]);
|
||||||
|
}
|
||||||
|
|
||||||
function StepperArrows({
|
function StepperArrows({
|
||||||
onUp,
|
onUp,
|
||||||
onDown,
|
onDown,
|
||||||
@@ -80,6 +97,17 @@ function ChoiceStepperList<T>({
|
|||||||
const pendingValue = useRef<T | null>(null);
|
const pendingValue = useRef<T | null>(null);
|
||||||
const wheelTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const wheelTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const fallbackTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const fallbackTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useNonPassiveWheel(inputRef, (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.deltaY < 0 && canUp) {
|
||||||
|
stepWheel(1);
|
||||||
|
} else if (e.deltaY > 0 && canDown) {
|
||||||
|
stepWheel(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (interacting.current) {
|
if (interacting.current) {
|
||||||
@@ -180,21 +208,13 @@ function ChoiceStepperList<T>({
|
|||||||
return (
|
return (
|
||||||
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
|
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
id={id}
|
id={id}
|
||||||
className="choice-stepper__input"
|
className="choice-stepper__input"
|
||||||
value={text}
|
value={text}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onBlur={commitText}
|
onBlur={commitText}
|
||||||
onWheel={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (e.deltaY < 0 && canUp) {
|
|
||||||
stepWheel(1);
|
|
||||||
} else if (e.deltaY > 0 && canDown) {
|
|
||||||
stepWheel(-1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.currentTarget.blur();
|
e.currentTarget.blur();
|
||||||
@@ -256,6 +276,17 @@ function ChoiceStepperRange({
|
|||||||
const pendingValue = useRef<number | null>(null);
|
const pendingValue = useRef<number | null>(null);
|
||||||
const wheelTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const wheelTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const fallbackTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const fallbackTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useNonPassiveWheel(inputRef, (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.deltaY < 0 && canUp) {
|
||||||
|
stepWheel(1);
|
||||||
|
} else if (e.deltaY > 0 && canDown) {
|
||||||
|
stepWheel(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (interacting.current) {
|
if (interacting.current) {
|
||||||
@@ -344,21 +375,13 @@ function ChoiceStepperRange({
|
|||||||
return (
|
return (
|
||||||
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
|
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
id={id}
|
id={id}
|
||||||
className="choice-stepper__input"
|
className="choice-stepper__input"
|
||||||
value={text}
|
value={text}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onBlur={commitText}
|
onBlur={commitText}
|
||||||
onWheel={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (e.deltaY < 0 && canUp) {
|
|
||||||
stepWheel(1);
|
|
||||||
} else if (e.deltaY > 0 && canDown) {
|
|
||||||
stepWheel(-1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.currentTarget.blur();
|
e.currentTarget.blur();
|
||||||
|
|||||||
@@ -92,6 +92,23 @@ function formatGainDb(value: number) {
|
|||||||
return `${sign}${value.toFixed(1)} dB`;
|
return `${sign}${value.toFixed(1)} dB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useNonPassiveWheel(
|
||||||
|
ref: React.RefObject<Element | null>,
|
||||||
|
handler: (e: WheelEvent) => void
|
||||||
|
) {
|
||||||
|
const handlerRef = useRef(handler);
|
||||||
|
handlerRef.current = handler;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const listener = (e: Event) => handlerRef.current(e as WheelEvent);
|
||||||
|
el.addEventListener('wheel', listener, { passive: false });
|
||||||
|
return () => el.removeEventListener('wheel', listener);
|
||||||
|
}, [ref]);
|
||||||
|
}
|
||||||
|
|
||||||
type GainTooltipState = {
|
type GainTooltipState = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
@@ -104,11 +121,13 @@ function SignalFlowMatrix({
|
|||||||
setMatrixRouting,
|
setMatrixRouting,
|
||||||
setMatrixGain,
|
setMatrixGain,
|
||||||
setActiveSidebar,
|
setActiveSidebar,
|
||||||
|
onGainPreview,
|
||||||
}: {
|
}: {
|
||||||
dsp: DSP408;
|
dsp: DSP408;
|
||||||
setMatrixRouting: (output: OutputChannel, inputs: InputChannel[]) => Promise<boolean>;
|
setMatrixRouting: (output: OutputChannel, inputs: InputChannel[]) => Promise<boolean>;
|
||||||
setMatrixGain: (input: InputChannel, output: OutputChannel, gain: number) => Promise<boolean>;
|
setMatrixGain: (input: InputChannel, output: OutputChannel, gain: number) => Promise<boolean>;
|
||||||
setActiveSidebar: React.Dispatch<React.SetStateAction<SidebarId>>;
|
setActiveSidebar: React.Dispatch<React.SetStateAction<SidebarId>>;
|
||||||
|
onGainPreview: (input: InputChannel, output: OutputChannel, value: number | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const nInputs = INPUT_ORDER.length;
|
const nInputs = INPUT_ORDER.length;
|
||||||
const nOutputs = OUTPUT_ORDER.length;
|
const nOutputs = OUTPUT_ORDER.length;
|
||||||
@@ -137,9 +156,16 @@ function SignalFlowMatrix({
|
|||||||
});
|
});
|
||||||
const hideTooltipTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hideTooltipTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const WHEEL_COMMIT_DELAY = 250;
|
||||||
|
|
||||||
|
// key = `${input}-${output}`
|
||||||
|
const pendingGains = useRef<Map<string, number>>(new Map());
|
||||||
|
const commitTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (hideTooltipTimer.current) clearTimeout(hideTooltipTimer.current);
|
if (hideTooltipTimer.current) clearTimeout(hideTooltipTimer.current);
|
||||||
|
commitTimers.current.forEach(clearTimeout);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -151,28 +177,55 @@ function SignalFlowMatrix({
|
|||||||
void setMatrixRouting(output, next);
|
void setMatrixRouting(output, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWheelGain = (e: React.WheelEvent, output: OutputChannel, input: InputChannel) => {
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const handleWheelGain = (e: WheelEvent, output: OutputChannel, input: InputChannel) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const current = getGain(dsp, output, input);
|
|
||||||
|
const key = `${input}-${output}`;
|
||||||
|
// Base on the pending value if we're mid-wheel, otherwise the real state
|
||||||
|
const current = pendingGains.current.get(key) ?? getGain(dsp, output, input);
|
||||||
const delta = e.deltaY < 0 ? MATRIX_GAIN_STEP : -MATRIX_GAIN_STEP;
|
const delta = e.deltaY < 0 ? MATRIX_GAIN_STEP : -MATRIX_GAIN_STEP;
|
||||||
const next =
|
const next =
|
||||||
Math.round(Math.min(MATRIX_GAIN_MAX, Math.max(MATRIX_GAIN_MIN, current + delta)) * 10) / 10;
|
Math.round(Math.min(MATRIX_GAIN_MAX, Math.max(MATRIX_GAIN_MIN, current + delta)) * 10) / 10;
|
||||||
void setMatrixGain(input, output, next);
|
|
||||||
|
|
||||||
// Show a small tooltip with the new gain value right next to the cursor,
|
pendingGains.current.set(key, next);
|
||||||
// then let it fade away shortly after wheeling stops.
|
onGainPreview(input, output, next);
|
||||||
|
|
||||||
|
// Debounced send: only the final value goes out once the wheel pauses
|
||||||
|
const existing = commitTimers.current.get(key);
|
||||||
|
if (existing) clearTimeout(existing);
|
||||||
|
commitTimers.current.set(
|
||||||
|
key,
|
||||||
|
setTimeout(async () => {
|
||||||
|
commitTimers.current.delete(key);
|
||||||
|
const value = pendingGains.current.get(key);
|
||||||
|
if (value === undefined) return;
|
||||||
|
await setMatrixGain(input, output, value);
|
||||||
|
// Only release if the user hasn't wheeled again while we were awaiting
|
||||||
|
if (pendingGains.current.get(key) === value) {
|
||||||
|
pendingGains.current.delete(key);
|
||||||
|
onGainPreview(input, output, null);
|
||||||
|
}
|
||||||
|
}, WHEEL_COMMIT_DELAY)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tooltip (unchanged, shows the pending value)
|
||||||
if (hideTooltipTimer.current) clearTimeout(hideTooltipTimer.current);
|
if (hideTooltipTimer.current) clearTimeout(hideTooltipTimer.current);
|
||||||
setGainTooltip({
|
setGainTooltip({ visible: true, x: e.clientX, y: e.clientY, label: formatGainDb(next) });
|
||||||
visible: true,
|
|
||||||
x: e.clientX,
|
|
||||||
y: e.clientY,
|
|
||||||
label: formatGainDb(next),
|
|
||||||
});
|
|
||||||
hideTooltipTimer.current = setTimeout(() => {
|
hideTooltipTimer.current = setTimeout(() => {
|
||||||
setGainTooltip((t) => ({ ...t, visible: false }));
|
setGainTooltip((t) => ({ ...t, visible: false }));
|
||||||
}, GAIN_TOOLTIP_HOLD_MS);
|
}, GAIN_TOOLTIP_HOLD_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useNonPassiveWheel(svgRef, (e) => {
|
||||||
|
const hit = (e.target as Element).closest<SVGElement>('[data-wheel-route]');
|
||||||
|
if (!hit) return; // not over a connected line: let the page scroll normally
|
||||||
|
const { input, output } = hit.dataset;
|
||||||
|
if (!input || !output) return;
|
||||||
|
handleWheelGain(e, output as OutputChannel, input as InputChannel);
|
||||||
|
});
|
||||||
|
|
||||||
const routePairs = useMemo(() => {
|
const routePairs = useMemo(() => {
|
||||||
const pairs: { input: InputChannel; output: OutputChannel; i: number; j: number }[] = [];
|
const pairs: { input: InputChannel; output: OutputChannel; i: number; j: number }[] = [];
|
||||||
OUTPUT_ORDER.forEach((output, j) => {
|
OUTPUT_ORDER.forEach((output, j) => {
|
||||||
@@ -196,6 +249,7 @@ function SignalFlowMatrix({
|
|||||||
return (
|
return (
|
||||||
<div className="signal-flow">
|
<div className="signal-flow">
|
||||||
<svg
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
viewBox={`0 0 ${W} ${H}`}
|
viewBox={`0 0 ${W} ${H}`}
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
className="signal-flow__svg"
|
className="signal-flow__svg"
|
||||||
@@ -221,7 +275,9 @@ function SignalFlowMatrix({
|
|||||||
strokeWidth={LINE_HITBOX_WIDTH}
|
strokeWidth={LINE_HITBOX_WIDTH}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => handleToggle(output, input)}
|
onClick={() => handleToggle(output, input)}
|
||||||
onWheel={connected ? (e) => handleWheelGain(e, output, input) : undefined}
|
data-wheel-route={connected ? '' : undefined}
|
||||||
|
data-input={input}
|
||||||
|
data-output={output}
|
||||||
/>
|
/>
|
||||||
{/* Visible line, purely decorative — no pointer events, so it never
|
{/* Visible line, purely decorative — no pointer events, so it never
|
||||||
competes with the hitbox above for hover/click/wheel. */}
|
competes with the hitbox above for hover/click/wheel. */}
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ import { getPeqCurvePoints, getPeqPhasePoints } from '../../utils/graph/peq';
|
|||||||
import { logSpace, sumCurvePoints, sumPhaseCurvePoints } from '../../utils/graph/curve';
|
import { logSpace, sumCurvePoints, sumPhaseCurvePoints } from '../../utils/graph/curve';
|
||||||
import { getCrossoverCurvePoints, getCrossoverPhasePoints } from '../../utils/graph/crossover';
|
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';
|
type GraphView = 'both' | 'magnitude' | 'phase';
|
||||||
|
|
||||||
function GraphViewToggle({
|
function GraphViewToggle({
|
||||||
@@ -133,9 +139,11 @@ function PeqPanelSliders({
|
|||||||
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
|
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
|
||||||
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
|
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
|
||||||
|
|
||||||
|
const nearestQ = nearestQValue(peq.q);
|
||||||
|
|
||||||
const commit = (overrides: Partial<PEQ>) => {
|
const commit = (overrides: Partial<PEQ>) => {
|
||||||
const next = { ...peq, ...overrides };
|
const next = { ...peq, ...overrides };
|
||||||
return setPEQ(next.gain, next.frequency, next.q, next.filter_type, next.bypass);
|
return setPEQ(next.gain, next.frequency, overrides.q ?? nearestQ, next.filter_type, next.bypass);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -169,9 +177,9 @@ function PeqPanelSliders({
|
|||||||
max={qMax}
|
max={qMax}
|
||||||
values={PEAK_Q_VALUES}
|
values={PEAK_Q_VALUES}
|
||||||
formatValue={(v) => v.toFixed(2)}
|
formatValue={(v) => v.toFixed(2)}
|
||||||
value={peq.q}
|
value={nearestQ}
|
||||||
onChange={(value) => commit({ q: value })}
|
onChange={(value) => commit({ q: value })}
|
||||||
onInput={(value) => onLiveValue({ ...peq, q: value })}
|
onInput={(value) => onLiveValue({ ...peq, q: nearestQValue(value) })}
|
||||||
disabled={qDisabled}
|
disabled={qDisabled}
|
||||||
style={{ width: '100%', flex: '1 1 auto' }}
|
style={{ width: '100%', flex: '1 1 auto' }}
|
||||||
/>
|
/>
|
||||||
@@ -318,9 +326,16 @@ function PeqBandList({
|
|||||||
bandsOrder: PEQBand[];
|
bandsOrder: PEQBand[];
|
||||||
}) {
|
}) {
|
||||||
const commit = (band: PEQBand, overrides: Partial<PEQ>) => {
|
const commit = (band: PEQBand, overrides: Partial<PEQ>) => {
|
||||||
const peq = liveBands[band] ?? bands[band];
|
const live = liveBands[band] ?? bands[band];
|
||||||
const next = { ...peq, ...overrides };
|
const next = { ...live, ...overrides };
|
||||||
return setPEQ(band, next.gain, next.frequency, next.q, next.filter_type, next.bypass);
|
return setPEQ(
|
||||||
|
band,
|
||||||
|
next.gain,
|
||||||
|
next.frequency,
|
||||||
|
nearestQValue(next.q),
|
||||||
|
next.filter_type,
|
||||||
|
next.bypass
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -340,6 +355,7 @@ 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 qValue = nearestQValue(peq.q, qValues.length ? qValues : PEAK_Q_VALUES);
|
||||||
|
|
||||||
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
|
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
|
||||||
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
|
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
|
||||||
@@ -376,7 +392,7 @@ function PeqBandList({
|
|||||||
|
|
||||||
<ChoiceStepper
|
<ChoiceStepper
|
||||||
className="peq-band-list__stepper"
|
className="peq-band-list__stepper"
|
||||||
value={peq.q}
|
value={qValue}
|
||||||
values={qValues}
|
values={qValues}
|
||||||
formatLabel={(v) => v.toFixed(2)}
|
formatLabel={(v) => v.toFixed(2)}
|
||||||
disabled={qDisabled}
|
disabled={qDisabled}
|
||||||
@@ -401,7 +417,14 @@ function PeqBandList({
|
|||||||
formatLabel={(v) =>
|
formatLabel={(v) =>
|
||||||
PEQ_FILTERS_LABEL.find((opt) => opt.value === v)?.label ?? String(v)
|
PEQ_FILTERS_LABEL.find((opt) => opt.value === v)?.label ?? String(v)
|
||||||
}
|
}
|
||||||
onChange={(value) => commit(band, { filter_type: value, q: 3.0 })}
|
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),
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -499,7 +522,7 @@ function ChannelSection({
|
|||||||
// Skip only if already flat — nothing to change.
|
// Skip only if already flat — nothing to change.
|
||||||
if (current.gain === 0 && current.filter_type === PEQFilter.Peak) continue;
|
if (current.gain === 0 && current.filter_type === PEQFilter.Peak) continue;
|
||||||
|
|
||||||
await commitPEQ(band, 0, current.frequency, current.q, PEQFilter.Peak, current.bypass);
|
await commitPEQ(band, 0, current.frequency, nearestQValue(3.0, PEAK_Q_VALUES), PEQFilter.Peak, current.bypass);
|
||||||
}
|
}
|
||||||
|
|
||||||
setResetting(false);
|
setResetting(false);
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ function CompressorSection({ dsp, set_compressor }: { dsp: DSP; set_compressor:
|
|||||||
const steps = 120;
|
const steps = 120;
|
||||||
const points: [number, number][] = [];
|
const points: [number, number][] = [];
|
||||||
for (let i = 0; i <= steps; i++) {
|
for (let i = 0; i <= steps; i++) {
|
||||||
const x = -100 + (100 * i) / steps;
|
const x = -100 + (120 * i) / steps;
|
||||||
points.push([x, compressorOutputDb(x, threshold, ratioNumeric, knee)]);
|
points.push([x, compressorOutputDb(x, threshold, ratioNumeric, knee)]);
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
@@ -227,9 +227,9 @@ function CompressorSection({ dsp, set_compressor }: { dsp: DSP; set_compressor:
|
|||||||
|
|
||||||
<LinearGraph
|
<LinearGraph
|
||||||
xMin={-100}
|
xMin={-100}
|
||||||
xMax={0}
|
xMax={20}
|
||||||
yMin={-100}
|
yMin={-100}
|
||||||
yMax={0}
|
yMax={20}
|
||||||
xLabel="in dB"
|
xLabel="in dB"
|
||||||
yLabel="out dB"
|
yLabel="out dB"
|
||||||
formatX={(v) => Math.round(v).toString()}
|
formatX={(v) => Math.round(v).toString()}
|
||||||
@@ -240,7 +240,7 @@ function CompressorSection({ dsp, set_compressor }: { dsp: DSP; set_compressor:
|
|||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.setLineDash([4, 4]);
|
ctx.setLineDash([4, 4]);
|
||||||
const [ux0, uy0] = toPx(-100, -100);
|
const [ux0, uy0] = toPx(-100, -100);
|
||||||
const [ux1, uy1] = toPx(0, 0);
|
const [ux1, uy1] = toPx(20, 20);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(ux0, uy0);
|
ctx.moveTo(ux0, uy0);
|
||||||
ctx.lineTo(ux1, uy1);
|
ctx.lineTo(ux1, uy1);
|
||||||
@@ -252,7 +252,7 @@ function CompressorSection({ dsp, set_compressor }: { dsp: DSP; set_compressor:
|
|||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.setLineDash([2, 3]);
|
ctx.setLineDash([2, 3]);
|
||||||
const [tx0, ty0] = toPx(activeComp.threshold, -100);
|
const [tx0, ty0] = toPx(activeComp.threshold, -100);
|
||||||
const [tx1, ty1] = toPx(activeComp.threshold, 0);
|
const [tx1, ty1] = toPx(activeComp.threshold, 20);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(tx0, ty0);
|
ctx.moveTo(tx0, ty0);
|
||||||
ctx.lineTo(tx1, ty1);
|
ctx.lineTo(tx1, ty1);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
import { SidebarId } from '../../constants';
|
import { SidebarId } from '../../constants';
|
||||||
import SignalFlowMatrix from '../../../../components/SignalFlowMatrix/SignalFlowMatrix';
|
import SignalFlowMatrix from '../../../../components/SignalFlowMatrix/SignalFlowMatrix';
|
||||||
|
import { useState } from 'react';
|
||||||
import './Matrixsection.css';
|
import './Matrixsection.css';
|
||||||
|
|
||||||
const GAIN_VALUES: number[] = (() => {
|
const GAIN_VALUES: number[] = (() => {
|
||||||
@@ -39,6 +40,12 @@ function getRoutedInputs(dsp: DSP, output: OutputChannel): InputChannel[] {
|
|||||||
return getMatrixRoutes(dsp, output).connected;
|
return getMatrixRoutes(dsp, output).connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapToGain(g: number): number {
|
||||||
|
return GAIN_VALUES.reduce((best, v) =>
|
||||||
|
Math.abs(v - g) < Math.abs(best - g) ? v : best
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MatrixSection({
|
function MatrixSection({
|
||||||
dsp,
|
dsp,
|
||||||
setMatrixRouting,
|
setMatrixRouting,
|
||||||
@@ -68,6 +75,21 @@ function MatrixSection({
|
|||||||
void setMatrixGain(input, output, gain);
|
void setMatrixGain(input, output, gain);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [liveGains, setLiveGains] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
// value = number while wheeling, null once the commit is done
|
||||||
|
const previewGain = (input: InputChannel, output: OutputChannel, value: number | null) => {
|
||||||
|
const key = `${input}-${output}`;
|
||||||
|
setLiveGains((prev) => {
|
||||||
|
if (value === null) {
|
||||||
|
if (!(key in prev)) return prev;
|
||||||
|
const { [key]: _removed, ...rest } = prev;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
return { ...prev, [key]: value };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="matrix-flow">
|
<div className="matrix-flow">
|
||||||
@@ -76,6 +98,7 @@ function MatrixSection({
|
|||||||
setMatrixRouting={setMatrixRouting}
|
setMatrixRouting={setMatrixRouting}
|
||||||
setMatrixGain={setMatrixGain}
|
setMatrixGain={setMatrixGain}
|
||||||
setActiveSidebar={setActiveSidebar}
|
setActiveSidebar={setActiveSidebar}
|
||||||
|
onGainPreview={previewGain}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="matrix-section">
|
<div className="matrix-section">
|
||||||
@@ -90,7 +113,7 @@ function MatrixSection({
|
|||||||
<div className="matrix-panel__rows">
|
<div className="matrix-panel__rows">
|
||||||
{INPUT_ORDER.map((input) => {
|
{INPUT_ORDER.map((input) => {
|
||||||
const connected = isRouted(dsp, output, input);
|
const connected = isRouted(dsp, output, input);
|
||||||
const gain = getLinkGain(dsp, output, input);
|
const gain = liveGains[`${input}-${output}`] ?? getLinkGain(dsp, output, input);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="matrix-panel__row" key={String(input)}>
|
<div className="matrix-panel__row" key={String(input)}>
|
||||||
@@ -111,7 +134,7 @@ function MatrixSection({
|
|||||||
|
|
||||||
<ChoiceStepper
|
<ChoiceStepper
|
||||||
className="matrix-gain-stepper"
|
className="matrix-gain-stepper"
|
||||||
value={gain}
|
value={snapToGain(gain)}
|
||||||
values={GAIN_VALUES}
|
values={GAIN_VALUES}
|
||||||
formatLabel={(v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
|
formatLabel={(v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
|
||||||
onChange={(g) => handleGainChange(output, input, g)}
|
onChange={(g) => handleGainChange(output, input, g)}
|
||||||
|
|||||||
@@ -37,10 +37,8 @@ function MiscellaneousPanel({
|
|||||||
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
|
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
|
||||||
const [isCopyModalOpen, setIsCopyModalOpen] = useState(false);
|
const [isCopyModalOpen, setIsCopyModalOpen] = useState(false);
|
||||||
|
|
||||||
const [inputSource, setInputSourceState] = useState<InputSource>({
|
const [liveInputSource, setLiveInputSource] = useState<InputSource | null>(null);
|
||||||
type: InputType.Analog,
|
const inputSource = liveInputSource ?? dsp.state.current_config.input_source;
|
||||||
frequency: DiscreteFrequency.F20,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
@@ -51,18 +49,19 @@ function MiscellaneousPanel({
|
|||||||
frequency: type === InputType.SineWave ? inputSource.frequency : DiscreteFrequency.F20,
|
frequency: type === InputType.SineWave ? inputSource.frequency : DiscreteFrequency.F20,
|
||||||
};
|
};
|
||||||
|
|
||||||
setInputSourceState(nextSource);
|
setLiveInputSource(nextSource);
|
||||||
await setInputSource(nextSource);
|
const success = await setInputSource(nextSource);
|
||||||
|
|
||||||
|
if (success) setLiveInputSource(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFrequencyChange = async (frequency: DiscreteFrequency) => {
|
const handleFrequencyChange = async (frequency: DiscreteFrequency) => {
|
||||||
const nextSource: InputSource = {
|
const nextSource: InputSource = { ...inputSource, frequency };
|
||||||
...inputSource,
|
|
||||||
frequency,
|
|
||||||
};
|
|
||||||
|
|
||||||
setInputSourceState(nextSource);
|
setLiveInputSource(nextSource);
|
||||||
await setInputSource(nextSource);
|
const success = await setInputSource(nextSource);
|
||||||
|
|
||||||
|
if (success) setLiveInputSource(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFactoryReset = () => {
|
const handleFactoryReset = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user