feat: add comparable localization algorithms

This commit is contained in:
2026-08-28 20:20:57 +09:00
parent 7fe846fbf9
commit d58f673402
20 changed files with 2104 additions and 68 deletions
+48 -9
View File
@@ -1,5 +1,12 @@
import { computeDasMap, createDasAnalysisPlan } from "./das.js";
import { findLocalPeaks } from "./evaluation.js";
import {
computeLocalizationMap,
createLocalizationAnalysisPlan,
} from "./localization.js";
import {
isDasAlgorithm,
requireLocalizationAlgorithmDefinition,
} from "./localization-algorithms.js";
import {
DEFAULT_MAX_COMPENSATION_DB,
MAX_FREQUENCY_RESPONSE_HZ,
@@ -214,6 +221,10 @@ function throwIfAborted(signal) {
}
export function detectBandPeaks(rawMap, scan, options = {}) {
const rawScale = options.rawScale ?? rawMap?.scale ?? "power";
if (rawScale !== "power" && rawScale !== "score") {
throw new RangeError('peak map rawScale must be "power" or "score".');
}
const peakDetection = copyPeakDetection(options, scan);
const localPeaks = findLocalPeaks(rawMap, scan);
const threshold = (localPeaks[0]?.value ?? 0) * peakDetection.thresholdRatio;
@@ -235,7 +246,9 @@ export function detectBandPeaks(rawMap, scan, options = {}) {
rank: detections.length + 1,
index: peak.index,
position: [...peak.position],
rawPower: peak.value,
rawValue: peak.value,
rawScale,
...(rawScale === "power" ? { rawPower: peak.value } : {}),
});
if (detections.length === peakDetection.maximumDetections) break;
}
@@ -244,7 +257,10 @@ export function detectBandPeaks(rawMap, scan, options = {}) {
function locationsFromBands(bands, scan, peakDetection) {
return bands.flatMap((band) =>
detectBandPeaks(band.rawMap, scan, peakDetection).map((peak) => ({
detectBandPeaks(band.rawMap, scan, {
...peakDetection,
rawScale: band.rawScale,
}).map((peak) => ({
bandId: band.id,
...peak,
})),
@@ -287,6 +303,13 @@ export function createLocalizationPlan(config) {
config.beamforming?.maxCompensationDb ??
DEFAULT_MAX_COMPENSATION_DB,
);
const algorithm = requireLocalizationAlgorithmDefinition(
config.algorithm ?? config.beamforming?.algorithm,
);
const phatRelativeFloor =
config.phatRelativeFloor ?? config.beamforming?.phatRelativeFloor;
const phatFloorRatio =
config.phatFloorRatio ?? config.beamforming?.phatFloorRatio;
const scene = Object.freeze({ speedOfSound });
const experiment = Object.freeze({
@@ -294,12 +317,14 @@ export function createLocalizationPlan(config) {
scan,
frequencyBands,
beamforming: Object.freeze({
algorithm: algorithm.id,
backend: "cpu",
normalization,
compensateFrequencyResponse,
maxCompensationDb,
}),
});
const dasPlan = createDasAnalysisPlan({
const analysisPlan = createLocalizationAnalysisPlan({
sampleRate,
arrayDefinition,
scene,
@@ -310,6 +335,7 @@ export function createLocalizationPlan(config) {
return Object.freeze({
kind: REALTIME_LOCALIZATION_PLAN_KIND,
algorithm: algorithm.id,
sampleRate,
channelCount: arrayDefinition.microphones.length,
arrayDefinition,
@@ -322,10 +348,15 @@ export function createLocalizationPlan(config) {
maxCompensationDb,
scene,
experiment,
dasPlan,
analysisPlan,
...(isDasAlgorithm(algorithm.id)
? { dasPlan: analysisPlan }
: { gccPhatPlan: analysisPlan }),
computeOptions: Object.freeze({
pointBatchSize: DEFAULT_REALTIME_POINT_BATCH_SIZE,
stftTransformBatchSize: DEFAULT_REALTIME_STFT_TRANSFORM_BATCH_SIZE,
...(phatRelativeFloor !== undefined ? { phatRelativeFloor } : {}),
...(phatFloorRatio !== undefined ? { phatFloorRatio } : {}),
...(config.computeOptions ?? {}),
}),
});
@@ -350,7 +381,7 @@ export async function localizeMultichannel(options) {
if (sampleCount === 0) throw new RangeError("channels must contain at least one sample.");
throwIfAborted(options.signal);
const processingStartedAt = monotonicNow();
const das = await computeDasMap({
const localization = await computeLocalizationMap({
...plan.computeOptions,
...(options.computeOptions ?? {}),
channels: options.channels,
@@ -358,14 +389,18 @@ export async function localizeMultichannel(options) {
arrayDefinition: plan.arrayDefinition,
scene: plan.scene,
experiment: plan.experiment,
analysisPlan: plan.dasPlan,
analysisPlan: plan.analysisPlan,
signal: options.signal,
onProgress: options.onProgress,
});
const locations = locationsFromBands(das.bands, plan.scan, plan.peakDetection);
const locations = locationsFromBands(
localization.bands,
plan.scan,
plan.peakDetection,
);
const processingTimeMs = Math.max(0, monotonicNow() - processingStartedAt);
return {
...das,
...localization,
sampleRate: plan.sampleRate,
sampleCount,
duration: sampleCount / plan.sampleRate,
@@ -488,6 +523,7 @@ export class StreamingDasLocalizer {
getState() {
return {
sampleRate: this.plan.sampleRate,
algorithm: this.plan.algorithm,
channelCount: this.plan.channelCount,
analysisWindowSamples: this.analysisWindowSamples,
analysisHopSamples: this.analysisHopSamples,
@@ -675,6 +711,9 @@ export class StreamingDasLocalizer {
}
}
/** Algorithm-neutral alias; the older class name remains for compatibility. */
export { StreamingDasLocalizer as StreamingLocalizer };
export function createStreamingLocalizer(config) {
return new StreamingDasLocalizer(config);
}