feat: expose acoustic realism controls
This commit is contained in:
@@ -76,6 +76,16 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
assert(!documentObject.querySelector("#advanced-settings").open, "advanced settings start collapsed");
|
||||
assert(!documentObject.querySelector("#array-details").open, "array developer detail starts collapsed");
|
||||
assert(!documentObject.querySelector("#camera-help").open, "limitations help starts collapsed");
|
||||
assertEqual(documentObject.querySelector("#acoustic-profile").value, "free-field", "default free-field profile");
|
||||
assertEqual(
|
||||
documentObject.querySelector("#frequency-response-mode").value,
|
||||
"compensate",
|
||||
"known microphone response is compensated by default",
|
||||
);
|
||||
assert(
|
||||
documentObject.querySelector("#acoustic-environment-summary").textContent.includes("sparse path"),
|
||||
"bounded acoustic cost is visible",
|
||||
);
|
||||
|
||||
const canvas = documentObject.querySelector("#scene-canvas");
|
||||
await waitFor(() => canvas.width > 0 && canvas.height > 0, "scene canvas 초기 렌더");
|
||||
@@ -107,7 +117,8 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
);
|
||||
assertEqual(documentObject.documentElement.dataset.runBackend, "cpu", "actual backend");
|
||||
assertEqual(documentObject.documentElement.dataset.resultBandCount, "1", "result band count");
|
||||
assert(documentObject.querySelectorAll("#metric-grid .metric-card").length >= 9, "result metrics rendered");
|
||||
assert(documentObject.querySelectorAll("#metric-grid .metric-card").length >= 10, "result metrics rendered");
|
||||
assert(documentObject.querySelector("#metric-grid").textContent.includes("음향 모델"), "acoustic model metric rendered");
|
||||
assert(!documentObject.querySelector("#metric-grid").classList.contains("is-hidden"), "metrics visible");
|
||||
assert(documentObject.querySelector("#result-badge").textContent.includes("CPU"), "backend badge rendered");
|
||||
assert(documentObject.documentElement.dataset.unsavedWork.includes("Result"), "completed Result is unsaved");
|
||||
|
||||
@@ -4,6 +4,12 @@ import {
|
||||
sceneHasTrajectories,
|
||||
scenePlaybackState,
|
||||
} from "/apps/acoustic-camera/timeline.js";
|
||||
import {
|
||||
applyAcousticProfile,
|
||||
detectAcousticProfile,
|
||||
inferShoeboxRoom,
|
||||
maximumSparsePathsPerReceiver,
|
||||
} from "/apps/acoustic-camera/acoustic-profiles.js";
|
||||
|
||||
import {
|
||||
alignExperimentToScene,
|
||||
@@ -246,6 +252,41 @@ test("Camera workload counts unique overlapping FFT bins", () => {
|
||||
assertNear(workload.distanceMiB, (80 * 3 * 4) / 1024 ** 2, 1e-15, "distance table size");
|
||||
});
|
||||
|
||||
test("Camera acoustic presets bound current geometry and expose predictable sparse cost", () => {
|
||||
const scene = {
|
||||
sources: [{
|
||||
id: "moving",
|
||||
position: [0, 0, 2],
|
||||
trajectory: [
|
||||
{ time: 0, position: [-0.5, 0, 2] },
|
||||
{ time: 1, position: [0.75, 0.5, 3] },
|
||||
],
|
||||
}],
|
||||
};
|
||||
const arrayDefinition = {
|
||||
microphones: [
|
||||
{ id: "left", position: [-0.2, 0, 0] },
|
||||
{ id: "right", position: [0.2, 0, 0] },
|
||||
],
|
||||
};
|
||||
const experiment = {
|
||||
scan: { xMin: -1, xMax: 1, yMin: -0.75, yMax: 0.75, z: 2.5 },
|
||||
};
|
||||
const room = inferShoeboxRoom(scene, arrayDefinition, experiment);
|
||||
assert(room.min[0] < -1 && room.max[0] > 1, "scan x is padded");
|
||||
assert(room.min[2] < 0 && room.max[2] > 3, "array and trajectory depth are padded");
|
||||
|
||||
applyAcousticProfile(scene, arrayDefinition, experiment, "typical-room");
|
||||
assertEqual(detectAcousticProfile(scene), "typical-room", "profile round trip");
|
||||
assertEqual(maximumSparsePathsPerReceiver(scene), 19, "bounded typical-room path count");
|
||||
assertEqual(scene.acoustics.room.min.join(","), room.min.join(","), "inferred room minimum");
|
||||
assertEqual(scene.acoustics.room.max.join(","), room.max.join(","), "inferred room maximum");
|
||||
|
||||
applyAcousticProfile(scene, arrayDefinition, experiment, "free-field");
|
||||
assertEqual(detectAcousticProfile(scene), "free-field", "free-field selection");
|
||||
assertEqual(maximumSparsePathsPerReceiver(scene), 1, "free-field path count");
|
||||
});
|
||||
|
||||
test("Camera distance shift keeps the scene and scan on one z plane", () => {
|
||||
const scene = {
|
||||
sources: [
|
||||
@@ -260,6 +301,17 @@ test("Camera distance shift keeps the scene and scan on one z plane", () => {
|
||||
{ id: "two", position: [-0.3, 0.4, 2.5] },
|
||||
],
|
||||
image: { plane: { z: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1 } },
|
||||
acoustics: {
|
||||
model: "shoebox",
|
||||
room: { min: [-2, -2, -0.5], max: [2, 2, 4] },
|
||||
occluders: [{
|
||||
id: "screen",
|
||||
min: [-0.2, -0.2, 1],
|
||||
max: [0.2, 0.2, 1.1],
|
||||
transmissionDb: -20,
|
||||
diffractionGainDb: -12,
|
||||
}],
|
||||
},
|
||||
};
|
||||
const experiment = { scan: { z: 2 } };
|
||||
|
||||
@@ -279,6 +331,10 @@ test("Camera distance shift keeps the scene and scan on one z plane", () => {
|
||||
);
|
||||
assertEqual(scene.sources[1].position.join(","), "-0.3,0.4,3.75", "relative source depth retained");
|
||||
assertEqual(scene.image.plane.z, 3.25, "background image plane z shift");
|
||||
assertEqual(scene.acoustics.room.min[2], 0.75, "room minimum z shift");
|
||||
assertEqual(scene.acoustics.room.max[2], 5.25, "room maximum z shift");
|
||||
assertEqual(scene.acoustics.occluders[0].min[2], 2.25, "occluder minimum z shift");
|
||||
assertEqual(scene.acoustics.occluders[0].max[2], 2.35, "occluder maximum z shift");
|
||||
});
|
||||
|
||||
test("Camera aligns an experiment to an imported scene without changing grid resolution", () => {
|
||||
@@ -364,7 +420,14 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
|
||||
};
|
||||
const run = {
|
||||
backend: "cpu",
|
||||
signalStats: { preNormalizationPeak: 0.75, appliedScale: 1, clippedSampleCount: 0 },
|
||||
signalStats: {
|
||||
preNormalizationPeak: 0.75,
|
||||
appliedScale: 1,
|
||||
clippedSampleCount: 0,
|
||||
acousticModel: "shoebox",
|
||||
acousticPathCount: 38,
|
||||
acousticPathCounts: { direct: 2, reflection: 12, scattering: 12, diffuse: 12 },
|
||||
},
|
||||
timingMs: { total: 12.5, signalGeneration: 2.5, beamforming: 10 },
|
||||
fallback: { code: "webgpu-unavailable", message: "CPU fallback" },
|
||||
bands: [{
|
||||
@@ -445,6 +508,9 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
|
||||
assertEqual(result.bands[0].evaluation.matchToleranceM, 0.2, "evaluation settings are exported");
|
||||
assertEqual(result.environment.scanXMin, -1, "scan bounds are recorded");
|
||||
assertEqual(result.environment.fallbackCode, "webgpu-unavailable", "fallback is recorded");
|
||||
assertEqual(result.signalStats.acousticModel, "shoebox", "acoustic model is exported");
|
||||
assertEqual(result.signalStats.acousticPathCount, 38, "acoustic path count is exported");
|
||||
assertEqual(result.signalStats.acousticPathCounts.diffuse, 12, "path kinds are exported");
|
||||
|
||||
const issues = await validateResult(result, { experiment, scene });
|
||||
assertEqual(issues.length, 0, `generated result validation: ${JSON.stringify(issues)}`);
|
||||
|
||||
Reference in New Issue
Block a user