feat: update
This commit is contained in:
@@ -3,11 +3,9 @@ import { InputChannel, Channel, OutputChannel } from '../../types';
|
||||
import { DSP408 as DSP } from '../../../../types';
|
||||
import Button from '../../../../components/Button/Button';
|
||||
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
||||
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
|
||||
import './GainSection.css';
|
||||
import { DiscreteFrequency } from '../../types';
|
||||
import {
|
||||
DISCRETE_FREQUENCIES,
|
||||
GEQ_BANDS_LABEL,
|
||||
GRAPH_X_MAX,
|
||||
GRAPH_X_MIN,
|
||||
GRAPH_Y_MAX,
|
||||
@@ -16,7 +14,13 @@ import {
|
||||
OUTPUT_ORDER,
|
||||
} from '../../constants';
|
||||
import { useState } from 'react';
|
||||
// import { OverlayChannelSpec, buildInputMagnitudeOverlayDraw } from '../../utils/overlayCurve';
|
||||
import LinearGraph from '../../../../components/LinearGraph/LinearGraph';
|
||||
import { drawCurves } from '../../utils/graph/draw';
|
||||
import { Curve, CurvePoint, sumCurvePoints } from '../../utils/graph/curve';
|
||||
import { getGeqCurvePoints } from '../../utils/graph/geq';
|
||||
import { getPeqCurvePoints } from '../../utils/graph/peq';
|
||||
import { getCrossoverCurvePoints } from '../../utils/graph/crossover';
|
||||
import { logSpace } from '../../utils/graph/curve';
|
||||
|
||||
const CHANNEL_COLORS = [
|
||||
'#4ade80',
|
||||
@@ -29,6 +33,9 @@ const CHANNEL_COLORS = [
|
||||
'#38bdf8',
|
||||
];
|
||||
|
||||
const GEQ_CURVE_Q = 5;
|
||||
const Y_AXIS_STEP = 3; // must match the graph's yStep
|
||||
|
||||
function buildColorMap<T extends string>(channels: T[]): Record<T, string> {
|
||||
const map = {} as Record<T, string>;
|
||||
channels.forEach((ch, i) => {
|
||||
@@ -37,6 +44,127 @@ function buildColorMap<T extends string>(channels: T[]): Record<T, string> {
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---- Curve construction -----------------------------------------------
|
||||
|
||||
type ChannelCurves = {
|
||||
geq: CurvePoint[];
|
||||
peqCrossover: CurvePoint[];
|
||||
total: CurvePoint[];
|
||||
};
|
||||
|
||||
function getGainCurvePoints(freqs: number[], gainDb: number): CurvePoint[] {
|
||||
return freqs.map((x) => ({ x, y: gainDb }));
|
||||
}
|
||||
|
||||
function getPeqCrossoverPoints(
|
||||
freqs: number[],
|
||||
peqChain: { bands: Record<string, unknown> },
|
||||
crossover: {
|
||||
high_pass: { frequency: number; slope: unknown };
|
||||
low_pass: { frequency: number; slope: unknown };
|
||||
}
|
||||
) {
|
||||
return sumCurvePoints([
|
||||
getPeqCurvePoints({ freqs, bands: peqChain.bands as any }),
|
||||
getCrossoverCurvePoints({
|
||||
freqs,
|
||||
highPass: { frequency: crossover.high_pass.frequency, filter_type: crossover.high_pass.slope as any },
|
||||
lowPass: { frequency: crossover.low_pass.frequency, filter_type: crossover.low_pass.slope as any },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function getInputChannelCurves(
|
||||
state: DSP['state']['current_config']['input_states'][InputChannel],
|
||||
freqs: number[],
|
||||
liveGain: number | undefined
|
||||
): ChannelCurves {
|
||||
const gain = liveGain ?? state.gain;
|
||||
const geqValues = GEQ_BANDS_LABEL.map(({ frequency }) => state.geq.gains[frequency] ?? 0);
|
||||
const geq = getGeqCurvePoints({ freqs, values: geqValues, q: GEQ_CURVE_Q });
|
||||
const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover);
|
||||
const total = sumCurvePoints([geq, peqCrossover, getGainCurvePoints(freqs, gain)]);
|
||||
|
||||
return { geq, peqCrossover, total };
|
||||
}
|
||||
|
||||
function getOutputChannelCurves(
|
||||
state: DSP['state']['current_config']['output_states'][OutputChannel],
|
||||
freqs: number[],
|
||||
liveGain: number | undefined
|
||||
): ChannelCurves {
|
||||
const gain = liveGain ?? state.gain;
|
||||
const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover);
|
||||
const total = sumCurvePoints([peqCrossover, getGainCurvePoints(freqs, gain)]);
|
||||
|
||||
return { geq: [], peqCrossover, total };
|
||||
}
|
||||
|
||||
// fill:false everywhere — with multiple overlapping curves the fill/fade
|
||||
// just muddies things; stroke-only makes each channel/stage legible.
|
||||
function buildCurvesForChannels<T extends string>(
|
||||
channels: T[],
|
||||
selected: Set<T>,
|
||||
colors: Record<T, string>,
|
||||
showStages: boolean,
|
||||
getCurves: (channel: T) => ChannelCurves
|
||||
): Curve[] {
|
||||
const curves: Curve[] = [];
|
||||
|
||||
channels.forEach((channel) => {
|
||||
if (!selected.has(channel)) return;
|
||||
const { geq, peqCrossover, total } = getCurves(channel);
|
||||
const color = colors[channel];
|
||||
|
||||
if (showStages) {
|
||||
if (geq.length > 0) {
|
||||
curves.push({
|
||||
points: geq,
|
||||
style: { strokeColor: color, fill: false, dimmed: true, lineWidth: 1 },
|
||||
});
|
||||
}
|
||||
if (peqCrossover.length > 0) {
|
||||
curves.push({
|
||||
points: peqCrossover,
|
||||
style: { strokeColor: color, fill: false, dimmed: true, lineWidth: 1 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
curves.push({ points: total, style: { strokeColor: color, fill: false, lineWidth: 2 } });
|
||||
});
|
||||
|
||||
return curves;
|
||||
}
|
||||
|
||||
// Expands [baseMin, baseMax] outward to fit whatever the curves actually
|
||||
// contain (e.g. gain + geq + peq stacking past the nominal graph range),
|
||||
// snapped to the axis step with one step of headroom so nothing touches
|
||||
// the plot edge. Never shrinks below the base range.
|
||||
function computeYAxisBounds(
|
||||
curves: Curve[],
|
||||
baseMin: number,
|
||||
baseMax: number,
|
||||
step: number
|
||||
): { yMin: number; yMax: number } {
|
||||
let dataMax = baseMax;
|
||||
let dataMin = baseMin;
|
||||
|
||||
for (const curve of curves) {
|
||||
for (const { y } of curve.points) {
|
||||
if (y > dataMax) dataMax = y;
|
||||
if (y < dataMin) dataMin = y;
|
||||
}
|
||||
}
|
||||
|
||||
const yMax = Math.ceil((dataMax + step) / step) * step;
|
||||
const yMin = Math.floor((dataMin - step) / step) * step;
|
||||
|
||||
return { yMin, yMax };
|
||||
}
|
||||
|
||||
// ---- Panels -------------------------------------------------------------
|
||||
|
||||
function GainPanel({
|
||||
dsp,
|
||||
channel,
|
||||
@@ -44,6 +172,7 @@ function GainPanel({
|
||||
setGain,
|
||||
setMute,
|
||||
setInverse,
|
||||
onLiveGain,
|
||||
}: {
|
||||
dsp: DSP;
|
||||
channel: Channel;
|
||||
@@ -51,6 +180,7 @@ function GainPanel({
|
||||
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||
onLiveGain: (value: number | null) => void;
|
||||
}) {
|
||||
const channelState =
|
||||
'Input' in channel
|
||||
@@ -69,6 +199,18 @@ function GainPanel({
|
||||
await setInverse(channel, !inverted);
|
||||
};
|
||||
|
||||
const handleLiveValue = (value: number) => {
|
||||
onLiveGain(value);
|
||||
};
|
||||
|
||||
const commitGain = async (value: number) => {
|
||||
const success = await setGain(channel, value);
|
||||
if (success) {
|
||||
onLiveGain(null);
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gain-panel">
|
||||
<div className="gain-panel__title">{title}</div>
|
||||
@@ -80,7 +222,8 @@ function GainPanel({
|
||||
unit=" dB"
|
||||
value={gain}
|
||||
formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))}
|
||||
onChange={(value) => setGain(channel, value)}
|
||||
onInput={handleLiveValue}
|
||||
onChange={commitGain}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -164,14 +307,40 @@ function GainSection({
|
||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||
}) {
|
||||
const currentChannel = dsp.state.current_config.input_states[InputChannel.InA];
|
||||
|
||||
const inputColors = buildColorMap(INPUT_ORDER);
|
||||
const outputColors = buildColorMap(OUTPUT_ORDER);
|
||||
|
||||
const [selectedInputs, setSelectedInputs] = useState<Set<InputChannel>>(new Set(INPUT_ORDER));
|
||||
const [selectedOutputs, setSelectedOutputs] = useState<Set<OutputChannel>>(new Set(OUTPUT_ORDER));
|
||||
|
||||
const [showInputStages, setShowInputStages] = useState(false);
|
||||
const [showOutputStages, setShowOutputStages] = useState(false);
|
||||
|
||||
const [liveInputGains, setLiveInputGains] = useState<Partial<Record<InputChannel, number>>>({});
|
||||
const [liveOutputGains, setLiveOutputGains] = useState<Partial<Record<OutputChannel, number>>>({});
|
||||
|
||||
const setLiveInputGain = (channel: InputChannel, value: number | null) => {
|
||||
setLiveInputGains((prev) => {
|
||||
if (value === null) {
|
||||
const next = { ...prev };
|
||||
delete next[channel];
|
||||
return next;
|
||||
}
|
||||
return { ...prev, [channel]: value };
|
||||
});
|
||||
};
|
||||
|
||||
const setLiveOutputGain = (channel: OutputChannel, value: number | null) => {
|
||||
setLiveOutputGains((prev) => {
|
||||
if (value === null) {
|
||||
const next = { ...prev };
|
||||
delete next[channel];
|
||||
return next;
|
||||
}
|
||||
return { ...prev, [channel]: value };
|
||||
});
|
||||
};
|
||||
|
||||
const inputLabels: Record<InputChannel, string> = INPUT_ORDER.reduce(
|
||||
(acc, channel) => ({
|
||||
...acc,
|
||||
@@ -180,6 +349,14 @@ function GainSection({
|
||||
{} as Record<InputChannel, string>
|
||||
);
|
||||
|
||||
const outputLabels: Record<OutputChannel, string> = OUTPUT_ORDER.reduce(
|
||||
(acc, channel) => ({
|
||||
...acc,
|
||||
[channel]: dsp.state.current_config.output_states[channel].name,
|
||||
}),
|
||||
{} as Record<OutputChannel, string>
|
||||
);
|
||||
|
||||
const toggleInput = (channel: InputChannel) => {
|
||||
setSelectedInputs((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -196,20 +373,52 @@ function GainSection({
|
||||
});
|
||||
};
|
||||
|
||||
// const inputOverlaySpecs: OverlayChannelSpec<InputChannel>[] = INPUT_ORDER.filter((c) =>
|
||||
// selectedInputs.has(c)
|
||||
// ).map((channel) => ({ channel, color: inputColors[channel] }));
|
||||
const freqs = logSpace(GRAPH_X_MIN, GRAPH_X_MAX, 1000);
|
||||
|
||||
// const outputOverlaySpecs: OverlayChannelSpec<OutputChannel>[] = OUTPUT_ORDER.filter((c) =>
|
||||
// selectedOutputs.has(c)
|
||||
// ).map((channel) => ({ channel, color: outputColors[channel] }));
|
||||
const inputCurves = buildCurvesForChannels(
|
||||
INPUT_ORDER,
|
||||
selectedInputs,
|
||||
inputColors,
|
||||
showInputStages,
|
||||
(channel) =>
|
||||
getInputChannelCurves(dsp.state.current_config.input_states[channel], freqs, liveInputGains[channel])
|
||||
);
|
||||
|
||||
const outputCurves = buildCurvesForChannels(
|
||||
OUTPUT_ORDER,
|
||||
selectedOutputs,
|
||||
outputColors,
|
||||
showOutputStages,
|
||||
(channel) =>
|
||||
getOutputChannelCurves(dsp.state.current_config.output_states[channel], freqs, liveOutputGains[channel])
|
||||
);
|
||||
|
||||
const { yMin: inputYMin, yMax: inputYMax } = computeYAxisBounds(
|
||||
inputCurves,
|
||||
GRAPH_Y_MIN,
|
||||
GRAPH_Y_MAX,
|
||||
Y_AXIS_STEP
|
||||
);
|
||||
const { yMin: outputYMin, yMax: outputYMax } = computeYAxisBounds(
|
||||
outputCurves,
|
||||
GRAPH_Y_MIN,
|
||||
GRAPH_Y_MAX,
|
||||
Y_AXIS_STEP
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gain-linear-graph-section">
|
||||
<div className="gain-graph-panel">
|
||||
<div className="gain-graph-title-row">
|
||||
<div className="gain-graph-title">Input Magnitude</div>
|
||||
<div className="gain-graph-title">Input Response</div>
|
||||
<Button
|
||||
variant="toggle"
|
||||
className={showInputStages ? 'active' : ''}
|
||||
onClick={() => setShowInputStages((v) => !v)}
|
||||
>
|
||||
Show stages
|
||||
</Button>
|
||||
<ChannelSelector
|
||||
channels={INPUT_ORDER}
|
||||
labels={inputLabels}
|
||||
@@ -218,48 +427,75 @@ function GainSection({
|
||||
onToggle={toggleInput}
|
||||
/>
|
||||
</div>
|
||||
{/* <LinearGraph
|
||||
<LinearGraph
|
||||
xMin={GRAPH_X_MIN}
|
||||
xMax={GRAPH_X_MAX}
|
||||
xScale="log"
|
||||
yMin={GRAPH_Y_MIN}
|
||||
yMax={GRAPH_Y_MAX}
|
||||
yStep={3}
|
||||
yMin={inputYMin}
|
||||
yMax={inputYMax}
|
||||
yStep={Y_AXIS_STEP}
|
||||
xLabel="Hz"
|
||||
yLabel="dB"
|
||||
showGrid={true}
|
||||
formatX={(v) => Math.round(v).toString()}
|
||||
formatY={(v) => Math.round(v).toString()}
|
||||
draw={buildInputMagnitudeOverlayDraw({
|
||||
channels: inputOverlaySpecs,
|
||||
getGains: (channel) => dsp.state.current_config.input_states[channel].geq.gains,
|
||||
getBypass: (channel) => dsp.state.current_config.input_states[channel].geq.bypass,
|
||||
})}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
{/* <div className="gain-graph-panel">
|
||||
<div className="gain-graph-title">Phase Response</div>
|
||||
<LinearGraph
|
||||
xMin={20}
|
||||
xMax={20000}
|
||||
yMin={-180}
|
||||
yMax={180}
|
||||
yStep={45}
|
||||
xLabel="Hz"
|
||||
yLabel="deg"
|
||||
draw={({ ctx, toPx }) => {
|
||||
ctx.strokeStyle = '#4ade80';
|
||||
ctx.lineWidth = 2;
|
||||
const [x0, y0] = toPx(20, 0);
|
||||
const [x1, y1] = toPx(20000, 0);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, y0);
|
||||
ctx.lineTo(x1, y1);
|
||||
ctx.stroke();
|
||||
drawCurves({
|
||||
ctx,
|
||||
toPx,
|
||||
xMin: GRAPH_X_MIN,
|
||||
xMax: GRAPH_X_MAX,
|
||||
yMin: inputYMin,
|
||||
yMax: inputYMax,
|
||||
curves: inputCurves,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className="gain-graph-panel">
|
||||
<div className="gain-graph-title-row">
|
||||
<div className="gain-graph-title">Output Response</div>
|
||||
<Button
|
||||
variant="toggle"
|
||||
className={showOutputStages ? 'active' : ''}
|
||||
onClick={() => setShowOutputStages((v) => !v)}
|
||||
>
|
||||
Show stages
|
||||
</Button>
|
||||
<ChannelSelector
|
||||
channels={OUTPUT_ORDER}
|
||||
labels={outputLabels}
|
||||
colors={outputColors}
|
||||
selected={selectedOutputs}
|
||||
onToggle={toggleOutput}
|
||||
/>
|
||||
</div>
|
||||
<LinearGraph
|
||||
xMin={GRAPH_X_MIN}
|
||||
xMax={GRAPH_X_MAX}
|
||||
xScale="log"
|
||||
yMin={outputYMin}
|
||||
yMax={outputYMax}
|
||||
yStep={Y_AXIS_STEP}
|
||||
xLabel="Hz"
|
||||
yLabel="dB"
|
||||
showGrid={true}
|
||||
formatX={(v) => 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: outputYMin,
|
||||
yMax: outputYMax,
|
||||
curves: outputCurves,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gain-section">
|
||||
@@ -274,6 +510,7 @@ function GainSection({
|
||||
setGain={setGain}
|
||||
setMute={setMute}
|
||||
setInverse={setInverse}
|
||||
onLiveGain={(value) => setLiveInputGain(channel, value)}
|
||||
/>
|
||||
))}
|
||||
</ChannelGroup>
|
||||
@@ -288,6 +525,7 @@ function GainSection({
|
||||
setGain={setGain}
|
||||
setMute={setMute}
|
||||
setInverse={setInverse}
|
||||
onLiveGain={(value) => setLiveOutputGain(channel, value)}
|
||||
/>
|
||||
))}
|
||||
</ChannelGroup>
|
||||
@@ -297,4 +535,4 @@ function GainSection({
|
||||
);
|
||||
}
|
||||
|
||||
export default GainSection;
|
||||
export default GainSection;
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CurveStyle,
|
||||
Curve,
|
||||
DEFAULT_CURVE_COLORS,
|
||||
logSpace,
|
||||
sumCurvePoints,
|
||||
} from './curve';
|
||||
|
||||
|
||||
@@ -319,115 +319,6 @@ function peqTotalPhaseDeg(
|
||||
return sum;
|
||||
}
|
||||
|
||||
function unwrapPhaseDeg(phases: number[]): number[] {
|
||||
if (phases.length === 0) return [];
|
||||
|
||||
const result = [phases[0]];
|
||||
|
||||
for (let i = 1; i < phases.length; i++) {
|
||||
let phase = phases[i];
|
||||
const previous = result[i - 1];
|
||||
|
||||
while (phase - previous > 180) {
|
||||
phase -= 360;
|
||||
}
|
||||
|
||||
while (phase - previous < -180) {
|
||||
phase += 360;
|
||||
}
|
||||
|
||||
result.push(phase);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// export function getPeqPhaseSegments({
|
||||
// freqs,
|
||||
// bands,
|
||||
// sampleRate = DEFAULT_SAMPLE_RATE,
|
||||
// }: {
|
||||
// freqs: number[];
|
||||
// bands: Record<PEQBand, PEQ>;
|
||||
// sampleRate?: number;
|
||||
// }): CurvePoint[][] {
|
||||
// if (freqs.length === 0) return [];
|
||||
|
||||
// const rawPhases = freqs.map((freq) => peqTotalPhaseDeg(freq, bands, sampleRate));
|
||||
// const phases = unwrapPhaseDeg(rawPhases);
|
||||
|
||||
// const wrapToDisplay = (p: number) => ((((p + 180) % 360) + 360) % 360) - 180;
|
||||
|
||||
// const segments: CurvePoint[][] = [[]];
|
||||
|
||||
// const pushPoint = (x: number, y: number) => {
|
||||
// segments[segments.length - 1].push({ x, y });
|
||||
// };
|
||||
|
||||
// let previousFreq = freqs[0];
|
||||
// let previousPhase = phases[0];
|
||||
|
||||
// pushPoint(previousFreq, wrapToDisplay(previousPhase));
|
||||
|
||||
// for (let i = 1; i < freqs.length; i++) {
|
||||
// const freq = freqs[i];
|
||||
// const phase = phases[i];
|
||||
|
||||
// const logPrev = Math.log10(previousFreq);
|
||||
// const logCurr = Math.log10(freq);
|
||||
|
||||
// const direction = phase >= previousPhase ? 1 : -1;
|
||||
|
||||
// const boundaries: number[] = [];
|
||||
|
||||
// if (direction > 0) {
|
||||
// let b = Math.floor((previousPhase - 180) / 360) * 360 + 180;
|
||||
|
||||
// while (b <= previousPhase) {
|
||||
// b += 360;
|
||||
// }
|
||||
|
||||
// while (b < phase) {
|
||||
// boundaries.push(b);
|
||||
// b += 360;
|
||||
// }
|
||||
// } else {
|
||||
// let b = Math.ceil((previousPhase - 180) / 360) * 360 + 180;
|
||||
|
||||
// while (b >= previousPhase) {
|
||||
// b -= 360;
|
||||
// }
|
||||
|
||||
// while (b > phase) {
|
||||
// boundaries.push(b);
|
||||
// b -= 360;
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (const boundary of boundaries) {
|
||||
// const t = (boundary - previousPhase) / (phase - previousPhase);
|
||||
// const crossingFreq = Math.pow(10, logPrev + t * (logCurr - logPrev));
|
||||
|
||||
// const approach = direction > 0 ? 180 : -180;
|
||||
// const depart = direction > 0 ? -180 : 180;
|
||||
|
||||
// // End current segment at ±180°.
|
||||
// pushPoint(crossingFreq, approach);
|
||||
|
||||
// // Start a new segment at the opposite side of the display.
|
||||
// segments.push([]);
|
||||
// pushPoint(crossingFreq, depart);
|
||||
// }
|
||||
|
||||
// pushPoint(freq, wrapToDisplay(phase));
|
||||
|
||||
// previousFreq = freq;
|
||||
// previousPhase = phase;
|
||||
// }
|
||||
|
||||
// return segments.filter((segment) => segment.length >= 2);
|
||||
// }
|
||||
|
||||
export function getPeqPhasePoints({
|
||||
freqs,
|
||||
bands,
|
||||
|
||||
Reference in New Issue
Block a user