Skip to content

Commit

Permalink
Merge branch 'master' into robin/feature/source_directivity
Browse files Browse the repository at this point in the history
  • Loading branch information
fakufaku committed Dec 8, 2024
2 parents d9f2819 + 8f64460 commit 3e177a7
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 66 deletions.
27 changes: 25 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,35 @@ Added
The filter bank is implemented in ``pyroomacoustics.acoustics.AntoniOctaveFilterBank``.


`0.8.3`_ - 2024-12-08
---------------------

Bugfix
~~~~~~

- Fixes issue #382: When providing a ``MicrophoneArray`` object with
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.

- Fixes issue #355: Makes the MicrophoneArray class more bug-proof and adds
some tests.

`0.8.2`_ - 2024-11-06
---------------------

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``.
Expand Down Expand Up @@ -688,7 +710,8 @@ Changed
``pyroomacoustics.datasets.timit``


.. _Unreleased: https://github.com/LCAV/pyroomacoustics/compare/v0.8.2...master
.. _Unreleased: https://github.com/LCAV/pyroomacoustics/compare/v0.8.3...master
.. _0.8.3: https://github.com/LCAV/pyroomacoustics/compare/v0.8.2...v0.8.3
.. _0.8.2: https://github.com/LCAV/pyroomacoustics/compare/v0.8.1...v0.8.2
.. _0.8.1: https://github.com/LCAV/pyroomacoustics/compare/v0.8.0...v0.8.1
.. _0.8.0: https://github.com/LCAV/pyroomacoustics/compare/v0.7.7...v0.8.0
Expand Down
73 changes: 51 additions & 22 deletions pyroomacoustics/beamforming.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

from __future__ import division

from typing import Sequence

import numpy as np
import scipy.linalg as la

Expand Down Expand Up @@ -342,56 +344,89 @@ class MicrophoneArray(object):
"""Microphone array class."""

def __init__(self, R, fs, directivity=None):
R = np.array(R)
self.dim = R.shape[0] # are we in 2D or in 3D
self.nmic = R.shape[1] # number of microphones
# The array geometry is stored in a (dim, n_mics) array.
self.R = np.array(R) # array geometry

# Check the shape of the passed array
if self.dim != 2 and self.dim != 3:
if self.dim not in (2, 3):
dim_mismatch = True
else:
dim_mismatch = False

if R.ndim != 2 or dim_mismatch:
if self.R.ndim != 2 or dim_mismatch:
raise ValueError(
"The location of microphones should be described by an array_like "
"object with 2 dimensions of shape `(2 or 3, n_mics)` "
"where `n_mics` is the number of microphones. Each column contains "
"the location of a microphone."
)

self.R = R # array geometry

self.fs = fs # sampling frequency of microphones
self.set_directivity(directivity)

self.signals = None

self.center = np.mean(R, axis=1, keepdims=True)

@property
def dim(self):
return self.R.shape[0] # are we in 2D or in 3D

def __len__(self):
return self.R.shape[1]

@property
def nmic(self):
"""The number of microphones of the array."""
return self.__len__()

@property
def M(self):
"""The number of microphones of the array."""
return self.__len__()

@property
def is_directive(self):
return any([d is not None for d in self.directivity])

def set_directivity(self, directivities):
"""
This functions sets self.directivity as a list of directivities with `n_mics` entries,
where `n_mics` is the number of microphones
This functions sets self.directivity as a list of directivities with
`n_mics` entries, where `n_mics` is the number of microphones.
Parameters
-----------
directivities:
single directivity for all microphones or a list of directivities for each microphone
A single directivity for all microphones or a list of directivities
for each microphone
"""

if isinstance(directivities, list):
def _is_correct_type(directivity):
return directivity is None or isinstance(directivity, Directivity)

if isinstance(directivities, Sequence):
# list of directivities specified
assert all(isinstance(x, Directivity) for x in directivities)
assert len(directivities) == self.nmic
self.directivity = directivities
for d in directivities:
if not _is_correct_type(d):
raise TypeError(
"Directivities should be of Directivity type, or None (got "
f"{type(d)})."
)
if not len(directivities) == self.nmic:
raise ValueError(
"Please provide a single Directivity for all microphones, or one "
f"per microphone. Got {len(directivities)} directivities for "
f"{self.nmic} mics."
)
self.directivity = list(directivities)
else:
if not _is_correct_type(directivities):
raise TypeError(
"Directivities should be of Directivity type, or None (got "
f"{type(directivities)})."
)
# only 1 directivity specified
assert directivities is None or isinstance(directivities, Directivity)
self.directivity = [directivities] * self.nmic

def record(self, signals, fs):
Expand Down Expand Up @@ -505,6 +540,7 @@ def append(self, locs):
self.directivity += locs.directivity
else:
self.R = np.concatenate((self.R, locs), axis=1)
self.directivity += [None] * locs.shape[1]

# in case there was already some signal recorded, just pad with zeros
if self.signals is not None:
Expand All @@ -518,13 +554,6 @@ def append(self, locs):
axis=0,
)

