Source code for phonlab.auditory.noise_vocoder

import numpy as np
import scipy
from ..utils.prep_audio_ import prep_audio
from ..acoustic.amp_env import amplitude_envelope


def design_filter(bounds, fs, order=4):
    ''' Use scipy.signal.butter() to design a Butterworth bandpass filter.

        inputs:
            -- bounds: the lower bound, upper bound of a bandpass filter
            -- fs: sampling frequency of the audio
        
        output:
            -- sos filter coefficients of the bandpass filter

    '''
    return scipy.signal.butter(order, bounds, fs=fs, btype='bandpass', output='sos')

def third_octave_bounds(cf):
    ''' Return the upper and lower frequency bounds for a 
        1/3 octave filter around a center frequency.
        
        input:
            -- cf: a center frequency (Hz)
    
        output:
            -- an array of two values, the upper and lower bound of the filter
    '''
    third_octave_ratio = 2 ** (1/6)  # 2 * 1/6 = 1/3
    return np.array([cf / third_octave_ratio, cf * third_octave_ratio])

def octave_spaced_frequencies(low, high):
    ''' Get octave spaced-center frequencies between low and high.  The first 
    is equal to 'low' and the remainder are twice the previous one on the list.
    
        inputs:
            -- low: the lowest center frequency
            -- high: the upper bound for center frequencies
        output:
            -- an array of center frequencies
    '''
    number_of_octaves = np.log2(high) - np.log2(low)
    return low * 2 ** np.arange(number_of_octaves)

[docs] def third_octave_bands(low = 100, high=5000): """Compute cutoff frequencies for 1/3 octave bands, the centers are spaced one octave apart Parameters ========== low : int, default = 100 low frequency of the range to be covered by the channels (Hz) high : int, default = 5000 high freq of range covered by the channels (at most fs/2 - 1) Returns ======= bands : list a list of band limits (low,high) for a bank of bandpass filters """ center_frequencies = octave_spaced_frequencies(low, high) return[third_octave_bounds(cf) for cf in center_frequencies]
[docs] def shannon_bands(nc = 24, low = 70, high = 5000): """ Split a frequency range (low, high) into nc (number of channels) frequency bands on log freq scale Parameters ========== nc : int, default = 24 the number of frequency bands in the vocoded output low : int, default = 70 low frequency of the range to be covered by the channels (Hz) high : int high freq of range covered by the channels (at most fs/2 - 1) Returns ======= bands : list a list of band limits for a bank of bandpass filters """ freqs = np.exp(np.linspace(np.log(low), np.log(high-1), num=nc+1)) return [(freqs[i],freqs[i+1]) for i in range(nc)]
[docs] def apply_filterbank(x, bands, fs = 22050, order = 8): """ Apply a bank of bandpass filters Filter using repeated calls to scipy.signal.sosfiltfilt(), once for each of a bank of 8th order Butterworth bandpass filters. The output array has one copy of x for each of the bands listed in the input "bands" parameter. Parameters ========== x : ndarray audio samples in a 1-D numpy array bands : list A list of c filter lower/upper cut off freq pairs (c,2) fs : int, default = 22050 the sampling frequency of the sound to be filtered order : int, default=8 the order of the bandpass filters (passed to scipy.signal.butter) Returns ======= y : ndarray, shape(c,n) a 2-D array of x filtered by each band, y[0] is the input filtered by bands[0] """ # if bands is not specified, complain n_samples = x.shape[-1] n_channels = len(bands) # this line makes coefficients for the filter bank filterbank = [design_filter(b, fs,order) for b in bands] y = np.zeros((n_channels, n_samples)) for idx, coefs in enumerate(filterbank): y[idx] = scipy.signal.sosfiltfilt(coefs, x) # filter each band return y
[docs] def vocode(x, fs, bands, target_fs = None): """ Noise vocoding - replace sound with bandpassed noise, using a bank of filters defined by bands. This module is based on the excellent vocoder notebook published by Alexandre Chabot-Leclerc ([@AlexChabotL](http://twitter.com/alexchabotl)). https://github.com/achabotl/vocoder.git Keith Johnson added the "Shannon" vocoding scheme and altered some of the functions to be a little more general, and also converted the filtering to use sos coefficients, which improved the numerical stability of the code. Parameters ========== x : ndarray A one-dimensional array of audio samples fs : int the sampling frequency of the audio samples in **x**. bands : list A list of n bandpass filter lower/upper cut off freqs (n,2), as returned by third_octive_bands() or shannon_bands() target_fs : int, default None the desired frequency of the resulting vocoded signal. **None** keeps the rate at the value passed in **fs**. Returns ======= y : ndarray an array of samples, the same length as **sig** fs : int the sampling frequency of **y** Example ======= .. code-block:: Python example_file = importlib.resources.files('phonlab') / 'data/example_audio/sf3_cln.wav' bands_third = phon.third_octave_bands(high=8000) # define filter bank bands_shan = phon.shannon_bands(high=8000,nc=10) # define filter bank x,fs = phon.loadsig(example_file,chansel=[0]) y,fs = phon.vocode(x, fs, bands_shan) # use one of the filter banks phon.sgram(y,fs) .. figure:: images/vocode.png :scale: 90 % :alt: a spectrogram of Shannon vocoded speech :align: center A spectrogram of Shannon vocoded speech """ y, fs = prep_audio(x, fs, target_fs = target_fs, pre=0, quiet = True) n_channels = len(bands) n_samples = y.shape[-1] noise = np.random.randn(n_samples) filtered_y = apply_filterbank(y, bands, fs) filtered_noise = apply_filterbank(noise, bands, fs) vocoded_noise = np.zeros((n_channels, n_samples)) for idx, (x_band, noise_band) in enumerate(zip(filtered_y, filtered_noise)): envelope,fs = amplitude_envelope(x_band,fs,target_fs=None) vocoded_noise[idx] = envelope * noise_band return np.sum(vocoded_noise, axis=0), fs