Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
 def interaural_level_spectrum(self, azimuth, level_spectrum_filter=None):
     '''
     Apply a frequency-dependend interaural level difference
     corresponding to a given azimuth to a binaural sound.
     The level difference cues are taken from a filter generated
     with the _make_level_spectrum_filter function from an hrtf
     recording. The default will generate the filter from the MIT
     KEMAR recordings. The left and right channel of the sound
     should have the same level.
     Example:
     >>> noise = Binaural.pinknoise(kind='diotic')
     >>> noise.interaural_level_spectrum(azimuth=-45).play()
     '''
     if not level_spectrum_filter:
         ils = Binaural._make_level_spectrum_filter(
         )  # TODO: should cache this as a file in /data or global
     ils = ils[:, 1:]  # remove the frequency values (not necessary here)
     azis = ils[0, :]  # get vector of azimuths in ils filter bank
     ils = ils[1:, :]  # the rest is the filter
     # interpolate levels at azimuth
     levels = [
         numpy.interp(azimuth, azis, ils[:, i]) for i in range(ils.shape[1])
     ]
     fbank = Filter.cos_filterbank(length=self.nsamples,
                                   samplerate=self.samplerate)
     subbands_left = fbank.apply(self.left)
     subbands_right = fbank.apply(self.right)
     # change subband levels:
     subbands_left.level = subbands_left.level + levels / 2
     subbands_right.level = subbands_right.level - levels / 2
     out_left = Filter.collapse_subbands(subbands_left, filter_bank=fbank)
     out_right = Filter.collapse_subbands(subbands_right, filter_bank=fbank)
     return Binaural([out_left, out_right])
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
 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
Ejemplo n.º 6
0
    def interaural_level_spectrum(self, azimuth, ils=None):
        """
        Apply an interaural level spectrum, corresponding to a sound sources azimuth, to a
        binaural sound. The interaural level spectrum consists of frequency specific interaural level differences
        which are computed from a head related transfer function (see the `make_interaural_level_spectrum()` method).
        The binaural sound is divided into frequency sub-bands and the levels of each sub-band are set according to
        the respective level in the interaural level spectrum. Then, the sub-bands are summed up again into one
        binaural sound.

        Arguments:
            azimuth (int | float): azimuth for which the interaural level spectrum is calculated.
            ils (dict): interaural level spectrum to apply.  If None, `make_interaural_level_spectrum()` is called.
            For repeated use, it is better to generate and keep the ils in a variable to avoid re-computing it.
        Returns:
            (slab.Binaural): A binaural sound with the interaural level spectrum corresponding to the given azimuth.
        Examples::

            noise = slab.Binaural.pinknoise(kind='diotic')
            ils = slab.Binaural.make_interaural_level_spectrum() # using default KEMAR HRTF
            noise.interaural_level_spectrum(azimuth=-45, ils=ils).play()
        """
        if ils is None:
            ils = Binaural.make_interaural_level_spectrum()
        ils_samplerate = ils['samplerate']
        original_samplerate = self.samplerate
        azis = ils['azimuths']
        level_diffs = ils['level_diffs']
        levels = numpy.array([numpy.interp(azimuth, azis, level_diffs[i, :]) for i in range(level_diffs.shape[0])])
        # resample the signal to the rate of the HRTF from which the filter was computed:
        resampled = self.resample(samplerate=ils_samplerate)
        fbank = Filter.cos_filterbank(length=resampled.n_samples, samplerate=ils_samplerate, pass_bands=True)
        subbands_left = fbank.apply(resampled.left)
        subbands_right = fbank.apply(resampled.right)
        # change subband levels:
        subbands_left.level = subbands_left.level + levels / 2
        subbands_right.level = subbands_right.level - levels / 2
        out_left = Filter.collapse_subbands(subbands_left, filter_bank=fbank)
        out_right = Filter.collapse_subbands(subbands_right, filter_bank=fbank)
        out = Binaural([out_left, out_right])
        return out.resample(samplerate=original_samplerate)
