feat: add comparable localization algorithms
This commit is contained in:
@@ -12,6 +12,13 @@ import {
|
||||
prepareDelaySpectrum,
|
||||
} from "/packages/acoustics-core/fractional-delay.js";
|
||||
import { dbToLinear, distance3 } from "/packages/acoustics-core/math.js";
|
||||
import {
|
||||
DEFAULT_LOCALIZATION_ALGORITHM,
|
||||
LOCALIZATION_ALGORITHM_IDS,
|
||||
isDasAlgorithm,
|
||||
localizationAlgorithmDefinition,
|
||||
localizationAlgorithmLabel,
|
||||
} from "/packages/acoustics-core/localization-algorithms.js";
|
||||
import { renderSceneMix } from "/packages/acoustics-core/mixdown.js";
|
||||
import { whiteNoiseAt } from "/packages/acoustics-core/noise.js";
|
||||
import { sphericalAttenuation, synthesizeMicrophoneSignals } from "/packages/acoustics-core/propagation.js";
|
||||
@@ -29,6 +36,115 @@ function assertNear(actual, expected, tolerance, label) {
|
||||
assert(Math.abs(actual - expected) <= tolerance, `${label}: expected ${expected} ± ${tolerance}, received ${actual}`);
|
||||
}
|
||||
|
||||
test("localization algorithm registry exposes stable ids and capabilities", () => {
|
||||
assertEqual(DEFAULT_LOCALIZATION_ALGORITHM, "das", "default algorithm");
|
||||
assertEqual(
|
||||
LOCALIZATION_ALGORITHM_IDS.join(","),
|
||||
"das,das-phat,gcc-phat",
|
||||
"stable algorithm ids",
|
||||
);
|
||||
assertEqual(localizationAlgorithmDefinition("das").rawScale, "power", "DAS map scale");
|
||||
assertEqual(
|
||||
localizationAlgorithmDefinition("das-phat").spectralWeighting,
|
||||
"phat",
|
||||
"DAS-PHAT weighting",
|
||||
);
|
||||
assertEqual(
|
||||
localizationAlgorithmDefinition("gcc-phat").backends.join(","),
|
||||
"cpu",
|
||||
"GCC-PHAT backend capability",
|
||||
);
|
||||
assert(isDasAlgorithm("das") && isDasAlgorithm("das-phat"), "DAS engine algorithms");
|
||||
assert(!isDasAlgorithm("gcc-phat"), "GCC-PHAT uses a separate engine");
|
||||
assertEqual(localizationAlgorithmLabel("das-phat"), "DAS-PHAT", "algorithm label");
|
||||
assertEqual(localizationAlgorithmDefinition("unknown"), undefined, "unknown lookup");
|
||||
});
|
||||
|
||||
test("CPU DAS-PHAT is invariant to spectrum and calibration amplitude gains", async () => {
|
||||
const sampleRate = 8_000;
|
||||
const fftSize = 256;
|
||||
const bin = 32;
|
||||
const frequency = (bin * sampleRate) / fftSize;
|
||||
const speedOfSound = 343;
|
||||
const target = [0.2, 0, 1];
|
||||
const positions = [[-0.15, 0, 0], [-0.02, 0, 0], [0.12, 0, 0]];
|
||||
const phaseOffsets = [30, -45, 75];
|
||||
const responsePhases = [10, 20, -35];
|
||||
const createDefinition = (withAmplitudeMetadata) => ({
|
||||
microphones: positions.map((position, index) => ({
|
||||
id: `mic-${index}`,
|
||||
position,
|
||||
sensitivityDb: withAmplitudeMetadata ? [-30, 24, 60][index] : 0,
|
||||
phaseOffsetDeg: phaseOffsets[index],
|
||||
frequencyResponse: [
|
||||
{
|
||||
frequencyHz: 0,
|
||||
gainDb: withAmplitudeMetadata ? [-60, 40, 100][index] : 0,
|
||||
phaseDeg: responsePhases[index],
|
||||
},
|
||||
{
|
||||
frequencyHz: 4_000,
|
||||
gainDb: withAmplitudeMetadata ? [-60, 40, 100][index] : 0,
|
||||
phaseDeg: responsePhases[index],
|
||||
},
|
||||
],
|
||||
})),
|
||||
});
|
||||
const createSpectrum = (magnitudes) => {
|
||||
const real = new Float32Array(positions.length);
|
||||
const imaginary = new Float32Array(positions.length);
|
||||
positions.forEach((position, index) => {
|
||||
const propagationPhase =
|
||||
(2 * Math.PI * frequency * distance3(target, position)) / speedOfSound;
|
||||
const calibrationPhase =
|
||||
((phaseOffsets[index] + responsePhases[index]) * Math.PI) / 180;
|
||||
const observedPhase = -propagationPhase + calibrationPhase;
|
||||
real[index] = Math.cos(observedPhase) * magnitudes[index];
|
||||
imaginary[index] = Math.sin(observedPhase) * magnitudes[index];
|
||||
});
|
||||
return { real, imaginary };
|
||||
};
|
||||
const scan = {
|
||||
z: 1,
|
||||
xMin: -0.2,
|
||||
xMax: 0.2,
|
||||
yMin: 0,
|
||||
yMax: 0,
|
||||
columns: 2,
|
||||
rows: 1,
|
||||
};
|
||||
const baselineArray = createDefinition(false);
|
||||
const gainedArray = createDefinition(true);
|
||||
const common = {
|
||||
bin,
|
||||
frameCount: 1,
|
||||
pointCount: 2,
|
||||
sampleRate,
|
||||
fftSize,
|
||||
speedOfSound,
|
||||
algorithm: "das-phat",
|
||||
pointBatchSize: 64,
|
||||
};
|
||||
const baseline = await computeBinMap({
|
||||
...common,
|
||||
spectrum: createSpectrum([1, 1, 1]),
|
||||
arrayDefinition: baselineArray,
|
||||
distances: createDistanceMatrix(baselineArray, scan),
|
||||
});
|
||||
const gained = await computeBinMap({
|
||||
...common,
|
||||
spectrum: createSpectrum([0.125, 8, 32]),
|
||||
arrayDefinition: gainedArray,
|
||||
distances: createDistanceMatrix(gainedArray, scan),
|
||||
});
|
||||
|
||||
baseline.forEach((value, index) => {
|
||||
assertNear(gained[index], value, 2e-6, `gain-invariant map value ${index}`);
|
||||
});
|
||||
assertNear(baseline[1], 1, 2e-6, "phase-compensated target score");
|
||||
assert(baseline[1] > baseline[0] * 1.1, "phase compensation preserves target peak");
|
||||
});
|
||||
|
||||
function rms(values, start = 0, end = values.length) {
|
||||
let sum = 0;
|
||||
for (let index = start; index < end; index += 1) sum += values[index] ** 2;
|
||||
@@ -521,12 +637,31 @@ test("CPU DAS peak가 calibration 후 단일 tone의 실제 grid 위치를 찾
|
||||
onProgress: (progress) => progressEvents.push(progress),
|
||||
});
|
||||
const peakPosition = result.bands[0].peak.position;
|
||||
assertEqual(result.algorithm, "das", "default result algorithm");
|
||||
assertEqual(result.bands[0].rawScale, "power", "DAS raw map scale");
|
||||
assert(distance3(peakPosition, scene.sources[0].position) <= 0.071, `peak=${peakPosition.join(",")}`);
|
||||
assert(result.bands[0].rawMap !== result.bands[0].displayMap, "raw/display buffers are separate");
|
||||
assertNear(Math.max(...result.bands[0].rawMap), result.bands[0].peak.rawPower, 1e-4, "raw map peak");
|
||||
assertNear(Math.max(...result.bands[0].displayMap), 1, 1e-6, "normalized display peak");
|
||||
assert(progressEvents.some(({ stage }) => stage === "beamforming"), "beamforming progress");
|
||||
|
||||
const phaseOnly = await computeDasMap({
|
||||
channels: synthesis.channels,
|
||||
sampleRate: synthesis.sampleRate,
|
||||
arrayDefinition,
|
||||
scene,
|
||||
experiment: {
|
||||
...experiment,
|
||||
beamforming: { ...experiment.beamforming, algorithm: "das-phat" },
|
||||
},
|
||||
});
|
||||
assertEqual(phaseOnly.algorithm, "das-phat", "phase-only result algorithm");
|
||||
assertEqual(phaseOnly.bands[0].rawScale, "score", "DAS-PHAT raw map scale");
|
||||
assert(
|
||||
distance3(phaseOnly.bands[0].peak.position, scene.sources[0].position) <= 0.071,
|
||||
`DAS-PHAT peak=${phaseOnly.bands[0].peak.position.join(",")}`,
|
||||
);
|
||||
|
||||
const uncompensated = await computeDasMap({
|
||||
channels: synthesis.channels,
|
||||
sampleRate: synthesis.sampleRate,
|
||||
|
||||
Reference in New Issue
Block a user