123 lines
4.5 KiB
JavaScript
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;
|
|
}
|