Files
dsp-thomann-interface/src/features/dsp408/utils/totalCurve.ts
T
2026-08-31 14:12:03 +02:00

255 lines
7.0 KiB
TypeScript

import { PEQ, PEQBand } from '../types';
import { GeqCurveColors, DEFAULT_GEQ_CURVE_COLORS, logSpace } from './geqCurve';
import { peqTotalCurveDb, peqTotalPhaseDeg } from './peqCurve';
import { CrossoverSpec, crossoverPairMagnitudeDb, crossoverPairPhaseDeg } from './crossoverCurve';
import { DEFAULT_SAMPLE_RATE } from './biquad';
export type TotalCurveOptions = {
bands: Record<PEQBand, PEQ>;
highPass?: CrossoverSpec;
lowPass?: CrossoverSpec;
xMin: number;
xMax: number;
yMin: number;
yMax: number;
steps?: number;
colors?: GeqCurveColors;
lineWidth?: number;
dimmed?: boolean;
sampleRate?: number;
};
export type TotalPhaseCurveOptions = {
bands: Record<PEQBand, PEQ>;
highPass?: CrossoverSpec;
lowPass?: CrossoverSpec;
xMin: number;
xMax: number;
yMin: number;
yMax: number;
steps?: number;
colors?: GeqCurveColors;
lineWidth?: number;
dimmed?: boolean;
sampleRate?: number;
};
function totalMagnitudeDb(
freq: number,
bands: Record<PEQBand, PEQ>,
highPass: CrossoverSpec | undefined,
lowPass: CrossoverSpec | undefined,
sampleRate: number
): number {
return (
peqTotalCurveDb(freq, bands, sampleRate) + crossoverPairMagnitudeDb(highPass, lowPass, freq)
);
}
function totalPhaseDeg(
freq: number,
bands: Record<PEQBand, PEQ>,
highPass: CrossoverSpec | undefined,
lowPass: CrossoverSpec | undefined,
sampleRate: number
): number {
return peqTotalPhaseDeg(freq, bands, sampleRate) + crossoverPairPhaseDeg(highPass, lowPass, freq);
}
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 buildTotalCurveDraw({
bands,
highPass,
lowPass,
xMin,
xMax,
yMin,
yMax,
steps = 1000,
colors = DEFAULT_GEQ_CURVE_COLORS,
lineWidth = 2,
dimmed = false,
sampleRate = DEFAULT_SAMPLE_RATE,
}: TotalCurveOptions) {
return ({
ctx,
toPx,
}: {
ctx: CanvasRenderingContext2D;
toPx: (x: number, y: number) => [number, number];
}) => {
const freqs = logSpace(xMin, xMax, steps);
const points = freqs.map((freq) =>
toPx(freq, totalMagnitudeDb(freq, bands, highPass, lowPass, sampleRate))
);
ctx.save();
if (dimmed) ctx.globalAlpha = 0.25;
ctx.beginPath();
points.forEach(([xPx, yPx], i) => (i === 0 ? ctx.moveTo(xPx, yPx) : ctx.lineTo(xPx, yPx)));
const [xEndPx, zeroYPx] = toPx(xMax, 0);
const [xStartPx] = toPx(xMin, 0);
ctx.lineTo(xEndPx, zeroYPx);
ctx.lineTo(xStartPx, zeroYPx);
ctx.closePath();
const [, topPx] = toPx(xMin, yMax);
const [, bottomPx] = toPx(xMin, yMin);
const gradient = ctx.createLinearGradient(0, topPx, 0, bottomPx);
gradient.addColorStop(0, colors.fillAbove);
gradient.addColorStop(0.5, colors.fillAboveFade);
gradient.addColorStop(0.5, colors.fillBelowFade);
gradient.addColorStop(1, colors.fillBelow);
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
points.forEach(([xPx, yPx], i) => (i === 0 ? ctx.moveTo(xPx, yPx) : ctx.lineTo(xPx, yPx)));
ctx.strokeStyle = colors.strokeColor;
ctx.lineWidth = lineWidth;
if (dimmed) ctx.setLineDash([6, 4]);
ctx.stroke();
ctx.restore();
};
}
export function buildTotalPhaseCurveDraw({
bands,
highPass,
lowPass,
xMin,
xMax,
yMin,
yMax,
steps = 3000,
colors = DEFAULT_GEQ_CURVE_COLORS,
lineWidth = 2,
dimmed = false,
sampleRate = DEFAULT_SAMPLE_RATE,
}: TotalPhaseCurveOptions) {
return ({
ctx,
toPx,
}: {
ctx: CanvasRenderingContext2D;
toPx: (x: number, y: number) => [number, number];
}) => {
const freqs = logSpace(xMin, xMax, steps);
const rawPhases = freqs.map((freq) =>
totalPhaseDeg(freq, bands, highPass, lowPass, sampleRate)
);
const phases = unwrapPhaseDeg(rawPhases);
const wrapToDisplay = (p: number) => ((((p + 180) % 360) + 360) % 360) - 180;
ctx.save();
if (dimmed) {
ctx.globalAlpha = 0.25;
ctx.setLineDash([6, 4]);
}
let segments: { freq: number; display: number }[][] = [[]];
const pushPoint = (freq: number, display: number) => {
segments[segments.length - 1].push({ freq, display });
};
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;
pushPoint(crossingFreq, approach);
segments.push([]);
pushPoint(crossingFreq, depart);
}
pushPoint(freq, wrapToDisplay(phase));
previousFreq = freq;
previousPhase = phase;
}
for (const seg of segments) {
if (seg.length < 2) continue;
ctx.beginPath();
seg.forEach((p, i) => {
const [x, y] = toPx(p.freq, p.display);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
const [xEndPx, zeroYPx] = toPx(seg[seg.length - 1].freq, 0);
const [xStartPx] = toPx(seg[0].freq, 0);
ctx.lineTo(xEndPx, zeroYPx);
ctx.lineTo(xStartPx, zeroYPx);
ctx.closePath();
const [, topPx] = toPx(xMin, yMax);
const [, bottomPx] = toPx(xMin, yMin);
const gradient = ctx.createLinearGradient(0, topPx, 0, bottomPx);
gradient.addColorStop(0, colors.fillAbove);
gradient.addColorStop(0.5, colors.fillAboveFade);
gradient.addColorStop(0.5, colors.fillBelowFade);
gradient.addColorStop(1, colors.fillBelow);
ctx.fillStyle = gradient;
ctx.fill();
}
for (const seg of segments) {
if (seg.length < 2) continue;
ctx.beginPath();
seg.forEach((p, i) => {
const [x, y] = toPx(p.freq, p.display);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = colors.strokeColor;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
ctx.restore();
};
}