Ejemplo n.º 7
0
    def make_interaural_level_spectrum(hrtf=None):
        """
        Compute the frequency band specific interaural intensity differences for all sound source azimuth's in
        a head-related transfer function. For every azimuth in the hrtf, the respective transfer function is applied
        to a sound. This sound is then divided into frequency sub-bands. The interaural level spectrum is the level
        difference between right and left for each of these sub-bands for each azimuth.
        When individual HRTFs are not avilable, the level spectrum of the KEMAR mannequin may be used (default).
        Note that the computation may take a few minutes. Save the level spectrum to avoid re-computation, for instance
        with pickle or numpy.save (see documentation on readthedocs for examples).

        Arguments:
            hrtf (None | slab.HRTF): The head-related transfer function used to compute the level spectrum. If None,
                use the recordings from the KEMAR mannequin.
        Returns:
            (dict): A dictionary with keys `samplerate`, `frequencies` [n], `azimuths` [m], and `level_diffs` [n x m],
                where `frequencies` lists the centres of sub-bands for which the level difference was computed, and
                `azimuths` lists the sound source azimuth's in the hrft. `level_diffs` is a matrix of the interaural
                level difference for each sub-band and azimuth.
        Examples::

            ils = slab.Binaural.make_interaural_level_spectrum()  # get the ils from the KEMAR recordings
            ils['samplerate'] # the sampling rate
            ils['frequencies'] # the sub-band frequencies
            ils['azimuths']  # the sound source azimuth's for which the level difference was calculated
            ils['level_diffs'][5, :]  # the level difference for each azimuth in the 5th sub-band
        """
        if not hrtf:
            hrtf = HRTF.kemar()  # load KEMAR by default
        # get the filters for the frontal horizontal arc
        idx = numpy.where((hrtf.sources[:, 1] == 0) & (
            (hrtf.sources[:, 0] <= 90) | (hrtf.sources[:, 0] >= 270)))[0]
        # at this point, we could just get the transfer function of each filter with hrtf.data[idx[i]].tf(),
        # but it may be better to get the spectral left/right differences with ERB-spaced frequency resolution:
        azi = hrtf.sources[idx, 0]
        # 270<azi<360 -> azi-360 to get negative angles on the left
        azi[azi >= 270] = azi[azi >= 270]-360
        sort = numpy.argsort(azi)
        fbank = Filter.cos_filterbank(samplerate=hrtf.samplerate, pass_bands=True)
        freqs = fbank.filter_bank_center_freqs()
        noise = Sound.pinknoise(duration=5., samplerate=hrtf.samplerate)
        ils = dict()
        ils['samplerate'] = hrtf.samplerate
        ils['frequencies'] = freqs
        ils['azimuths'] = azi[sort]
        ils['level_diffs'] = numpy.zeros((len(freqs), len(idx)))
        for n, i in enumerate(idx[sort]):  # put the level differences in order of increasing angle
            noise_filt = Binaural(hrtf.data[i].apply(noise))
            noise_bank_left = fbank.apply(noise_filt.left)
            noise_bank_right = fbank.apply(noise_filt.right)
            ils['level_diffs'][:, n] = noise_bank_right.level - noise_bank_left.level
        return ils
Ejemplo n.º 8
0
 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)
Ejemplo n.º 9
0
    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)
Ejemplo n.º 10
0
 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)
Ejemplo n.º 11
0
 def _make_level_spectrum_filter(hrtf=None):
     '''
     Generate a level spectrum from the horizontal recordings in an HRTF file. The defaut Filter.cos_filterbank is used and the same filter bank has to be used when applying the level spectrum to a sound.
     '''
     from slab import DATAPATH
     if not hrtf:
         try:
             ils = numpy.load(DATAPATH +
                              'KEMAR_interaural_level_spectrum.npy')
             return ils
         except FileNotFoundError:
             hrtf = HRTF(
                 DATAPATH +
                 'mit_kemar_normal_pinna.sofa')  # load the hrtf file
             save_standard = True
     # get the filters for the frontal horizontal arc
     idx = numpy.where((hrtf.sources[:, 1] == 0) & (
         (hrtf.sources[:, 0] <= 90) | (hrtf.sources[:, 0] >= 270)))[0]
     # at this point, we could just get the transfer function of each filter with hrtf.data[idx[i]].tf(),
     # but it may be better to get the spectral left/right differences with ERB-spaced frequency resolution:
     azi = hrtf.sources[idx, 0]
     # 270<azi<360 -> azi-360 to get negative angles the left
     azi[azi >= 270] = azi[azi >= 270] - 360
     sort = numpy.argsort(azi)
     fbank = Filter.cos_filterbank(samplerate=hrtf.samplerate)
     freqs = fbank.filter_bank_center_freqs()
     noise = Sound.pinknoise(samplerate=hrtf.samplerate)
     ils = numpy.zeros((len(freqs) + 1, len(idx) + 1))
     ils[:, 0] = numpy.concatenate(
         ([0], freqs))  # first row are the frequencies
     for n, i in enumerate(
             idx[sort]
     ):  # put the level differences in order of increasing angle
         noise_filt = Binaural(hrtf.data[i].apply(noise))
         noise_bank_left = fbank.apply(noise_filt.left)
         noise_bank_right = fbank.apply(noise_filt.right)
         ils[1:, n + 1] = noise_bank_right.level - noise_bank_left.level
         ils[0, n + 1] = azi[sort[n]]  # first entry is the angle
     if save_standard:
         numpy.save(DATAPATH + 'KEMAR_interaural_level_spectrum.npy', ils)
     return ils
Ejemplo n.º 12
0
    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