Files
micarrsim/packages/acoustics-core/frequency-response.js
T

63 lines
2.2 KiB
JavaScript

import { dbToLinear } from "./math.js";
export const MAX_FREQUENCY_RESPONSE_POINTS = 64;
export const MAX_FREQUENCY_RESPONSE_HZ = 1_000_000;
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);
}