Files
2026-08-28 18:24:10 +09:00

123 lines
4.5 KiB
JavaScript

const PROFILE_DEFINITIONS = Object.freeze({
"treated-room": Object.freeze({
reflection: Object.freeze({ wallGainDb: -14, scattering: 0.15 }),
diffuse: Object.freeze({ rt60Seconds: 0.2, gainDb: -26, tapCount: 4 }),
}),
"typical-room": Object.freeze({
reflection: Object.freeze({ wallGainDb: -8, scattering: 0.25 }),
diffuse: Object.freeze({ rt60Seconds: 0.4, gainDb: -20, tapCount: 6 }),
}),
"reflective-room": Object.freeze({
reflection: Object.freeze({ wallGainDb: -4, scattering: 0.4 }),
diffuse: Object.freeze({ rt60Seconds: 0.8, gainDb: -14, tapCount: 10 }),
}),
});
function finitePosition(position) {
return Array.isArray(position) && position.length === 3 && position.every(Number.isFinite);
}
function roundCoordinate(value) {
return Number(value.toFixed(6));
}
function geometryPositions(scene, arrayDefinition, experiment) {
const positions = [];
for (const microphone of arrayDefinition?.microphones ?? []) {
if (finitePosition(microphone.position)) positions.push(microphone.position);
}
for (const source of scene?.sources ?? []) {
if (finitePosition(source.position)) positions.push(source.position);
for (const keyframe of source.trajectory ?? []) {
if (finitePosition(keyframe.position)) positions.push(keyframe.position);
}
}
const scan = experiment?.scan;
if (scan && ["xMin", "xMax", "yMin", "yMax", "z"].every((key) => Number.isFinite(scan[key]))) {
positions.push(
[scan.xMin, scan.yMin, scan.z],
[scan.xMin, scan.yMax, scan.z],
[scan.xMax, scan.yMin, scan.z],
[scan.xMax, scan.yMax, scan.z],
);
}
return positions.length > 0 ? positions : [[0, 0, 0], [1, 1, 1]];
}
export function inferShoeboxRoom(scene, arrayDefinition, experiment) {
const positions = geometryPositions(scene, arrayDefinition, experiment);
const minimum = [0, 1, 2].map((axis) =>
Math.min(...positions.map((position) => position[axis])),
);
const maximum = [0, 1, 2].map((axis) =>
Math.max(...positions.map((position) => position[axis])),
);
const padding = [0, 1, 2].map((axis) =>
Math.max(axis === 2 ? 0.75 : 1, (maximum[axis] - minimum[axis]) * 0.35),
);
return {
min: minimum.map((value, axis) => roundCoordinate(value - padding[axis])),
max: maximum.map((value, axis) => roundCoordinate(value + padding[axis])),
};
}
export function applyAcousticProfile(scene, arrayDefinition, experiment, profileId) {
if (!scene || typeof scene !== "object") throw new TypeError("scene이 필요합니다.");
if (profileId === "free-field") {
scene.acoustics = { model: "free-field" };
return scene.acoustics;
}
const definition = PROFILE_DEFINITIONS[profileId];
if (!definition) throw new RangeError("지원하지 않는 음향 환경 profile: " + profileId);
scene.acoustics = {
model: "shoebox",
room: inferShoeboxRoom(scene, arrayDefinition, experiment),
reflections: {
order: 1,
wallGainDb: definition.reflection.wallGainDb,
scattering: definition.reflection.scattering,
},
diffuseReverb: {
enabled: true,
rt60Seconds: definition.diffuse.rt60Seconds,
gainDb: definition.diffuse.gainDb,
tapCount: definition.diffuse.tapCount,
},
};
return scene.acoustics;
}
export function detectAcousticProfile(scene) {
const acoustics = scene?.acoustics;
if (!acoustics || acoustics.model === "free-field") return "free-field";
if (acoustics.model !== "shoebox" || (acoustics.occluders?.length ?? 0) > 0) return "custom";
for (const [profileId, definition] of Object.entries(PROFILE_DEFINITIONS)) {
if (
acoustics.reflections?.order === 1 &&
acoustics.reflections.wallGainDb === definition.reflection.wallGainDb &&
acoustics.reflections.scattering === definition.reflection.scattering &&
acoustics.diffuseReverb?.enabled === true &&
acoustics.diffuseReverb.rt60Seconds === definition.diffuse.rt60Seconds &&
acoustics.diffuseReverb.gainDb === definition.diffuse.gainDb &&
acoustics.diffuseReverb.tapCount === definition.diffuse.tapCount
) {
return profileId;
}
}
return "custom";
}
export function maximumSparsePathsPerReceiver(scene) {
const acoustics = scene?.acoustics;
if (!acoustics || acoustics.model === "free-field") return 1;
let count = 1;
const reflections = acoustics.reflections;
if (reflections?.order === 1) {
count += 6;
if (reflections.scattering > 0) count += 6;
}
count += acoustics.occluders?.length ?? 0;
if (acoustics.diffuseReverb?.enabled) count += acoustics.diffuseReverb.tapCount;
return count;
}