def fractional_octave_bands(signal, num_fractions, sampling_rate=None, freq_range=(20.0, 20e3), order=14): """Create and/or apply an energy preserving fractional octave filter bank. The filters are designed using second order sections of Butterworth band-pass filters. Note that if the upper cut-off frequency of a band lies above the Nyquist frequency, a high-pass filter is applied instead. Due to differences in the design of band-pass and high-pass filters, their slopes differ, potentially introducing an error in the summed energy in the stop- band region of the respective filters. .. note:: This filter bank has -3 dB cut-off frequencies. For sufficiently large values of ``'order'``, the summed energy of the filter bank equals the energy of input signal, i.e., the filter bank is energy preserving (reconstructing). This is usefull for analysis energetic properties of the input signal such as the room acoustic propertie reverberation time. For an amplitude preserving filter bank with -6 dB cut-off frequencies see :py:func:`~pyfar.dsp.filter.reconstructing_fractional_octave_bands`. Parameters ---------- signal : Signal, None The signal to be filtered. Pass ``None`` to create the filter without applying it. num_fractions : int, optional The number of bands an octave is divided into. Eg., ``1`` refers to octave bands and ``3`` to third octave bands. The default is ``1``. sampling_rate : None, int The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. frequency_range : array, tuple, optional The lower and upper frequency limits. The default is ``frequency_range=(20, 20e3)`` order : int, optional Order of the Butterworth filter. The default is ``14``. Returns ------- signal : Signal The filtered signal. Only returned if ``sampling_rate = None``. filter : FilterSOS Filter object. Only returned if ``signal = None``. Examples -------- Filter an impulse into octave bands. The summed energy of all bands equals the energy of the input signal. .. plot:: >>> import pyfar as pf >>> import numpy as np >>> import matplotlib.pyplot as plt >>> # generate the data >>> x = pf.signals.impulse(2**17) >>> y = pf.dsp.filter.fractional_octave_bands( ... x, 1, freq_range=(20, 8e3)) >>> # frequency domain plot >>> y_sum = pf.FrequencyData( ... np.sum(np.abs(y.freq)**2, 0), y.frequencies) >>> pf.plot.freq(y) >>> ax = pf.plot.freq(y_sum, color='k', log_prefix=10, linestyle='--') >>> ax.set_title( ... "Filter bands and the sum of their squared magnitudes") >>> plt.tight_layout() """ # check input if (signal is None and sampling_rate is None) \ or (signal is not None and sampling_rate is not None): raise ValueError('Either signal or sampling_rate must be none.') fs = signal.sampling_rate if sampling_rate is None else sampling_rate sos = _coefficients_fractional_octave_bands(sampling_rate=fs, num_fractions=num_fractions, freq_range=freq_range, order=order) filt = pf.FilterSOS(sos, fs) filt.comment = ( "Second order section 1/{num_fractions} fractional octave band" "filter of order {order}") # return the filter object if signal is None: # return the filter object return filt else: # return the filtered signal signal_filt = filt.process(signal) return signal_filt
def crossover(signal, N, frequency, sampling_rate=None): """ Create and apply Linkwitz-Riley crossover network [1]_, [2]_. Linkwitz-Riley crossover filters are designed by cascading Butterworth filters of order `N/2`. where `N` must be even. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. N : int The order of the Linkwitz-Riley crossover network, must be even. frequency : number, array-like Characteristic frequencies of the crossover network. If a single number is passed, the network consists of a single lowpass and highpass. If `M` frequencies are passed, the network consists of 1 lowpass, M-1 bandpasses, and 1 highpass. sampling_rate : None, number The sampling rate in Hz. Only required if `signal` is ``None``. The default is ``None``. Returns ------- signal : Signal The filtered signal. Only returned if ``sampling_rate = None``. filter : FilterSOS Filter object. Only returned if ``signal = None``. References ---------- .. [1] S. H. Linkwitz, 'Active crossover networks for noncoincident drivers,' J. Audio Eng. Soc., vol. 24, no. 1, pp. 2–8, Jan. 1976. .. [2] D. Bohn, 'Linkwitz Riley crossovers: A primer,' Rane, RaneNote 160, 2005. """ # check input if (signal is None and sampling_rate is None) \ or (signal is not None and sampling_rate is not None): raise ValueError('Either signal or sampling_rate must be none.') if N % 2: raise ValueError("The order 'N' must be an even number.") # sampling frequency in Hz fs = signal.sampling_rate if sampling_rate is None else sampling_rate # order of Butterworth filters N = int(N / 2) # normalized frequency (half-cycle / per sample) freq = np.atleast_1d(np.asarray(frequency)) / fs * 2 # init neutral SOS matrix of shape (freq.size+1, SOS_dim_2, 6) n_sos = int(np.ceil(N / 2)) # number of lowpass sos SOS_dim_2 = n_sos if freq.size == 1 else 2 * n_sos SOS = np.tile(np.array([1, 0, 0, 1, 0, 0], dtype='float64'), (freq.size + 1, SOS_dim_2, 1)) # get filter coefficients for lowpass sos = spsignal.butter(N, freq[0], 'lowpass', analog=False, output='sos') SOS[0, 0:n_sos] = sos # get filter coefficients for the bandpass if more than one frequency is # provided for n in range(1, freq.size): sos_high = spsignal.butter(N, freq[n - 1], 'highpass', analog=False, output='sos') sos_low = spsignal.butter(N, freq[n], 'lowpass', analog=False, output='sos') SOS[n] = np.concatenate((sos_high, sos_low)) # get filter coefficients for the highpass sos = spsignal.butter(N, freq[-1], 'highpass', analog=False, output='sos') SOS[-1, 0:n_sos] = sos # Apply every Butterworth filter twice SOS = np.tile(SOS, (1, 2, 1)) # invert phase in every second channel if the Butterworth order is odd # (realized by reversing b-coefficients of the first sos) if N % 2: SOS[np.arange(1, freq.size + 1, 2), 0, 0:3] *= -1 # generate filter object filt = pf.FilterSOS(SOS, fs) freq_list = [str(f) for f in np.array(frequency, ndmin=1)] filt.comment = (f"Linkwitz-Riley cross over network of order {N*2} at " f"{', '.join(freq_list)} Hz.") # return the filter object if signal is None: # return the filter object return filt else: # return the filtered signal signal_filt = filt.process(signal) return signal_filt
def cheby1(signal, N, ripple, frequency, btype='lowpass', sampling_rate=None): """ Create and apply digital Chebyshev Type I IIR filter. This is a wrapper for ``scipy.signal.cheby1``. Which creates digital Chebyshev Type I filter coefficients in second-order sections (SOS). Parameters ---------- signal : Signal, None The Signal to be filtered. Pass None to create the filter without applying it. N : int The order of the Chebychev filter ripple : number The passband ripple in dB. frequency : number, array like The cut off-frequency in Hz if `btype` is ``'lowpass'`` or ``'highpass'``. An array like containing the lower and upper cut-off frequencies in Hz if `btype` is ``'bandpass'`` or ``'bandstop'``. btype : str One of the following ``'lowpass'``, ``'highpass'``, ``'bandpass'``, ``'bandstop'``. The default is ``'lowpass'``. sampling_rate : None, number The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. Returns ------- signal : Signal The filtered signal. Only returned if ``sampling_rate = None``. filter : FilterSOS SOS Filter object. Only returned if ``signal = None``. """ # check input if (signal is None and sampling_rate is None) \ or (signal is not None and sampling_rate is not None): raise ValueError('Either signal or sampling_rate must be none.') # sampling frequency in Hz fs = signal.sampling_rate if sampling_rate is None else sampling_rate # normalized frequency (half-cycle / per sample) frequency_norm = np.asarray(frequency) / fs * 2 # get filter coefficients sos = spsignal.cheby1(N, ripple, frequency_norm, btype, analog=False, output='sos') # generate filter object filt = pf.FilterSOS(sos, fs) filt.comment = (f"Chebychev Type I {btype} of order {N}. " f"Cut-off frequency {frequency} Hz. " f"Pass band ripple {ripple} dB.") # return the filter object if signal is None: # return the filter object return filt else: # return the filtered signal signal_filt = filt.process(signal) return signal_filt
def bessel(signal, N, frequency, btype='lowpass', norm='phase', sampling_rate=None): """ Create and apply digital Bessel/Thomson IIR filter. This is a wrapper for ``scipy.signal.bessel``. Which creates digital Butterworth filter coefficients in second-order sections (SOS). Parameters ---------- signal : Signal, None The Signal to be filtered. Pass None to create the filter without applying it. N : int The order of the Bessel/Thomson filter frequency : number, array like The cut off-frequency in Hz if `btype` is ``'lowpass'`` or ``'highpass'``. An array like containing the lower and upper cut-off frequencies in Hz if `btype` is bandpass or bandstop. btype : str One of the following ``'lowpass'``, ``'highpass'``, ``'bandpass'``, ``'bandstop'``. The default is ``'lowpass'``. norm : str Critical frequency normalization: ``'phase'`` The filter is normalized such that the phase response reaches its midpoint at angular (e.g. rad/s) frequency `Wn`. This happens for both low-pass and high-pass filters, so this is the "phase-matched" case. The magnitude response asymptotes are the same as a Butterworth filter of the same order with a cutoff of `Wn`. This is the default, and matches MATLAB's implementation. ``'delay'`` The filter is normalized such that the group delay in the passband is 1/`Wn` (e.g., seconds). This is the "natural" type obtained by solving Bessel polynomials. ``'mag'`` The filter is normalized such that the gain magnitude is -3 dB at the angular frequency `Wn`. The default is 'phase'. sampling_rate : None, number The sampling rate in Hz. Only required if signal is None. The default is None. Returns ------- signal : Signal The filtered signal. Only returned if ``sampling_rate = None``. filter : FilterSOS SOS Filter object. Only returned if ``signal = None``. """ # check input if (signal is None and sampling_rate is None) \ or (signal is not None and sampling_rate is not None): raise ValueError('Either signal or sampling_rate must be none.') # sampling frequency in Hz fs = signal.sampling_rate if sampling_rate is None else sampling_rate # normalized frequency (half-cycle / per sample) frequency_norm = np.asarray(frequency) / fs * 2 # get filter coefficients sos = spsignal.bessel(N, frequency_norm, btype, analog=False, output='sos', norm=norm) # generate filter object filt = pf.FilterSOS(sos, fs) filt.comment = (f"Bessel/Thomson {btype} of order {N} and '{norm}' " f"normalization. Cut-off frequency {frequency} Hz.") # return the filter object if signal is None: # return the filter object return filt else: # return the filtered signal signal_filt = filt.process(signal) return signal_filt