import { dbToLinear } from "./math.js"; export const MAX_FREQUENCY_RESPONSE_POINTS = 64; export const MAX_FREQUENCY_RESPONSE_HZ = 1_000_000; export const DEFAULT_MAX_COMPENSATION_DB = 24; export const MAX_COMPENSATION_DB = 120; export function requireMaxCompensationDb( value = DEFAULT_MAX_COMPENSATION_DB, label = "maxCompensationDb", ) { if (!Number.isFinite(value) || value < 0 || value > MAX_COMPENSATION_DB) { throw new RangeError(`${label} must lie inside 0..${MAX_COMPENSATION_DB} dB.`); } return value; } /** * Return the inverse response gain in dB while limiting only positive boost. * Attenuation remains exact because it cannot amplify a response notch or the * microphone self-noise that occupies it. */ export function inverseResponseGainDb(responseGainDb, maxCompensationDb) { if (!Number.isFinite(responseGainDb)) { throw new RangeError("responseGainDb must be finite."); } return Math.min( -responseGainDb, requireMaxCompensationDb(maxCompensationDb), ); } function phaseAt(point) { return point.phaseDeg ?? 0; } function wrapPhaseDeg(value) { const wrapped = ((value + 180) % 360 + 360) % 360 - 180; return Object.is(wrapped, -0) ? 0 : wrapped; } function writeResponse(output, gainDb, phaseDeg) { output.gainDb = gainDb; output.phaseDeg = wrapPhaseDeg(phaseDeg); output.linearGain = dbToLinear(gainDb); output.phaseRadians = (output.phaseDeg * Math.PI) / 180; return output; } /** * Piecewise-linear microphone response interpolation. * * Frequencies outside the measured curve use the nearest endpoint. Phase follows * the shortest wrapped path between points, avoiding a discontinuity at ±180°. * Passing an output object lets FFT loops reuse storage and avoid per-bin garbage. */ export function interpolateFrequencyResponse(points, frequencyHz, output = {}) { if (!Number.isFinite(frequencyHz) || frequencyHz < 0) { throw new RangeError(`frequencyHz must be a finite non-negative number: ${frequencyHz}`); } if (points === undefined || points === null || points.length === 0) { return writeResponse(output, 0, 0); } if (!Array.isArray(points)) throw new TypeError("frequency response points must be an array."); const first = points[0]; const last = points[points.length - 1]; if (frequencyHz <= first.frequencyHz) { return writeResponse(output, first.gainDb, phaseAt(first)); } if (frequencyHz >= last.frequencyHz) { return writeResponse(output, last.gainDb, phaseAt(last)); } let low = 0; let high = points.length - 1; while (high - low > 1) { const middle = Math.floor((low + high) / 2); if (frequencyHz < points[middle].frequencyHz) high = middle; else low = middle; } const left = points[low]; const right = points[high]; const ratio = (frequencyHz - left.frequencyHz) / (right.frequencyHz - left.frequencyHz); const gainDb = left.gainDb + (right.gainDb - left.gainDb) * ratio; const leftPhase = phaseAt(left); const phaseDelta = wrapPhaseDeg(phaseAt(right) - leftPhase); return writeResponse(output, gainDb, leftPhase + phaseDelta * ratio); }