feat: add peq foor input
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user