feat: play moving-source acoustic timelines

This commit is contained in:
Codex
2026-08-28 16:23:26 +09:00
parent 01a37eac69
commit a54aa25cd9
8 changed files with 665 additions and 8 deletions
+91
View File
@@ -209,3 +209,94 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
frame.remove();
}
});
test("Acoustic Camera prepares, scrubs, plays, and pauses the moving-source example", async () => {
const frame = document.createElement("iframe");
frame.src = "/apps/acoustic-camera/?integration-test=1";
frame.style.cssText = "position:fixed;left:-20000px;top:0;width:1400px;height:1000px;border:0";
document.body.append(frame);
await new Promise((resolve, reject) => {
frame.addEventListener("load", resolve, { once: true });
frame.addEventListener("error", () => reject(new Error("Acoustic Camera live iframe load failed")), {
once: true,
});
});
const windowObject = frame.contentWindow;
const documentObject = frame.contentDocument;
try {
await waitFor(
() => documentObject.documentElement.dataset.appReady === "true",
"Acoustic Camera live initialization",
);
documentObject.querySelector("#load-moving-example-button").click();
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.documentElement.dataset.cameraValid === "true" &&
documentObject.documentElement.dataset.liveState === "idle" &&
documentObject.querySelector("#experiment-id").value === "moving-tone-live" &&
!documentObject.querySelector("#prepare-live-button").disabled,
"valid moving example trajectory",
);
dispatchValue(windowObject, documentObject.querySelector("#scan-columns"), 8);
dispatchValue(windowObject, documentObject.querySelector("#scan-rows"), 8);
dispatchValue(windowObject, documentObject.querySelector("#backend"), "cpu");
dispatchValue(windowObject, documentObject.querySelector("#live-map-rate"), 2);
await waitFor(
() => documentObject.documentElement.dataset.cameraValid === "true" &&
documentObject.documentElement.dataset.liveState === "idle" &&
documentObject.querySelector("#scan-columns").value === "8" &&
documentObject.querySelector("#scan-rows").value === "8" &&
documentObject.querySelector("#backend").value === "cpu" &&
documentObject.querySelector("#live-map-rate").value === "2" &&
!documentObject.querySelector("#prepare-live-button").disabled,
"8x8 CPU 2 Hz live configuration",
);
documentObject.querySelector("#prepare-live-button").click();
await waitFor(
() => documentObject.documentElement.dataset.liveState === "ready",
"moving-source live preparation",
45000,
);
const frameCount = Number(documentObject.documentElement.dataset.liveFrameCount);
assert(frameCount > 1, `multiple live frames: ${frameCount}`);
assert(documentObject.querySelector("#live-status").textContent.includes("CPU"), "live backend is CPU");
dispatchValue(windowObject, documentObject.querySelector("#live-time"), 0.6, "input");
await waitFor(
() => Number(documentObject.documentElement.dataset.liveTime) >= 0.599,
"live scrub to a later time",
);
const scrubbedTime = Number(documentObject.documentElement.dataset.liveTime);
assert(Number(documentObject.documentElement.dataset.liveFrame) > 0, "scrub selects a later map frame");
documentObject.querySelector("#live-play-button").click();
await waitFor(
() => documentObject.documentElement.dataset.liveState === "playing",
"live playback start",
);
await waitFor(
() => Number(documentObject.documentElement.dataset.liveTime) > scrubbedTime + 0.05,
"live playback time advance",
5000,
);
documentObject.querySelector("#live-play-button").click();
await waitFor(
() => documentObject.documentElement.dataset.liveState === "ready",
"live playback pause",
);
const pausedTime = Number(documentObject.documentElement.dataset.liveTime);
await new Promise((resolve) => setTimeout(resolve, 120));
const timeAfterPause = Number(documentObject.documentElement.dataset.liveTime);
assert(
Math.abs(timeAfterPause - pausedTime) <= 0.01,
`paused live time remains stable: ${pausedTime} -> ${timeAfterPause}`,
);
} finally {
frame.remove();
}
});
+18
View File
@@ -11,6 +11,24 @@ async function loadJson(path) {
return response.json();
}
test("moving-tone sample Scene and Experiment satisfy their linked contracts", async () => {
const [arrayDefinition, scene, experiment] = await Promise.all([
loadJson("/assets/array-geometries/rectangular-16x12.array.json"),
loadJson("/assets/sample-scenes/moving-tone.scene.json"),
loadJson("/assets/sample-scenes/moving-tone.experiment.json"),
]);
const sceneIssues = await validateScene(scene);
const experimentIssues = await validateExperiment(experiment, { arrayDefinition, scene });
assertEqual(sceneIssues.length, 0, `moving Scene issues: ${JSON.stringify(sceneIssues)}`);
assertEqual(experimentIssues.length, 0, `moving Experiment issues: ${JSON.stringify(experimentIssues)}`);
assertEqual(experiment.sceneId, scene.id, "moving example linkage");
assert(
scene.sources.some((source) => Array.isArray(source.trajectory) && source.trajectory.length >= 2),
"moving example contains a usable trajectory",
);
});
test("192채널 기준 배열이 계약을 통과한다", async () => {
const arrayDefinition = await loadJson("/assets/array-geometries/rectangular-16x12.array.json");
assertEqual(arrayDefinition.microphones.length, 192, "microphone count");