Skip to content

Commit 9614d67

Browse files
committed
Implement named Tupel 'HRIR Signal' analogous to 'Array Signal' to store HRIR impulse responses
Implement 'read_SOFA_file' function to read SOFA format Array IRs and HRIRs Extending Exp4 by loading SOFA files instead of miro structs Edit cart2sph Raise of version number
1 parent be5f1c5 commit 9614d67

File tree

4 files changed

+98604
-98476
lines changed

4 files changed

+98604
-98476
lines changed

examples/Exp4_BinauralRendering.ipynb

Lines changed: 98439 additions & 98468 deletions
Large diffs are not rendered by default.

sound_field_analysis/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Version information."""
2-
__version__ = '0.8.3'
2+
__version__ = '0.8.4'

sound_field_analysis/io.py

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ def __new__(cls, signal, grid, center_signal=None, configuration=None, temperatu
133133
Parameters
134134
----------
135135
signal : TimeSignal
136-
Holds time domain signals and sampling frequency fs
136+
Array Time domain signals and sampling frequency
137137
grid : SphericalGrid
138-
Location grid of all time domain signals
138+
Measurement grid of time domain signals
139139
center_signal : TimeSignal
140-
Center signal measurement
140+
Center measurement time domain signal and sampling frequency
141141
configuration : ArrayConfiguration
142142
Information on array configuration
143143
temperature : array_like, optional
@@ -158,6 +158,40 @@ def __repr__(self):
158158
for name, data in zip(['signal', 'grid', 'center_signal', 'configuration', 'temperature'], self)) + ')'
159159

160160

161+
class HrirSignal(namedtuple('HrirSignal', 'l r grid center_signal')):
162+
"""Named tuple HrirSignal"""
163+
__slots__ = ()
164+
165+
def __new__(cls, l, r, grid, center_signal=None):
166+
"""
167+
Parameters
168+
----------
169+
l : TimeSignal
170+
Left ear time domain signals and sampling frequency
171+
r : TimeSignal
172+
Right ear time domain signals and sampling frequency
173+
grid : SphericalGrid
174+
Measurement grid of time domain signals
175+
center_signal : TimeSignal
176+
Center measurement time domain signal and sampling frequency
177+
"""
178+
l = TimeSignal(*l)
179+
r = TimeSignal(*r)
180+
181+
grid = SphericalGrid(*grid)
182+
if center_signal is not None:
183+
center_signal = TimeSignal(*center_signal)
184+
185+
# noinspection PyArgumentList
186+
self = super(HrirSignal, cls).__new__(cls, l, r, grid, center_signal)
187+
return self
188+
189+
def __repr__(self):
190+
return 'HrirSignal(\n' + ',\n'.join(
191+
' {0} = {1}'.format(name, repr(data).replace('\n', '\n '))
192+
for name, data in zip(['l', 'r', 'grid', 'center_signal'], self)) + ')'
193+
194+
161195
def read_miro_struct(file_name, channel='irChOne', transducer_type='omni', scatter_radius=None,
162196
get_center_signal=False):
163197
""" Reads miro matlab files.
@@ -222,6 +256,128 @@ def read_miro_struct(file_name, channel='irChOne', transducer_type='omni', scatt
222256
return ArraySignal(time_signal, mic_grid, center_signal, array_config, _np.squeeze(current_data['avgAirTemp']))
223257

224258

259+
def read_SOFA_file(file_name):
260+
""" Reads Head Related Impulse Responses or Array impuse responses (DRIRs) stored as Spatially Oriented Format for Acoustics (SOFA) files files,
261+
and convert them to Array Signal or HRIR Signal class
262+
263+
Parameters
264+
----------
265+
file_name : filepath
266+
Path to SOFA file
267+
268+
Returns
269+
-------
270+
sofa_signal : ArraySignal
271+
Tuple containing a TimeSignal `signal`, SphericalGrid `grid`, TimeSignal 'center_signal',
272+
ArrayConfiguration `configuration` and the air temperature
273+
274+
sofa_signal : HRIRSignal
275+
Tuple containing the TimeSignals 'l' for the left, and 'r' for the right ear, SphericalGrid `grid`, TimeSignal 'center_signal'
276+
277+
Notes
278+
-----
279+
* Depends python package pysofaconventions:
280+
https://github.com/andresperezlopez/pysofaconventions
281+
* Up to now, importing 'SimpleFreeFieldHRIR' and 'SingleRoomDRIR' are provided only.
282+
283+
"""
284+
# check if package 'pysofaconventions' is available
285+
try:
286+
import pysofaconventions as sofa
287+
except ImportError:
288+
print('Could not found pysofaconventions. Could not load SOFA file')
289+
return None
290+
291+
def print_sofa_infos(SOFA_convention):
292+
print(f'\n --> samplerate: {SOFA_convention.getSamplingRate()[0]:.0f} Hz' \
293+
f', receivers: {SOFA_convention.ncfile.file.dimensions["R"].size}' \
294+
f', emitters: {SOFA_convention.ncfile.file.dimensions["E"].size}' \
295+
f', measurements: {SOFA_convention.ncfile.file.dimensions["M"].size}' \
296+
f', samples: {SOFA_convention.ncfile.file.dimensions["N"].size}' \
297+
f', format: {SOFA_convention.getDataIR().dtype}' \
298+
f'\n --> SOFA_convention: {SOFA_convention.getGlobalAttributeValue("SOFAConventions")}' \
299+
f', version: {SOFA_convention.getGlobalAttributeValue("SOFAConventionsVersion")}')
300+
try:
301+
print(f' --> listener: {SOFA_convention.getGlobalAttributeValue("ListenerDescription")}')
302+
except sofa.SOFAError:
303+
pass
304+
try:
305+
print(f' --> author: {SOFA_convention.getGlobalAttributeValue("Author")}')
306+
except sofa.SOFAError:
307+
pass
308+
309+
def load_convention(SOFA_file):
310+
convention = SOFA_file.getGlobalAttributeValue('SOFAConventions')
311+
if convention == 'SimpleFreeFieldHRIR':
312+
return sofa.SOFASimpleFreeFieldHRIR(SOFA_file.ncfile.filename, "r")
313+
elif convention == 'SingleRoomDRIR':
314+
return sofa.SOFASingleRoomDRIR(SOFA_file.ncfile.filename, "r")
315+
else:
316+
raise ValueError(f'Unknown or unimplemented SOFA convention!')
317+
318+
# load SOFA file
319+
SOFA_file = sofa.SOFAFile(file_name, 'r')
320+
321+
# load SOFA convention
322+
SOFA_convention = load_convention(SOFA_file)
323+
324+
# check validity of sofa_file and sofa_convention
325+
if not SOFA_file.isValid():
326+
raise ValueError('Invalid SOFA file.')
327+
elif not SOFA_convention.isValid():
328+
raise ValueError('Invalid SOFA convention.')
329+
else:
330+
# print SOFA file infos
331+
print(f'\n open {file_name}')
332+
print_sofa_infos(SOFA_convention)
333+
334+
# store SOFA data as named tupel
335+
if SOFA_file.getGlobalAttributeValue('SOFAConventions') == 'SimpleFreeFieldHRIR':
336+
left_ear = TimeSignal(signal=_np.squeeze(SOFA_file.getDataIR()[:, 0, :]),
337+
fs=_np.squeeze(int(SOFA_file.getSamplingRate()[0])))
338+
right_ear = TimeSignal(signal=_np.squeeze(SOFA_file.getDataIR()[:, 1, :]),
339+
fs=_np.squeeze(int(SOFA_file.getSamplingRate()[0])))
340+
341+
# given spherical coordinates azimuth: [degree], elevation: [degree], radius: [meters]
342+
pos_grid_deg = SOFA_file.getSourcePositionValues()
343+
pos_grid_deg = pos_grid_deg.T.filled(0).copy() # transform into regular `numpy.ndarray`
344+
345+
# transform spherical grid to radiants, and elevation to colatitude
346+
pos_grid_deg[1, :] = 90 - pos_grid_deg[1, :]
347+
pos_grid_rad = utils.deg2rad(pos_grid_deg[0:2])
348+
349+
hrir_gird = SphericalGrid(azimuth=_np.squeeze(pos_grid_rad[0]),
350+
colatitude=_np.squeeze(pos_grid_rad[1]),
351+
radius=_np.squeeze(pos_grid_deg[2])) # store original radius
352+
353+
return HrirSignal(l=left_ear, r=right_ear, grid=hrir_gird)
354+
355+
elif SOFA_file.getGlobalAttributeValue('SOFAConventions') == 'SingleRoomDRIR':
356+
time_signal = TimeSignal(signal=_np.squeeze(SOFA_file.getDataIR()),
357+
fs=_np.squeeze(int(SOFA_file.getSamplingRate()[0])))
358+
359+
# given cartesian coordinates x: [meters], y: [meters], z: [meters]
360+
pos_grid_cart = SOFA_file.getReceiverPositionValues()[:, :, 0]
361+
pos_grid_cart = pos_grid_cart.T.filled(0) # transform into regular `numpy.ndarray`
362+
363+
# transform cartesian grid to spherical coordinates in radiants
364+
pos_grid_sph = utils.cart2sph(pos_grid_cart, is_deg=False)
365+
366+
mic_grid = SphericalGrid(azimuth=pos_grid_sph[0],
367+
colatitude=pos_grid_sph[1],
368+
radius=pos_grid_sph[2])
369+
370+
# assume rigid sphere and omnidirectional transducers according to SOFA 1.0, AES69-2015
371+
array_config = ArrayConfiguration(array_radius=pos_grid_sph[2].mean(),
372+
array_type='rigid',
373+
transducer_type='omni')
374+
375+
return ArraySignal(signal=time_signal, grid=mic_grid, configuration=array_config)
376+
else:
377+
import sys
378+
print('WARNING: Could not load SOFA file.', file=sys.stderr)
379+
380+
225381
def empty_time_signal(no_of_signals, signal_length):
226382
"""Returns an empty np rec array that has the proper data structure
227383

sound_field_analysis/utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ def db(data, power=False):
7979

8080

8181
def deg2rad(deg):
82-
"""Converts from degree [0 ... 360] to radiant [0 ... 2 pi]
82+
"""Converts from degree [0 ... 360] to radiant [0 ... 2pi]
8383
"""
8484
return deg % 360 / 180 * np.pi
8585

8686

8787
def rad2deg(rad):
88-
"""Converts from radiant [0 ... 2 pi] to degree [0 ... 360]
88+
"""Converts from radiant [0 ... 2pi] to degree [0 ... 360]
8989
"""
9090
return rad / np.pi * 180 % 360
9191

@@ -101,16 +101,17 @@ def cart2sph(cartesian_coords, is_deg=False):
101101
Returns
102102
-------
103103
numpy.ndarray
104-
calculated spherical coordinates (azimuth, colatitude, radius) of size [3; number of coordinates]
104+
calculated spherical coordinates (azimuth [0 ... 2pi], colatitude [0 ... pi], radius [meter]) of size [3; number of coordinates]
105105
"""
106106
x = cartesian_coords[0]
107107
y = cartesian_coords[1]
108108
z = cartesian_coords[2]
109109

110-
az = np.arctan2(y, x)
110+
az = np.arctan2(y, x) # return values -pi ... pi
111111
r = np.sqrt(np.power(x, 2) + np.power(y, 2) + np.power(z, 2))
112112
col = np.arccos(z/r)
113113

114+
az %= (2 * np.pi) # converting to 0 ... 2pi
114115
if is_deg:
115116
az = rad2deg(az)
116117
col = rad2deg(col)

0 commit comments

Comments
 (0)