From 01a37eac69291b4b2c53174c192a7707a17eb5da Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 28 Aug 2026 16:11:16 +0900 Subject: [PATCH] feat: analyze moving sources frame by frame --- apps/acoustic-camera/simulation-worker.js | 158 ++++++++++++++++++++-- 1 file changed, 148 insertions(+), 10 deletions(-) diff --git a/apps/acoustic-camera/simulation-worker.js b/apps/acoustic-camera/simulation-worker.js index 932242d..abb1da7 100644 --- a/apps/acoustic-camera/simulation-worker.js +++ b/apps/acoustic-camera/simulation-worker.js @@ -1,4 +1,5 @@ import { synthesizeMicrophoneSignals } from "/packages/acoustics-core/propagation.js"; +import { sampleQuasiStaticWindow } from "/packages/acoustics-core/trajectory.js"; import { computeDasWithBackend } from "/packages/webgpu-kernels/compute.js"; function post(type, jobId, payload = {}, transfer = []) { @@ -17,11 +18,155 @@ function serializeError(error) { }; } +function signalStats(synthesis) { + return { + preNormalizationPeak: synthesis.preNormalizationPeak, + appliedScale: synthesis.appliedScale, + clippedSampleCount: synthesis.clippedSampleCount, + peak: synthesis.peak, + }; +} + +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 beamforming = 0; + + 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, + ); + 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 beamformingStartedAt = performance.now(); + const das = await computeDasWithBackend({ + channels: synthesis.channels, + sampleRate: synthesis.sampleRate, + arrayDefinition, + scene: synthesisWindow.scene, + experiment, + 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 frameBeamforming = performance.now() - beamformingStartedAt; + beamforming += frameBeamforming; + collectBandTransfers(das, transfer); + + frames.push({ + index: plannedFrame.index ?? frameIndex, + startTime: synthesisWindow.startTime, + endTime: synthesisWindow.endTime, + sampleTime: synthesisWindow.sampleTime, + duration: synthesisWindow.duration, + run: { + ...das, + signalStats: signalStats(synthesis), + timingMs: { + total: performance.now() - frameStartedAt, + signalGeneration: frameSignalGeneration, + beamforming: frameBeamforming, + }, + }, + }); + + 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", + requestedBackend: experiment.beamforming.backend ?? "auto", + duration: scene.duration, + frameCount: frames.length, + frames, + ...(fallbacks.length > 0 ? { fallbacks } : {}), + timingMs: { + total: performance.now() - totalStartedAt, + signalGeneration, + beamforming, + }, + }, + }, + transfer, + ); +} + self.addEventListener("message", async (event) => { - if (event.data?.type !== "run") return; + 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: "192채널 자유음장 신호를 합성하고 있습니다…" }); const signalStartedAt = performance.now(); const synthesis = synthesizeMicrophoneSignals({ @@ -52,21 +197,14 @@ self.addEventListener("message", async (event) => { }); const beamforming = performance.now() - beamformingStartedAt; const transfer = []; - for (const band of das.bands) { - transfer.push(band.rawMap.buffer, band.displayMap.buffer); - } + collectBandTransfers(das, transfer); post( "result", jobId, { run: { ...das, - signalStats: { - preNormalizationPeak: synthesis.preNormalizationPeak, - appliedScale: synthesis.appliedScale, - clippedSampleCount: synthesis.clippedSampleCount, - peak: synthesis.peak, - }, + signalStats: signalStats(synthesis), timingMs: { total: performance.now() - totalStartedAt, signalGeneration,