feat: add comparable localization algorithms
This commit is contained in:
+131
-32
@@ -5,6 +5,11 @@ import {
|
||||
requireMaxCompensationDb,
|
||||
} from "./frequency-response.js";
|
||||
import { cooperativeYield } from "./cooperative-yield.js";
|
||||
import {
|
||||
DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
isDasAlgorithm,
|
||||
requireLocalizationAlgorithmDefinition,
|
||||
} from "./localization-algorithms.js";
|
||||
import { binsForBand, createStftWorkspace, extractStftBins, getFrameCount } from "./stft.js";
|
||||
|
||||
function throwIfAborted(signal) {
|
||||
@@ -48,20 +53,24 @@ function createBandIndexLookup(bandBins) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function createBinCalibration(
|
||||
arrayDefinition,
|
||||
bin,
|
||||
sampleRate,
|
||||
fftSize,
|
||||
compensateFrequencyResponse,
|
||||
maximumCompensationDb,
|
||||
) {
|
||||
export function createBinCalibration(options) {
|
||||
const {
|
||||
arrayDefinition,
|
||||
bin,
|
||||
sampleRate,
|
||||
fftSize,
|
||||
compensateFrequencyResponse = true,
|
||||
maxCompensationDb: maximumCompensationDb,
|
||||
includeAmplitude = true,
|
||||
} = options;
|
||||
const microphoneCount = arrayDefinition.microphones.length;
|
||||
const gains = new Float64Array(microphoneCount);
|
||||
const phases = new Float64Array(microphoneCount);
|
||||
const frequencyHz = (bin * sampleRate) / fftSize;
|
||||
const response = {};
|
||||
const maxCompensationDb = requireMaxCompensationDb(maximumCompensationDb);
|
||||
const maxCompensationDb = includeAmplitude
|
||||
? requireMaxCompensationDb(maximumCompensationDb)
|
||||
: 0;
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const microphone = arrayDefinition.microphones[microphoneIndex];
|
||||
let responseGainDb = 0;
|
||||
@@ -71,23 +80,72 @@ function createBinCalibration(
|
||||
responseGainDb = response.gainDb;
|
||||
responsePhaseDeg = response.phaseDeg;
|
||||
}
|
||||
const responseCompensationDb = compensateFrequencyResponse
|
||||
const responseCompensationDb = includeAmplitude && compensateFrequencyResponse
|
||||
? inverseResponseGainDb(responseGainDb, maxCompensationDb)
|
||||
: 0;
|
||||
gains[microphoneIndex] = dbToLinear(
|
||||
-(microphone.sensitivityDb ?? 0) + responseCompensationDb,
|
||||
);
|
||||
gains[microphoneIndex] = includeAmplitude
|
||||
? dbToLinear(-(microphone.sensitivityDb ?? 0) + responseCompensationDb)
|
||||
: 1;
|
||||
phases[microphoneIndex] =
|
||||
((microphone.phaseOffsetDeg ?? 0) + responsePhaseDeg) * Math.PI / 180;
|
||||
}
|
||||
return { gains, phases };
|
||||
}
|
||||
|
||||
function requirePhatRelativeFloor(value) {
|
||||
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
||||
throw new RangeError("phatRelativeFloor must lie inside 0..1.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function phaseNormalizeSpectrum(
|
||||
spectrum,
|
||||
frameCount,
|
||||
microphoneCount,
|
||||
relativeFloor = DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
) {
|
||||
const expectedLength = frameCount * microphoneCount;
|
||||
if (
|
||||
!spectrum ||
|
||||
spectrum.real?.length !== expectedLength ||
|
||||
spectrum.imaginary?.length !== expectedLength
|
||||
) {
|
||||
throw new RangeError("spectrum length must match frameCount and microphoneCount.");
|
||||
}
|
||||
requirePhatRelativeFloor(relativeFloor);
|
||||
const real = new Float32Array(expectedLength);
|
||||
const imaginary = new Float32Array(expectedLength);
|
||||
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
|
||||
const frameOffset = frameIndex * microphoneCount;
|
||||
let maximumMagnitude = 0;
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const spectrumIndex = frameOffset + microphoneIndex;
|
||||
maximumMagnitude = Math.max(
|
||||
maximumMagnitude,
|
||||
Math.hypot(spectrum.real[spectrumIndex], spectrum.imaginary[spectrumIndex]),
|
||||
);
|
||||
}
|
||||
const magnitudeFloor = maximumMagnitude * relativeFloor;
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const spectrumIndex = frameOffset + microphoneIndex;
|
||||
const inputReal = spectrum.real[spectrumIndex];
|
||||
const inputImaginary = spectrum.imaginary[spectrumIndex];
|
||||
const magnitude = Math.hypot(inputReal, inputImaginary);
|
||||
if (!(magnitude > magnitudeFloor)) continue;
|
||||
real[spectrumIndex] = inputReal / magnitude;
|
||||
imaginary[spectrumIndex] = inputImaginary / magnitude;
|
||||
}
|
||||
}
|
||||
return { real, imaginary };
|
||||
}
|
||||
|
||||
function fillSteeringVector({
|
||||
angularWaveNumber,
|
||||
distances,
|
||||
distanceOffset,
|
||||
calibration,
|
||||
applyCalibrationGains = true,
|
||||
outputReal,
|
||||
outputImaginary,
|
||||
outputOffset,
|
||||
@@ -96,10 +154,9 @@ function fillSteeringVector({
|
||||
const phase =
|
||||
angularWaveNumber * distances[distanceOffset + microphoneIndex] -
|
||||
calibration.phases[microphoneIndex];
|
||||
outputReal[outputOffset + microphoneIndex] =
|
||||
Math.cos(phase) * calibration.gains[microphoneIndex];
|
||||
outputImaginary[outputOffset + microphoneIndex] =
|
||||
Math.sin(phase) * calibration.gains[microphoneIndex];
|
||||
const gain = applyCalibrationGains ? calibration.gains[microphoneIndex] : 1;
|
||||
outputReal[outputOffset + microphoneIndex] = Math.cos(phase) * gain;
|
||||
outputImaginary[outputOffset + microphoneIndex] = Math.sin(phase) * gain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +170,7 @@ async function createSteeringMatrix(options) {
|
||||
fftSize,
|
||||
speedOfSound,
|
||||
calibration,
|
||||
applyCalibrationGains = true,
|
||||
signal,
|
||||
pointBatchSize,
|
||||
} = options;
|
||||
@@ -127,6 +185,7 @@ async function createSteeringMatrix(options) {
|
||||
distances,
|
||||
distanceOffset: offset,
|
||||
calibration,
|
||||
applyCalibrationGains,
|
||||
outputReal: real,
|
||||
outputImaginary: imaginary,
|
||||
outputOffset: offset,
|
||||
@@ -158,21 +217,32 @@ export async function computeBinMap(options) {
|
||||
steeringImaginary: providedSteeringImaginary,
|
||||
signal,
|
||||
pointBatchSize = 64,
|
||||
algorithm: algorithmId = "das",
|
||||
phatRelativeFloor = DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
} = options;
|
||||
const algorithm = requireLocalizationAlgorithmDefinition(algorithmId);
|
||||
if (!isDasAlgorithm(algorithm.id)) {
|
||||
throw new RangeError(`computeBinMap does not implement ${algorithm.id}.`);
|
||||
}
|
||||
const phaseOnly = algorithm.spectralWeighting === "phat";
|
||||
const microphoneCount = arrayDefinition.microphones.length;
|
||||
const output = providedOutput ?? new Float32Array(pointCount);
|
||||
const steeringReal = providedSteeringReal ?? new Float64Array(microphoneCount);
|
||||
const steeringImaginary = providedSteeringImaginary ?? new Float64Array(microphoneCount);
|
||||
const angularWaveNumber = (TAU * bin * sampleRate) / (fftSize * speedOfSound);
|
||||
const microphoneScale = 1 / microphoneCount;
|
||||
const calibration = providedCalibration ?? createBinCalibration(
|
||||
const calibration = providedCalibration ?? createBinCalibration({
|
||||
arrayDefinition,
|
||||
bin,
|
||||
sampleRate,
|
||||
fftSize,
|
||||
compensateFrequencyResponse,
|
||||
maxCompensationDb,
|
||||
);
|
||||
includeAmplitude: !phaseOnly,
|
||||
});
|
||||
const inputSpectrum = phaseOnly
|
||||
? phaseNormalizeSpectrum(spectrum, frameCount, microphoneCount, phatRelativeFloor)
|
||||
: spectrum;
|
||||
const phaseRealValues = steeringMatrix?.real ?? steeringReal;
|
||||
const phaseImaginaryValues = steeringMatrix?.imaginary ?? steeringImaginary;
|
||||
if (output.length !== pointCount) throw new RangeError("output length must match pointCount.");
|
||||
@@ -208,6 +278,7 @@ export async function computeBinMap(options) {
|
||||
outputReal: steeringReal,
|
||||
outputImaginary: steeringImaginary,
|
||||
outputOffset: 0,
|
||||
applyCalibrationGains: !phaseOnly,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,8 +289,8 @@ export async function computeBinMap(options) {
|
||||
let sumImaginary = 0;
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const spectrumIndex = spectrumOffset + microphoneIndex;
|
||||
const inputReal = spectrum.real[spectrumIndex];
|
||||
const inputImaginary = spectrum.imaginary[spectrumIndex];
|
||||
const inputReal = inputSpectrum.real[spectrumIndex];
|
||||
const inputImaginary = inputSpectrum.imaginary[spectrumIndex];
|
||||
const phaseReal = phaseRealValues[steeringOffset + microphoneIndex];
|
||||
const phaseImaginary = phaseImaginaryValues[steeringOffset + microphoneIndex];
|
||||
sumReal += inputReal * phaseReal - inputImaginary * phaseImaginary;
|
||||
@@ -265,6 +336,12 @@ export function createDasAnalysisPlan(options) {
|
||||
if (!Number.isSafeInteger(maximumPlanBytes) || maximumPlanBytes <= 0) {
|
||||
throw new RangeError("maximumPlanBytes must be a positive safe integer.");
|
||||
}
|
||||
const algorithm = requireLocalizationAlgorithmDefinition(
|
||||
experiment.beamforming?.algorithm ?? "das",
|
||||
);
|
||||
if (!isDasAlgorithm(algorithm.id)) {
|
||||
throw new RangeError(`createDasAnalysisPlan does not implement ${algorithm.id}.`);
|
||||
}
|
||||
|
||||
const { fftSize } = experiment.stft;
|
||||
const bandBins = experiment.frequencyBands.map((band) => binsForBand(band, sampleRate, fftSize));
|
||||
@@ -319,6 +396,7 @@ export function createDasAnalysisPlan(options) {
|
||||
arrayDefinition,
|
||||
scene,
|
||||
experiment,
|
||||
algorithm,
|
||||
microphoneCount,
|
||||
pointCount,
|
||||
bandBins,
|
||||
@@ -394,14 +472,16 @@ function planCalibration(plan, bin) {
|
||||
plan.statistics.calibrationCacheHits += 1;
|
||||
return calibration;
|
||||
}
|
||||
calibration = createBinCalibration(
|
||||
plan.arrayDefinition,
|
||||
calibration = createBinCalibration({
|
||||
arrayDefinition: plan.arrayDefinition,
|
||||
bin,
|
||||
plan.sampleRate,
|
||||
plan.experiment.stft.fftSize,
|
||||
plan.experiment.beamforming.compensateFrequencyResponse !== false,
|
||||
plan.experiment.beamforming.maxCompensationDb,
|
||||
);
|
||||
sampleRate: plan.sampleRate,
|
||||
fftSize: plan.experiment.stft.fftSize,
|
||||
compensateFrequencyResponse:
|
||||
plan.experiment.beamforming.compensateFrequencyResponse !== false,
|
||||
maxCompensationDb: plan.experiment.beamforming.maxCompensationDb,
|
||||
includeAmplitude: plan.algorithm.spectralWeighting !== "phat",
|
||||
});
|
||||
plan.calibrations.set(bin, calibration);
|
||||
plan.statistics.calibrationBuilds += 1;
|
||||
plan.statistics.retainedBytes += calibration.gains.byteLength + calibration.phases.byteLength;
|
||||
@@ -431,6 +511,7 @@ async function planSteeringMatrix(plan, bin, calibration, signal, pointBatchSize
|
||||
fftSize: plan.experiment.stft.fftSize,
|
||||
speedOfSound: plan.scene.speedOfSound,
|
||||
calibration,
|
||||
applyCalibrationGains: plan.algorithm.spectralWeighting !== "phat",
|
||||
signal,
|
||||
pointBatchSize,
|
||||
});
|
||||
@@ -520,7 +601,10 @@ export function estimateDasWorkload(options) {
|
||||
};
|
||||
}
|
||||
|
||||
export function finalizeDasBands(rawMaps, workload, experiment) {
|
||||
export function finalizeDasBands(rawMaps, workload, experiment, rawScale = "power") {
|
||||
if (rawScale !== "power" && rawScale !== "score") {
|
||||
throw new RangeError('rawScale must be "power" or "score".');
|
||||
}
|
||||
return experiment.frequencyBands.map((band, bandIndex) => {
|
||||
const rawMap = rawMaps[bandIndex];
|
||||
for (let pointIndex = 0; pointIndex < workload.pointCount; pointIndex += 1) {
|
||||
@@ -528,7 +612,7 @@ export function finalizeDasBands(rawMaps, workload, experiment) {
|
||||
}
|
||||
const rawPeak = findPeak(rawMap);
|
||||
const displayMap = Float32Array.from(rawMap);
|
||||
let displayScale = "power";
|
||||
let displayScale = rawScale;
|
||||
if (experiment.beamforming.normalization === "peak" && rawPeak.value > 0) {
|
||||
displayScale = "normalized";
|
||||
for (let pointIndex = 0; pointIndex < workload.pointCount; pointIndex += 1) {
|
||||
@@ -540,12 +624,17 @@ export function finalizeDasBands(rawMaps, workload, experiment) {
|
||||
color: band.color,
|
||||
bins: workload.bandBins[bandIndex],
|
||||
rawMap,
|
||||
rawScale,
|
||||
displayMap,
|
||||
displayScale,
|
||||
peak: {
|
||||
index: rawPeak.index,
|
||||
position: scanIndexToPosition(rawPeak.index, experiment.scan),
|
||||
rawPower: rawPeak.value,
|
||||
rawValue: rawPeak.value,
|
||||
rawScale,
|
||||
...(rawScale === "power"
|
||||
? { rawPower: rawPeak.value }
|
||||
: { rawScore: rawPeak.value }),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -566,7 +655,14 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
maximumSpectrumBatchBins = Number.MAX_SAFE_INTEGER,
|
||||
pointBatchSize = 64,
|
||||
stftTransformBatchSize = 32,
|
||||
phatRelativeFloor = DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
} = options;
|
||||
const algorithm = analysisPlan?.algorithm ?? requireLocalizationAlgorithmDefinition(
|
||||
experiment.beamforming?.algorithm ?? "das",
|
||||
);
|
||||
if (!isDasAlgorithm(algorithm.id)) {
|
||||
throw new RangeError(`computeDasMap does not implement ${algorithm.id}.`);
|
||||
}
|
||||
if (channels.length !== arrayDefinition.microphones.length) {
|
||||
throw new Error(`채널 수 ${channels.length}와 마이크 수 ${arrayDefinition.microphones.length}가 다릅니다.`);
|
||||
}
|
||||
@@ -685,6 +781,8 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
steeringImaginary: analysisPlan?.steeringImaginary,
|
||||
signal,
|
||||
pointBatchSize,
|
||||
algorithm: algorithm.id,
|
||||
phatRelativeFloor,
|
||||
});
|
||||
if (analysisPlan) {
|
||||
analysisPlan.statistics.beamformingComplexMacs +=
|
||||
@@ -701,9 +799,10 @@ async function computeDasMapInternal(options, analysisPlan) {
|
||||
}
|
||||
}
|
||||
|
||||
const bands = finalizeDasBands(rawMaps, workload, experiment);
|
||||
const bands = finalizeDasBands(rawMaps, workload, experiment, algorithm.rawScale);
|
||||
|
||||
return {
|
||||
algorithm: algorithm.id,
|
||||
backend: "cpu",
|
||||
frameCount: workload.frameCount,
|
||||
microphoneCount: workload.microphoneCount,
|
||||
|
||||
Reference in New Issue
Block a user