feat: expose acoustic realism controls
This commit is contained in:
@@ -7,6 +7,11 @@ import {
|
||||
validateScene,
|
||||
} from "/packages/contracts/validation.js";
|
||||
import { createCameraAssetRuntime } from "./asset-runtime.js";
|
||||
import {
|
||||
applyAcousticProfile,
|
||||
detectAcousticProfile,
|
||||
maximumSparsePathsPerReceiver,
|
||||
} from "./acoustic-profiles.js";
|
||||
import {
|
||||
createAnalysisFramePlan,
|
||||
playbackFrameAtTime,
|
||||
@@ -90,6 +95,9 @@ const elements = {
|
||||
sceneDistance: byId("scene-distance"),
|
||||
speedOfSound: byId("speed-of-sound"),
|
||||
sceneSeed: byId("scene-seed"),
|
||||
acousticProfile: byId("acoustic-profile"),
|
||||
acousticEnvironmentSummary: byId("acoustic-environment-summary"),
|
||||
frequencyResponseMode: byId("frequency-response-mode"),
|
||||
backend: byId("backend"),
|
||||
gridPreset: byId("grid-preset"),
|
||||
scanColumns: byId("scan-columns"),
|
||||
@@ -358,6 +366,9 @@ function syncControls() {
|
||||
elements.sceneDistance.value = experiment.scan.z;
|
||||
elements.speedOfSound.value = scene.speedOfSound;
|
||||
elements.sceneSeed.value = scene.seed;
|
||||
elements.acousticProfile.value = detectAcousticProfile(scene);
|
||||
elements.frequencyResponseMode.value =
|
||||
experiment.beamforming.compensateFrequencyResponse === false ? "raw" : "compensate";
|
||||
elements.backend.value = experiment.beamforming.backend;
|
||||
elements.scanColumns.value = experiment.scan.columns;
|
||||
elements.scanRows.value = experiment.scan.rows;
|
||||
@@ -381,6 +392,30 @@ function syncControls() {
|
||||
: "custom";
|
||||
}
|
||||
|
||||
function renderAcousticSummary() {
|
||||
if (!state.scene || !state.arrayDefinition || !state.experiment) return;
|
||||
const profile = detectAcousticProfile(state.scene);
|
||||
const profileLabels = {
|
||||
"free-field": "자유음장",
|
||||
"treated-room": "흡음된 실내",
|
||||
"typical-room": "일반 실내",
|
||||
"reflective-room": "반사 강한 실내",
|
||||
custom: "Scene 사용자 환경",
|
||||
};
|
||||
const responseCurveCount = state.arrayDefinition.microphones.filter(
|
||||
(microphone) => microphone.frequencyResponse?.length >= 2,
|
||||
).length;
|
||||
const responseMode =
|
||||
state.experiment.beamforming.compensateFrequencyResponse === false
|
||||
? "응답 역보정 꺼짐"
|
||||
: "알려진 응답 역보정";
|
||||
elements.acousticEnvironmentSummary.textContent =
|
||||
profileLabels[profile] + " · 수신점당 최대 " +
|
||||
maximumSparsePathsPerReceiver(state.scene) + "개 sparse path · 응답 curve " +
|
||||
responseCurveCount + "/" + state.arrayDefinition.microphones.length + " · " + responseMode;
|
||||
document.documentElement.dataset.acousticProfile = profile;
|
||||
}
|
||||
|
||||
function describeSignal(signal) {
|
||||
if (signal.type === "tone") return `${formatNumber(signal.frequency, 0)} Hz tone`;
|
||||
if (signal.type === "noise") return `${signal.color} noise`;
|
||||
@@ -390,6 +425,7 @@ function describeSignal(signal) {
|
||||
function renderDocumentSummary() {
|
||||
const { arrayDefinition, scene, experiment } = state;
|
||||
if (!arrayDefinition || !scene || !experiment) return;
|
||||
renderAcousticSummary();
|
||||
elements.workspaceTitle.textContent = scene.name;
|
||||
elements.sceneMeta.textContent =
|
||||
`${scene.sampleRate.toLocaleString("ko-KR")} Hz · ${formatNumber(scene.duration, 3)} s · ` +
|
||||
@@ -502,12 +538,19 @@ function renderWorkload() {
|
||||
if (!state.arrayDefinition || !state.scene || !state.experiment) return;
|
||||
try {
|
||||
const workload = estimateExperimentWorkload(state.arrayDefinition, state.scene, state.experiment);
|
||||
const maximumPaths = maximumSparsePathsPerReceiver(state.scene);
|
||||
const signalPathBudget =
|
||||
workload.microphoneCount * state.scene.sources.length * maximumPaths;
|
||||
elements.workloadSummary.textContent =
|
||||
`${workload.pointCount.toLocaleString("ko-KR")} points × ${workload.microphoneCount} ch · ` +
|
||||
`${workload.frameCount} frames · ${workload.selectedBinCount} bins · ` +
|
||||
`약 ${formatCount(workload.complexMacCount)} complex MAC · steering ${formatNumber(workload.distanceMiB, 1)} MiB`;
|
||||
`약 ${formatCount(workload.complexMacCount)} complex MAC · ` +
|
||||
`signal ≤ ${formatCount(signalPathBudget)} paths · steering ${formatNumber(workload.distanceMiB, 1)} MiB`;
|
||||
const heavyCpu = state.experiment.beamforming.backend === "cpu" && workload.complexMacCount > 5e8;
|
||||
elements.workloadSummary.classList.toggle("is-warning", heavyCpu || workload.pointCount >= 40000);
|
||||
elements.workloadSummary.classList.toggle(
|
||||
"is-warning",
|
||||
heavyCpu || workload.pointCount >= 40000 || signalPathBudget > 100000,
|
||||
);
|
||||
} catch (error) {
|
||||
elements.workloadSummary.textContent = `연산량을 계산할 수 없습니다 · ${error.message}`;
|
||||
elements.workloadSummary.classList.add("is-warning");
|
||||
@@ -646,6 +689,15 @@ function renderResult() {
|
||||
`${formatNumber(displayed.run.timingMs.signalGeneration, 0)} ms`,
|
||||
`${displayed.snapshot.arrayDefinition.microphones.length} channels`,
|
||||
);
|
||||
if (displayed.run.signalStats.acousticModel) {
|
||||
const modelLabel =
|
||||
displayed.run.signalStats.acousticModel === "shoebox" ? "SPARSE ROOM" : "FREE FIELD";
|
||||
appendMetric(
|
||||
"음향 모델",
|
||||
modelLabel,
|
||||
`${(displayed.run.signalStats.acousticPathCount ?? 0).toLocaleString("ko-KR")} paths 합성`,
|
||||
);
|
||||
}
|
||||
appendMetric(
|
||||
"Beamforming",
|
||||
`${formatNumber(displayed.run.timingMs.beamforming, 0)} ms`,
|
||||
@@ -1475,6 +1527,21 @@ function installDocumentInputListeners() {
|
||||
state.scene.seed = value;
|
||||
markChanged({ dirty: "scene" });
|
||||
});
|
||||
elements.acousticProfile.addEventListener("change", () => {
|
||||
if (elements.acousticProfile.value === "custom") return syncControls();
|
||||
applyAcousticProfile(
|
||||
state.scene,
|
||||
state.arrayDefinition,
|
||||
state.experiment,
|
||||
elements.acousticProfile.value,
|
||||
);
|
||||
markChanged({ dirty: "scene", syncControls: true });
|
||||
});
|
||||
elements.frequencyResponseMode.addEventListener("change", () => {
|
||||
state.experiment.beamforming.compensateFrequencyResponse =
|
||||
elements.frequencyResponseMode.value === "compensate";
|
||||
markChanged();
|
||||
});
|
||||
elements.backend.addEventListener("change", () => {
|
||||
state.experiment.beamforming.backend = elements.backend.value;
|
||||
markChanged();
|
||||
|
||||
Reference in New Issue
Block a user