def __len__(self):
return self.R.shape[1]

@property
def M(self):
return self.__len__()


class Beamformer(MicrophoneArray):
"""
Expand Down
3 changes: 1 addition & 2 deletions pyroomacoustics/doa/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,7 @@ def __init__(

# Create convenient arrays
# to access both in cartesian and spherical coordinates
self.azimuth[:] = np.arctan2(self.y, self.x)
self.colatitude[:] = np.arctan2(np.sqrt(self.x**2 + self.y**2), self.z)
self.azimuth[:], self.colatitude[:], _ = cart2spher(self.cartesian)

self._neighbors = None
if precompute_neighbors:
Expand Down
41 changes: 39 additions & 2 deletions pyroomacoustics/doa/tests/test_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pytest
from scipy.spatial import SphericalVoronoi

from pyroomacoustics.doa import fibonacci_spherical_sampling
import pyroomacoustics as pra


@pytest.mark.parametrize("n", [20, 100, 200, 500, 1000, 2000, 5000, 10000])
Expand All @@ -18,7 +18,7 @@ def test_voronoi_area(n, tol):
We observed empirically that the relative max error is
around 6% so we set that as the threshold for the test
"""
points = fibonacci_spherical_sampling(n_points=n)
points = pra.doa.fibonacci_spherical_sampling(n_points=n)
sphere_area = 4.0 * np.pi
area_one_pt = sphere_area / n

Expand All @@ -34,6 +34,43 @@ def test_voronoi_area(n, tol):
assert max_err < tol


def _check_grid_consistency(grid):

cart = pra.doa.spher2cart(grid.azimuth, grid.colatitude)
assert np.allclose(grid.cartesian, np.array([grid.x, grid.y, grid.z]))
assert np.allclose(grid.cartesian, cart)

az, co, _ = pra.doa.cart2spher(grid.cartesian)
assert np.allclose(grid.spherical, np.array([grid.azimuth, grid.colatitude]))
assert np.allclose(grid.spherical, np.array([az, co]))


@pytest.mark.parametrize("n_points", [20, 100, 200, 500, 1000])
def test_grid_sphere_from_spherical(n_points):

x, y, z = pra.doa.fibonacci_spherical_sampling(n_points)
az, co, _ = pra.doa.cart2spher(np.array([x, y, z]))

grid = pra.doa.GridSphere(spherical_points=np.array([az, co]))
_check_grid_consistency(grid)


@pytest.mark.parametrize("n_points", [20, 100, 200, 500, 1000])
def test_grid_sphere_from_cartesian(n_points):

x, y, z = pra.doa.fibonacci_spherical_sampling(n_points)

grid = pra.doa.GridSphere(cartesian_points=np.array([x, y, z]))
_check_grid_consistency(grid)


@pytest.mark.parametrize("n_points", [20, 100, 200, 500, 1000])
def test_grid_sphere_from_fibonacci(n_points):

grid = pra.doa.GridSphere(n_points=n_points)
_check_grid_consistency(grid)


if __name__ == "__main__":
test_voronoi_area(20, 0.01)
test_voronoi_area(100, 0.01)
Expand Down
73 changes: 58 additions & 15 deletions pyroomacoustics/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,24 +1393,45 @@ 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],
np.r_[floor_corners[:, (i + 1) % nw], 0] + height * v_vec,
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 #
Expand Down Expand Up @@ -1475,12 +1496,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,
)
)
Expand Down Expand Up @@ -1978,7 +2010,7 @@ def add(self, obj):
).format(self.dim, obj.dim)
)

if "mic_array" not in self.__dict__ or self.mic_array is None:
if not hasattr(self, "mic_array") or self.mic_array is None:
self.mic_array = obj
else:
self.mic_array.append(obj)
Expand Down Expand Up @@ -2044,6 +2076,12 @@ def add_microphone_array(self, mic_array, directivity=None):
As an alternative, a
:py:obj:`~pyroomacoustics.beamforming.MicrophoneArray` can be
provided.
directivity: list of Directivity objects, optional
If ``mic_array`` is provided as a numpy array, an optional
:py:obj:`~pyroomacoustics.directivities.Directivity` object or
list thereof can be provided.
If ``mic_array`` is a MicrophoneArray object, passing an argument here
will result in an error.
Returns
-------
Expand All @@ -2062,7 +2100,12 @@ def add_microphone_array(self, mic_array, directivity=None):
mic_array = MicrophoneArray(mic_array, self.fs, directivity)
else:
# if the type is microphone array
mic_array.set_directivity(directivity)
if directivity is not None:
raise ValueError(
"When providing a MicrophoneArray object, the directivities should "
"be provided in the object, not via the `directivity` parameter "
"of this method."
)

if self.simulator_state["rt_needed"] and mic_array.is_directive:
raise NotImplementedError("Directivity not supported with ray tracing.")
Expand Down
Loading

0 comments on commit 3e177a7

Please sign in to comment.