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; 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; 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, highPass: CrossoverSpec | undefined, lowPass: CrossoverSpec | undefined, sampleRate: number ): number { return ( peqTotalCurveDb(freq, bands, sampleRate) + crossoverPairMagnitudeDb(highPass, lowPass, freq) ); } function totalPhaseDeg( freq: number, bands: Record, 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(); }; }