def __init__(self, data, samplerate=None, sources=None, listener=None, verbose=False): if isinstance(data, str): if samplerate is not None: raise ValueError( 'Cannot specify samplerate when initialising HRTF from a file.' ) if pathlib.Path(data).suffix != '.sofa': raise NotImplementedError('Only .sofa files can be read.') else: # load from SOFA file try: f = HRTF._sofa_load(data, verbose) except: raise ValueError('Unable to read file.') data = HRTF._sofa_get_FIR(f) self.samplerate = HRTF._sofa_get_samplerate(f) self.data = [] for idx in range(data.shape[0]): # ntaps x 2 (left, right) filter self.data.append(Filter(data[idx, :, :].T, self.samplerate)) self.listener = HRTF._sofa_get_listener(f) self.sources = HRTF._sofa_get_sourcepositions(f) elif isinstance(data, Filter): 'This is a hacky shortcut for casting a filterbank as HRTF. Avoid unless you know what you are doing.' if sources is None: raise ValueError( 'Must provide source positions when using a Filter object.' ) self.samplerate = data.samplerate fir = data.fir # save the fir property of the filterbank # reshape the filterbank data to fit into HRTF (ind x taps x ear) data = data.data.T[..., None] self.data = [] for idx in range(data.shape[0]): self.data.append( Filter(data[idx, :, :].T, self.samplerate, fir=fir)) self.sources = sources if listener is None: self.listener = [0, 0, 0] else: self.samplerate = samplerate self.data = [] for idx in range(data.shape[0]): # (ind x taps x ear), 2 x ntaps filter (left right) self.data.append(Filter(data[idx, :, :].T, self.samplerate)) self.sources = sources self.listener = listener
def externalize(self, hrtf=None): """ Convolve the sound with a smoothed HRTF to evoke the impression of an external sound source without adding directional information, see Kulkarni & Colburn (1998) for why that works. Arguments: hrtf (None | slab.HRTF): The HRTF to use. If None use the one from the MIT KEMAR mannequin. The sound source at zero azimuth and elevation is used for convolution so it has to be present in the HRTF. Returns: (slab.Binaural): externalized copy of the instance. """ if hrtf is None: hrtf = HRTF.kemar() # load KEMAR as default # get HRTF for [0,0] direction: idx_frontal = numpy.where((hrtf.sources[:, 1] == 0) & (hrtf.sources[:, 0] == 0))[0][0] if not idx_frontal.size: # idx_frontal is empty raise ValueError('No frontal direction [0,0] found in HRTF.') _, h = hrtf.data[idx_frontal].tf(channels=0, n_bins=12, show=False) # get low-res version of HRTF spectrum h[0] = 1 # avoids low-freq attenuation in KEMAR HRTF (unproblematic for other HRTFs) resampled_signal = copy.deepcopy(self) # if sound and HRTF has different samplerates, resample the sound, apply the HRTF, and resample back: resampled_signal = resampled_signal.resample(hrtf.data[0].samplerate) # resample to hrtf rate filt = Filter(10**(h/20), fir=False, samplerate=hrtf.data[0].samplerate) filtered_signal = filt.apply(resampled_signal) filtered_signal = filtered_signal.resample(self.samplerate) return filtered_signal
def diffuse_field_equalization(self, dfa=None): """ Equalize the HRTF by dividing each filter by the diffuse field average. The resulting filters have a mean close to 0 and are Fourier filters. Arguments: dfa (None): Filter object containing the diffuse field average transfer function of the HRTF. If none is provided, the `diffuse_field_avg` method is called to obtain it. Returns: (HRTF): diffuse field equalized version of the HRTF. """ if dfa is None: dfa = self.diffuse_field_avg() # invert the diffuse field average dfa.data = 1 / dfa.data dtfs = copy.deepcopy(self) # apply the inverted filter to the HRTFs for source in range(dtfs.n_sources): filt = dtfs.data[source] _, h = filt.tf(show=False) h = 10**(h / 20) * dfa dtfs.data[source] = Filter(data=h, fir=False, samplerate=self.samplerate) return dtfs
def diffuse_field_equalization(self): ''' Apply a diffuse field equalization to an HRTF in place. The resulting filters have zero mean and are of type FFR. ''' dfa = self.diffuse_field_avg() # invert the diffuse field average dfa.data = 1 / dfa.data # apply the inverted filter to the HRTFs for source in range(self.nsources): filt = self.data[source] _, h = filt.tf(plot=False) h = 10**(h / 20) * dfa self.data[source] = Filter(data=h, fir=False, samplerate=self.samplerate)
def diffuse_field_avg(self): """ Compute the diffuse field average transfer function, i.e. the constant non-spatial portion of a set of HRTFs. The filters for all sources are averaged, which yields an unbiased average only if the sources are uniformly distributed around the head. Returns: (Filter): the diffuse field average as FFR filter object. """ dfa = [] for source in range(self.n_sources): filt = self[source] for chan in range(filt.n_channels): _, h = filt.tf(channels=chan, show=False) dfa.append(h) dfa = 10 ** (numpy.mean(dfa, axis=0)/20) # average and convert from dB to gain return Filter(dfa, fir=False, samplerate=self.samplerate)
def diffuse_field_avg(self): ''' Compute the diffuse field average transfer function, i.e. the constant non-spatial portion of a set of HRTFs. The filters for all sources are averaged, which yields an unbiased average only if the sources are uniformely distributed around the head. Returns the diffuse field average as FFR filter object. ''' # TODO: could make the contribution of each HRTF # depend on local density of sources. dfa = [] for source in range(self.nsources): filt = self.data[source] for chan in range(filt.nchannels): _, h = filt.tf(channels=chan, plot=False) dfa.append(h) dfa = 10**(numpy.mean(dfa, axis=0) / 20 ) # average and convert from dB to gain return Filter(dfa, fir=False, samplerate=self.samplerate)
def externalize(self, hrtf=None): ''' Convolve the sound object in place with a smoothed HRTF (KEMAR if no slab.HRTF object is supplied) to evoke the impression of an external sound source without adding directional information. See Kulkarni & Colburn (1998) for why that works. ''' from slab import DATAPATH if not hrtf: hrtf = HRTF(DATAPATH + 'mit_kemar_normal_pinna.sofa') # load the hrtf file idx_frontal = numpy.where((hrtf.sources[:, 1] == 0) & ( hrtf.sources[:, 0] == 0))[0][0] # get HRTF for [0,0] direction w, h = hrtf.data[idx_frontal].tf(channels=0, nbins=12, plot=False) # get low-res spectrum # samplerate shoulf be hrtf.data[0].samplerate, hack to avoid having to resample, ok for externalization if rate are similar filt = Filter(10**(h / 20), fir=False, samplerate=self.samplerate) out = filt.apply(copy.deepcopy(self)) return out
def interpolate(self, azimuth=0, elevation=0, method='nearest', plot_tri=False): """ Interpolate a filter at a given azimuth and elevation from the neighboring HRTFs. A weighted average of the 3 closest HRTFs in the set is computed in the spectral domain with barycentric weights. The resulting filter values vary smoothly with changes in azimuth and elevation. The fidelity of the interpolated filter decreases with increasing distance of the closest sources and should only be regarded as appropriate approximation when the contributing filters are less than 20˚ away. Arguments: azimuth (float): the azimuth component of the direction of the interpolated filter elevation (float): the elevation component of the direction of the interpolated filter method (str): interpolation method, 'nearest' returns the filter of the nearest direction. Any other string returns a barycentric interpolation. plot_tri (bool): plot the triangulation of source positions used of interpolation. Useful for checking for areas where the interpolation may not be accurate (look for irregular or elongated triangles). Returns: (slab.HRTF): an HRTF object with a single source """ from slab.binaural import Binaural # inporting here to avoid circular import at top of class # spherical to cartesian coords = self.cartesian_source_locations() r = self.sources[:, 2].mean() target = self.cartesian_source_locations((azimuth, elevation, r)) # compute distances from target direction distances = numpy.sqrt(((target - coords)**2).sum(axis=1)) if method == 'nearest': idx_nearest = numpy.argmin(distances) filt = self[idx_nearest] else: # triangulate source positions into triangles if not scipy: raise ImportError('Need scipy.spatial for barycentric interpolation.') tri = scipy.spatial.ConvexHull(coords) if plot_tri: ax = plt.subplot(projection='3d') for simplex in tri.points[tri.simplices]: polygon = Poly3DCollection([simplex]) polygon.set_color(numpy.random.rand(3)) ax.add_collection3d(polygon) mins = coords.min(axis=0) maxs = coords.max(axis=0) xlim, ylim, zlim = list(zip(mins, maxs)) ax.set_xlim(xlim) ax.set_ylim(ylim) ax.set_zlim(zlim) ax.set_xlabel('X [m]') ax.set_ylabel('Y [m]') ax.set_zlabel('Z [m]') plt.show() # for each simplex, find the coords, test if target in triangle (by finding minimal d) d_min = numpy.inf for i, vertex_list in enumerate(tri.simplices): simplex = tri.points[vertex_list] d, a = HRTF._barycentric_weights(simplex, target) if d < d_min: d_min, idx, weights = d, i, a vertex_list = tri.simplices[idx] # we now have the indices of the filters and the corresponding weights amplitudes = list() for idx in vertex_list: freqs, amps = self[idx].tf(show=False) # get their transfer functions amplitudes.append(amps) # we could interpolate here if frequencies differ between filters avg_amps = amplitudes[0] * weights[0] + amplitudes[1] * weights[1] + amplitudes[2] * weights[2] # average gains = avg_amps - avg_amps.max() # shift so that maximum is zero, because we can only attenuate gains[gains < -60] = -60 # limit dynamic range to 60 dB gains_lin = 10**(gains/20) # transform attenuations in dB to factors filt_l = Filter.band(frequency=list(freqs), gain=list(gains_lin[:, 0]), length=self[idx].n_samples, fir=True, samplerate=self[vertex_list[0]].samplerate) filt_r = Filter.band(frequency=list(freqs), gain=list(gains_lin[:, 1]), length=self[idx].n_samples, fir=True, samplerate=self[vertex_list[0]].samplerate) filt = Filter(data=[filt_l, filt_r]) itds = list() for idx in vertex_list: taps = Binaural(self[idx]) # recast filter taps as Binaural sound itds.append(taps.itd()) # use Binaural.itd to compute correlation lag between channels avg_itd = itds[0] * weights[0] + itds[1] * weights[1] + itds[2] * weights[2] # average ITD filt = filt.delay(avg_itd / self.samplerate) data = filt.data[numpy.newaxis, ...] # get into correct shape (idx, taps, ear) source_loc = numpy.array([[azimuth, elevation, r]]) out = HRTF(data, sources=source_loc, listener=self.listener, samplerate=self.samplerate) return out