103 lines
3.1 KiB
JavaScript
103 lines
3.1 KiB
JavaScript
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,
|
|
};
|
|
}
|