import { computeGccPhatMap, createGccPhatAnalysisPlan, estimateGccPhatWorkload, selectGccPhatReference, } from "/packages/acoustics-core/gcc-phat.js"; import { addFrequencyDomainDelay, createDelayWorkspace, prepareDelaySpectrum, } from "/packages/acoustics-core/fractional-delay.js"; import { distance3 } from "/packages/acoustics-core/math.js"; function assertNear(actual, expected, tolerance, label) { assert( Math.abs(actual - expected) <= tolerance, label + ": expected " + expected + " ± " + tolerance + ", received " + actual, ); } function deterministicBroadbandSamples(length) { let state = 0x6d2b79f5; return Float32Array.from({ length }, (_, index) => { state ^= state << 13; state ^= state >>> 17; state ^= state << 5; const noise = ((state >>> 0) / 0xffff_ffff) * 2 - 1; const taper = Math.min(1, index / 64, (length - 1 - index) / 64); return noise * Math.max(0, taper); }); } function createBroadbandFixture() { const sampleRate = 4096; const speedOfSound = 343; const target = [0.3, 0.2, 0.6]; const arrayDefinition = { id: "gcc-array", microphones: [ { id: "a-ref", position: [-0.45, -0.35, 0], sensitivityDb: 9, phaseOffsetDeg: 31, frequencyResponse: [ { frequencyHz: 0, gainDb: -8, phaseDeg: 19 }, { frequencyHz: sampleRate / 2, gainDb: -8, phaseDeg: 19 }, ], }, { id: "b-ne", position: [0.45, 0.35, 0], sensitivityDb: -7, phaseOffsetDeg: -47, frequencyResponse: [ { frequencyHz: 0, gainDb: 6, phaseDeg: -23 }, { frequencyHz: sampleRate / 2, gainDb: 6, phaseDeg: -23 }, ], }, { id: "c-nw", position: [-0.45, 0.35, 0], sensitivityDb: 4, phaseOffsetDeg: 73, }, { id: "d-se", position: [0.45, -0.35, 0], sensitivityDb: -3, phaseOffsetDeg: -11, }, ], }; const scene = { speedOfSound }; const experiment = { stft: { fftSize: 256, hopSize: 128, window: "hann" }, scan: { z: target[2], xMin: -0.4, xMax: 0.4, yMin: -0.4, yMax: 0.4, columns: 9, rows: 9, }, frequencyBands: [ { id: "broadband", minHz: 128, maxHz: 1800, color: "#65f2bf" }, ], beamforming: { algorithm: "gcc-phat", backend: "cpu", normalization: "peak", compensateFrequencyResponse: true, maxCompensationDb: 24, }, }; const emitted = deterministicBroadbandSamples(2048); const distances = arrayDefinition.microphones.map((microphone) => distance3(target, microphone.position) ); const maximumDelaySamples = Math.max(...distances) * sampleRate / speedOfSound; const spectrum = prepareDelaySpectrum(emitted, maximumDelaySamples); const workspace = createDelayWorkspace(spectrum.fftSize); const channels = arrayDefinition.microphones.map((microphone, index) => { const channel = new Float32Array(emitted.length); addFrequencyDomainDelay({ output: channel, spectrum, delaySamples: distances[index] * sampleRate / speedOfSound, gain: 1 / distances[index], phaseOffsetDeg: microphone.phaseOffsetDeg, frequencyResponse: microphone.frequencyResponse, sampleRate, workspace, }); return channel; }); return { sampleRate, speedOfSound, target, arrayDefinition, scene, experiment, channels, }; } function scanIndexFor(scan, x, y) { const column = Math.round((x - scan.xMin) / (scan.xMax - scan.xMin) * (scan.columns - 1)); const row = Math.round((scan.yMax - y) / (scan.yMax - scan.yMin) * (scan.rows - 1)); return row * scan.columns + column; } function assertFloat32Exact(actual, expected, label) { assertEqual(actual.length, expected.length, label + " length"); for (let index = 0; index < actual.length; index += 1) { assertEqual(actual[index], expected[index], label + "[" + index + "]"); } } test("GCC-PHAT reference selection uses the farthest centroid radius and id tie-break", () => { const arrayDefinition = { microphones: [ { id: "z-left", position: [-1, 0, 0] }, { id: "middle", position: [0, 0, 0] }, { id: "a-right", position: [1, 0, 0] }, ], }; const selected = selectGccPhatReference(arrayDefinition); assertEqual(selected.id, "a-right", "lexicographically first id wins a radius tie"); assertEqual(selected.index, 2, "selected microphone index"); assertEqual(selected.microphone, arrayDefinition.microphones[2], "selected microphone identity"); }); test("GCC-PHAT workload reports reference-pair scan reduction and rejects aliased lag", () => { const fixture = createBroadbandFixture(); const workload = estimateGccPhatWorkload(fixture); assertEqual(workload.pairCount, fixture.arrayDefinition.microphones.length - 1, "reference pair count"); assertEqual( workload.scanLookupCount, workload.pointCount * workload.pairCount * workload.bandCount, "one correlation lookup per point, pair, and band", ); assert( workload.scanLookupCount < workload.dasEquivalentComplexMacCount, "reference-pair lookup count is below equivalent DAS complex MAC count", ); assert( workload.maximumLagSamples > 0 && workload.maximumLagSamples <= fixture.experiment.stft.fftSize / 2, "maximum geometric lag is exposed and unaliased", ); let preflightError; try { estimateGccPhatWorkload({ ...fixture, maximumWorkingBytes: 1 }); } catch (error) { preflightError = error; } assert( preflightError instanceof RangeError && preflightError.message.includes("before geometry allocation"), "unplanned workload is rejected before allocating its distance matrix", ); let memoryError; try { createGccPhatAnalysisPlan({ ...fixture, maximumPlanBytes: 1 }); } catch (error) { memoryError = error; } assert( memoryError instanceof RangeError && memoryError.message.includes("maximumPlanBytes"), "plan hard memory bound", ); const aliasedArray = { microphones: [ { id: "far", position: [-1, 0, 0] }, { id: "near", position: [1, 0, 0] }, ], }; let lagError; try { createGccPhatAnalysisPlan({ sampleRate: 48000, arrayDefinition: aliasedArray, scene: { speedOfSound: 343 }, experiment: { stft: { fftSize: 128, hopSize: 64, window: "hann" }, scan: { z: 0.1, xMin: 0.9, xMax: 0.9, yMin: 0, yMax: 0, columns: 1, rows: 1, }, frequencyBands: [{ id: "wide", minHz: 500, maxHz: 5000, color: "#65f2bf" }], beamforming: { algorithm: "gcc-phat", backend: "cpu", normalization: "peak" }, }, }); } catch (error) { lagError = error; } assert( lagError instanceof RangeError && lagError.message.includes("maximum absolute geometric lag") && lagError.message.includes("fftSize/2"), "circular-lag alias guard", ); }); test("GCC-PHAT corrected lag sign finds a deterministic broadband source and emits score maps", async () => { const fixture = createBroadbandFixture(); const result = await computeGccPhatMap({ ...fixture, phatFloorRatio: 1e-5, pointBatchSize: 1024, stftTransformBatchSize: 1024, }); const band = result.bands[0]; const peakPosition = band.peak.position; assertEqual(result.backend, "cpu", "CPU backend"); assertEqual(result.algorithm, "gcc-phat", "algorithm id"); assertEqual(result.referenceMicrophoneId, "a-ref", "deterministic reference metadata"); assertEqual(result.pairCount, 3, "pair metadata"); assertEqual(result.phatFloorRatio, 1e-5, "PHAT floor metadata"); assertNear(result.maximumLagSamples, result.workload.maximumLagSamples, 0, "lag metadata"); assertEqual(band.rawScale, "score", "raw map scale"); assert( band.rawMap.every((value) => Number.isFinite(value) && value >= 0 && value <= 1), "pair- and bandwidth-normalized score map is finite and bounded", ); assert( distance3(peakPosition, fixture.target) <= 0.15, "broadband GCC-PHAT peak is within one grid diagonal: " + peakPosition.join(","), ); const targetIndex = scanIndexFor( fixture.experiment.scan, fixture.target[0], fixture.target[1], ); const wrongSignIndex = scanIndexFor( fixture.experiment.scan, -fixture.target[0], fixture.target[1], ); assert( band.rawMap[targetIndex] > band.rawMap[wrongSignIndex], "X_m * conj(X_ref) correlation uses the physical arrival-lag sign", ); assertNear(Math.max(...band.displayMap), 1, 1e-6, "normalized score display"); const expandedFixture = createBroadbandFixture(); expandedFixture.experiment.scan = { ...expandedFixture.experiment.scan, xMin: -0.8, xMax: 0.8, yMin: -0.8, yMax: 0.8, columns: 17, rows: 17, }; const expanded = await computeGccPhatMap({ ...expandedFixture, phatFloorRatio: 1e-5, pointBatchSize: 1024, stftTransformBatchSize: 1024, }); const expandedTargetIndex = scanIndexFor( expandedFixture.experiment.scan, expandedFixture.target[0], expandedFixture.target[1], ); assertNear( expanded.bands[0].rawMap[expandedTargetIndex], band.rawMap[targetIndex], 1e-6, "raw score at a fixed hypothesis is independent of scan extent", ); }); test("GCC-PHAT compute rejects an oversized unplanned working set before geometry", async () => { const fixture = createBroadbandFixture(); let error; try { await computeGccPhatMap({ ...fixture, maximumWorkingBytes: 1 }); } catch (candidate) { error = candidate; } assert( error instanceof RangeError && error.message.includes("before geometry allocation"), "compute preflight error", ); }); test("GCC-PHAT analysis plan reuses geometry and FFT workspaces and releases its busy lock", async () => { const fixture = createBroadbandFixture(); const plan = createGccPhatAnalysisPlan(fixture); const retainedDistances = plan.distances; const retainedStftWorkspace = plan.stftWorkspace; const retainedCorrelationReal = plan.correlationReal; const computeOptions = { ...fixture, analysisPlan: plan, pointBatchSize: 1024, stftTransformBatchSize: 1024, }; const first = await computeGccPhatMap(computeOptions); const second = await computeGccPhatMap(computeOptions); assertEqual(plan.distances, retainedDistances, "distance matrix identity is retained"); assertEqual(plan.stftWorkspace, retainedStftWorkspace, "STFT workspace identity is retained"); assertEqual(plan.correlationReal, retainedCorrelationReal, "correlation workspace identity is retained"); assertFloat32Exact(second.bands[0].rawMap, first.bands[0].rawMap, "reused plan raw map"); assertEqual(second.analysisCache.analysisAttempts, 2, "analysis attempts"); assertEqual(second.analysisCache.completedAnalyses, 2, "completed analyses"); assertEqual(second.analysisCache.distanceMatrixBuilds, 1, "distance matrix built once"); assertEqual( second.analysisCache.avoidedDistanceEvaluations, plan.pointCount * plan.microphoneCount, "second geometry pass avoided", ); assertEqual(second.analysisCache.stftWorkspaceUses, 2, "STFT workspace reused"); assertEqual(second.analysisCache.correlationWorkspaceUses, 2, "correlation workspace reused"); assertEqual( second.analysisCache.inverseFftTransforms, plan.pairCount * plan.bandBins.length * 2, "one inverse FFT per pair and band per analysis", ); const controller = new AbortController(); controller.abort(); let abortError; try { await computeGccPhatMap({ ...computeOptions, signal: controller.signal }); } catch (error) { abortError = error; } assertEqual(abortError?.name, "AbortError", "AbortError propagated"); assertEqual(plan.busy, false, "plan busy lock released after cancellation"); assertEqual(plan.statistics.completedAnalyses, 2, "canceled analysis is not completed"); });