import { computeLocalizationMap, createLocalizationAnalysisPlan, estimateLocalizationWorkload, } from "/packages/acoustics-core/localization.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 = [ { id: "wide", minHz: 128, maxHz: 1800, color: "#65f2bf" }, ]; const experiment = { stft: { fftSize: 64, hopSize: 32, window: "hann" }, scan, frequencyBands, beamforming: { algorithm, backend: "cpu", normalization: "peak", compensateFrequencyResponse: true, }, }; 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", "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"); assert(localizationAlgorithmSupportsBackend("das-phat", "webgpu"), "DAS-PHAT WebGPU support"); assert(!localizationAlgorithmSupportsBackend("gcc-phat", "webgpu"), "GCC-PHAT 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("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 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(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"); });