Skip to content

Commit cf35908

Browse files
committed
Switches to using source directions computed as part of the image source model for source directivities. This enables the use of source directivities with non-shoebox rooms.
1 parent 3e177a7 commit cf35908

File tree

5 files changed

+142
-87
lines changed

5 files changed

+142
-87
lines changed

examples/directivities/source_and_microphone.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,15 @@
22
import numpy as np
33

44
import pyroomacoustics as pra
5-
from pyroomacoustics.directivities import (
6-
CardioidFamily,
7-
DirectionVector,
8-
DirectivityPattern,
9-
)
5+
from pyroomacoustics.directivities import DirectionVector, FigureEight, HyperCardioid
106

117
three_dim = True # 2D or 3D
128
shoebox = True # source directivity not supported for non-shoebox!
139
energy_absorption = 0.4
1410
source_pos = [2, 1.8]
1511
# source_dir = None # to disable
16-
source_dir = DirectivityPattern.FIGURE_EIGHT
1712
mic_pos = [3.5, 1.8]
1813
# mic_dir = None # to disable
19-
mic_dir = DirectivityPattern.HYPERCARDIOID
2014

2115

2216
# make 2-D room
@@ -42,19 +36,15 @@
4236
colatitude = None
4337

4438
# add source with directivity
45-
if source_dir is not None:
46-
source_dir = CardioidFamily(
47-
orientation=DirectionVector(azimuth=90, colatitude=colatitude, degrees=True),
48-
pattern_enum=source_dir,
49-
)
39+
source_dir = FigureEight(
40+
orientation=DirectionVector(azimuth=90, colatitude=colatitude, degrees=True),
41+
)
5042
room.add_source(position=source_pos, directivity=source_dir)
5143

5244
# add microphone with directivity
53-
if mic_dir is not None:
54-
mic_dir = CardioidFamily(
55-
orientation=DirectionVector(azimuth=0, colatitude=colatitude, degrees=True),
56-
pattern_enum=mic_dir,
57-
)
45+
mic_dir = HyperCardioid(
46+
orientation=DirectionVector(azimuth=0, colatitude=colatitude, degrees=True),
47+
)
5848
room.add_microphone(loc=mic_pos, directivity=mic_dir)
5949

6050
# plot room
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import numpy as np
2+
import pytest
3+
4+
import pyroomacoustics as pra
5+
6+
"""
7+
+ - - - - - - - - - - - +
8+
| |
9+
| |
10+
| |
11+
| + - - - - - - - +
12+
| |
13+
| |
14+
| |
15+
+ - - - +
16+
"""
17+
18+
corners = np.array([[0, 0], [4, 0], [4, 4], [12, 4], [12, 8], [0, 8]])
19+
source_location = [6, 6, 1.5]
20+
mic_location = [1, 6, 1.5]
21+
22+
images_expected = np.array(
23+
[
24+
source_location, # direct
25+
[6, 10, 1.5], # north
26+
[-6, 6, 1.5], # west
27+
[18, 6, 1.5], # east
28+
[6, 6, 4.5], # ceilling
29+
[6, 6, -1.5], # floor
30+
]
31+
)
32+
directions_expected = np.array(
33+
[
34+
[-1, 0, 0],
35+
np.array([-2.5, 2, 0]) / np.sqrt(2.5**2 + 2**2),
36+
[-1, 0, 0],
37+
[1, 0, 0],
38+
np.array([-2.5, 0, 1.5]) / np.sqrt(2.5**2 + 1.5**2),
39+
np.array([-2.5, 0, -1.5]) / np.sqrt(2.5**2 + 1.5**2),
40+
]
41+
)
42+
43+
44+
room = pra.Room.from_corners(corners.T, materials=pra.Material(0.1), max_order=1)
45+
room.extrude(3.0, materials=pra.Material(0.1))
46+
room.add_source(
47+
source_location,
48+
directivity=pra.directivities.Cardioid(
49+
orientation=pra.directivities.DirectionVector(
50+
azimuth=0, colatitude=np.pi / 2, degrees=False
51+
)
52+
),
53+
).add_microphone(mic_location).add_microphone([3, 6, 1.5])
54+
# We add one microphone with other images visible to make sure we have some
55+
# image sources not visible.
56+
57+
58+
def test_source_directions_nonshoebox():
59+
room.image_source_model()
60+
visible = room.visibility[0][0, :]
61+
directions_obtained = room.sources[0].directions[0, :, visible]
62+
images_obtained = room.sources[0].images[:, visible]
63+
64+
order_obtained = np.lexsort(images_obtained)
65+
images_obtained = images_obtained[:, order_obtained].T
66+
directions_obtained = directions_obtained[order_obtained, :]
67+
68+
order_expected = np.lexsort(images_expected.T)
69+
images_expected_reordered = images_expected[order_expected, :]
70+
directions_expected_reordered = directions_expected[order_expected, :]
71+
72+
np.testing.assert_almost_equal(images_obtained, images_expected_reordered)
73+
np.testing.assert_almost_equal(directions_obtained, directions_expected_reordered)

