@@ -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+
161195def 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+
225381def empty_time_signal (no_of_signals , signal_length ):
226382 """Returns an empty np rec array that has the proper data structure
227383
0 commit comments