Skip to content

Commit d8d5e84

Browse files
authored
Fixes a few potential bugs with the MicrophoneArray class and adds some tests. (#387)
1 parent e4d183a commit d8d5e84

File tree

3 files changed

+155
-22
lines changed

3 files changed

+155
-22
lines changed

CHANGELOG.rst

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Bugfix
2121
- Fixes issue #380: Caused by the attribute ``cartesian`` of ``GridSphere`` not
2222
being set properly when the grid is only initialized with a number of points.
2323

24+
- Fixes issue #355: Makes the MicrophoneArray class more bug-proof and adds
25+
some tests.
26+
2427
`0.8.2`_ - 2024-11-06
2528
---------------------
2629

pyroomacoustics/beamforming.py

+51-22
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
from __future__ import division
2626

27+
from typing import Sequence
28+
2729
import numpy as np
2830
import scipy.linalg as la
2931

@@ -342,56 +344,89 @@ class MicrophoneArray(object):
342344
"""Microphone array class."""
343345

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

349350
# Check the shape of the passed array
350-
if self.dim != 2 and self.dim != 3:
351+
if self.dim not in (2, 3):
351352
dim_mismatch = True
352353
else:
353354
dim_mismatch = False
354355

355-
if R.ndim != 2 or dim_mismatch:
356+
if self.R.ndim != 2 or dim_mismatch:
356357
raise ValueError(
357358
"The location of microphones should be described by an array_like "
358359
"object with 2 dimensions of shape `(2 or 3, n_mics)` "
359360
"where `n_mics` is the number of microphones. Each column contains "
360361
"the location of a microphone."
361362
)
362363

363-
self.R = R # array geometry
364-
365364
self.fs = fs # sampling frequency of microphones
366365
self.set_directivity(directivity)
367366

368367
self.signals = None
369368

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

371+
@property
372+
def dim(self):
373+
return self.R.shape[0] # are we in 2D or in 3D
374+
375+
def __len__(self):
376+
return self.R.shape[1]
377+
378+
@property
379+
def nmic(self):
380+
"""The number of microphones of the array."""
381+
return self.__len__()
382+
383+
@property
384+
def M(self):
385+
"""The number of microphones of the array."""
386+
return self.__len__()
387+
372388
@property
373389
def is_directive(self):
374390
return any([d is not None for d in self.directivity])
375391

376392
def set_directivity(self, directivities):
377393
"""
378-
This functions sets self.directivity as a list of directivities with `n_mics` entries,
379-
where `n_mics` is the number of microphones
394+
This functions sets self.directivity as a list of directivities with
395+
`n_mics` entries, where `n_mics` is the number of microphones.
396+
380397
Parameters
381398
-----------
382399
directivities:
383-
single directivity for all microphones or a list of directivities for each microphone
400+
A single directivity for all microphones or a list of directivities
401+
for each microphone
384402
385403
"""
386404

387-
if isinstance(directivities, list):
405+
def _is_correct_type(directivity):
406+
return directivity is None or isinstance(directivity, Directivity)
407+
408+
if isinstance(directivities, Sequence):
388409
# list of directivities specified
389-
assert all(isinstance(x, Directivity) for x in directivities)
390-
assert len(directivities) == self.nmic
391-
self.directivity = directivities
410+
for d in directivities:
411+
if not _is_correct_type(d):
412+
raise TypeError(
413+
"Directivities should be of Directivity type, or None (got "
414+
f"{type(d)})."
415+
)
416+
if not len(directivities) == self.nmic:
417+
raise ValueError(
418+
"Please provide a single Directivity for all microphones, or one "
419+
f"per microphone. Got {len(directivities)} directivities for "
420+
f"{self.nmic} mics."
421+
)
422+
self.directivity = list(directivities)
392423
else:
424+
if not _is_correct_type(directivities):
425+
raise TypeError(
426+
"Directivities should be of Directivity type, or None (got "
427+
f"{type(directivities)})."
428+
)
393429
# only 1 directivity specified
394-
assert directivities is None or isinstance(directivities, Directivity)
395430
self.directivity = [directivities] * self.nmic
396431

397432
def record(self, signals, fs):
@@ -505,6 +540,7 @@ def append(self, locs):
505540
self.directivity += locs.directivity
506541
else:
507542
self.R = np.concatenate((self.R, locs), axis=1)
543+
self.directivity += [None] * locs.shape[1]
508544

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

521-
def __len__(self):
522-
return self.R.shape[1]
523-
524-
@property
525-
def M(self):
526-
return self.__len__()
527-
528557

529558
class Beamformer(MicrophoneArray):
530559
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import numpy as np
2+
import pytest
3+
4+
import pyroomacoustics as pra
5+
6+
_FS = 16000
7+
8+
mic_dir0 = pra.FigureEight(
9+
orientation=pra.DirectionVector(azimuth=90, colatitude=15, degrees=True)
10+
)
11+
mic_dir1 = pra.FigureEight(
12+
orientation=pra.DirectionVector(azimuth=180, colatitude=15, degrees=True)
13+
)
14+
15+
16+
@pytest.mark.parametrize("shape", ((1, 2, 3), (10, 2), (1, 10), (10,)))
17+
def test_microphone_array_invalid_shape(shape):
18+
19+
locs = np.ones(shape)
20+
with pytest.raises(ValueError):
21+
pra.MicrophoneArray(locs, fs=_FS)
22+
23+
24+
@pytest.mark.parametrize(
25+
"directivity, exception_type",
26+
(
27+
("omni", TypeError),
28+
(["omni"] * 3, TypeError),
29+
([mic_dir0, "omni", mic_dir1] * 3, TypeError),
30+
([mic_dir0, mic_dir1], ValueError),
31+
),
32+
)
33+
def test_microphone_array_invalid_directivity(directivity, exception_type):
34+
35+
locs = np.ones((3, 3))
36+
with pytest.raises(exception_type):
37+
pra.MicrophoneArray(locs, fs=_FS, directivity=directivity)
38+
39+
40+
@pytest.mark.parametrize(
41+
"shape, with_dir, same_dir",
42+
(
43+
((2, 1), False, False),
44+
((2, 2), False, False),
45+
((2, 3), False, False),
46+
((3, 1), False, False),
47+
((3, 3), False, False),
48+
((3, 4), False, False),
49+
((2, 3), True, False),
50+
((3, 4), True, False),
51+
((2, 3), True, True),
52+
((3, 4), True, True),
53+
),
54+
)
55+
def test_microphone_array_shape_correct(shape, with_dir, same_dir):
56+
57+
locs = np.ones(shape)
58+
if with_dir:
59+
if same_dir:
60+
mdir = [mic_dir0] * shape[1]
61+
else:
62+
mdir = [mic_dir0, mic_dir1] + [None] * (shape[1] - 2)
63+
else:
64+
mdir = None
65+
mic_array = pra.MicrophoneArray(locs, fs=_FS, directivity=mdir)
66+
67+
assert mic_array.dim == shape[0]
68+
assert mic_array.M == shape[1]
69+
assert mic_array.nmic == mic_array.M
70+
assert len(mic_array.directivity) == shape[1]
71+
72+
73+
@pytest.mark.parametrize(
74+
"shape1, shape2, with_dir, from_raw_locs",
75+
(
76+
((3, 2), (3, 2), False, False),
77+
((3, 2), (3, 2), False, True),
78+
((3, 2), (3, 2), False, False),
79+
((3, 2), (3, 2), False, True),
80+
((3, 2), (3, 2), True, False),
81+
((3, 2), (3, 2), True, True),
82+
((3, 2), (3, 1), False, False),
83+
((3, 2), (3, 1), False, True),
84+
),
85+
)
86+
def test_microphone_array_append(shape1, shape2, with_dir, from_raw_locs):
87+
if with_dir:
88+
mdir = [mic_dir0, mic_dir1] + [None] * (shape1[1] - 2)
89+
else:
90+
mdir = None
91+
92+
mic_array = pra.MicrophoneArray(np.ones(shape1), fs=_FS, directivity=mdir)
93+
94+
if from_raw_locs:
95+
mic_array.append(np.ones(shape2))
96+
97+
else:
98+
mic_array.append(pra.MicrophoneArray(np.ones(shape2), fs=_FS))
99+
100+
assert mic_array.nmic == shape1[1] + shape2[1]
101+
assert len(mic_array.directivity) == shape1[1] + shape2[1]

0 commit comments

Comments
 (0)