pyroomacoustics/tests/test_source_directivity_flipping.py renamed to pyroomacoustics/directivities/tests/test_source_directivity_flipping.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,57 @@
33
import numpy as np
44

55
import pyroomacoustics as pra
6-
from pyroomacoustics.simulation.ism import source_angle_shoebox
6+
7+
8+
def source_angle_shoebox(image_source_loc, wall_flips, mic_loc):
9+
"""
10+
Determine outgoing angle for each image source for a ShoeBox configuration.
11+
12+
Implementation of the method described in the paper:
13+
https://www2.ak.tu-berlin.de/~akgroup/ak_pub/2018/000458.pdf
14+
15+
Parameters
16+
-----------
17+
image_source_loc : array_like
18+
Locations of image sources.
19+
wall_flips: array_like
20+
Number of x, y, z flips for each image source.
21+
mic_loc: array_like
22+
Microphone location.
23+
24+
Returns
25+
-------
26+
azimuth : :py:class:`~numpy.ndarray`
27+
Azimith for each image source, in radians
28+
colatitude : :py:class:`~numpy.ndarray`
29+
Colatitude for each image source, in radians.
30+
31+
"""
32+
33+
image_source_loc = np.array(image_source_loc)
34+
wall_flips = np.array(wall_flips)
35+
mic_loc = np.array(mic_loc)
36+
37+
dim, n_sources = image_source_loc.shape
38+
assert wall_flips.shape[0] == dim
39+
assert mic_loc.shape[0] == dim
40+
41+
p_vector_array = image_source_loc - np.array(mic_loc)[:, np.newaxis]
42+
d_array = np.linalg.norm(p_vector_array, axis=0)
43+
44+
# Using (12) from the paper
45+
power_array = np.ones_like(image_source_loc) * -1
46+
power_array = np.power(power_array, (wall_flips + np.ones_like(image_source_loc)))
47+
p_dash_array = p_vector_array * power_array
48+
49+
# Using (13) from the paper
50+
azimuth = np.arctan2(p_dash_array[1], p_dash_array[0])
51+
if dim == 2:
52+
colatitude = np.ones(n_sources) * np.pi / 2
53+
else:
54+
colatitude = np.pi / 2 - np.arcsin(p_dash_array[2] / d_array)
55+
56+
return azimuth, colatitude
757

858

959
class TestSourceDirectivityFlipping(TestCase):

pyroomacoustics/room.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,8 +1409,8 @@ def extrude(self, height, v_vec=None, absorption=None, materials=None):
14091409
if len(self.walls[i].absorption) == 1:
14101410
# Single band
14111411
wall_materials[name] = Material(
1412-
energy_absorption=float(self.walls[i].absorption),
1413-
scattering=float(self.walls[i].scatter),
1412+
energy_absorption=float(self.walls[i].absorption[0]),
1413+
scattering=float(self.walls[i].scatter[0]),
14141414
)
14151415
elif len(self.walls[i].absorption) == self.octave_bands.n_bands:
14161416
# Multi-band
@@ -2139,14 +2139,6 @@ def add_source(self, position, signal=None, delay=0, directivity=None):
21392139
if self.dim != 3 and directivity is not None:
21402140
raise NotImplementedError("Directivity is only supported for 3D rooms.")
21412141

