575 lines
20 KiB
JavaScript
575 lines
20 KiB
JavaScript
import {
|
|
computeLocalizationMap,
|
|
createLocalizationAnalysisPlan,
|
|
estimateLocalizationWorkload,
|
|
} from "/packages/acoustics-core/localization.js";
|
|
import {
|
|
createMusicBinCalibration,
|
|
isMusicUnavailableError,
|
|
} from "/packages/acoustics-core/music.js";
|
|
import {
|
|
DEFAULT_GCC_PHAT_CROSS_SPECTRUM_BATCH_SIZE,
|
|
DEFAULT_GCC_PHAT_POINT_BATCH_SIZE,
|
|
} from "/packages/acoustics-core/gcc-phat.js";
|
|
import {
|
|
DEFAULT_LOCALIZATION_ALGORITHM,
|
|
LOCALIZATION_ALGORITHM_IDS,
|
|
localizationAlgorithmDefinition,
|
|
localizationAlgorithmSupportsBackend,
|
|
} from "/packages/acoustics-core/localization-algorithms.js";
|
|
import {
|
|
StreamingDasLocalizer,
|
|
StreamingLocalizer,
|
|
createLocalizationPlan,
|
|
detectBandPeaks,
|
|
localizeMultichannel,
|
|
} from "/packages/acoustics-core/realtime-localizer.js";
|
|
import {
|
|
computeDasWithBackend,
|
|
computeLocalizationWithBackend,
|
|
} from "/packages/webgpu-kernels/compute.js";
|
|
|
|
function createFixture(algorithm = "das") {
|
|
const sampleRate = 4096;
|
|
const arrayDefinition = {
|
|
id: "algorithm-fixture",
|
|
microphones: [
|
|
{ id: "nw", position: [-0.1, 0.1, 0] },
|
|
{ id: "ne", position: [0.1, 0.1, 0] },
|
|
{ id: "sw", position: [-0.1, -0.1, 0] },
|
|
{ id: "se", position: [0.1, -0.1, 0] },
|
|
],
|
|
};
|
|
let state = 0x9e3779b9;
|
|
const emitted = Float32Array.from({ length: 128 }, () => {
|
|
state ^= state << 13;
|
|
state ^= state >>> 17;
|
|
state ^= state << 5;
|
|
return ((state >>> 0) / 0xffff_ffff) * 2 - 1;
|
|
});
|
|
const channels = arrayDefinition.microphones.map((_, microphoneIndex) =>
|
|
Float32Array.from(
|
|
{ length: emitted.length },
|
|
(_, sampleIndex) => emitted[Math.max(0, sampleIndex - microphoneIndex)],
|
|
)
|
|
);
|
|
const scan = {
|
|
z: 1,
|
|
xMin: -0.2,
|
|
xMax: 0.2,
|
|
yMin: -0.2,
|
|
yMax: 0.2,
|
|
columns: 3,
|
|
rows: 3,
|
|
};
|
|
const frequencyBands = algorithm === "music"
|
|
? [{ id: "single-bin", minHz: 500, maxHz: 530, color: "#65f2bf" }]
|
|
: [{ id: "wide", minHz: 128, maxHz: 1800, color: "#65f2bf" }];
|
|
const experiment = {
|
|
stft: { fftSize: 64, hopSize: 32, window: "hann" },
|
|
scan,
|
|
frequencyBands,
|
|
beamforming: {
|
|
algorithm,
|
|
backend: "cpu",
|
|
normalization: algorithm === "music" ? "none" : "peak",
|
|
compensateFrequencyResponse: true,
|
|
...(algorithm === "music"
|
|
? { musicSourceCountMethod: "fixed", musicSourceCount: 1 }
|
|
: {}),
|
|
},
|
|
};
|
|
return {
|
|
channels,
|
|
sampleRate,
|
|
arrayDefinition,
|
|
scene: { speedOfSound: 343 },
|
|
experiment,
|
|
};
|
|
}
|
|
|
|
test("localization algorithm registry exposes explicit scale and backend semantics", () => {
|
|
assertEqual(DEFAULT_LOCALIZATION_ALGORITHM, "das", "default algorithm");
|
|
assertEqual(
|
|
LOCALIZATION_ALGORITHM_IDS.join(","),
|
|
"das,das-phat,gcc-phat,music",
|
|
"registered ids",
|
|
);
|
|
assertEqual(localizationAlgorithmDefinition("das").rawScale, "power", "DAS power scale");
|
|
assertEqual(localizationAlgorithmDefinition("das-phat").rawScale, "score", "DAS-PHAT score scale");
|
|
assertEqual(localizationAlgorithmDefinition("gcc-phat").rawScale, "score", "GCC-PHAT score scale");
|
|
assertEqual(
|
|
localizationAlgorithmDefinition("music").rawScale,
|
|
"noise-projection",
|
|
"MUSIC raw noise-projection scale",
|
|
);
|
|
assert(localizationAlgorithmSupportsBackend("das-phat", "webgpu"), "DAS-PHAT WebGPU support");
|
|
assert(!localizationAlgorithmSupportsBackend("gcc-phat", "webgpu"), "GCC-PHAT CPU-only support");
|
|
assert(!localizationAlgorithmSupportsBackend("music", "webgpu"), "MUSIC CPU-only support");
|
|
});
|
|
|
|
test("generic CPU dispatcher runs all algorithms with explicit raw-map scales", async () => {
|
|
for (const algorithm of LOCALIZATION_ALGORITHM_IDS) {
|
|
const fixture = createFixture(algorithm);
|
|
const analysisPlan = createLocalizationAnalysisPlan(fixture);
|
|
const workload = estimateLocalizationWorkload({ ...fixture, analysisPlan });
|
|
const result = await computeLocalizationMap({
|
|
...fixture,
|
|
analysisPlan,
|
|
pointBatchSize: 1024,
|
|
stftTransformBatchSize: 1024,
|
|
});
|
|
const definition = localizationAlgorithmDefinition(algorithm);
|
|
|
|
assertEqual(result.algorithm, algorithm, `${algorithm} result id`);
|
|
assertEqual(result.backend, "cpu", `${algorithm} backend`);
|
|
assertEqual(result.bands[0].rawScale, definition.rawScale, `${algorithm} raw scale`);
|
|
assertEqual(result.bands[0].rawMap.length, 9, `${algorithm} map shape`);
|
|
assertEqual(result.bands[0].peak.rawScale, definition.rawScale, `${algorithm} peak scale`);
|
|
assertEqual(
|
|
"rawPower" in result.bands[0].peak,
|
|
definition.rawScale === "power",
|
|
`${algorithm} peak power field`,
|
|
);
|
|
assert(
|
|
result.bands[0].rawMap.every((value) => Number.isFinite(value) && value >= 0),
|
|
`${algorithm} finite nonnegative map`,
|
|
);
|
|
assert(workload.pointCount === 9 && workload.microphoneCount === 4, `${algorithm} workload`);
|
|
}
|
|
});
|
|
|
|
test("backend dispatcher keeps GCC-PHAT on CPU and rejects an explicit GPU request", async () => {
|
|
const fixture = createFixture("gcc-phat");
|
|
const calls = [];
|
|
const result = await computeLocalizationWithBackend(fixture, {
|
|
computeGpu: async () => {
|
|
calls.push("gpu");
|
|
throw new Error("GCC-PHAT must not call WebGPU");
|
|
},
|
|
computeGccCpu: async () => {
|
|
calls.push("gcc-cpu");
|
|
return { backend: "cpu", algorithm: "gcc-phat", bands: [] };
|
|
},
|
|
});
|
|
assertEqual(calls.join(","), "gcc-cpu", "GCC auto routing");
|
|
assertEqual(result.algorithm, "gcc-phat", "GCC result");
|
|
|
|
fixture.experiment.beamforming.backend = "webgpu";
|
|
let backendError;
|
|
try {
|
|
await computeLocalizationWithBackend(fixture);
|
|
} catch (error) {
|
|
backendError = error;
|
|
}
|
|
assert(backendError instanceof TypeError, "explicit GCC WebGPU is rejected");
|
|
|
|
let legacyError;
|
|
try {
|
|
await computeDasWithBackend(fixture);
|
|
} catch (error) {
|
|
legacyError = error;
|
|
}
|
|
assert(legacyError instanceof RangeError, "legacy DAS entry rejects GCC");
|
|
});
|
|
|
|
test("backend dispatcher keeps MUSIC on CPU and rejects an explicit GPU request", async () => {
|
|
const fixture = createFixture("music");
|
|
fixture.experiment.beamforming.backend = "auto";
|
|
const calls = [];
|
|
const result = await computeLocalizationWithBackend(fixture, {
|
|
computeGpu: async () => {
|
|
calls.push("gpu");
|
|
throw new Error("MUSIC must not call WebGPU");
|
|
},
|
|
computeMusicCpu: async () => {
|
|
calls.push("music-cpu");
|
|
return { backend: "cpu", algorithm: "music", bands: [] };
|
|
},
|
|
});
|
|
assertEqual(calls.join(","), "music-cpu", "MUSIC auto routing");
|
|
assertEqual(result.algorithm, "music", "MUSIC result");
|
|
|
|
fixture.experiment.beamforming.backend = "webgpu";
|
|
let backendError;
|
|
try {
|
|
await computeLocalizationWithBackend(fixture);
|
|
} catch (error) {
|
|
backendError = error;
|
|
}
|
|
assert(backendError instanceof TypeError, "explicit MUSIC WebGPU is rejected");
|
|
});
|
|
|
|
test("fixed-K MUSIC reports silent rank loss instead of choosing a grid cell", async () => {
|
|
const fixture = createFixture("music");
|
|
fixture.channels = fixture.channels.map(
|
|
(channel) => new Float32Array(channel.length),
|
|
);
|
|
let unavailable;
|
|
try {
|
|
await computeLocalizationMap(fixture);
|
|
} catch (error) {
|
|
unavailable = error;
|
|
}
|
|
assert(
|
|
isMusicUnavailableError(unavailable),
|
|
"silence is a typed MUSIC unavailable condition",
|
|
);
|
|
assertEqual(
|
|
unavailable.reason,
|
|
"signal-noise-partition-unidentifiable",
|
|
"silence has an explicit identifiability reason",
|
|
);
|
|
});
|
|
|
|
test("MUSIC uses an explicit above-Nyquist carrier hypothesis without Scene truth", async () => {
|
|
const baseband = createFixture("music");
|
|
const aliased = createFixture("music");
|
|
aliased.channels = baseband.channels;
|
|
aliased.experiment.frequencyBands = [
|
|
{ id: "aliased-single-bin", minHz: 4590, maxHz: 4620, color: "#65f2bf" },
|
|
];
|
|
|
|
const basebandResult = await computeLocalizationMap(baseband);
|
|
const aliasedResult = await computeLocalizationMap(aliased);
|
|
assertEqual(aliasedResult.bands[0].bins.join(","), "8", "aliased hypothesis maps to bin 8");
|
|
assertEqual(
|
|
aliasedResult.bands[0].observedFrequencyHz,
|
|
512,
|
|
"covariance uses the observed ADC bin center",
|
|
);
|
|
assertEqual(
|
|
aliasedResult.bands[0].analogFrequencyHz,
|
|
4608,
|
|
"band config supplies the analog FFT-grid carrier",
|
|
);
|
|
assertEqual(
|
|
aliasedResult.bands[0].signedAdcFrequencyHz,
|
|
512,
|
|
"signed ADC frequency is retained",
|
|
);
|
|
assertEqual(
|
|
aliasedResult.model.originalCarrierUsed,
|
|
true,
|
|
"configured analog carrier is used",
|
|
);
|
|
assertEqual(
|
|
aliasedResult.model.hypothesisSource,
|
|
"experiment-band-not-scene-truth",
|
|
"Scene truth is never used to choose the carrier",
|
|
);
|
|
assertEqual(
|
|
aliasedResult.model.multibandFusion,
|
|
"none-each-band-is-separate-single-analog-hypothesis",
|
|
"no cross-band fusion is invented",
|
|
);
|
|
assert(
|
|
JSON.stringify(Array.from(aliasedResult.bands[0].rawMap)) !==
|
|
JSON.stringify(Array.from(basebandResult.bands[0].rawMap)),
|
|
"same ADC data is tested against a distinct configured spatial wavelength",
|
|
);
|
|
});
|
|
|
|
test("near-field MUSIC applies the configured analog carrier and inversion without hiding grating lobes", async () => {
|
|
const sampleRate = 8192;
|
|
const fftSize = 128;
|
|
const speedOfSound = 343;
|
|
const basebandFrequency = 1024;
|
|
const invertedAnalogFrequency = sampleRate - basebandFrequency;
|
|
const source = [0.1, -0.1, 1];
|
|
const arrayDefinition = {
|
|
id: "physical-music-array",
|
|
microphones: [
|
|
{ id: "nw", position: [-0.12, 0.12, 0] },
|
|
{ id: "ne", position: [0.12, 0.12, 0] },
|
|
{ id: "sw", position: [-0.12, -0.12, 0] },
|
|
{ id: "se", position: [0.12, -0.12, 0] },
|
|
],
|
|
};
|
|
const channelsForAnalogFrequency = (frequency) =>
|
|
arrayDefinition.microphones.map((microphone) => {
|
|
const distance = Math.hypot(
|
|
source[0] - microphone.position[0],
|
|
source[1] - microphone.position[1],
|
|
source[2] - microphone.position[2],
|
|
);
|
|
return Float64Array.from(
|
|
{ length: 384 },
|
|
(_, sampleIndex) =>
|
|
Math.cos(
|
|
2 * Math.PI * frequency *
|
|
(sampleIndex / sampleRate - distance / speedOfSound),
|
|
) / distance,
|
|
);
|
|
});
|
|
const experimentForBand = (band) => ({
|
|
stft: { fftSize, hopSize: 64, window: "hann" },
|
|
scan: {
|
|
z: 1,
|
|
xMin: -0.2,
|
|
xMax: 0.2,
|
|
yMin: -0.2,
|
|
yMax: 0.2,
|
|
columns: 5,
|
|
rows: 5,
|
|
},
|
|
frequencyBands: [{ id: "tone", color: "#65f2bf", ...band }],
|
|
beamforming: {
|
|
algorithm: "music",
|
|
backend: "cpu",
|
|
minimumDistance: 0.01,
|
|
normalization: "none",
|
|
compensateFrequencyResponse: true,
|
|
musicSourceCountMethod: "fixed",
|
|
musicSourceCount: 1,
|
|
},
|
|
});
|
|
const common = {
|
|
sampleRate,
|
|
arrayDefinition,
|
|
scene: { speedOfSound },
|
|
};
|
|
const baseband = await computeLocalizationMap({
|
|
...common,
|
|
channels: channelsForAnalogFrequency(basebandFrequency),
|
|
experiment: experimentForBand({ minHz: 1000, maxHz: 1050 }),
|
|
});
|
|
const truthIndex = 18;
|
|
assertEqual(baseband.bands[0].peak.index, truthIndex, "baseband physical truth minimizes q");
|
|
const truthQ = baseband.bands[0].rawMap[truthIndex];
|
|
assert(
|
|
baseband.bands[0].rawMap.every((value) => value >= truthQ),
|
|
"truth q is the global minimum",
|
|
);
|
|
|
|
const aliased = await computeLocalizationMap({
|
|
...common,
|
|
channels: channelsForAnalogFrequency(invertedAnalogFrequency),
|
|
experiment: experimentForBand({ minHz: 7140, maxHz: 7190 }),
|
|
});
|
|
assertEqual(aliased.bands[0].bins.join(","), "16", "analog carrier folds to observed bin 16");
|
|
assertEqual(
|
|
aliased.bands[0].observedFrequencyHz,
|
|
basebandFrequency,
|
|
"covariance still uses the observed ADC frequency",
|
|
);
|
|
assertEqual(
|
|
aliased.bands[0].analogFrequencyHz,
|
|
invertedAnalogFrequency,
|
|
"manifold uses the configured analog carrier",
|
|
);
|
|
assertEqual(
|
|
aliased.bands[0].signedAdcFrequencyHz,
|
|
-basebandFrequency,
|
|
"inverted signed ADC frequency is explicit",
|
|
);
|
|
assert(aliased.bands[0].spectralInversion, "full received manifold is conjugated");
|
|
const aliasedTruthQ = aliased.bands[0].rawMap[truthIndex];
|
|
const aliasedMinimumQ = Math.min(...aliased.bands[0].rawMap);
|
|
assert(
|
|
Math.abs(aliasedTruthQ - aliasedMinimumQ) <= 1e-10,
|
|
"physical truth is a q minimum while grating-lobe ties remain allowed",
|
|
);
|
|
assertEqual(aliased.model.originalCarrierUsed, true, "configured carrier is used");
|
|
assertEqual(
|
|
aliased.model.aliasOrderResolution,
|
|
"configured-single-hypothesis-no-selection",
|
|
"no data-driven alias-order choice is claimed",
|
|
);
|
|
});
|
|
|
|
test("MUSIC conjugates analog microphone phase and preserves capped residual gain in inverted zones", () => {
|
|
const arrayDefinition = {
|
|
microphones: [{
|
|
id: "calibrated",
|
|
position: [0, 0, 0],
|
|
phaseOffsetDeg: 20,
|
|
frequencyResponse: [
|
|
{ frequencyHz: 3_000, gainDb: 0, phaseDeg: 0 },
|
|
{ frequencyHz: 5_000, gainDb: -30, phaseDeg: 40 },
|
|
],
|
|
}],
|
|
};
|
|
const corrected = createMusicBinCalibration({
|
|
arrayDefinition,
|
|
bin: 48,
|
|
sampleRate: 8_000,
|
|
fftSize: 128,
|
|
analogFrequencyHz: 5_000,
|
|
signedAdcFrequencyHz: -3_000,
|
|
spectralInversion: true,
|
|
compensateFrequencyResponse: true,
|
|
maxCompensationDb: 24,
|
|
});
|
|
assert(
|
|
Math.abs(corrected.dataPhases[0] - Math.PI / 3) <= 1e-12,
|
|
"inverted positive-bin calibration conjugates 20° + 40°",
|
|
);
|
|
assert(
|
|
Math.abs(
|
|
20 * Math.log10(corrected.manifoldGains[0]) + 6
|
|
) <= 1e-9,
|
|
"-30 dB response with 24 dB inverse cap leaves -6 dB",
|
|
);
|
|
assertEqual(corrected.manifoldPhases[0], 0, "compensated phase has no residual");
|
|
|
|
const raw = createMusicBinCalibration({
|
|
arrayDefinition,
|
|
bin: 48,
|
|
sampleRate: 8_000,
|
|
fftSize: 128,
|
|
analogFrequencyHz: 5_000,
|
|
signedAdcFrequencyHz: -3_000,
|
|
spectralInversion: true,
|
|
compensateFrequencyResponse: false,
|
|
});
|
|
assert(
|
|
Math.abs(raw.dataPhases[0] - 20 * Math.PI / 180) <= 1e-12,
|
|
"fixed phase offset correction follows inversion sign",
|
|
);
|
|
assert(
|
|
Math.abs(raw.manifoldPhases[0] + 40 * Math.PI / 180) <= 1e-12,
|
|
"uncompensated analog response phase remains conjugated in the manifold",
|
|
);
|
|
});
|
|
|
|
test("portable MUSIC plan retains its physical and source-count configuration", () => {
|
|
const fixture = createFixture("music");
|
|
const plan = createLocalizationPlan({
|
|
sampleRate: fixture.sampleRate,
|
|
array: fixture.arrayDefinition,
|
|
scan: fixture.experiment.scan,
|
|
bands: fixture.experiment.frequencyBands,
|
|
speedOfSound: fixture.scene.speedOfSound,
|
|
stft: fixture.experiment.stft,
|
|
algorithm: "music",
|
|
minimumDistance: 0.025,
|
|
musicSourceCountMethod: "fixed",
|
|
musicSourceCount: 1,
|
|
});
|
|
assertEqual(plan.normalization, "none", "portable MUSIC defaults to no map normalization");
|
|
assertEqual(plan.minimumDistance, 0.025, "minimum distance is retained");
|
|
assertEqual(plan.experiment.beamforming.musicSourceCount, 1, "fixed K reaches core experiment");
|
|
assertEqual(plan.musicPlan, plan.analysisPlan, "portable MUSIC plan has a dedicated alias");
|
|
assert(!("gccPhatPlan" in plan), "MUSIC is not mislabeled as a GCC plan");
|
|
assert(!("crossSpectrumBatchSize" in plan.computeOptions), "GCC-only batching is omitted");
|
|
});
|
|
|
|
test("MUSIC rejects peak normalization at both direct and portable API boundaries", () => {
|
|
const direct = createFixture("music");
|
|
direct.experiment.beamforming.normalization = "peak";
|
|
let directError;
|
|
try {
|
|
createLocalizationAnalysisPlan(direct);
|
|
} catch (error) {
|
|
directError = error;
|
|
}
|
|
assert(
|
|
directError instanceof RangeError &&
|
|
directError.message.includes("normalization"),
|
|
"direct core rejects peak normalization",
|
|
);
|
|
|
|
let portableError;
|
|
try {
|
|
createLocalizationPlan({
|
|
sampleRate: direct.sampleRate,
|
|
array: direct.arrayDefinition,
|
|
scan: direct.experiment.scan,
|
|
bands: direct.experiment.frequencyBands,
|
|
speedOfSound: direct.scene.speedOfSound,
|
|
stft: direct.experiment.stft,
|
|
algorithm: "music",
|
|
normalization: "peak",
|
|
musicSourceCount: 1,
|
|
});
|
|
} catch (error) {
|
|
portableError = error;
|
|
}
|
|
assert(
|
|
portableError instanceof RangeError &&
|
|
portableError.message.includes("normalization"),
|
|
"portable API rejects peak normalization",
|
|
);
|
|
});
|
|
|
|
test("MUSIC MDL/AIC workload excludes a zero-padded partial snapshot", () => {
|
|
const fixture = createFixture("music");
|
|
const completeSampleCount = 6 * fixture.experiment.stft.fftSize;
|
|
const partialSampleCount = completeSampleCount + 10;
|
|
fixture.channels = fixture.channels.map((channel) =>
|
|
Float64Array.from(
|
|
{ length: partialSampleCount },
|
|
(_, index) => channel[index % channel.length],
|
|
));
|
|
fixture.experiment.stft.hopSize = fixture.experiment.stft.fftSize;
|
|
fixture.experiment.beamforming.musicSourceCountMethod = "mdl";
|
|
delete fixture.experiment.beamforming.musicSourceCount;
|
|
|
|
const workload = estimateLocalizationWorkload(fixture);
|
|
assertEqual(workload.frameCount, 6, "only six complete non-overlapping frames");
|
|
assertEqual(
|
|
workload.sampleCount,
|
|
partialSampleCount,
|
|
"input length remains disclosed without treating its tail as a snapshot",
|
|
);
|
|
});
|
|
|
|
test("portable and streaming API accept an algorithm choice without Scene synthesis", async () => {
|
|
const fixture = createFixture("gcc-phat");
|
|
const config = {
|
|
sampleRate: fixture.sampleRate,
|
|
array: fixture.arrayDefinition,
|
|
scan: fixture.experiment.scan,
|
|
bands: fixture.experiment.frequencyBands,
|
|
speedOfSound: fixture.scene.speedOfSound,
|
|
stft: fixture.experiment.stft,
|
|
algorithm: "gcc-phat",
|
|
phatFloorRatio: 1e-5,
|
|
normalization: "peak",
|
|
computeOptions: { pointBatchSize: 1024, stftTransformBatchSize: 1024 },
|
|
};
|
|
const plan = createLocalizationPlan(config);
|
|
const defaultBatchPlan = createLocalizationPlan({ ...config, computeOptions: undefined });
|
|
const localized = await localizeMultichannel({ plan, channels: fixture.channels });
|
|
assertEqual(plan.algorithm, "gcc-phat", "portable plan algorithm");
|
|
assertEqual(plan.computeOptions.phatFloorRatio, 1e-5, "portable PHAT floor forwarding");
|
|
assertEqual(plan.analysisPlan, plan.gccPhatPlan, "portable GCC plan alias");
|
|
assertEqual(plan.computeOptions.pointBatchSize, 1024, "explicit portable point batch override");
|
|
assertEqual(
|
|
defaultBatchPlan.computeOptions.pointBatchSize,
|
|
DEFAULT_GCC_PHAT_POINT_BATCH_SIZE,
|
|
"GCC lookup-oriented default batch",
|
|
);
|
|
assertEqual(
|
|
defaultBatchPlan.computeOptions.crossSpectrumBatchSize,
|
|
DEFAULT_GCC_PHAT_CROSS_SPECTRUM_BATCH_SIZE,
|
|
"GCC cross-spectrum cancellation batch",
|
|
);
|
|
assertEqual(localized.algorithm, "gcc-phat", "portable result algorithm");
|
|
assertEqual(localized.phatFloorRatio, 1e-5, "portable PHAT floor reaches GCC core");
|
|
assertEqual(localized.bands[0].rawScale, "score", "portable score map");
|
|
assertEqual(StreamingLocalizer, StreamingDasLocalizer, "streaming compatibility alias");
|
|
|
|
const stream = new StreamingLocalizer({
|
|
...config,
|
|
analysisWindowSamples: 128,
|
|
analysisHopSamples: 128,
|
|
maximumBufferedSamples: 256,
|
|
});
|
|
const streamed = await stream.push(fixture.channels);
|
|
assertEqual(streamed.length, 1, "one GCC streaming result");
|
|
assertEqual(streamed[0].algorithm, "gcc-phat", "streaming result algorithm");
|
|
assertEqual(stream.getState().algorithm, "gcc-phat", "streaming state algorithm");
|
|
assertEqual(stream.plan.gccPhatPlan.busy, false, "streaming GCC plan lock released");
|
|
|
|
const scorePeaks = detectBandPeaks(
|
|
Float32Array.of(0, 2, 0, 1),
|
|
{ z: 1, xMin: 0, xMax: 1, yMin: 0, yMax: 1, columns: 2, rows: 2 },
|
|
{ rawScale: "score", minimumPeakDistanceM: 0 },
|
|
);
|
|
assertEqual(scorePeaks[0].rawScale, "score", "portable peak scale");
|
|
assertEqual(scorePeaks[0].rawScore, scorePeaks[0].rawValue, "portable score alias");
|
|
assert(!("rawPower" in scorePeaks[0]), "score is not mislabeled as acoustic power");
|
|
});
|