Source code for pyfar.signals.deterministic

"""Module for generating deterministic signals as part of pyfar.signals."""
import numpy as np
import warnings
import pyfar


[docs] def sine(frequency, n_samples, amplitude=1, phase=0, sampling_rate=44100, full_period=False): """Generate a single or multi channel sine signal. Parameters ---------- frequency : double, array like Frequency of the sine in Hz (0 <= `frequency` <= `sampling_rate`/2). n_samples : int Length of the signal in samples. amplitude : double, array like, optional The amplitude. The default is ``1``. phase : double, array like, optional The phase in radians. The default is ``0``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. full_period : boolean, optional Make sure that the returned signal contains an integer number of periods resulting in a periodic signal. This is done by adjusting the frequency of the sine. The default is ``False``. Returns ------- signal : Signal The sine signal. The Signal is in the time domain and has the ``rms`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). The exact frequency, amplitude and phase are written to `comment`. Notes ----- The parameters `frequency`, `amplitude`, and `phase` are broadcasted using the :doc:`numpy rules<numpy:user/basics.broadcasting>`. For example `frequency` could be of shape ``(2, 4)``, `amplitude` of shape ``(2, 1)``, and `phase` could be a scalar. In this case all parameters would be broadcasted to a shape of ``(2, 4)``. """ # check and match the cshape try: cshape, (frequency, amplitude, phase) = _match_shape( frequency, amplitude, phase) except ValueError as error: raise ValueError(("The parameters frequency, amplitude, and phase can " "not be broadcasted to the same shape")) from error if np.any(frequency < 0) or np.any(frequency > sampling_rate/2): raise ValueError( f"The frequency must be between 0 and {sampling_rate/2} Hz") # generate the sine signal n_samples = int(n_samples) times = np.arange(n_samples) / sampling_rate sine = np.zeros(cshape + (n_samples, )) for idx in np.ndindex(cshape): if full_period: # nearest number of full periods num_periods = np.round( n_samples / sampling_rate * frequency[idx]) # corresponding frequency frequency[idx] = num_periods * sampling_rate / n_samples sine[idx] = amplitude[idx] * \ np.sin(2 * np.pi * frequency[idx] * times + phase[idx]) # save to Signal nl = "\n" # required as variable because f-strings cannot contain "\" comment = (f"Sine signal (f = {str(frequency).replace(nl, ',')} Hz, " f"amplitude = {str(amplitude).replace(nl, ',')}, " f"phase = {str(phase).replace(nl, ',')} rad)") signal = pyfar.Signal( sine, sampling_rate, fft_norm="rms", comment=comment) return signal
[docs] def impulse(n_samples, delay=0, amplitude=1, sampling_rate=44100): r""" Generate a single or multi channel impulse signal, also known as the Dirac delta function. .. math:: s(n) = \begin{cases} \text{amplitude}, & \text{if $n$ = delay}\\ 0, & \text{else} \end{cases} Parameters ---------- n_samples : int Length of the impulse in samples delay : double, array like, optional Delay in samples. The default is ``0``. amplitude : double, array like, optional The peak amplitude of the impulse. The default is ``1``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. Returns ------- signal : Signal The impulse signal. The Signal is in the time domain and has the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). The delay and amplitude are written to `comment`. Notes ----- The parameters `delay` and `amplitude` are broadcasted using the :doc:`numpy rules<numpy:user/basics.broadcasting>`. For example `delay` could be of shape ``(2, 4)``, `amplitude` of shape ``(2, 1)`` or a scalar. In this case all parameters would be broadcasted to a shape of ``(2, 4)``. """ # check and match the cshape try: cshape, (delay, amplitude) = _match_shape(delay, amplitude) except ValueError as error: raise ValueError( ("The parameters delay and amplitude can not be " "broadcasted to the same shape")) from error # generate the impulse n_samples = int(n_samples) impulse = np.zeros(cshape + (n_samples, ), dtype=np.double) for idx in np.ndindex(cshape): impulse[idx + (delay[idx], )] = amplitude[idx] # save to Signal nl = "\n" # required as variable because f-strings cannot contain "\" comment = (f"Impulse signal (delay = {str(delay).replace(nl, ',')} " f"samples, amplitude = {str(amplitude).replace(nl, ',')})") signal = pyfar.Signal(impulse, sampling_rate, comment=comment) return signal
[docs] def linear_sweep_time(n_samples, frequency_range, n_fade_out=90, amplitude=1, sampling_rate=44100): r""" Generate sine sweep with linearly increasing frequency in the time domain. Time domain sweep generation according to [#]_: .. math:: s(t) = \sin(2\pi f_\mathrm{low} t + 2\pi (f_\mathrm{high}- f_\mathrm{low}) / T \cdot t^2 / 2), with :math:`T` the duration in seconds, :math:`t` the sampling points in seconds, and the frequency limits :math:`f_\mathrm{low}` and :math:`f_\mathrm{high}`. The linear sweep can also be generated in the frequency domain (see :py:func:`~linear_sweep_freq`). Time domain synthesis exhibits a constant temporal envelope at the cost of slight ripples in the magnitude response. Frequency domain synthesis exhibits smooth magnitude spectra at the cost of a slightly irregular temporal envelope. Parameters ---------- n_samples : int The length of the sweep in samples frequency_range : array like Frequency range of the sweep given by the lower and upper cut-off frequency in Hz. n_fade_out : int, optional The length of the squared cosine fade-out in samples. This is done to avoid discontinuities at the end of the sweep. The default is ``90``, which equals approximately 2 ms at sampling rates of 44.1 and 48 kHz. amplitude : double, optional The amplitude of the signal. The default is ``1``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain and has the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). The sweep type, frequency range, and length of the fade our are written to `comment`. References ---------- .. [#] Farina, Angelo (2000): "Simultaneous measurement of impulse response and distortion with a swept-sine technique." 108th AES Convention, Paris: France. Examples -------- Linear sweep between 0 and 22050 Hz .. plot:: >>> import pyfar as pf >>> sweep = pf.signals.linear_sweep_time(2**16, [0, 22050]) >>> pf.plot.time_freq(sweep) """ signal = _time_domain_sweep( n_samples, frequency_range, n_fade_out, amplitude, sampling_rate, "linear") return signal
[docs] def linear_sweep_freq( n_samples, frequency_range, start_margin, stop_margin, n_fade_in=0, n_fade_out=0, bandpass_order=8, sampling_rate=44100, return_group_delay=False): """ Generate sine sweep with linearly increasing frequency in the frequency domain. Sine sweep synthesis according to [#]_. The linear sweep can also be generated in the time domain (:py:func:`~linear_sweep_time`). Frequency domain synthesis exhibits smooth magnitude spectra at the cost of a slightly irregular temporal envelope. Time domain synthesis exhibits a constant temporal envelope in trade of slight ripples in the magnitude response. .. note:: The envelope of the sweep time signal should be constant, apart from slight overshoots at the beginning and end. If this is not the case, try to increase `n_samples`, `start_margin`, `stop_margin`, `n_fade_in` or `n_fade_out`. Parameters ---------- n_samples : int The length of the sweep in samples. frequency_range : array like Frequency range of the sweep given by the lower and upper cut-off frequency in Hz. The restriction of the frequency range is realized by applying a Butterworth high-pass if ``frequency_range[0] > 0`` and/or by a low-pass if ``frequency_range[1] < sampling_rate / 2``. start_margin : int, float The time in samples, at which the sweep starts. The start margin is required because the frequency domain sweep synthesis has pre-ringing in the time domain. stop_margin : int, float Time in samples, at which the sweep stops. This is relative to `n_samples`, e.g., a stop margin of 100 samples means that the sweep ends at sample ``n_samples-100``. This is required, because the frequency domain sweep synthesis has post-ringing in the time domain. n_fade_in : int, optional Duration of a squared sine fade-in in samples. The fade starts at the first sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. The default ``0`` does not apply a fade-in. n_fade_out : int, optional Duration of a squared cosine fade-out in samples. The fade ends at the last sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. The default ``0`` does not apply a fade-out. bandpass_order : int, optional The order of the Butterworth filters that are applied to limit the frequency range (see above). The default is ``8``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. return_group_delay : boolean, optional Return the analytical group delay of the sweep. This can be used to compute the times at which distortion products appear. The default is ``False``. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain, has a maximum absolute amplitude of 1 and the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). group_delay_sweep : FrequencyData The analytical group delay of the sweep in seconds as a single sided spectrum. Only returned if `return_group_delay` is ``True``. References ---------- .. [#] S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps. Directors Cut Including Previously Unreleased Material and some Corrections.' J. Audio Eng. Soc. 2001, 49 (6), 443-471. Examples -------- Linear sweep between 50 and 22050 Hz .. plot:: >>> import pyfar as pf >>> sweep = pf.signals.linear_sweep_freq( ... 2**16, [50, 22050], 1000, 1000) >>> pf.plot.time_freq(sweep) """ return _frequency_domain_sweep( n_samples=n_samples, sweep_type='linear', frequency_range=frequency_range, bandpass_order=bandpass_order, start_margin=start_margin, stop_margin=stop_margin, n_fade_in=n_fade_in, n_fade_out=n_fade_out, sampling_rate=sampling_rate, return_group_delay=return_group_delay)
[docs] def exponential_sweep_time(n_samples, frequency_range, n_fade_out=90, amplitude=1, sweep_rate=None, sampling_rate=44100): r""" Generate sine sweep with exponentially increasing frequency in the time domain. Time domain sweep generation according to [#]_: .. math:: s(t) = \sin(2\pi f_\mathrm{low} L \left( e^{t/L} - 1 \right)) with .. math:: L = T / \log(f_\mathrm{high}/f_\mathrm{low}), :math:`T` the duration in seconds, :math:`t` the sampling points in seconds, and the frequency limits :math:`f_\mathrm{low}` and :math:`f_\mathrm{high}`. The exponential sweep can also be generated in the frequency domain (see see :py:func:`~exponential_sweep_freq`). Time domain synthesis exhibits a constant temporal envelope at the cost of slight ripples in the magnitude response. Frequency domain synthesis exhibits smooth magnitude spectra at the cost of a slightly irregular temporal envelope. Parameters ---------- n_samples : int The length of the sweep in samples frequency_range : array like Frequency range of the sweep given by the lower and upper cut-off frequency in Hz. n_fade_out : int, optional The length of the squared cosine fade-out in samples. This is done to avoid discontinuities at the end of the sweep. The default is ``90``, which equals approximately 2 ms at sampling rates of 44.1 and 48 kHz. amplitude : double, optional The amplitude of the signal. The default is ``1``. sweep_rate : double, optional Rate at which the sine frequency increases over time in octaves per second. If this is given, `n_samples` is calculated according to the `sweep_rate` and `frequency_range`. The default is ``None``, which uses `n_samples` without modifications. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain and has the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). The sweep type, frequency range, and length of the fade our are written to `comment`. References ---------- .. [#] Farina, Angelo (2000): "Simultaneous measurement of impulse response and distortion with a swept-sine technique." 108th AES Convention, Paris: France. Examples -------- Exponential sweep between 50 and 22050 Hz .. plot:: >>> import pyfar as pf >>> sweep = pf.signals.exponential_sweep_time(2**16, [50, 22050]) >>> pf.plot.time_freq(sweep) """ signal = _time_domain_sweep( n_samples, frequency_range, n_fade_out, amplitude, sampling_rate, "exponential", sweep_rate) return signal
[docs] def exponential_sweep_freq( n_samples, frequency_range, start_margin, stop_margin, n_fade_in=0, n_fade_out=0, bandpass_order=8, sampling_rate=44100, return_group_delay=False): """ Generate sine sweep with exponentially increasing frequency in the frequency domain. Sweep synthesis according to [#]_. The exponential sweep can also be generated in the time domain (:py:func:`~exponential_sweep_time`). Frequency domain synthesis exhibits smooth magnitude spectra at the cost of a slightly irregular temporal envelope. Time domain synthesis exhibits a constant temporal envelope at the cost of slight ripples in the magnitude response. .. note:: The envelope of the sweep time signal should be constant, apart from slight overshoots at the beginning and end. If this is not the case, try to increase `n_samples`, `start_margin`, `stop_margin`, `n_fade_in` or `n_fade_out`. Parameters ---------- n_samples : int The length of the sweep in samples. frequency_range : array like Frequency range of the sweep given by the lower and upper cut-off frequency in Hz. The restriction of the frequency range is realized by applying a Butterworth high-pass if ``frequency_range[0] > 0`` and/or by a low-pass if ``frequency_range[1] < sampling_rate / 2``. Note that the exponential sweep can not start at 0 Hz, because its magnitude is defined by 1/frequency. start_margin : int, float The time in samples, at which the sweep starts. The start margin is required because the frequency domain sweep synthesis has pre-ringing in the time domain. stop_margin : int, float Time in samples, at which the sweep stops. This is relative to `n_samples`, e.g., a stop margin of 100 samples means that the sweep ends at sample ``n_samples-100``. This is required, because the frequency domain sweep synthesis has post-ringing in the time domain. n_fade_in : int, optional Duration of a squared sine fade-in in samples. The fade starts at the first sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. The default ``0`` does not apply a fade-in. n_fade_out : int, optional Duration of a squared cosine fade-out in samples. The fade ends at the last sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. The default ``0`` does not apply a fade-out. bandpass_order : int, optional The order of the Butterworth filters that are applied to limit the frequency range (see above). The default is ``8``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. return_group_delay : boolean, optional Return the analytical group delay of the sweep. This can be used to compute the times at which distortion products appear. The default is ``False``. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain, has a maximum absolute amplitude of 1 and the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). group_delay_sweep : FrequencyData The analytical group delay of the sweep in seconds as a single sided spectrum. Only returned if `return_group_delay` is ``True``. References ---------- .. [#] S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps. Directors Cut Including Previously Unreleased Material and some Corrections.' J. Audio Eng. Soc. 2001, 49 (6), 443-471. Examples -------- Exponential sweep between 50 and 22050 Hz .. plot:: >>> import pyfar as pf >>> sweep = pf.signals.exponential_sweep_freq( ... 2**16, [50, 22050], 5000, 1000) >>> pf.plot.time_freq(sweep) """ return _frequency_domain_sweep( n_samples=n_samples, sweep_type='exponential', frequency_range=frequency_range, bandpass_order=bandpass_order, start_margin=start_margin, stop_margin=stop_margin, n_fade_in=n_fade_in, n_fade_out=n_fade_out, sampling_rate=sampling_rate, return_group_delay=return_group_delay)
[docs] def magnitude_spectrum_weighted_sweep( n_samples, magnitude_spectrum, start_margin, stop_margin, n_fade_in=0, n_fade_out=0, sampling_rate=44100, return_group_delay=False): """ Generate sine sweep with arbitrary magnitude spectrum in the frequency domain. Frequency domain sine sweep synthesis according to [#]_. There is currently no method to design sine sweeps with arbitrary magnitude response in the time domain. .. note:: The envelope of the sweep time signal should be constant, apart from slight overshoots at the beginning and end. If this is not the case, try to increase `n_samples`, `start_margin`, `stop_margin`, `n_fade_in` or `n_fade_out`, or provide a more smooth magnitude spectrum. Parameters ---------- n_samples : int The length of the sweep in samples. magnitude_spectrum : Signal The magnitude spectrum as a Signal object. Must be at least `n_samples` long and is zero-padded to `n_samples` otherwise. start_margin : int, float The time in samples, at which the sweep starts. The start margin is required because the frequency domain sweep synthesis has pre-ringing in the time domain. stop_margin : int, float Time in samples, at which the sweep stops. This is relative to `n_samples`, e.g., a stop margin of 100 samples means that the sweep ends at sample ``n_samples-100``. This is required, because the frequency domain sweep synthesis has post-ringing in the time domain. n_fade_in : int, optional Duration of a squared sine fade-in in samples. The fade starts at the first sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. The default ``0`` does not apply a fade-in. n_fade_out : int, optional Duration of a squared cosine fade-out in samples. The fade ends at the last sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. The default ``0`` does not apply a fade-out. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. return_group_delay : boolean, optional Return the analytical group delay of the sweep. This can be used to compute the times at which distortion products appear. The default is ``False``. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain, has a maximum absolute amplitude of 1 and the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). group_delay_sweep : FrequencyData The analytical group delay of the sweep in seconds as a single sided spectrum. Only returned if `return_group_delay` is ``True``. References ---------- .. [#] S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps. Directors Cut Including Previously Unreleased Material and some Corrections.' J. Audio Eng. Soc. 2001, 49 (6), 443-471. Examples -------- .. plot:: >>> import pyfar as pf >>> magnitude = pf.dsp.filter.low_shelf( ... pf.signals.impulse(2**16), 500, 20, 2) >>> magnitude = pf.dsp.filter.butterworth(magnitude, 8, 50, 'highpass') >>> sweep = pf.signals.magnitude_spectrum_weighted_sweep( ... 2**16, magnitude, 5000, 1000) >>> pf.plot.time_freq(sweep) """ return _frequency_domain_sweep( n_samples=n_samples, sweep_type=magnitude_spectrum, frequency_range=[0, sampling_rate / 2], bandpass_order=0, start_margin=start_margin, stop_margin=stop_margin, n_fade_in=n_fade_in, n_fade_out=n_fade_out, sampling_rate=sampling_rate, return_group_delay=return_group_delay)
[docs] def linear_perfect_sweep( n_samples, sampling_rate=44100, return_group_delay=False): """ Generate a perfect linear sweep in the frequency domain. The perfect sweep is generated according to [#]_ and is used for adaptive system identification. It is orthogonal to delayed versions of itself. Parameters ---------- n_samples : int The length of the sweep in samples. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. return_group_delay : boolean, optional Return the analytical group delay of the sweep. This can be used to compute the times at which distortion products appear. The default is ``False``. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain, has a maximum absolute amplitude of 1 and the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). group_delay_sweep : FrequencyData The analytical group delay of the sweep in seconds as a single sided spectrum. Only returned if `return_group_delay` is ``True``. References ---------- .. [#] C. Antweiler, A. Telle, P. Vary, and G. Enzner, “Perfect-sweep NLMS for time-variant acoustic system identification,” in IEEE Int. Conf. Acoustics, Speech and Signal Processing (ICASSP), Prague, Czech Republic, May 2011. doi: 10.1109/ICASSP.2012.6287930. Examples -------- Plot a shifted perfect sweep to show that it can be looped .. plot:: >>> import pyfar as pf >>> sweep = pf.signals.linear_perfect_sweep(2**8) >>> sweep_shifted = pf.dsp.time_shift(sweep, 2**7) >>> ax = pf.plot.time_freq(sweep_shifted, unit='samples') >>> ax[0].axvline(2**7, ls='--', c='r', label='start/end') >>> ax[0].legend(loc=4) Show that the sweep is orthogonal to all shifted version of itself. This property is important for adaptive system identification. .. plot:: >>> import pyfar as pf >>> import numpy as np >>> import matplotlib.pyplot as plt >>> >>> sweep = pf.signals.linear_perfect_sweep(2**8) >>> >>> # compute auto-correlation >>> auto_correlation = np.empty(2**8) >>> for idx, shift in enumerate(range(-2**7, 2**7)): >>> auto_correlation[idx] = np.dot( ... sweep.time.flatten(), ... np.roll(sweep.time.flatten(), shift)) >>> >>> auto_correlation /= pf.dsp.energy(sweep) >>> >>> # plot auto-correlation >>> with pf.plot.context(): >>> plt.plot(np.arange(-2**7, 2**7), auto_correlation) >>> plt.gca().set_xlim(-2**7, 2**7) >>> plt.gca().set_xlabel('time lag in samples') >>> plt.gca().set_ylabel('auto correlation') """ return _frequency_domain_sweep( n_samples=n_samples, sweep_type='perfect_linear', frequency_range=[0, sampling_rate / 2], bandpass_order=0, start_margin=0, stop_margin=0, n_fade_in=0, n_fade_out=0, sampling_rate=sampling_rate, return_group_delay=return_group_delay)
def _frequency_domain_sweep( n_samples, sweep_type, frequency_range, bandpass_order, start_margin, stop_margin, n_fade_in, n_fade_out, sampling_rate, return_group_delay): """ Frequency domain sweep synthesis with arbitrary magnitude response [#]_. Non-unique parameters are documented in ``linear_sweep_freq``, ``exponential_sweep_freq``, ``linear_perfect_sweep``, and ``magnitude_spectrum_weighted_sweep``. Parameters ---------- n_samples : int The length of the sweep in samples. sweep_type : Signal, string Specify the magnitude response of the sweep. signal The magnitude response as :py:class:`~pyfar.Signal` object. If ``signal.n_samples`` is smaller than `n_samples`, zeros are padded to the end of `signal`. Note that `frequency_range` is not required in this case. ``'linear'`` Design a sweep with linearly increasing frequency and a constant magnitude spectrum. ``'exponential'`` Design a sweep with exponentially increasing frequency. The magnitude decreases by 3 dB per frequency doubling and has constant energy in filters of relative constant bandwidth (e.g. octaves). ``'perfect_linear'`` Perfect linear sweep. Note that the parameters `start_margin`, `stop_margin`, and `frequency_range` are not required in this case. frequency_range : array like Frequency range of the sweep given by the lower and upper cut-off frequency in Hz. The restriction of the frequency range is realized by applying a Butterworth high-pass if ``frequency_range[0] > 0`` and/or by a low-pass if ``frequency_range[1] < sampling_rate / 2``. Note that the exponential sweep can not start at 0 Hz, because its magnitude is defined by 1/frequency. bandpass_order : int The order of the Butterworth filters that are applied to limit the frequency range (see above). start_margin : int, float The time in samples, at which the sweep starts. The start margin is required because the frequency domain sweep synthesis has pre-ringing in the time domain. stop_margin : int, float Time in samples, at which the sweep stops. This is relative to `n_samples`, e.g., a stop margin of 100 samples means that the sweep ends at sample ``n_samples-100``. This is required, because the frequency domain sweep synthesis has post-ringing in the time domain. n_fade_in : int Duration of a squared sine fade-in in samples. The fade starts at the first sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. n_fade_out : int Duration of a squared cosine fade-out in samples. The fade ends at the last sample of the sweep that is closer than 60 dB to the absolute maximum of the sweep time signal. sampling_rate : int The sampling rate in Hz. return_group_delay : boolean Return the analytical group delay of the sweep. This can be used to compute the times at which distortion products appear. Returns ------- sweep : Signal The sweep signal. The Signal is in the time domain and has the ``none`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). The sweep parameters are written to `comment`. group_delay_sweep : FrequencyData The analytical group delay of the sweep in seconds as a single sided spectrum. Only returned if `return_group_delay` is ``True``. References ---------- .. [#] S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps. Directors Cut Including Previously Unreleased Material and some Corrections.' J. Audio Eng. Soc. 2001, 49 (6), 443-471. (all equations referenced in the code refer to this). """ # check input (only checks for user input, no checks for calling the # private function from the public functions) if isinstance(sweep_type, pyfar.Signal): magnitude = sweep_type sweep_type = 'signal' if np.atleast_1d(frequency_range).size != 2: raise ValueError( "Frequency_range must be an array like with two elements.") if frequency_range[0] < 0 or frequency_range[1] > sampling_rate/2: raise ValueError( ("Lower frequency limit must be at least 0 Hz and upper frequency " "limit must be below half the sampling rate.")) if frequency_range[0] == 0 and sweep_type == "exponential": warnings.warn(( "The exponential sweep has a 1/sqrt(frequency) magnitude spectrum." " The magnitude is set to 0 at 0 Hz to avoid a division by zero."), stacklevel=2) # initialize basic parameters --------------------------------------------- # spacing between frequency bins of FFT df = sampling_rate / n_samples # get number of bins (works for even and odd n_samples) n_bins = n_samples // 2 + 1 # compute magnitude spectrum ---------------------------------------------- if sweep_type == 'signal': # zero pad magnitude Signal or raise error if needed if n_samples > magnitude.n_samples: magnitude = pyfar.dsp.pad_zeros( magnitude, n_samples - magnitude.n_samples) elif magnitude.n_samples > n_samples: raise ValueError( (f'magnitude_spectrum has {magnitude.n_samples} samples ' f'but must not be longer than {n_samples}')) sweep_abs = np.abs(magnitude.freq_raw.flatten()) elif sweep_type in ['linear', 'perfect_linear']: # constant spectrum sweep_abs = np.ones(n_bins) elif sweep_type == 'exponential': # 1/sqrt(f) spectrum sweep_abs = np.zeros(n_bins) sweep_abs[1:] = 1 / np.sqrt(2 * np.pi * np.arange(1, n_bins) * df) # band limit to magnitude spectrum if sweep_type in ['linear', 'exponential']: if frequency_range[0] > 0 and frequency_range[1] < sampling_rate / 2: band_limit = pyfar.dsp.filter.butterworth( pyfar.signals.impulse(n_samples, sampling_rate=sampling_rate), bandpass_order, frequency_range, 'bandpass') elif frequency_range[0] > 0: band_limit = pyfar.dsp.filter.butterworth( pyfar.signals.impulse(n_samples, sampling_rate=sampling_rate), bandpass_order, frequency_range[0], 'highpass') elif frequency_range[1] < sampling_rate / 2: band_limit = pyfar.dsp.filter.butterworth( pyfar.signals.impulse(n_samples, sampling_rate=sampling_rate), bandpass_order, frequency_range[1], 'lowpass') else: band_limit = pyfar.Signal(np.ones_like(sweep_abs), sampling_rate, n_samples, 'freq') sweep_abs *= np.abs(band_limit.freq.flatten()) # compute group delay ----------------------------------------------------- # group delay at 0 Hz must be 0 sweep_gd = np.zeros(n_bins) # group delay at df equals starting time unless it's 0 if start_margin > 0: sweep_gd[1] = start_margin / sampling_rate tg_start = 2 else: tg_start = 1 # group delay at Nyquist equals stopping time sweep_gd[-1] = (n_samples - stop_margin) / sampling_rate # FORMULA (11, p.40, in Müller and Massarani 2001) sweep_power = np.sum(np.abs(sweep_abs**2)) C = (sweep_gd[-1] - sweep_gd[1]) / sweep_power # FORMULA (10, p.40, in Müller and Massarani 2001) for k in range(tg_start, n_bins): # index 2 to nyq sweep_gd[k] = sweep_gd[k-1] + C * np.abs(sweep_abs[k])**2 # compute phase from group delay ------------------------------------------ sweep_ang = -1 * np.cumsum(sweep_gd) * 2 * np.pi * df # wrap and correct phase to be real 0 at Nyquist sweep_ang = pyfar.dsp.wrap_to_2pi(sweep_ang) sweep_ang[sweep_ang > np.pi] -= 2*np.pi if sweep_type == 'perfect_linear': sweep_ang[-1] = 0 elif sweep_ang[-1] != 0 and not n_samples % 2: factor = np.cumsum(np.ones_like(sweep_ang)) - 1 offset = df * sweep_ang[-1] / (sampling_rate / 2) sweep_ang -= factor * offset sweep_ang[-1] = np.abs(sweep_ang[-1]) # compute and finalize return data ---------------------------------------- # put group delay on pyfar FrequencyData sweep_gd = pyfar.FrequencyData( sweep_gd, pyfar.dsp.fft.rfftfreq(n_samples, sampling_rate)) # combine magnitude and phase of sweep sweep = sweep_abs * np.exp(1j * sweep_ang) # put sweep in pyfar.Signal an transform to time domain sweep = pyfar.Signal(sweep, sampling_rate, n_samples, 'freq', 'none') # find windowing end points heuristically. The start and stop margin are # not reliable, because freq. domain synthesis causes pre/post-ringing if n_fade_in or n_fade_out: threshold = 10**(-60/20) * np.max(np.abs(sweep.time)) fade_start = np.argmax(np.abs(sweep.time) > threshold) fade_end = n_samples - \ np.argmax(np.abs(sweep.time.flatten()[::-1]) > threshold) print([fade_start, fade_end]) # generate windows for fade in and/or out if n_fade_in and n_fade_out: fade = [fade_start, fade_start + n_fade_in, fade_end - n_fade_out, fade_end] shape = 'symmetric' elif n_fade_in: fade = [fade_start, fade_start + n_fade_in] shape = 'left' elif n_fade_out: fade = [fade_end - n_fade_out, fade_end] shape = 'right' else: fade = None # apply window if fade is not None: sweep = pyfar.dsp.time_window(sweep, fade, shape=shape) # normalize to time domain amplitude sweep = pyfar.dsp.normalize(sweep) if return_group_delay: return sweep, sweep_gd else: return sweep def _time_domain_sweep(n_samples, frequency_range, n_fade_out, amplitude, sampling_rate, sweep_type, sweep_rate=None): # check input if np.atleast_1d(frequency_range).size != 2: raise ValueError( "frequency_range must be an array like with to elements.") if frequency_range[1] > sampling_rate/2: raise ValueError( "Upper frequency limit is larger than half the sampling rate.") if frequency_range[0] == 0 and sweep_type == "exponential": raise ValueError("The exponential sweep can not start at 0 Hz.") # generate sweep if sweep_type == "linear": sweep = _linear_sweep( n_samples, frequency_range, amplitude, sampling_rate) elif sweep_type == 'exponential': sweep = _exponential_sweep( n_samples, frequency_range, amplitude, sweep_rate, sampling_rate) # fade out n_fade_out = int(n_fade_out) if n_fade_out > 0: # check must be done here because n_samples might not be defined if # using the sweep_rate for exponential sweeps if sweep.size < n_fade_out: raise ValueError("The sweep must be longer than n_fade_out.") sweep[-n_fade_out:] *= np.cos(np.linspace(0, np.pi/2, n_fade_out))**2 # save to signal comment = (f"{sweep_type} sweep between {frequency_range[0]} " f"and {frequency_range[1]} Hz " f"with {n_fade_out} samples squared cosine fade-out.") signal = pyfar.Signal( sweep, sampling_rate, fft_norm="none", comment=comment) return signal def _linear_sweep(n_samples, frequency_range, amplitude, sampling_rate): # generate sweep n_samples = int(n_samples) t = np.arange(n_samples) / sampling_rate T = n_samples / sampling_rate # [1, page 5] sweep = amplitude * np.sin( 2 * np.pi * frequency_range[0] * t + 2 * np.pi * (frequency_range[1]-frequency_range[0]) / T * t**2 / 2) return sweep def _exponential_sweep(n_samples, frequency_range, amplitude, sweep_rate, sampling_rate): c = np.log(frequency_range[1] / frequency_range[0]) # get n_samples if sweep_rate is not None: L = 1 / sweep_rate / np.log(2) T = L * c n_samples = np.round(T * sampling_rate) else: n_samples = int(n_samples) # L for actual n_samples L = n_samples / sampling_rate / c # make the sweep times = np.arange(n_samples) / sampling_rate sweep = amplitude * np.sin( 2 * np.pi * frequency_range[0] * L * (np.exp(times / L) - 1)) return sweep def _match_shape(*args): """ Match the shape of *args to the shape of the arg with the largest size using np.broadcast_shapes and np.broadcast_to(). Parameters ---------- *args : data for matching shape Returns ------- shape : tuple new common shape of the args args : list args with new common shape (*arg_1, *arg_2, ..., *arg_N) """ # broadcast shapes shape = np.broadcast_shapes(*[np.atleast_1d(arg).shape for arg in args]) # match the shape result = [] for arg in args: arg = np.broadcast_to(arg, shape) arg.setflags(write=1) result.append(arg) return shape, result