feat: add physically explicit narrowband MUSIC
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { findLocalPeaks } from "./evaluation.js";
|
||||
import {
|
||||
findLocalPeaks,
|
||||
normalizedMusicPseudospectrum,
|
||||
} from "./evaluation.js";
|
||||
import {
|
||||
computeLocalizationMap,
|
||||
createLocalizationAnalysisPlan,
|
||||
@@ -17,6 +20,8 @@ import {
|
||||
MAX_FREQUENCY_RESPONSE_POINTS,
|
||||
requireMaxCompensationDb,
|
||||
} from "./frequency-response.js";
|
||||
import { isMusicUnavailableError } from "./music.js";
|
||||
import { getCompleteFrameCount } from "./stft.js";
|
||||
|
||||
export const REALTIME_LOCALIZATION_PLAN_KIND = "micarrsim.realtime-localization-plan/1";
|
||||
export const DEFAULT_STREAM_RESULTS_PER_CALL = 1;
|
||||
@@ -224,11 +229,36 @@ 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".');
|
||||
if (rawScale !== "power" && rawScale !== "score" && rawScale !== "noise-projection") {
|
||||
throw new RangeError(
|
||||
'peak map rawScale must be "power", "score", or "noise-projection".',
|
||||
);
|
||||
}
|
||||
const objectiveDirection =
|
||||
options.objectiveDirection ??
|
||||
rawMap?.objectiveDirection ??
|
||||
(rawScale === "noise-projection" ? "minimum" : "maximum");
|
||||
if (
|
||||
(rawScale === "noise-projection" && objectiveDirection !== "minimum") ||
|
||||
(rawScale !== "noise-projection" && objectiveDirection !== "maximum")
|
||||
) {
|
||||
throw new RangeError(
|
||||
"noise-projection uses a minimum objective; power and score use a maximum objective.",
|
||||
);
|
||||
}
|
||||
const rawValues =
|
||||
rawMap && !Array.isArray(rawMap) && !ArrayBuffer.isView(rawMap)
|
||||
? rawMap.values
|
||||
: rawMap;
|
||||
let detectionMap = rawMap;
|
||||
if (objectiveDirection === "minimum") {
|
||||
const pseudospectrum =
|
||||
normalizedMusicPseudospectrum(rawValues);
|
||||
if (pseudospectrum.flat) return [];
|
||||
detectionMap = pseudospectrum.values;
|
||||
}
|
||||
const peakDetection = copyPeakDetection(options, scan);
|
||||
const localPeaks = findLocalPeaks(rawMap, scan);
|
||||
const localPeaks = findLocalPeaks(detectionMap, scan);
|
||||
const threshold = (localPeaks[0]?.value ?? 0) * peakDetection.thresholdRatio;
|
||||
const detections = [];
|
||||
for (const peak of localPeaks) {
|
||||
@@ -248,11 +278,15 @@ export function detectBandPeaks(rawMap, scan, options = {}) {
|
||||
rank: detections.length + 1,
|
||||
index: peak.index,
|
||||
position: [...peak.position],
|
||||
rawValue: peak.value,
|
||||
rawValue: rawValues[peak.index],
|
||||
rawScale,
|
||||
objectiveDirection,
|
||||
detectionMerit: peak.value,
|
||||
...(rawScale === "power"
|
||||
? { rawPower: peak.value }
|
||||
: { rawScore: peak.value }),
|
||||
? { rawPower: rawValues[peak.index] }
|
||||
: rawScale === "score"
|
||||
? { rawScore: rawValues[peak.index] }
|
||||
: { noiseProjection: rawValues[peak.index] }),
|
||||
});
|
||||
if (detections.length === peakDetection.maximumDetections) break;
|
||||
}
|
||||
@@ -291,10 +325,21 @@ export function createLocalizationPlan(config) {
|
||||
);
|
||||
if (hopSize > fftSize) throw new RangeError("stftHopSize must not exceed fftSize.");
|
||||
const speedOfSound = requirePositiveFinite(config.speedOfSound ?? 343, "speedOfSound");
|
||||
const normalization = config.normalization ?? config.beamforming?.normalization ?? "peak";
|
||||
const algorithm = requireLocalizationAlgorithmDefinition(
|
||||
config.algorithm ?? config.beamforming?.algorithm,
|
||||
);
|
||||
const normalization =
|
||||
config.normalization ??
|
||||
config.beamforming?.normalization ??
|
||||
(algorithm.id === "music" ? "none" : "peak");
|
||||
if (normalization !== "peak" && normalization !== "none") {
|
||||
throw new RangeError('normalization must be "peak" or "none".');
|
||||
}
|
||||
if (algorithm.id === "music" && normalization !== "none") {
|
||||
throw new RangeError(
|
||||
'MUSIC normalization must be "none"; raw noise projection q is not peak-normalized.',
|
||||
);
|
||||
}
|
||||
const compensateFrequencyResponse =
|
||||
config.compensateFrequencyResponse ??
|
||||
config.beamforming?.compensateFrequencyResponse ??
|
||||
@@ -307,9 +352,20 @@ export function createLocalizationPlan(config) {
|
||||
config.beamforming?.maxCompensationDb ??
|
||||
DEFAULT_MAX_COMPENSATION_DB,
|
||||
);
|
||||
const algorithm = requireLocalizationAlgorithmDefinition(
|
||||
config.algorithm ?? config.beamforming?.algorithm,
|
||||
const minimumDistance = requirePositiveFinite(
|
||||
config.minimumDistance ??
|
||||
config.beamforming?.minimumDistance ??
|
||||
0.01,
|
||||
"minimumDistance",
|
||||
);
|
||||
const musicSourceCountMethod =
|
||||
config.musicSourceCountMethod ??
|
||||
config.beamforming?.musicSourceCountMethod ??
|
||||
"fixed";
|
||||
const musicSourceCount =
|
||||
config.musicSourceCount ??
|
||||
config.beamforming?.musicSourceCount ??
|
||||
1;
|
||||
const phatRelativeFloor =
|
||||
config.phatRelativeFloor ?? config.beamforming?.phatRelativeFloor;
|
||||
const phatFloorRatio =
|
||||
@@ -326,6 +382,10 @@ export function createLocalizationPlan(config) {
|
||||
normalization,
|
||||
compensateFrequencyResponse,
|
||||
maxCompensationDb,
|
||||
minimumDistance,
|
||||
...(algorithm.id === "music"
|
||||
? { musicSourceCountMethod, musicSourceCount }
|
||||
: {}),
|
||||
}),
|
||||
});
|
||||
const analysisPlan = createLocalizationAnalysisPlan({
|
||||
@@ -350,18 +410,22 @@ export function createLocalizationPlan(config) {
|
||||
speedOfSound,
|
||||
normalization,
|
||||
maxCompensationDb,
|
||||
minimumDistance,
|
||||
scene,
|
||||
experiment,
|
||||
analysisPlan,
|
||||
...(isDasAlgorithm(algorithm.id)
|
||||
? { dasPlan: analysisPlan }
|
||||
: { gccPhatPlan: analysisPlan }),
|
||||
: algorithm.id === "gcc-phat"
|
||||
? { gccPhatPlan: analysisPlan }
|
||||
: { musicPlan: analysisPlan }),
|
||||
computeOptions: Object.freeze({
|
||||
pointBatchSize: isDasAlgorithm(algorithm.id)
|
||||
? DEFAULT_REALTIME_POINT_BATCH_SIZE
|
||||
: DEFAULT_GCC_PHAT_POINT_BATCH_SIZE,
|
||||
pointBatchSize:
|
||||
algorithm.id === "gcc-phat"
|
||||
? DEFAULT_GCC_PHAT_POINT_BATCH_SIZE
|
||||
: DEFAULT_REALTIME_POINT_BATCH_SIZE,
|
||||
stftTransformBatchSize: DEFAULT_REALTIME_STFT_TRANSFORM_BATCH_SIZE,
|
||||
...(!isDasAlgorithm(algorithm.id)
|
||||
...(algorithm.id === "gcc-phat"
|
||||
? { crossSpectrumBatchSize: DEFAULT_GCC_PHAT_CROSS_SPECTRUM_BATCH_SIZE }
|
||||
: {}),
|
||||
...(phatRelativeFloor !== undefined ? { phatRelativeFloor } : {}),
|
||||
@@ -390,18 +454,49 @@ 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 localization = await computeLocalizationMap({
|
||||
...plan.computeOptions,
|
||||
...(options.computeOptions ?? {}),
|
||||
channels: options.channels,
|
||||
sampleRate: plan.sampleRate,
|
||||
arrayDefinition: plan.arrayDefinition,
|
||||
scene: plan.scene,
|
||||
experiment: plan.experiment,
|
||||
analysisPlan: plan.analysisPlan,
|
||||
signal: options.signal,
|
||||
onProgress: options.onProgress,
|
||||
});
|
||||
let localization;
|
||||
try {
|
||||
localization = await computeLocalizationMap({
|
||||
...plan.computeOptions,
|
||||
...(options.computeOptions ?? {}),
|
||||
channels: options.channels,
|
||||
sampleRate: plan.sampleRate,
|
||||
arrayDefinition: plan.arrayDefinition,
|
||||
scene: plan.scene,
|
||||
experiment: plan.experiment,
|
||||
analysisPlan: plan.analysisPlan,
|
||||
signal: options.signal,
|
||||
onProgress: options.onProgress,
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
plan.algorithm !== "music" ||
|
||||
!isMusicUnavailableError(error)
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
localization = {
|
||||
algorithm: "music",
|
||||
backend: "cpu",
|
||||
status: "unavailable",
|
||||
unavailable: {
|
||||
code: error.code,
|
||||
reason: error.reason,
|
||||
message: error.message,
|
||||
details: { ...error.details },
|
||||
},
|
||||
frameCount: getCompleteFrameCount(
|
||||
sampleCount,
|
||||
plan.stft.fftSize,
|
||||
plan.stft.hopSize,
|
||||
),
|
||||
microphoneCount: plan.channelCount,
|
||||
pointCount: plan.scan.rows * plan.scan.columns,
|
||||
objectiveDirection: "minimum",
|
||||
rawScale: "noise-projection",
|
||||
bands: [],
|
||||
};
|
||||
}
|
||||
const locations = locationsFromBands(
|
||||
localization.bands,
|
||||
plan.scan,
|
||||
|
||||
Reference in New Issue
Block a user