Skip to content

Commit 61731e2

Browse files
je-fleischhauerErik Fleischhauer
andauthored
Adds Real Spherical Harmonics Directivity (#422)
* Add directivity pattern using real spherical harmonics. * Adds a test for the real spherical harmonics directivity. --------- Co-authored-by: Erik Fleischhauer <fleischhauer@iks.rwth-aachen.de>
1 parent 9e6d697 commit 61731e2

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Changed
1919
- Improves ``pra.experimental.measure_rt60``: Compute the T60 using a fit. Default is
2020
log-domain. Adds option to fit in linear domain.
2121

22+
- Add Directivity pattern for real spherical harmonics.
23+
2224
Bugfix
2325
~~~~~~
2426

pyroomacoustics/directivities/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,5 @@
9292
)
9393
from .base import Directivity
9494
from .direction import DirectionVector, Rotation3D
95+
from .harmonics import RealSphericalHarmonicsDirectivity
9596
from .measured import MeasuredDirectivity, MeasuredDirectivityFile
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright (C) 2026 Erik Fleischhauer
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in all
11+
# copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
# SOFTWARE.
20+
#
21+
# You should have received a copy of the MIT License along with this program. If
22+
# not, see <https://opensource.org/licenses/MIT>.
23+
import numpy as np
24+
import scipy.special
25+
26+
from .base import Directivity
27+
28+
29+
def get_mn_in_acn_order(order):
30+
"""
31+
Calculates the (m,n) pairs in ACN order up to a given order.
32+
33+
Parameters:
34+
----------
35+
order : int
36+
Maximum degree of the spherical harmonics.
37+
38+
Returns:
39+
----------
40+
all_m : ndarray
41+
Array of orders m in ACN order.
42+
all_n : ndarray
43+
Array of degrees n in ACN order.
44+
"""
45+
all_m = np.array([j - i for i in range(0, order + 1) for j in range(0, 2 * i + 1)])
46+
all_n = np.array([i for i in range(0, order + 1) for _ in range(0, 2 * i + 1)])
47+
return all_m, all_n
48+
49+
50+
def real_sph_harm(n, m, theta, phi, condon_shortley_phase=False):
51+
"""
52+
Calculates the real spherical harmonics.
53+
54+
Parameters:
55+
----------
56+
n : int
57+
Degree of the spherical harmonic.
58+
m : int
59+
Order of the spherical harmonic.
60+
theta : array_like
61+
Polar (colatitudinal) coordinate in radians.
62+
phi : array_like
63+
Azimuthal coordinate in radians.
64+
condon_shortley_phase : bool, optional
65+
If True, includes the Condon-Shortley phase factor (-1)^m. Default is False.
66+
67+
Returns:
68+
----------
69+
y_real : ndarray
70+
Real spherical harmonics evaluated at the given angles.
71+
"""
72+
m = np.atleast_1d(m)
73+
74+
try:
75+
ysh_complx = scipy.special.sph_harm_y(n, m, theta, phi)
76+
except AttributeError:
77+
# Deprecated since scipy v1.15.0.
78+
ysh_complx = scipy.special.sph_harm(m, n, phi, theta)
79+
80+
y_real = np.empty_like(ysh_complx, dtype=np.float64)
81+
y_real[(m >= 0)] = np.real(ysh_complx[(m >= 0)])
82+
y_real[(m < 0)] = np.imag(ysh_complx[(m < 0)])
83+
84+
if not condon_shortley_phase:
85+
# Cancel Condon-Shortley Phase (term (-1) ** m) by multiplying with (-1) ** m
86+
# In Rafaely's book, this step is not done
87+
y_real[(m != 0)] *= np.sqrt(2) * np.array([-1.0]) ** m[(m != 0)]
88+
# In the formular, for m<0, |m| is used. We consider this by the use of the constraint.
89+
y_real[np.logical_and(m < 0, (m % 2) == 0)] = -y_real[
90+
np.logical_and(m < 0, (m % 2) == 0)
91+
]
92+
93+
return y_real
94+
95+
96+
class RealSphericalHarmonicsDirectivity(Directivity):
97+
"""
98+
A class for real spherical harmonic directivity patterns.
99+
100+
Parameters:
101+
----------
102+
m: int
103+
Order of the spherical harmonic.
104+
n: int
105+
Degree of the spherical harmonic.
106+
condon_shortley_phase: bool, optional
107+
If True, includes the Condon-Shortley phase factor (-1)^m. Default is False.
108+
"""
109+
110+
def __init__(self, m, n, condon_shortley_phase: bool = False):
111+
self.m = m
112+
self.n = n
113+
114+
self.condon_shortley_phase = condon_shortley_phase
115+
116+
@property
117+
def is_impulse_response(self):
118+
return True
119+
120+
@property
121+
def filter_len_ir(self):
122+
return 1
123+
124+
def get_response(
125+
self, azimuth, colatitude=None, magnitude=False, frequency=None, degrees=True
126+
):
127+
128+
if degrees:
129+
azimuth = np.radians(azimuth)
130+
colatitude = np.radians(colatitude)
131+
return real_sph_harm(
132+
self.m,
133+
self.n,
134+
colatitude,
135+
azimuth,
136+
condon_shortley_phase=self.condon_shortley_phase,
137+
)[:, np.newaxis]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import numpy as np
2+
3+
import pyroomacoustics as pra
4+
from pyroomacoustics.directivities.harmonics import (
5+
get_mn_in_acn_order,
6+
real_sph_harm,
7+
)
8+
9+
10+
def test_harmonics_directivity():
11+
"""
12+
Test the Room Impulse Response (RIR) generated by a set of microphones
13+
with real spherical harmonics directivities against the analytical values
14+
of the spherical harmonics.
15+
"""
16+
order = 2
17+
condon_shortley_phase = True
18+
azimuth = np.deg2rad(50)
19+
20+
room = pra.AnechoicRoom(fs=16000)
21+
room.add_source([np.cos(azimuth), np.sin(azimuth), 1.0])
22+
23+
for m, n in zip(*get_mn_in_acn_order(order)):
24+
room.add_microphone(
25+
[0.0, 0.0, 1.0],
26+
directivity=pra.directivities.RealSphericalHarmonicsDirectivity(
27+
m, n, condon_shortley_phase=condon_shortley_phase
28+
),
29+
)
30+
31+
room.compute_rir()
32+
rir_sum = np.asarray(room.rir).squeeze().sum(axis=1)
33+
34+
m, n = get_mn_in_acn_order(order)
35+
sh = real_sph_harm(
36+
n, m, np.pi / 2, azimuth, condon_shortley_phase=condon_shortley_phase
37+
)
38+
39+
np.allclose(rir_sum, sh, atol=1e-2)

0 commit comments

Comments
 (0)