From 033126f555f28c0c4967cb2f47dc2344076a970c Mon Sep 17 00:00:00 2001 From: Robin Scheibler Date: Sun, 8 Dec 2024 22:54:11 +0900 Subject: [PATCH] Fixes from_corner/extrude for mixed single/multi-band materials (#388) Fixes a bug where the single/multi-band materials is not properly handled when using from_corners together with extrude. (issue #381) --- CHANGELOG.rst | 8 ++- pyroomacoustics/room.py | 58 ++++++++++++---- .../tests/test_from_corners_extrude.py | 68 +++++++++++++++++++ 3 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 pyroomacoustics/tests/test_from_corners_extrude.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f9793536..759cbe16 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,12 @@ Bugfix directivity to ``Room.add_microphone_array``, the directivity was dropped from the object. +- Fixes issues #381: When creating a room with from_corners with multi-band + material, and then making it 3D with ``extrude`` with a single band material + for the floor and ceiling, an error would occur. + After the fix, the materials for floor and ceiling are automatically extended + to multi-band, as expected. + - Fixes issue #380: Caused by the attribute ``cartesian`` of ``GridSphere`` not being set properly when the grid is only initialized with a number of points. @@ -30,7 +36,7 @@ Bugfix Changed ~~~~~~~ -- Makes the ``pyroomacoustics.utilities.resample`` backend is made configurable +- Makes the ``pyroomacoustics.utilities.resample`` backend configurable to avoid ``soxr`` dependency. The resample backend is configurable to ``soxr``, ``samplerate``, if these packages are available, and otherwise falls back to ``scipy.signal.resample_poly``. diff --git a/pyroomacoustics/room.py b/pyroomacoustics/room.py index e9620347..d299d7a1 100644 --- a/pyroomacoustics/room.py +++ b/pyroomacoustics/room.py @@ -1395,9 +1395,11 @@ def extrude(self, height, v_vec=None, absorption=None, materials=None): if libroom.area_2d_polygon(floor_corners) <= 0: floor_corners = np.fliplr(floor_corners) - walls = [] + wall_corners = {} + wall_materials = {} for i in range(nw): - corners = np.array( + name = str(i) + wall_corners[name] = np.array( [ np.r_[floor_corners[:, i], 0], np.r_[floor_corners[:, (i + 1) % nw], 0], @@ -1405,14 +1407,33 @@ def extrude(self, height, v_vec=None, absorption=None, materials=None): np.r_[floor_corners[:, i], 0] + height * v_vec, ] ).T - walls.append( - wall_factory( - corners, - self.walls[i].absorption, - self.walls[i].scatter, - name=str(i), + + if len(self.walls[i].absorption) == 1: + # Single band + wall_materials[name] = Material( + energy_absorption=float(self.walls[i].absorption), + scattering=float(self.walls[i].scatter), + ) + elif len(self.walls[i].absorption) == self.octave_bands.n_bands: + # Multi-band + abs_dict = { + "coeffs": self.walls[i].absorption, + "center_freqs": self.octave_bands.centers, + "description": "", + } + sca_dict = { + "coeffs": self.walls[i].scatter, + "center_freqs": self.octave_bands.centers, + "description": "", + } + wall_materials[name] = Material( + energy_absorption=abs_dict, + scattering=sca_dict, + ) + else: + raise ValueError( + "Encountered a material with inconsistent number of bands." ) - ) ############################ # BEGIN COMPATIBILITY CODE # @@ -1477,12 +1498,23 @@ def extrude(self, height, v_vec=None, absorption=None, materials=None): # we need the floor corners to ordered clockwise (for the normal to point outward) new_corners["floor"] = np.fliplr(new_corners["floor"]) - for key in ["floor", "ceiling"]: + # Concatenate new walls param with old ones. + wall_corners.update(new_corners) + wall_materials.update(materials) + + # If some of the materials used are multi-band, we need to resample + # all of them to have the same number of values + if not Material.all_flat(wall_materials): + for name, mat in wall_materials.items(): + mat.resample(self.octave_bands) + + walls = [] + for key, corners in wall_corners.items(): walls.append( wall_factory( - new_corners[key], - materials[key].absorption_coeffs, - materials[key].scattering_coeffs, + corners, + wall_materials[key].absorption_coeffs, + wall_materials[key].scattering_coeffs, name=key, ) ) diff --git a/pyroomacoustics/tests/test_from_corners_extrude.py b/pyroomacoustics/tests/test_from_corners_extrude.py new file mode 100644 index 00000000..a816f062 --- /dev/null +++ b/pyroomacoustics/tests/test_from_corners_extrude.py @@ -0,0 +1,68 @@ +import numpy as np +import pytest + +import pyroomacoustics as pra + +_FS = 16000 +_MAX_ORDER = 3 + + +def test_from_corner_extrude(): + + room_dim = [3, 4, 5] + # This test is sensitive to the selection of this value. + # If some symmetries are present, for some reasons differences between + # the two simulated methods happen. + src_loc = [1.001, 0.999, 1.002] + mic_loc = [2, 3, 4] + mat = pra.Material(energy_absorption=0.1) + + room_ref = pra.ShoeBox(room_dim, fs=_FS, max_order=_MAX_ORDER, materials=mat) + room_ref.add_source(src_loc).add_microphone(mic_loc) + room_ref.compute_rir() + + # Now construct the same room with the other set of primitives. + corners = np.array( + [[0, 0], [room_dim[0], 0], [room_dim[0], room_dim[1]], [0, room_dim[1]]] + ).T + room = pra.Room.from_corners(corners, fs=_FS, max_order=_MAX_ORDER, materials=mat) + room.extrude(height=room_dim[2], materials=mat) + room.add_source(src_loc).add_microphone(mic_loc) + room.compute_rir() + + assert np.allclose(room_ref.rir[0][0], room.rir[0][0], rtol=1e-4, atol=1e-4) + + +def test_from_corner_extrude_different_materials(): + + room_dim = [3, 4, 5] + # This test is sensitive to the selection of this value. + # If some symmetries are present, for some reasons differences between + # the two simulated methods happen. + src_loc = [1.001, 0.999, 1.002] + mic_loc = [2, 3, 4] + mat1 = "hard_surface" + mat2 = 0.1 + + materials = pra.make_materials( + east=mat1, west=mat1, south=mat1, north=mat1, floor=mat2, ceiling=mat2 + ) + room_ref = pra.ShoeBox(room_dim, fs=_FS, max_order=_MAX_ORDER, materials=materials) + room_ref.add_source(src_loc).add_microphone(mic_loc) + room_ref.compute_rir() + + # Now construct the same room with the other set of primitives. + corners = np.array( + [[0, 0], [room_dim[0], 0], [room_dim[0], room_dim[1]], [0, room_dim[1]]] + ).T + room = pra.Room.from_corners( + corners, + fs=_FS, + max_order=_MAX_ORDER, + materials=pra.Material(energy_absorption=mat1), + ) + room.extrude(height=room_dim[2], materials=pra.Material(energy_absorption=mat2)) + room.add_source(src_loc).add_microphone(mic_loc) + room.compute_rir() + + assert np.allclose(room_ref.rir[0][0], room.rir[0][0], rtol=1e-4, atol=1e-4)