Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes a few potential bugs with the MicrophoneArray class and adds some tests. #387

Merged
merged 4 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Bugfix
- 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
---------------------

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
101 changes: 101 additions & 0 deletions pyroomacoustics/tests/test_microphone_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import numpy as np
import pytest

import pyroomacoustics as pra

_FS = 16000

mic_dir0 = pra.FigureEight(
orientation=pra.DirectionVector(azimuth=90, colatitude=15, degrees=True)
)
mic_dir1 = pra.FigureEight(
orientation=pra.DirectionVector(azimuth=180, colatitude=15, degrees=True)
)


@pytest.mark.parametrize("shape", ((1, 2, 3), (10, 2), (1, 10), (10,)))
def test_microphone_array_invalid_shape(shape):

locs = np.ones(shape)
with pytest.raises(ValueError):
pra.MicrophoneArray(locs, fs=_FS)


@pytest.mark.parametrize(
"directivity, exception_type",
(
("omni", TypeError),
(["omni"] * 3, TypeError),
([mic_dir0, "omni", mic_dir1] * 3, TypeError),
([mic_dir0, mic_dir1], ValueError),
),
)
def test_microphone_array_invalid_directivity(directivity, exception_type):

locs = np.ones((3, 3))
with pytest.raises(exception_type):
pra.MicrophoneArray(locs, fs=_FS, directivity=directivity)


@pytest.mark.parametrize(
"shape, with_dir, same_dir",
(
((2, 1), False, False),
((2, 2), False, False),
((2, 3), False, False),
((3, 1), False, False),
((3, 3), False, False),
((3, 4), False, False),
((2, 3), True, False),
((3, 4), True, False),
((2, 3), True, True),
((3, 4), True, True),
),
)
def test_microphone_array_shape_correct(shape, with_dir, same_dir):

locs = np.ones(shape)
if with_dir:
if same_dir:
mdir = [mic_dir0] * shape[1]
else:
mdir = [mic_dir0, mic_dir1] + [None] * (shape[1] - 2)
else:
mdir = None
mic_array = pra.MicrophoneArray(locs, fs=_FS, directivity=mdir)

assert mic_array.dim == shape[0]
assert mic_array.M == shape[1]
assert mic_array.nmic == mic_array.M
assert len(mic_array.directivity) == shape[1]


@pytest.mark.parametrize(
"shape1, shape2, with_dir, from_raw_locs",
(
((3, 2), (3, 2), False, False),
((3, 2), (3, 2), False, True),
((3, 2), (3, 2), False, False),
((3, 2), (3, 2), False, True),
((3, 2), (3, 2), True, False),
((3, 2), (3, 2), True, True),
((3, 2), (3, 1), False, False),
((3, 2), (3, 1), False, True),
),
)
def test_microphone_array_append(shape1, shape2, with_dir, from_raw_locs):
if with_dir:
mdir = [mic_dir0, mic_dir1] + [None] * (shape1[1] - 2)
else:
mdir = None

mic_array = pra.MicrophoneArray(np.ones(shape1), fs=_FS, directivity=mdir)

if from_raw_locs:
mic_array.append(np.ones(shape2))

else:
mic_array.append(pra.MicrophoneArray(np.ones(shape2), fs=_FS))

assert mic_array.nmic == shape1[1] + shape2[1]
assert len(mic_array.directivity) == shape1[1] + shape2[1]
Loading