feat: build reproducible dataset editor

This commit is contained in:
Codex
2026-08-28 10:46:33 +09:00
parent 6a1197d60e
commit 08fcf578de
16 changed files with 2579 additions and 11 deletions
+102
View File
@@ -0,0 +1,102 @@
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,
};
}