import { estimateSceneMix, MAX_MIX_WORKING_BYTES } from "/packages/acoustics-core/mixdown.js"; export function createAudioRuntime() { let worker = null; let rejectWorker = null; let playbackContext = null; let playbackSource = null; function cancelMix() { if (!worker) return false; worker.terminate(); worker = null; const reject = rejectWorker; rejectWorker = null; reject?.(new DOMException("사용자가 mix 생성을 취소했습니다.", "AbortError")); return true; } function renderMix(scene, assetBuffers) { if (worker) throw new Error("이미 확인용 mix를 만들고 있습니다."); const estimate = estimateSceneMix(scene, assetBuffers); const crossThreadBytes = estimate.workingBytes + estimate.assetBytes; if (crossThreadBytes > MAX_MIX_WORKING_BYTES) { throw new Error( `decode buffer와 Worker 복사본을 포함한 예상 메모리 ${(crossThreadBytes / 1024 ** 2).toFixed(1)} MiB가 ` + `${(MAX_MIX_WORKING_BYTES / 1024 ** 2).toFixed(0)} MiB 한도를 넘습니다.`, ); } return new Promise((resolve, reject) => { const currentWorker = new Worker("/apps/dataset-builder/mix-worker.js", { type: "module" }); worker = currentWorker; rejectWorker = reject; currentWorker.addEventListener("message", ({ data }) => { if (worker !== currentWorker) return; worker = null; rejectWorker = null; currentWorker.terminate(); if (data.ok) resolve(data.result); else reject(new Error(data.message)); }); currentWorker.addEventListener("error", (event) => { if (worker !== currentWorker) return; worker = null; rejectWorker = null; currentWorker.terminate(); reject(new Error(event.message || "mix Worker가 중단되었습니다.")); }); currentWorker.postMessage({ scene, assetBuffers: [...assetBuffers] }); }); } function stopPlayback() { if (!playbackSource) return false; playbackSource.onended = null; try { playbackSource.stop(); } catch { // 이미 끝난 source는 정리만 한다. } playbackSource.disconnect(); playbackSource = null; return true; } async function play(result, onEnded) { stopPlayback(); playbackContext ??= new AudioContext(); await playbackContext.resume(); const buffer = playbackContext.createBuffer(1, result.samples.length, result.sampleRate); buffer.copyToChannel(result.samples, 0); playbackSource = playbackContext.createBufferSource(); playbackSource.buffer = buffer; playbackSource.connect(playbackContext.destination); playbackSource.onended = () => { playbackSource?.disconnect(); playbackSource = null; onEnded?.(); }; playbackSource.start(); } function dispose() { cancelMix(); stopPlayback(); playbackContext?.close(); playbackContext = null; } return { cancelMix, dispose, get isMixing() { return Boolean(worker); }, get isPlaying() { return Boolean(playbackSource); }, play, renderMix, stopPlayback, }; }