feat: add comparable localization algorithms
This commit is contained in:
@@ -9,6 +9,11 @@ import {
|
||||
requireMaxCompensationDb,
|
||||
} from "../acoustics-core/frequency-response.js";
|
||||
import { dbToLinear } from "../acoustics-core/math.js";
|
||||
import {
|
||||
DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
isDasAlgorithm,
|
||||
requireLocalizationAlgorithmDefinition,
|
||||
} from "../acoustics-core/localization-algorithms.js";
|
||||
import { extractStftBins } from "../acoustics-core/stft.js";
|
||||
import {
|
||||
DAS_PARAMS_BYTE_LENGTH,
|
||||
@@ -72,11 +77,60 @@ function minimumBufferSize(byteLength) {
|
||||
return Math.max(4, Math.ceil(byteLength / 4) * 4);
|
||||
}
|
||||
|
||||
function requirePhatRelativeFloor(value) {
|
||||
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
||||
throw new RangeError("phatRelativeFloor must lie inside 0..1.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function phaseNormalizePackedStft(
|
||||
packed,
|
||||
binCount,
|
||||
frameCount,
|
||||
microphoneCount,
|
||||
relativeFloor,
|
||||
) {
|
||||
requirePhatRelativeFloor(relativeFloor);
|
||||
const valuesPerBin = frameCount * microphoneCount;
|
||||
for (let binIndex = 0; binIndex < binCount; binIndex += 1) {
|
||||
const binOffset = binIndex * valuesPerBin * 2;
|
||||
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
|
||||
const frameOffset = binOffset + frameIndex * microphoneCount * 2;
|
||||
let maximumMagnitude = 0;
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const valueOffset = frameOffset + microphoneIndex * 2;
|
||||
maximumMagnitude = Math.max(
|
||||
maximumMagnitude,
|
||||
Math.hypot(packed[valueOffset], packed[valueOffset + 1]),
|
||||
);
|
||||
}
|
||||
const magnitudeFloor = maximumMagnitude * relativeFloor;
|
||||
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
|
||||
const valueOffset = frameOffset + microphoneIndex * 2;
|
||||
const magnitude = Math.hypot(packed[valueOffset], packed[valueOffset + 1]);
|
||||
if (!(magnitude > magnitudeFloor)) {
|
||||
packed[valueOffset] = 0;
|
||||
packed[valueOffset + 1] = 0;
|
||||
continue;
|
||||
}
|
||||
packed[valueOffset] /= magnitude;
|
||||
packed[valueOffset + 1] /= magnitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function packStftBatch(stft, bins, options = {}) {
|
||||
if (!stft || !Number.isInteger(stft.frameCount) || !Number.isInteger(stft.microphoneCount)) {
|
||||
throw new TypeError("유효한 STFT 결과가 필요합니다.");
|
||||
}
|
||||
if (!Array.isArray(bins) || bins.length === 0) throw new TypeError("하나 이상의 FFT bin이 필요합니다.");
|
||||
const algorithm = requireLocalizationAlgorithmDefinition(options.algorithm ?? "das");
|
||||
if (!isDasAlgorithm(algorithm.id)) {
|
||||
throw new RangeError(`packStftBatch does not implement ${algorithm.id}.`);
|
||||
}
|
||||
const phaseOnly = algorithm.spectralWeighting === "phat";
|
||||
const valuesPerBin = stft.frameCount * stft.microphoneCount;
|
||||
const packed = new Float32Array(bins.length * valuesPerBin * 2);
|
||||
const microphones = options.microphones;
|
||||
@@ -105,9 +159,9 @@ export function packStftBatch(stft, bins, options = {}) {
|
||||
microphones.forEach((microphone, microphoneIndex) => {
|
||||
if (!Array.isArray(microphone.frequencyResponse)) return;
|
||||
interpolateFrequencyResponse(microphone.frequencyResponse, frequencyHz, response);
|
||||
const inverseGain = dbToLinear(
|
||||
inverseResponseGainDb(response.gainDb, maxCompensationDb),
|
||||
);
|
||||
const inverseGain = phaseOnly
|
||||
? 1
|
||||
: dbToLinear(inverseResponseGainDb(response.gainDb, maxCompensationDb));
|
||||
const responseOffset = (binIndex * stft.microphoneCount + microphoneIndex) * 2;
|
||||
inverseResponses[responseOffset] = inverseGain * Math.cos(response.phaseRadians);
|
||||
inverseResponses[responseOffset + 1] = -inverseGain * Math.sin(response.phaseRadians);
|
||||
@@ -138,6 +192,15 @@ export function packStftBatch(stft, bins, options = {}) {
|
||||
inputReal * inverseImaginary + inputImaginary * inverseReal;
|
||||
}
|
||||
});
|
||||
if (phaseOnly) {
|
||||
phaseNormalizePackedStft(
|
||||
packed,
|
||||
bins.length,
|
||||
stft.frameCount,
|
||||
stft.microphoneCount,
|
||||
options.phatRelativeFloor ?? DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
);
|
||||
}
|
||||
return packed;
|
||||
}
|
||||
|
||||
@@ -445,9 +508,16 @@ export async function computeDasWebGpu(options) {
|
||||
maximumGpuComplexMacs = 20_000_000_000,
|
||||
maximumFftButterflies = 1_000_000_000,
|
||||
stftTransformBatchSize = 32,
|
||||
phatRelativeFloor = DEFAULT_PHAT_RELATIVE_FLOOR,
|
||||
gpu,
|
||||
powerPreference,
|
||||
} = options;
|
||||
const algorithm = requireLocalizationAlgorithmDefinition(
|
||||
experiment.beamforming?.algorithm ?? "das",
|
||||
);
|
||||
if (!isDasAlgorithm(algorithm.id)) {
|
||||
throw new RangeError(`computeDasWebGpu does not implement ${algorithm.id}.`);
|
||||
}
|
||||
if (!Array.isArray(channels) || channels.length !== arrayDefinition?.microphones?.length) {
|
||||
throw new Error(`채널 수 ${channels?.length ?? 0}와 마이크 수 ${arrayDefinition?.microphones?.length ?? 0}가 다릅니다.`);
|
||||
}
|
||||
@@ -501,7 +571,9 @@ export async function computeDasWebGpu(options) {
|
||||
label: "DAS beamforming pipeline",
|
||||
}),
|
||||
}));
|
||||
const calibration = packMicrophoneCalibration(arrayDefinition.microphones);
|
||||
const calibration = packMicrophoneCalibration(arrayDefinition.microphones, {
|
||||
includeAmplitude: algorithm.spectralWeighting !== "phat",
|
||||
});
|
||||
const calibrationBuffer = await withWebGpuErrorScopes(context.device, async () =>
|
||||
createBuffer(context.device, {
|
||||
label: "DAS microphone calibration",
|
||||
@@ -543,12 +615,14 @@ export async function computeDasWebGpu(options) {
|
||||
}),
|
||||
});
|
||||
const packedSpectrum = packStftBatch(stft, batchBins, {
|
||||
algorithm: algorithm.id,
|
||||
microphones: arrayDefinition.microphones,
|
||||
sampleRate,
|
||||
fftSize: experiment.stft.fftSize,
|
||||
compensateFrequencyResponse:
|
||||
experiment.beamforming.compensateFrequencyResponse !== false,
|
||||
maxCompensationDb: experiment.beamforming.maxCompensationDb,
|
||||
phatRelativeFloor,
|
||||
});
|
||||
stftMs += now() - stftStartedAt;
|
||||
const binIndices = Uint32Array.from(batchBins);
|
||||
@@ -688,9 +762,10 @@ export async function computeDasWebGpu(options) {
|
||||
completedBins += batchBins.length;
|
||||
}
|
||||
|
||||
const bands = finalizeDasBands(rawMaps, workload, experiment);
|
||||
const bands = finalizeDasBands(rawMaps, workload, experiment, algorithm.rawScale);
|
||||
const totalMs = now() - totalStartedAt;
|
||||
return {
|
||||
algorithm: algorithm.id,
|
||||
backend: "webgpu",
|
||||
frameCount: workload.frameCount,
|
||||
microphoneCount: workload.microphoneCount,
|
||||
|
||||
Reference in New Issue
Block a user