2142-
if directivity is not None:
2143-
from pyroomacoustics import ShoeBox
2144-
2145-
if not isinstance(self, ShoeBox):
2146-
raise NotImplementedError(
2147-
"Source directivity only supported for ShoeBox room."
2148-
)
2149-
21502142
if isinstance(position, SoundSource):
21512143
if directivity is not None:
21522144
if isinstance(directivity, CardioidFamily) or isinstance(
@@ -2241,8 +2233,11 @@ def image_source_model(self):
22412233
self.visibility[-1][m, :] = 0
22422234
else:
22432235
# if we are here, this means even the direct path is not visible
2244-
# we set the visibility of the direct path as 0
2236+
# we set the visibility of the direct path as 0.
22452237
self.visibility.append(np.zeros((self.mic_array.M, 1), dtype=np.int32))
2238+
# We also need a fake array of directions as this is expected later in
2239+
# the code.
2240+
source.directions = np.zeros((self.mic_array.M, self.dim, 1), dtype=np.float32)
22462241

22472242
# Update the state
22482243
self.simulator_state["ism_done"] = True
@@ -2305,13 +2300,14 @@ def compute_rir(self):
23052300
src,
23062301
mic,
23072302
self.mic_array.directivity[m],
2303+
src.directions[m, :, :],
23082304
self.visibility[s][m, :],
23092305
fdl,
23102306
self.c,
23112307
self.fs,
23122308
self.octave_bands,
2313-
air_abs_coeffs=self.air_absorption,
23142309
min_phase=self.min_phase,
2310+
air_abs_coeffs=self.air_absorption,
23152311
)
23162312
rir_parts.append(ir_ism)
23172313

pyroomacoustics/simulation/ism.py

Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import numpy as np
3232
from scipy.signal import fftconvolve, hilbert
3333

34-
from .. import libroom
34+
from .. import doa, libroom
3535
from ..parameters import constants
3636
from ..utilities import angle_function
3737

@@ -109,61 +109,11 @@ def interpolate_octave_bands(
109109
return ir
110110

111111

112-
def source_angle_shoebox(image_source_loc, wall_flips, mic_loc):
113-
"""
114-
Determine outgoing angle for each image source for a ShoeBox configuration.
115-
116-
Implementation of the method described in the paper:
117-
https://www2.ak.tu-berlin.de/~akgroup/ak_pub/2018/000458.pdf
118-
119-
Parameters
120-
-----------
121-
image_source_loc : array_like
122-
Locations of image sources.
123-
wall_flips: array_like
124-
Number of x, y, z flips for each image source.
125-
mic_loc: array_like
126-
Microphone location.
127-
128-
Returns
129-
-------
130-
azimuth : :py:class:`~numpy.ndarray`
131-
Azimith for each image source, in radians
132-
colatitude : :py:class:`~numpy.ndarray`
133-
Colatitude for each image source, in radians.
134-
135-
"""
136-
137-
image_source_loc = np.array(image_source_loc)
138-
wall_flips = np.array(wall_flips)
139-
mic_loc = np.array(mic_loc)
140-
141-
dim, n_sources = image_source_loc.shape
142-
assert wall_flips.shape[0] == dim
143-
assert mic_loc.shape[0] == dim
144-
145-
p_vector_array = image_source_loc - np.array(mic_loc)[:, np.newaxis]
146-
d_array = np.linalg.norm(p_vector_array, axis=0)
147-
148-
# Using (12) from the paper
149-
power_array = np.ones_like(image_source_loc) * -1
150-
power_array = np.power(power_array, (wall_flips + np.ones_like(image_source_loc)))
151-
p_dash_array = p_vector_array * power_array
152-
153-
# Using (13) from the paper
154-
azimuth = np.arctan2(p_dash_array[1], p_dash_array[0])
155-
if dim == 2:
156-
colatitude = np.ones(n_sources) * np.pi / 2
157-
else:
158-
colatitude = np.pi / 2 - np.arcsin(p_dash_array[2] / d_array)
159-
160-
return azimuth, colatitude
161-
162-
163112
def compute_ism_rir(
164113
src,
165114
mic,
166115
mic_dir,
116+
src_directions,
167117
is_visible,
168118
fdl,
169119
c,
@@ -226,11 +176,7 @@ def compute_ism_rir(
226176
oct_band_amplitude *= mic_gain
227177

228178
if src.directivity is not None:
229-
azimuth_s, colatitude_s = source_angle_shoebox(
230-
image_source_loc=images,
231-
wall_flips=abs(src.orders_xyz[:, is_visible]),
232-
mic_loc=mic,
233-
)
179+
azimuth_s, colatitude_s, _ = doa.cart2spher(src_directions[:, is_visible])
234180
src_gain = src.directivity.get_response(
235181
azimuth=azimuth_s,
236182
colatitude=colatitude_s,

0 commit comments

Comments
 (0)