import { synthesizeMicrophoneSignals } from "/packages/acoustics-core/propagation.js"; import { sampleQuasiStaticWindow } from "/packages/acoustics-core/trajectory.js"; import { createLocalizationAnalysisPlan } from "/packages/acoustics-core/localization.js"; import { computeLocalizationWithBackend } from "/packages/webgpu-kernels/compute.js"; function post(type, jobId, payload = {}, transfer = []) { self.postMessage({ type, jobId, ...payload }, transfer); } function serializeError(error) { return { name: error?.name ?? "Error", code: error?.code ?? "", message: error instanceof Error ? error.message : String(error), stack: error?.stack ?? "", causes: error instanceof AggregateError ? error.errors.map((cause) => ({ name: cause?.name, code: cause?.code, message: cause?.message })) : [], }; } function signalStats(synthesis) { return { preNormalizationPeak: synthesis.preNormalizationPeak, appliedScale: synthesis.appliedScale, clippedSampleCount: synthesis.clippedSampleCount, peak: synthesis.peak, acousticModel: synthesis.acousticModel, acousticPathCount: synthesis.acousticPathCount, acousticPathCounts: synthesis.acousticPathCounts, windowing: synthesis.windowing, sampling: synthesis.sampling, }; } function collectBandTransfers(run, transfer) { for (const band of run.bands) { transfer.push(band.rawMap.buffer, band.displayMap.buffer); } } async function runTimeline({ jobId, scene, arrayDefinition, experiment, assetBuffers, framePlan }) { if (!Array.isArray(framePlan?.frames) || framePlan.frames.length === 0) { throw new TypeError("framePlan.frames must be a non-empty array."); } const totalStartedAt = performance.now(); const totalFrames = framePlan.frames.length; const frames = []; const transfer = []; let signalGeneration = 0; let localizationTime = 0; const localizationScene = { speedOfSound: scene.speedOfSound }; const algorithm = experiment.beamforming.algorithm ?? "das"; const requestedBackend = experiment.beamforming.backend ?? "auto"; const reuseCpuPlan = algorithm === "gcc-phat" || requestedBackend === "cpu"; let localizationExperiment = experiment; let analysisPlan = reuseCpuPlan ? createLocalizationAnalysisPlan({ sampleRate: scene.sampleRate, arrayDefinition, scene: localizationScene, experiment: localizationExperiment, }) : undefined; post("progress", jobId, { stage: "timeline-start", message: `라이브 맵 ${totalFrames}개를 준비하고 있습니다…`, completedFrames: 0, totalFrames, }); for (let frameIndex = 0; frameIndex < totalFrames; frameIndex += 1) { const plannedFrame = framePlan.frames[frameIndex]; const synthesisWindow = sampleQuasiStaticWindow( scene, plannedFrame.startTime, plannedFrame.endTime, { arrayDefinition, minimumDistance: experiment.beamforming.minimumDistance, }, ); post("progress", jobId, { stage: "timeline-signal", message: `라이브 신호 합성 중 · ${frameIndex + 1}/${totalFrames}`, frameIndex, completedFrames: frameIndex, totalFrames, }); const frameStartedAt = performance.now(); const signalStartedAt = performance.now(); const synthesis = synthesizeMicrophoneSignals({ scene: synthesisWindow.scene, arrayDefinition, assetBuffers, minimumDistance: experiment.beamforming.minimumDistance, signalOffsets: synthesisWindow.signalOffsets, }); const frameSignalGeneration = performance.now() - signalStartedAt; signalGeneration += frameSignalGeneration; const localizationStartedAt = performance.now(); const localization = await computeLocalizationWithBackend({ channels: synthesis.channels, sampleRate: synthesis.sampleRate, arrayDefinition, scene: localizationScene, experiment: localizationExperiment, ...(analysisPlan ? { analysisPlan } : {}), onFallback: ({ error }) => post("fallback", jobId, { code: error.code, message: error.message, frameIndex, totalFrames, }), onProgress: (progress) => post("progress", jobId, { stage: `timeline-${progress.stage}`, frameIndex, completedFrames: frameIndex, totalFrames, progress, }), }); const frameLocalization = performance.now() - localizationStartedAt; localizationTime += frameLocalization; collectBandTransfers(localization, transfer); if ( !analysisPlan && localization.backend === "cpu" && localization.fallback?.from === "webgpu" ) { // Auto has established that WebGPU is unavailable for this worker. Keep // the first frame's honest fallback metadata, then avoid retrying the GPU // and rebuilding CPU geometry/workspaces on every remaining live frame. localizationExperiment = { ...experiment, beamforming: { ...experiment.beamforming, backend: "cpu", }, }; analysisPlan = createLocalizationAnalysisPlan({ sampleRate: scene.sampleRate, arrayDefinition, scene: localizationScene, experiment: localizationExperiment, }); } frames.push({ index: plannedFrame.index ?? frameIndex, startTime: synthesisWindow.startTime, endTime: synthesisWindow.endTime, sampleTime: synthesisWindow.sampleTime, duration: synthesisWindow.duration, run: { ...localization, signalStats: signalStats(synthesis), timingMs: { total: performance.now() - frameStartedAt, signalGeneration: frameSignalGeneration, localization: frameLocalization, beamforming: frameLocalization, }, }, }); post("progress", jobId, { stage: "timeline-frame-complete", message: `라이브 맵 준비 · ${frameIndex + 1}/${totalFrames}`, frameIndex, completedFrames: frameIndex + 1, totalFrames, }); } const backends = new Set(frames.map((frame) => frame.run.backend)); const fallbacks = frames .filter((frame) => frame.run.fallback) .map((frame) => ({ frameIndex: frame.index, ...frame.run.fallback })); post( "timeline-result", jobId, { timeline: { backend: backends.size === 1 ? frames[0].run.backend : "mixed", algorithm: experiment.beamforming.algorithm ?? "das", requestedBackend: experiment.beamforming.backend ?? "auto", duration: scene.duration, frameCount: frames.length, frames, ...(fallbacks.length > 0 ? { fallbacks } : {}), timingMs: { total: performance.now() - totalStartedAt, signalGeneration, localization: localizationTime, beamforming: localizationTime, }, }, }, transfer, ); } self.addEventListener("message", async (event) => { if (event.data?.type !== "run" && event.data?.type !== "run-timeline") return; const { jobId, scene, arrayDefinition, experiment, assetBuffers } = event.data; const totalStartedAt = performance.now(); try { if (event.data.type === "run-timeline") { await runTimeline(event.data); return; } post("progress", jobId, { stage: "signal", message: "마이크 수신 신호를 합성하고 있습니다…" }); const signalStartedAt = performance.now(); const synthesis = synthesizeMicrophoneSignals({ scene, arrayDefinition, assetBuffers, minimumDistance: experiment.beamforming.minimumDistance, }); const signalGeneration = performance.now() - signalStartedAt; post("progress", jobId, { stage: "signal-complete", message: `마이크 신호 합성 완료 · ${signalGeneration.toFixed(0)} ms`, }); const localizationStartedAt = performance.now(); const localization = await computeLocalizationWithBackend({ channels: synthesis.channels, sampleRate: synthesis.sampleRate, arrayDefinition, scene, experiment, onFallback: ({ error }) => post("fallback", jobId, { code: error.code, message: error.message, }), onProgress: (progress) => post("progress", jobId, { stage: progress.stage, progress }), }); const localizationTime = performance.now() - localizationStartedAt; const transfer = []; collectBandTransfers(localization, transfer); post( "result", jobId, { run: { ...localization, signalStats: signalStats(synthesis), timingMs: { total: performance.now() - totalStartedAt, signalGeneration, localization: localizationTime, beamforming: localizationTime, }, }, }, transfer, ); } catch (error) { post("error", jobId, { error: serializeError(error) }); } });