feat: add realistic portable localization core
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user