/** * What the Parametric EQ's tab (#367) lays onto its picture beside the curve, whose axes are in * `parametricEq.ts`: the handles of the bands, and the spectrum of what passes the node (#374). */ import { type EqArea, type EqBand, dbToY, EQ_Q, EQ_RANGES, frequencyToX, hasGain, xToFrequency, } from '../../core/soundfx/parametricEq'; /** * Where a band's handle sits: at its frequency, and at its gain or – without one – on the 0 dB line. * A gain beyond what the curve is zoomed to keeps its handle on the edge, in sight and in reach. */ export function handleOf(band: EqBand, area: EqArea): { x: number; y: number } { const y = dbToY(hasGain(band.type) ? band.gain : 0, area); return { x: frequencyToX(band.frequency, area), y: Math.max(0, Math.min(area.h, y)) }; } /** The band whose handle is under a point, the nearest one if several are; -1 for none. */ export function bandAt(bands: readonly EqBand[], x: number, y: number, area: EqArea, radius: number): number { let found = -1; let best = radius * radius; bands.forEach((band, i) => { const at = handleOf(band, area); const d = (at.x - x) ** 2 + (at.y - y) ** 2; if (d <= best) { best = d; found = i; } }); return found; } /** The Q after a turn of the wheel: a notch of it is a sixth of an octave narrower or wider. */ export function wheeledQ(q: number, deltaY: number): number { const turned = q * 2 ** (-Math.sign(deltaY) / 6); return Math.max(EQ_Q.min, Math.min(EQ_Q.max, Math.round(turned * 1000) / 1000)); } /** The zoom after a turn of the wheel (#376): up shows the next smaller range, down the next larger. */ export function wheeledRange(range: number, deltaY: number): number { const at = EQ_RANGES.indexOf(range) + Math.sign(deltaY); return EQ_RANGES[Math.max(0, Math.min(EQ_RANGES.length - 1, at))]!; } /** The lines the picture is ruled with, and what they say. */ export const EQ_GRID_FREQUENCIES: readonly { hz: number; label: string }[] = [ { hz: 20, label: '20' }, { hz: 50, label: '50' }, { hz: 100, label: '100' }, { hz: 200, label: '200' }, { hz: 500, label: '500' }, { hz: 1000, label: '1k' }, { hz: 2000, label: '2k' }, { hz: 5000, label: '5k' }, { hz: 10000, label: '10k' }, { hz: 20000, label: '20k' }, ]; /** The dB lines for a range: in thirds of it up to ±6 dB, in quarters above, so whole numbers. */ export function eqGridDb(range: number): number[] { const parts = range <= 6 ? 3 : 4; return Array.from({ length: parts * 2 - 1 }, (_, i) => ((i + 1 - parts) * range) / parts); } /** The spectrum's own scale (#374): full scale at the top of the picture, this many dB at its foot. */ export const SPECTRUM_FLOOR_DB = -96; const spectrumY = (db: number, area: EqArea): number => (Math.min(0, Math.max(SPECTRUM_FLOOR_DB, db)) / SPECTRUM_FLOOR_DB) * area.h; /** * A spectrum, one level in dB every `binHz`, laid onto the picture at `xs`: each point takes the * loudest bin from halfway to the point before to halfway to the next, or – where the bins lie * further apart than the points, down in the bass – the level between the two nearest. */ export function spectrumYs( dbs: ArrayLike, binHz: number, xs: readonly number[], area: EqArea, ): number[] { const last = dbs.length - 1; const level = (bin: number) => { const db = dbs[Math.max(0, Math.min(last, bin))]!; return Number.isFinite(db) ? db : SPECTRUM_FLOOR_DB; }; return xs.map((x, i) => { const from = Math.ceil(xToFrequency(i > 0 ? (xs[i - 1]! + x) / 2 : x, area) / binHz); const to = Math.floor(xToFrequency(i < xs.length - 1 ? (x + xs[i + 1]!) / 2 : x, area) / binHz); let db = -Infinity; for (let bin = from; bin <= Math.min(to, last); bin++) db = Math.max(db, level(bin)); if (db === -Infinity) { const at = xToFrequency(x, area) / binHz; const below = Math.floor(at); db = level(below) + (level(below + 1) - level(below)) * (at - below); } return spectrumY(db, area); }); } /** The frequency of the loudest bin, and how loud it is; null for a spectrum that is all silence. */ export function spectrumPeak(dbs: ArrayLike, binHz: number): { hz: number; db: number } | null { let best = -1; for (let bin = 1; bin < dbs.length; bin++) if (Number.isFinite(dbs[bin]) && (best < 0 || dbs[bin]! > dbs[best]!)) best = bin; return best < 0 ? null : { hz: best * binHz, db: dbs[best]! }; }