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; }