Source code for pyfar.dsp.filter.audiofilter

"""Audio filter design and application."""
import warnings
from pyfar.classes.warnings import PyfarDeprecationWarning
import numpy as np
import pyfar as pf
from . import _audiofilter as iir


[docs] def allpass(signal, frequency, order, coefficients=None, sampling_rate=None): r""" Create and apply first or second order allpass filter. Allpass filters have an almost constant group delay below their cut-off frequency and are often used in analogue loudspeaker design. The filter transfer function is based on Tietze et al. [#]_: .. math:: A(s) = \frac{1-\frac{a_i}{\omega_c} s+\frac{b_i} {\omega_c^2} s^2}{1+\frac{a_i}{\omega_c} s +\frac{b_i}{\omega_c^2} s^2}, where :math:`\omega_c = 2 \pi f_c` with the cut-off frequency :math:`f_c` and :math:`s=\mathrm{i} \omega`. By definition the ``bi`` coefficient of a first order allpass is ``0``. Uses the implementation of [#]_. Parameters ---------- signal : Signal, None The signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Cutoff frequency of the allpass in Hz. order : number Order of the allpass filter. Must be ``1`` or ``2``. coefficients: number, list, optional Filter characteristic coefficients ``bi`` and ``ai``. - For 1st order allpass provide ai-coefficient as single value.\n The default is ``ai = 0.6436``. - For 2nd order allpass provide coefficients as list ``[bi, ai]``.\n The default is ``bi = 0.8832``, ``ai = 1.6278``. Defaults are chosen according to Tietze et al. (Fig. 12.66) for maximum flat group delay. 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 : FilterIIR Filter object. Only returned if ``signal = None``. References ---------- .. [#] Tietze, U., Schenk, C. & Gamm, E. (2019). Halbleiter- Schaltungstechnik (16th ed.). Springer Vieweg .. [#] https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py Examples -------- First and second order allpass filter with ``fc = 1000`` Hz. .. plot:: import pyfar as pf import matplotlib.pyplot as plt # impulse to be filtered impulse = pf.signals.impulse(256) orders = [1, 2] labels = ['First order', 'Second order'] fig, (ax1, ax2) = plt.subplots(2,1, layout='constrained') for (order, label) in zip(orders, labels): # create and apply allpass filter sig_filt = pf.dsp.filter.allpass(impulse, 1000, order) pf.plot.group_delay(sig_filt, unit='samples', label=label, ax=ax1) pf.plot.phase(sig_filt, label=label, ax=ax2, unwrap = True) ax1.set_title('1. and 2. order allpass filter with fc = 1000 Hz') ax2.legend() """ # 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.') # check if coefficients match filter order if coefficients is not None and ( (order == 1 and np.isscalar(coefficients) is False) or (order == 2 and ( not isinstance(coefficients, (list, np.ndarray)) or len(coefficients) != 2))): print(type(coefficients), order) raise ValueError('Coefficients must match the allpass order') # sampling frequency in Hz fs = signal.sampling_rate if sampling_rate is None else sampling_rate if order == 1: if coefficients is None: coefficients = 0.6436 # get filter coefficients for first order allpass b, a = iir.biquad_ap1st(frequency, fs, ai=coefficients)[2:] elif order == 2: if coefficients is None: coefficients = [0.8832, 1.6278] # get filter coefficients for second order allpass b, a = iir.biquad_ap2nd( frequency, fs, bi=coefficients[0], ai=coefficients[1])[2:] else: raise ValueError('Order must be 1 or 2') filter_coeffs = np.stack((b, a), axis=0) filt = pf.FilterIIR(filter_coeffs, fs) filt.comment = (f"Allpass of order {order} with cutoff frequency " f"{frequency} Hz.") if signal is None: # return the filter-object return filt else: # return filtered signal signal_filt = filt.process(signal) return signal_filt
[docs] def bell(signal, center_frequency, gain, quality, bell_type='II', quality_warp='cos', sampling_rate=None): """ Create and apply second order bell (parametric equalizer) filter. Uses the implementation of [#]_. Parameters ---------- signal : Signal, None The signal to be filtered. Pass ``None`` to create the filter without applying it. center_frequency : number Center frequency of the parametric equalizer in Hz gain : number Gain of the parametric equalizer in dB quality : number Quality of the parametric equalizer, i.e., the inverse of the bandwidth bell_type : str Defines the bandwidth/quality. The default is ``'II'``. ``'I'`` not recommended. Also known as 'constant Q'. ``'II'`` defines the bandwidth by the points 3 dB below the maximum if the gain is positive and 3 dB above the minimum if the gain is negative. Also known as 'symmetric'. ``'III'`` defines the bandwidth by the points at gain/2. Also known as 'half pad loss'. quality_warp : str Sets the pre-warping for the quality (``'cos'``, ``'sin'``, or ``'tan'``). The default is ``'cos'``. 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 : FilterIIR Filter object. Only returned if ``signal = None``. References ---------- .. [#] https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py """ # 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 bell_type not in ['I', 'II', 'III']: raise ValueError(("bell_type must be 'I', 'II' or " f"'III' but is '{bell_type}'.'")) if quality_warp not in ['cos', 'sin', 'tan']: raise ValueError(("quality_warp must be 'cos', 'sin' or " f"'tan' but is '{quality_warp}'.'")) # sampling frequency in Hz fs = signal.sampling_rate if sampling_rate is None else sampling_rate # get filter coefficients ba = np.zeros((2, 3)) _, _, b, a = iir.biquad_peq2nd( center_frequency, gain, quality, fs, bell_type, quality_warp) ba[0] = b ba[1] = a # generate filter object filt = pf.FilterIIR(ba, fs) filt.comment = ("Second order bell (parametric equalizer) " f"of type {bell_type} with {gain} dB gain at " f"{center_frequency} Hz (Quality = {quality}).") # 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
[docs] def high_shelve(signal, frequency, gain, order, shelve_type='I', sampling_rate=None): """ :py:func:`~pyfar.dsp.filter.high_shelve` will be deprecated in pyfar 0.9.0 in favor of :py:func:`~pyfar.dsp.filter.high_shelf`. Create and/or apply first or second order high shelf filter. Uses the implementation of [#]_. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency of the shelf in Hz. gain : number Gain of the shelf in dB. order : number The shelf order. Must be ``1`` or ``2``. shelve_type : str Defines the characteristic frequency. The default is ``'I'``. ``'I'`` Defines the characteristic frequency 3 dB below the `gain` value if the `gain` is positive and 3 dB above the `gain` value if the `gain` is negative. ``'II'`` Defines the characteristic frequency at 3 dB if the `gain` is positive and at -3 dB if the `gain` is negative. ``'III'`` Defines the characteristic frequency at `gain`/2 dB. For types ``I`` and ``II`` the absolute value of the `gain` must be sufficiently large (> 9 dB) to set the characteristic frequency according to the above rules with an error below 0.5 dB. For smaller absolute `gain` values the gain at the characteristic frequency becomes less accurate. For type ``III`` the characteristic frequency is always set correctly. 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 : FilterIIR Filter object. Only returned if ``signal = None``. References ---------- .. [#] https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py """ warnings.warn(("'high_shelve' will be deprecated in pyfar 0.9.0 in favor" " of 'high_shelf'"), PyfarDeprecationWarning, stacklevel=2) return high_shelf(signal, frequency, gain, order, shelve_type, sampling_rate)
[docs] def high_shelf(signal, frequency, gain, order, shelf_type='I', sampling_rate=None): """ Create and/or apply first or second order high shelf filter. Uses the implementation of [#]_. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency of the shelf in Hz. gain : number Gain of the shelf in dB. order : number The shelf order. Must be ``1`` or ``2``. shelf_type : str Defines the characteristic frequency. The default is ``'I'``. ``'I'`` Defines the characteristic frequency 3 dB below the `gain` value if the `gain` is positive and 3 dB above the `gain` value if the `gain` is negative. ``'II'`` Defines the characteristic frequency at 3 dB if the `gain` is positive and at -3 dB if the `gain` is negative. ``'III'`` Defines the characteristic frequency at `gain`/2 dB. For types ``I`` and ``II`` the absolute value of the `gain` must be sufficiently large (> 9 dB) to set the characteristic frequency according to the above rules with an error below 0.5 dB. For smaller absolute `gain` values the gain at the characteristic frequency becomes less accurate. For type ``III`` the characteristic frequency is always set correctly. 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 : FilterIIR Filter object. Only returned if ``signal = None``. References ---------- .. [#] https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py """ output = _shelf( signal, frequency, gain, order, shelf_type, sampling_rate, 'high') return output
[docs] def low_shelve(signal, frequency, gain, order, shelve_type='I', sampling_rate=None): """ :py:func:`~pyfar.dsp.filter.low_shelve` will be deprecated in pyfar 0.9.0 in favor of :py:func:`~pyfar.dsp.filter.low_shelf`. Create and apply first or second order low shelf filter. Uses the implementation of [#]_. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency of the shelf in Hz. gain : number Gain of the shelf in dB. order : number The shelf order. Must be ``1`` or ``2``. shelve_type : str Defines the characteristic frequency. The default is ``'I'``. ``'I'`` Defines the characteristic frequency 3 dB below the `gain` value if the `gain` is positive and 3 dB above the `gain` value if the `gain` is negative. ``'II'`` Defines the characteristic frequency at 3 dB if the `gain` is positive and at -3 dB if the `gain` is negative. ``'III'`` Defines the characteristic frequency at `gain`/2 dB. For types ``I`` and ``II`` the absolute value of the `gain` must be sufficiently large (> 9 dB) to set the characteristic frequency according to the above rules with an error below 0.5 dB. For smaller absolute `gain` values the gain at the characteristic frequency becomes less accurate. For type ``III`` the characteristic frequency is always set correctly. 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 : FilterIIR Filter object. Only returned if ``signal = None``. References ---------- .. [#] https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py """ warnings.warn(("'low_shelve' will be deprecated in pyfar 0.9.0 in favor " "of 'low_shelf'"), PyfarDeprecationWarning, stacklevel=2) return low_shelf(signal, frequency, gain, order, shelve_type, sampling_rate)
[docs] def low_shelf(signal, frequency, gain, order, shelf_type='I', sampling_rate=None): """ Create and apply first or second order low shelf filter. Uses the implementation of [#]_. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency of the shelf in Hz. gain : number Gain of the shelf in dB. order : number The shelf order. Must be ``1`` or ``2``. shelf_type : str Defines the characteristic frequency. The default is ``'I'``. ``'I'`` Defines the characteristic frequency 3 dB below the `gain` value if the `gain` is positive and 3 dB above the `gain` value if the `gain` is negative. ``'II'`` Defines the characteristic frequency at 3 dB if the `gain` is positive and at -3 dB if the `gain` is negative. ``'III'`` Defines the characteristic frequency at `gain`/2 dB. For types ``I`` and ``II`` the absolute value of the `gain` must be sufficiently large (> 9 dB) to set the characteristic frequency according to the above rules with an error below 0.5 dB. For smaller absolute `gain` values the gain at the characteristic frequency becomes less accurate. For type ``III`` the characteristic frequency is always set correctly. 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 : FilterIIR Filter object. Only returned if ``signal = None``. References ---------- .. [#] https://github.com/spatialaudio/digital-signal-processing-lecture/blob/master/filter_design/audiofilter.py """ output = _shelf( signal, frequency, gain, order, shelf_type, sampling_rate, 'low') return output
[docs] def high_shelve_cascade( signal, frequency, frequency_type="lower", gain=None, slope=None, bandwidth=None, N=None, sampling_rate=None): """ :py:func:`~pyfar.dsp.filter.high_shelve_cascade` will be deprecated in pyfar 0.9.0 in favor of :py:func:`~pyfar.dsp.filter.high_shelf_cascade`. Create and apply constant slope filter from cascaded 2nd order high shelves. The filters - also known as High-Schultz filters (cf. [#]_) - are defined by their characteristic frequency, gain, slope, and bandwidth. Two out of the three parameter `gain`, `slope`, and `bandwidth` must be specified, while the third parameter is calculated as ``gain = bandwidth * slope`` ``bandwidth = abs(gain/slope)`` ``slope = gain/bandwidth`` Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency in Hz (see `frequency_type`) frequency_type : string Defines how `frequency` is used ``'upper'`` `frequency` gives the upper characteristic frequency. In this case the lower characteristic frequency is given by ``2**bandwidth / frequency`` ``'lower'`` `frequency` gives the lower characteristic frequency. In this case the upper characteristic frequency is given by ``2**bandwidth * frequency`` gain : number The filter gain in dB. The default is ``None``, which calculates the gain from the `slope` and `bandwidth` (must be given if `gain` is ``None``). slope : number Filter slope in dB per octave, with positive values denoting a rising filter slope and negative values denoting a falling filter slope. The default is ``None``, which calculates the slope from the `gain` and `bandwidth` (must be given if `slope` is ``None``). bandwidth : number The bandwidth of the filter in octaves. The default is ``None``, which calculates the bandwidth from `gain` and `slope` (must be given if `bandwidth` is ``None``). N : int Number of shelf filters that are cascaded. The default is ``None``, which calculated the minimum ``N`` that is required to satisfy Eq. (11) in Schultz et al. 2020, i.e., the minimum ``N`` that is required for a good approximation of the ideal filter response. sampling_rate : None, number The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. Returns ------- signal : :py:class:`~pyfar.Signal`, :py:class:`~pyfar.FilterSOS` The filtered signal (returned if ``sampling_rate = None``) or the Filter object (returned if ``signal = None``). N : int The number of shelf filters that were cascaded ideal : :py:class:`~pyfar.FrequencyData` The ideal, piece-wise magnitude response of the filter References ---------- .. [#] F. Schultz, N. Hahn, and S. Spors, “Shelving Filter Cascade with Adjustable Transition Slope and Bandwidth,” in 148th AES Convention (Vienna, Austria, 2020). Examples -------- Generate a filter with a bandwith of 4 octaves and a gain of -60 dB and compare it to the piece-wise constant idealized magnitude response. .. plot:: >>> import pyfar as pf >>> import matplotlib.pyplot as plt >>> >>> impulse = pf.signals.impulse(40e3, sampling_rate=40000) >>> impulse, N, ideal = pf.dsp.filter.high_shelf_cascade( >>> impulse, 250, "lower", -60, None, 4) >>> >>> pf.plot.freq(ideal, c='k', ls='--', label="ideal") >>> pf.plot.freq(impulse, label="actual") >>> plt.legend() """ warnings.warn(("'high_shelve_cascade' will be deprecated in pyfar 0.9.0 " "in favor of 'high_shelf_cascade'"), PyfarDeprecationWarning, stacklevel=2) return high_shelf_cascade(signal, frequency, frequency_type, gain, slope, bandwidth, N, sampling_rate)
[docs] def high_shelf_cascade( signal, frequency, frequency_type="lower", gain=None, slope=None, bandwidth=None, N=None, sampling_rate=None): """ Create and apply constant slope filter from cascaded 2nd order high shelves. The filters - also known as High-Schultz filters (cf. [#]_) - are defined by their characteristic frequency, gain, slope, and bandwidth. Two out of the three parameter `gain`, `slope`, and `bandwidth` must be specified, while the third parameter is calculated as ``gain = bandwidth * slope`` ``bandwidth = abs(gain/slope)`` ``slope = gain/bandwidth`` .. note:: The `bandwidth` must be at least 1 octave to obtain a good approximation of the desired frequency response. Make sure to specify the parameters `gain`, `slope`, and `bandwidth` accordingly. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency in Hz (see `frequency_type`) frequency_type : string Defines how `frequency` is used ``'upper'`` `frequency` gives the upper characteristic frequency. In this case the lower characteristic frequency is given by ``2**bandwidth / frequency`` ``'lower'`` `frequency` gives the lower characteristic frequency. In this case the upper characteristic frequency is given by ``2**bandwidth * frequency`` gain : number The filter gain in dB. The default is ``None``, which calculates the gain from the `slope` and `bandwidth` (must be given if `gain` is ``None``). slope : number Filter slope in dB per octave, with positive values denoting a rising filter slope and negative values denoting a falling filter slope. The default is ``None``, which calculates the slope from the `gain` and `bandwidth` (must be given if `slope` is ``None``). bandwidth : number The bandwidth of the filter in octaves. The default is ``None``, which calculates the bandwidth from `gain` and `slope` (must be given if `bandwidth` is ``None``). N : int Number of shelf filters that are cascaded. The default is ``None``, which calculated the minimum ``N`` that is required to satisfy Eq. (11) in Schultz et al. 2020, i.e., the minimum ``N`` that is required for a good approximation of the ideal filter response. sampling_rate : None, number The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. Returns ------- signal : :py:class:`~pyfar.Signal`, :py:class:`~pyfar.FilterSOS` The filtered signal (returned if ``sampling_rate = None``) or the Filter object (returned if ``signal = None``). N : int The number of shelf filters that were cascaded ideal : :py:class:`~pyfar.FrequencyData` The ideal, piece-wise magnitude response of the filter References ---------- .. [#] F. Schultz, N. Hahn, and S. Spors, “Shelving Filter Cascade with Adjustable Transition Slope and Bandwidth,” in 148th AES Convention (Vienna, Austria, 2020). Examples -------- Generate a filter with a bandwith of 4 octaves and a gain of -60 dB and compare it to the piece-wise constant idealized magnitude response. .. plot:: >>> import pyfar as pf >>> import matplotlib.pyplot as plt >>> >>> impulse = pf.signals.impulse(40e3, sampling_rate=40000) >>> impulse, N, ideal = pf.dsp.filter.high_shelf_cascade( >>> impulse, 250, "lower", -60, None, 4) >>> >>> pf.plot.freq(ideal, c='k', ls='--', label="ideal") >>> pf.plot.freq(impulse, label="actual") >>> plt.legend() """ signal, N, ideal_response = _shelf_cascade( signal, frequency, frequency_type, gain, slope, bandwidth, N, sampling_rate, shelf_type="high") return signal, N, ideal_response
[docs] def low_shelve_cascade( signal, frequency, frequency_type="upper", gain=None, slope=None, bandwidth=None, N=None, sampling_rate=None): """ :py:func:`~pyfar.dsp.filter.low_shelve_cascade` will be deprecated in pyfar 0.9.0 in favor of :py:func:`~pyfar.dsp.filter.low_shelf_cascade`. Create and apply constant slope filter from cascaded 2nd order low shelves. The filters - also known as Low-Schultz filters (cf. [#]_) - are defined by their characteristic frequency, gain, slope, and bandwidth. Two out of the three parameter `gain`, `slope`, and `bandwidth` must be specified, while the third parameter is calculated as ``gain = -bandwidth * slope`` ``bandwidth = abs(gain/slope)`` ``slope = -gain/bandwidth`` Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency in Hz (see `frequency_type`) frequency_type : string Defines how `frequency` is used ``'upper'`` `frequency` gives the upper characteristic frequency. In this case the lower characteristic frequency is given by ``2**bandwidth / frequency`` ``'lower'`` `frequency` gives the lower characteristic frequency. In this case the upper characteristic frequency is given by ``2**bandwidth * frequency`` gain : number The filter gain in dB. The default is ``None``, which calculates the gain from the `slope` and `bandwidth` (must be given if `gain` is ``None``). slope : number Filter slope in dB per octave, with positive values denoting a rising filter slope and negative values denoting a falling filter slope. The default is ``None``, which calculates the slope from the `gain` and `bandwidth` (must be given if `slope` is ``None``). bandwidth : number The bandwidth of the filter in octaves. The default is ``None``, which calculates the bandwidth from `gain` and `slope` (must be given if `bandwidth` is ``None``). N : int Number of shelf filters that are cascaded. The default is ``None``, which calculated the minimum ``N`` that is required to satisfy Eq. (11) in Schultz et al. 2020, i.e., the minimum ``N`` that is required for a good approximation of the ideal filter response. sampling_rate : None, number The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. Returns ------- signal : :py:class:`~pyfar.Signal`, :py:class:`~pyfar.FilterSOS` The filtered signal (returned if ``sampling_rate = None``) or the Filter object (returned if ``signal = None``). N : int The number of shelf filters that were cascaded ideal : :py:class:`~pyfar.FrequencyData` The ideal, piece-wise magnitude response of the filter References ---------- .. [#] F. Schultz, N. Hahn, and S. Spors, “Shelving Filter Cascade with Adjustable Transition Slope and Bandwidth,” in 148th AES Convention (Vienna, Austria, 2020). Examples -------- Generate a filter with a bandwith of 4 octaves and a gain of -60 dB and compare it to the piece-wise constant idealized magnitude response. .. plot:: >>> import pyfar as pf >>> import matplotlib.pyplot as plt >>> >>> impulse = pf.signals.impulse(40e3, sampling_rate=40000) >>> impulse, N, ideal = pf.dsp.filter.low_shelf_cascade( >>> impulse, 4000, "upper", -60, None, 4) >>> >>> pf.plot.freq(ideal, c='k', ls='--', label="ideal") >>> pf.plot.freq(impulse, label="actual") >>> plt.legend() """ warnings.warn(("'low_shelve_cascade' will be deprecated in pyfar 0.9.0 " "in favor of 'low_shelf_cascade'"), PyfarDeprecationWarning, stacklevel=2) return low_shelf_cascade(signal, frequency, frequency_type, gain, slope, bandwidth, N, sampling_rate)
[docs] def low_shelf_cascade( signal, frequency, frequency_type="upper", gain=None, slope=None, bandwidth=None, N=None, sampling_rate=None): """ Create and apply constant slope filter from cascaded 2nd order low shelves. The filters - also known as Low-Schultz filters (cf. [#]_) - are defined by their characteristic frequency, gain, slope, and bandwidth. Two out of the three parameter `gain`, `slope`, and `bandwidth` must be specified, while the third parameter is calculated as ``gain = -bandwidth * slope`` ``bandwidth = abs(gain/slope)`` ``slope = -gain/bandwidth`` .. note:: The `bandwidth` must be at least 1 octave to obtain a good approximation of the desired frequency response. Make sure to specify the parameters `gain`, `slope`, and `bandwidth` accordingly. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency in Hz (see `frequency_type`) frequency_type : string Defines how `frequency` is used ``'upper'`` `frequency` gives the upper characteristic frequency. In this case the lower characteristic frequency is given by ``2**bandwidth / frequency`` ``'lower'`` `frequency` gives the lower characteristic frequency. In this case the upper characteristic frequency is given by ``2**bandwidth * frequency`` gain : number The filter gain in dB. The default is ``None``, which calculates the gain from the `slope` and `bandwidth` (must be given if `gain` is ``None``). slope : number Filter slope in dB per octave, with positive values denoting a rising filter slope and negative values denoting a falling filter slope. The default is ``None``, which calculates the slope from the `gain` and `bandwidth` (must be given if `slope` is ``None``). bandwidth : number The bandwidth of the filter in octaves. The default is ``None``, which calculates the bandwidth from `gain` and `slope` (must be given if `bandwidth` is ``None``). N : int Number of shelf filters that are cascaded. The default is ``None``, which calculated the minimum ``N`` that is required to satisfy Eq. (11) in Schultz et al. 2020, i.e., the minimum ``N`` that is required for a good approximation of the ideal filter response. sampling_rate : None, number The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. Returns ------- signal : :py:class:`~pyfar.Signal`, :py:class:`~pyfar.FilterSOS` The filtered signal (returned if ``sampling_rate = None``) or the Filter object (returned if ``signal = None``). N : int The number of shelf filters that were cascaded ideal : :py:class:`~pyfar.FrequencyData` The ideal, piece-wise magnitude response of the filter References ---------- .. [#] F. Schultz, N. Hahn, and S. Spors, “Shelving Filter Cascade with Adjustable Transition Slope and Bandwidth,” in 148th AES Convention (Vienna, Austria, 2020). Examples -------- Generate a filter with a bandwith of 4 octaves and a gain of -60 dB and compare it to the piece-wise constant idealized magnitude response. .. plot:: >>> import pyfar as pf >>> import matplotlib.pyplot as plt >>> >>> impulse = pf.signals.impulse(40e3, sampling_rate=40000) >>> impulse, N, ideal = pf.dsp.filter.low_shelf_cascade( >>> impulse, 4000, "upper", -60, None, 4) >>> >>> pf.plot.freq(ideal, c='k', ls='--', label="ideal") >>> pf.plot.freq(impulse, label="actual") >>> plt.legend() """ signal, N, ideal_response = _shelf_cascade( signal, frequency, frequency_type, gain, slope, bandwidth, N, sampling_rate, shelf_type="low") return signal, N, ideal_response
def _shelf(signal, frequency, gain, order, shelf_type, sampling_rate, kind): """ First and second order high and low shelves. For the documentation refer to high_shelf and low_shelf. The only additional parameter is `kind`, which has to be 'high' or 'low'. """ # 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 shelf_type not in ['I', 'II', 'III']: raise ValueError(("shelf_type must be 'I', 'II' or " f"'III' but is '{shelf_type}'.'")) # sampling frequency in Hz fs = signal.sampling_rate if sampling_rate is None else sampling_rate # get filter coefficients ba = np.zeros((2, 3)) if order == 1 and kind == 'high': shelf = iir.biquad_hshv1st elif order == 2 and kind == 'high': shelf = iir.biquad_hshv2nd elif order == 1 and kind == 'low': shelf = iir.biquad_lshv1st elif order == 2 and kind == 'low': shelf = iir.biquad_lshv2nd else: raise ValueError(f"order must be 1 or 2 but is {order}") _, _, b, a = shelf(frequency, gain, fs, shelf_type) ba[0] = b ba[1] = a # generate filter object filt = pf.FilterIIR(ba, fs) kind = "High" if kind == "high" else "Low" filt.comment = (f"{kind}-shelf of order {order} and type " f"{shelf_type} with {gain} dB gain at {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 def _shelf_cascade(signal, frequency, frequency_type, gain, slope, bandwidth, N, sampling_rate, shelf_type): """Design constant slope filter from shelf filter cascade. Parameters ---------- signal : Signal, None The Signal to be filtered. Pass ``None`` to create the filter without applying it. frequency : number Characteristic frequency in Hz (see `frequency_type`) frequency_type : string Defines how `frequency` is used ``'upper'`` `frequency` gives the upper characteristic frequency. In this case the lower characteristic frequency is given by ``2**bandwidth / frequency`` ``'lower'`` `frequency` gives the lower characteristic frequency. In this case the upper characteristic frequency is given by ``2**bandwidth * frequency`` gain : number The filter gain in dB. The default is ``None``, which calculates the gain from the `slope` and `bandwidth` (must be given if `gain` is ``None``). slope : number Filter slope in dB per octave, with positive values denoting a rising filter slope and negative values denoting a falling filter slope. The default is ``None``, which calculates the slope from the `gain` and `bandwidth` (must be given if `slope` is ``None``). bandwidth : number The bandwidth of the filter in octaves. The default is ``None``, which calculates the bandwidth from `gain` and `slope` (must be given if `bandwidth` is ``None``). N : int Number of shelf filters that are cascaded. The default is ``None``, which calculated the minimum ``N`` that is required to satisfy Eq. (11) in Schultz et al. 2020, i.e., the minimum ``N`` that is required for a good approximation of the ideal filter response. sampling_rate : None, number The sampling rate in Hz. Only required if signal is ``None``. The default is ``None``. shelf_type : string ``'low'`` or ``'high'`` for low- or high-shelf. [1] F. Schultz, N. Hahn, and S. Spors, “Shelving Filter Cascade with Adjustable Transition Slope and Bandwidth,” in 148th AES Convention (Vienna, Austria, 2020). """ # 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 not isinstance(signal, (pf.Signal, type(None))): raise ValueError("signal must be a pyfar Signal object or None") # check and set filter slope parameters according to Eq. (4) gain, slope, bandwidth = _shelving_cascade_slope_parameters( gain, slope, bandwidth, shelf_type) if bandwidth < 1: warnings.warn(( f"The bandwidth is {bandwidth} octaves but should be at least 1 " "to obtain a good approximation of the desired frequency response." ), stacklevel=2) # get sampling rate sampling_rate = sampling_rate if signal is None else signal.sampling_rate # get upper and lower cut-off frequency if frequency_type == "upper": frequency = [frequency / 2**bandwidth, frequency] elif frequency_type == "lower": frequency = [frequency, 2**bandwidth * frequency] else: raise ValueError((f"frequency_type is '{frequency_type}' but must be " "'lower' or 'upper'")) # check characteristic frequencies if frequency[0] == 0: raise ValueError("The lower characteristic frequency must not be 0 Hz") if frequency[0] > sampling_rate/2: raise ValueError(("The lower characteristic frequency must be smaller " "than half the sampling rate")) if frequency[1] > sampling_rate/2 and shelf_type == "low": raise ValueError(("The upper characteristic frequency must be smaller " "than half the sampling rate")) if frequency[1] > sampling_rate/2: frequency[1] = sampling_rate/2 gain *= np.log2(frequency[1]/frequency[0]) / bandwidth bandwidth = np.log2(frequency[1]/frequency[0]) warnings.warn((f"The upper frequency exceeded the Nyquist frequency " f"It was set to {sampling_rate/2} Hz, which equals " f"a restriction of the bandwidth to {bandwidth} " f"octaves and a reduction of the gain to {gain} dB to " f"maintain the intended slope of {slope} dB/octave."), stacklevel=2) # determine number of shelf filters per octave --------------------------- # recommended minimum shelf filters per octave according to Eq. (11.2) N_octave_min = 1 if abs(slope) < 12.04 else abs(slope) / 12.04 # minimum total shelf filters according to Eq. (9) N_min = np.ceil(N_octave_min*bandwidth).astype(int) # actual total shelf filters either from user input or recommended minimum N = int(N) if N else N_min if N < N_min: warnings.warn(( f"N is {N} but should be at least {N_min} to obtain an good " "approximation of the desired frequency response"), stacklevel=2) # used shelf filters per octave N_octave = N / bandwidth # get the filter ---------------------------------------------------------- # initialize variables filter_func = high_shelf if shelf_type == "high" else low_shelf shelf_gain = gain / N SOS = np.zeros((1, N, 6)) # get the filter coefficients for n in range(N): # current frequency according to Eq. (5) f = 2**(-(n+.5)/N_octave) * frequency[1] # get shelf and cascade coefficients shelf = filter_func(None, f, shelf_gain, 2, 'III', sampling_rate) SOS[:, n] = shelf.coefficients.flatten() # make filter object comment = (f"Constant slope filter cascaded from {N} {shelf_type}-shelf " f"filters ({frequency_type} frequency: {frequency} Hz, " f"bandwidth: {bandwidth} octaves, gain: {gain} dB, {N_octave} " "shelf filters per octave") filt = pf.FilterSOS(SOS, sampling_rate, comment=comment) # get the ideal filter response ------------------------------------------- magnitudes = np.array([10**(gain/20), 10**(gain/20), 1, 1]) if shelf_type == "high": magnitudes = np.flip(magnitudes) frequencies = [0, frequency[0], frequency[1], sampling_rate/2] # remove duplicate entries (happens if the slope ends at Nyquist) if frequencies[-2] == frequencies[-1]: magnitudes = magnitudes[:-1] frequencies = frequencies[:-1] ideal_response = pf.FrequencyData( magnitudes, frequencies, "ideal magnitude response of cascaded shelf filter") # return parameter -------------------------------------------------------- if signal is None: return filt, N, ideal_response else: return filt.process(signal), N, ideal_response def _shelving_cascade_slope_parameters(gain, slope, bandwidth, shelf_type): """Compute the third parameter from the given two. Parameters ---------- gain : float Desired gain of the stop band in decibel. slope : float Desired shelving slope in decibel per octave. bandwidth : float Desired bandwidth of the slope in octave. shelf_type : string ``'low'`` or ``'high'`` for low- or high-shelf. """ if slope == 0: raise ValueError("slope must be non-zero.") if gain is None and slope is not None and bandwidth is not None: bandwidth = abs(bandwidth) gain = -bandwidth * slope if shelf_type == "low" \ else bandwidth * slope elif slope is None and gain is not None and bandwidth is not None: bandwidth = abs(bandwidth) slope = -gain / bandwidth if shelf_type == "low" \ else gain / bandwidth elif bandwidth is None and gain is not None and slope is not None: if shelf_type == "low" and np.sign(gain * slope) == 1: raise ValueError("gain and slope must have different signs") if shelf_type == "high" and np.sign(gain * slope) == -1: raise ValueError("gain and slope must have the same signs") bandwidth = abs(gain / slope) else: raise ValueError(("Exactly two out of the parameters gain, slope, and " "bandwidth must be given.")) return gain, slope, bandwidth