Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5dc0594

Browse files
committedDec 7, 2024
Fixes a few potential bugs with the MicrophoneArray class and adds some tests. (issue #355)
1 parent f6d11c9 commit 5dc0594

File tree

2 files changed

+151
-22
lines changed

2 files changed

+151
-22
lines changed
 

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

0 commit comments

Comments
 (0)
Please sign in to comment.