def test_time_window_interval_order_error(): """ Test errors for incorrect order of values in interval.""" sig = pyfar.Signal(np.ones(10), 2) with pytest.raises(ValueError, match='ascending'): dsp.time_window(sig, interval=[2, 1]) with pytest.raises(ValueError, match='ascending'): dsp.time_window(sig, interval=[1, 2, 3, 0])
def test_time_window_interval_four_values(): """ Test time_window with four values given in interval.""" sig = pyfar.Signal(np.ones(9), 1) sig_win = dsp.time_window(sig, window='triang', interval=[1, 3, 6, 7], crop='none') time_win = np.array([[0, 0.25, 0.75, 1, 1, 1, 1, 0.5, 0]]) npt.assert_allclose(sig_win.time, time_win) sig = pyfar.Signal(np.ones(10), 1) sig_win = dsp.time_window(sig, window='triang', interval=[1, 3, 6, 7], crop='none') time_win = np.array([[0, 0.25, 0.75, 1, 1, 1, 1, 0.5, 0, 0]]) npt.assert_allclose(sig_win.time, time_win)
def test_time_window_interval_unit_error(): """ Test errors for incorrect boundaries in combinations with unit.""" sig = pyfar.Signal(np.ones(10), 2) with pytest.raises(ValueError, match='than signal'): dsp.time_window(sig, interval=[0, 11], unit='samples') with pytest.raises(ValueError, match='than signal'): dsp.time_window(sig, interval=[0, 6], unit='s')
def test_time_window_right(): """ Test window options right.""" sig = pyfar.Signal(np.ones(7), 1) # Odd number of samples, crop='none' sig_win = dsp.time_window(sig, window='triang', interval=[2, 4], shape='right', crop='none') time_win = np.array([[1, 1, 1, 0.75, 0.25, 0, 0]]) npt.assert_allclose(sig_win.time, time_win) # Even number of samples, crop='none' sig_win = dsp.time_window(sig, window='triang', interval=[2, 5], shape='right', crop='none') time_win = np.array([[1, 1, 1, 5 / 6, 3 / 6, 1 / 6, 0]]) npt.assert_allclose(sig_win.time, time_win) # crop='end' sig_win = dsp.time_window(sig, window='triang', interval=[2, 5], shape='right', crop='end') time_win = np.array([[1, 1, 1, 5 / 6, 3 / 6, 1 / 6]]) npt.assert_allclose(sig_win.time, time_win) # crop='window' sig_win = dsp.time_window(sig, window='triang', interval=[2, 5], shape='right', crop='window') time_win = np.array([[1, 1, 1, 5 / 6, 3 / 6, 1 / 6]]) npt.assert_allclose(sig_win.time, time_win)
def test_minimum_phase(): # tests are separated since their reliability depends on the type of # filters. The homomorphic method works best for filters with odd numbers # of taps # method = 'hilbert' n_samples = 9 filter_linphase = pyfar.Signal([0, 0, 0, 0, 1, 1, 0, 0, 0, 0], 44100) imp_minphase = pyfar.dsp.minimum_phase(filter_linphase, pad=False, method='hilbert', n_fft=2**18) ref = np.array([1, 1, 0, 0, 0], dtype=float) npt.assert_allclose(np.squeeze(imp_minphase.time), ref, rtol=1e-4, atol=1e-4) # method = 'homomorphic' n_samples = 8 imp_linphase = pyfar.signals.impulse(n_samples + 1, delay=int(n_samples / 2)) ref = pyfar.signals.impulse(int(n_samples / 2) + 1) imp_minphase = pyfar.dsp.minimum_phase(imp_linphase, method='homomorphic', pad=False) npt.assert_allclose(imp_minphase.time, ref.time) # test pad length ref = pyfar.signals.impulse(n_samples + 1) imp_minphase = pyfar.dsp.minimum_phase(imp_linphase, method='homomorphic', pad=True) assert imp_minphase.n_samples == imp_linphase.n_samples npt.assert_allclose(imp_minphase.time, ref.time) # test error ref = pyfar.signals.impulse(n_samples + 1) imp_minphase, mag_error = pyfar.dsp.minimum_phase( imp_linphase, method='homomorphic', return_magnitude_ratio=True) npt.assert_allclose(np.squeeze(mag_error.freq), np.ones(int(n_samples / 2 + 1), dtype=complex)) # test multidim ref = pyfar.signals.impulse(n_samples + 1, amplitude=np.ones((2, 3))) imp_linphase = pyfar.signals.impulse(n_samples + 1, delay=int(n_samples / 2), amplitude=np.ones((2, 3))) imp_minphase = pyfar.dsp.minimum_phase(imp_linphase, method='homomorphic', pad=True) assert imp_minphase.n_samples == imp_linphase.n_samples npt.assert_allclose(imp_minphase.time, ref.time)
def test_time_window_crop_interval(): """ Test truncation of windowed signal to interval.""" sig = pyfar.Signal(np.ones(10), 2) sig_win = dsp.time_window(sig, interval=[1, 3], shape='symmetric', unit='samples', crop='window') assert sig_win.n_samples == 3 sig_win = dsp.time_window(sig, interval=[0.5, 1.5], shape='symmetric', unit='s', crop='window') assert sig_win.n_samples == 3 sig_win = dsp.time_window(sig, interval=[1, 3], shape='left', crop='window') assert sig_win.n_samples == 9 sig_win = dsp.time_window(sig, interval=[1, 3], shape='right', crop='window') assert sig_win.n_samples == 4
def test_time_window_symmetric_zero(): """ Test window option symmetric_zero.""" sig = pyfar.Signal(np.ones(12), 2) sig_win = dsp.time_window(sig, window='triang', interval=[2, 4], shape='symmetric_zero') time_win = np.array([[1, 1, 1, 0.75, 0.25, 0, 0, 0, 0.25, 0.75, 1, 1]]) npt.assert_allclose(sig_win.time, time_win)
def test_time_window_symmetric(): """ Test window option symmetric.""" sig = pyfar.Signal(np.ones(10), 2) sig_win = dsp.time_window(sig, interval=[1, 5], window='hann', shape='symmetric', crop='window') time_win = np.atleast_2d(sgn.windows.hann(5, sym=True)) npt.assert_allclose(sig_win.time, time_win)
def impulse(n_samples, delay=0, amplitude=1, sampling_rate=44100): """ 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, 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` must be scalars and/or array likes of the same shape. """ # check and match the cshape cshape = _get_common_shape(delay, amplitude) delay, amplitude = _match_shape(cshape, delay, amplitude) # 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
def test_time_window_multichannel(): """ Test time_window of multichannel signal.""" time = np.array([[[1, 1, 1, 1], [2, 2, 2, 2]], [[3, 3, 3, 3], [4, 4, 4, 4]]]) sig = pyfar.Signal(time, 1) sig_win = dsp.time_window(sig, window='triang', interval=[1, 2], shape='symmetric', crop='window') time_win = np.array([[[0.5, 0.5], [1, 1]], [[1.5, 1.5], [2, 2]]]) npt.assert_allclose(sig_win.time, time_win)
def test_zero_phase(): """Test zero phase generation.""" # generate test signal and zero phase version signal = pf.Signal([0, 0, 0, 2], 44100) signal_zero = dsp.zero_phase(signal) # assert type and id assert isinstance(signal_zero, pf.Signal) assert id(signal) != id(signal_zero) # assert freq data assert np.any(np.abs(np.imag(signal.freq)) > 1e-15) assert np.all(np.abs(np.imag(signal_zero.freq)) == 0) # assert time data npt.assert_allclose(signal_zero.time, np.atleast_2d([2, 0, 0, 0]))
def test_time_window_crop_end(): """ Test crop option 'end'.""" sig = pyfar.Signal(np.ones(10), 2) sig_win = dsp.time_window(sig, interval=[1, 3], shape='symmetric', unit='samples', crop='end') assert sig_win.n_samples == 4 sig_win = dsp.time_window(sig, interval=[0.5, 1.5], shape='symmetric', unit='s', crop='end') assert sig_win.n_samples == 4 sig_win = dsp.time_window(sig, interval=[1, 3], shape='left', crop='end') assert sig_win.n_samples == 10 sig_win = dsp.time_window(sig, interval=[1, 3], shape='right', crop='end') assert sig_win.n_samples == 4
def test_time_window_input(): """Test errors when calling with incorrect parameters.""" sig = pyfar.Signal(np.ones(5), 2) with pytest.raises(TypeError, match='signal'): dsp.time_window([1., 2.], interval=(0, 4)) with pytest.raises(ValueError, match='shape'): dsp.time_window(sig, interval=(0, 4), shape='top') with pytest.raises(TypeError, match='crop'): dsp.time_window(sig, interval=(0, 4), crop='t') with pytest.raises(ValueError, match='unit'): dsp.time_window(sig, interval=[0, 1], unit='kg') with pytest.raises(TypeError, match='interval'): dsp.time_window(sig, interval=1) with pytest.raises(ValueError, match='contain'): dsp.time_window(sig, interval=[1, 2, 3]) with pytest.raises(ValueError, match='longer'): dsp.time_window(sig, interval=[1, 11]) with pytest.raises(ValueError): dsp.time_window(sig, interval=['a', 'b'])
def test_copy(sphericalvoronoi, timedata, frequencydata): """ Test copy method used by several classes.""" obj_list = [ pyfar.Signal(1000, 44100), pyfar.Orientations(), pyfar.Coordinates(), pyfar.dsp.Filter(), sphericalvoronoi, timedata, frequencydata ] for obj in obj_list: # Create copy obj_copy = obj.copy() # Check class assert isinstance(obj_copy, obj.__class__) # Check ID assert id(obj) != id(obj_copy) # Check attributes assert len(obj.__dict__) == len(obj_copy.__dict__) # Check if copied objects are equal assert obj_copy == obj
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 pulsed_noise(n_pulse, n_pause, n_fade=90, repetitions=5, rms=1, spectrum="pink", frozen=True, sampling_rate=44100, seed=None): """ Generate single channel normally distributed pulsed white or pink noise. The pink noise is generated by applying a ``sqrt(1/f)`` filter to the spectrum. Parameters ---------- n_pulse : int The length of the pulses in samples n_pause : int The length of the pauses between the pulses in samples. n_fade : int, optional The length of the squared sine/cosine fade-in and fade outs in samples. The default is ``90``, which equals approximately 2 ms at sampling rates of 44.1 and 48 kHz. repetitions : int, optional Specifies the number of noise pulses. The default is ``5``. rms : double, array like, optional The RMS amplitude of the white signal. The default is ``1``. spectrum: string, optional The noise spectrum, which can be ``pink`` or ``white``. The default is ``pink``. frozen : boolean, optional If ``True``, all noise pulses are identical. If ``False`` each noise pulse is a separate stochastic process. The default is ``True``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. seed : int, None, optional The seed for the random generator. Pass a seed to obtain identical results for multiple calls. The default is ``None``, which will yield different results with every call. Returns ------- signal : Signal The noise signal. The Signal is in the time domain and has the ``rms`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). `comment` contains information about the selected parameters. """ if n_pulse < 2 * n_fade: raise ValueError( "n_fade too large. It must be smaller than n_pulse/2.") # get the noise sample n_pulse = int(n_pulse) repetitions = int(repetitions) n_samples = n_pulse if frozen else n_pulse * repetitions p_noise = noise(n_samples, spectrum, rms, sampling_rate, seed).time p_noise = np.tile(p_noise, (repetitions, 1)) if frozen else \ p_noise.reshape((repetitions, n_pulse)) # fade the noise if n_fade > 0: n_fade = int(n_fade) fade = np.sin(np.linspace(0, np.pi / 2, n_fade))**2 p_noise[..., 0:n_fade] *= fade p_noise[..., -n_fade:] *= fade[::-1] # add the pause p_noise = np.concatenate((p_noise, np.zeros((repetitions, int(n_pause)))), -1) # reshape to single channel signal and discard final pause p_noise = p_noise.reshape((1, -1))[..., :-int(n_pause)] # save to Signal frozen_str = "frozen" if frozen else "" comment = (f"{frozen_str} {spectrum} pulsed noise signal (rms = {rms}, " f"{repetitions} repetitions, {n_pulse} samples pulse duration, " f"{n_pause} samples pauses, and {n_fade} samples fades.") signal = pyfar.Signal(p_noise, sampling_rate, fft_norm="rms", comment=comment) return signal
def noise(n_samples, spectrum="white", rms=1, sampling_rate=44100, seed=None): """ Generate single or multi channel normally distributed white or pink noise. The pink noise is generated by applying a ``sqrt(1/f)`` filter to the spectrum. Parameters ---------- n_samples : int The length of the signal in samples spectrum : str, optional ``white`` to generate noise with constant energy across frequency. ``pink`` to generate noise with constant energy across filters with constant relative bandwith. The default is ``white``. rms : double, array like, optional The route mean square (RMS) value of the noise signal. A multi channel noise signal is generated if an array of RMS values is passed. The default is ``1``. sampling_rate : int, optional The sampling rate in Hz. The default is ``44100``. seed : int, None, optional The seed for the random generator. Pass a seed to obtain identical results for multiple calls. The default is ``None``, which will yield different results with every call. Returns ------- signal : Signal The noise signal. The signal is in the time domain and has the ``rms`` FFT normalization (see :py:func:`~pyfar.dsp.fft.normalization`). The type of the spectrum (``white``, ``pink``) and the RMS amplitude are written to `comment`. """ # generate the noise rms = np.atleast_1d(rms) n_samples = int(n_samples) cshape = np.atleast_1d(rms).shape rng = np.random.default_rng(seed) noise = rng.standard_normal(np.prod(cshape + (n_samples, ))) noise = noise.reshape(cshape + (n_samples, )) if spectrum == "pink": # apply 1/f filter in the frequency domain noise = fft.rfft(noise, n_samples, sampling_rate, 'none') noise /= np.sqrt(np.arange(1, noise.shape[-1] + 1)) noise = fft.irfft(noise, n_samples, sampling_rate, 'none') elif spectrum != "white": raise ValueError( f"spectrum is '{spectrum}' but must be 'white' or 'pink'") # level the noise rms_current = np.atleast_1d(np.sqrt(np.mean(noise**2, axis=-1))) for idx in np.ndindex(rms.shape): noise[idx] = noise[idx] / rms_current[idx] * rms[idx] # save to Signal nl = "\n" # required as variable because f-strings cannot contain "\" comment = f"{spectrum} noise signal (rms = {str(rms).replace(nl, ',')})" signal = pyfar.Signal(noise, sampling_rate, fft_norm="rms", comment=comment) return signal
def test_time_window_default(): """ Test time_window function with default values.""" sig = pyfar.Signal(np.ones(10), 2) sig_win = dsp.time_window(sig, interval=(0, sig.n_samples - 1)) time_win = np.atleast_2d(sgn.windows.hann(10, sym=True)) npt.assert_allclose(sig_win.time, time_win)
def test_time_window_interval_types(): sig = pyfar.Signal(np.ones(10), 2) dsp.time_window(sig, interval=(1, 2)) dsp.time_window(sig, interval=[1, 2]) dsp.time_window(sig, interval=(1, 2, 3, 4)) dsp.time_window(sig, interval=[1, 2, 3, 4])
def test_time_window_crop_none(): """ Test crop option 'none'.""" sig = pyfar.Signal(np.ones(10), 2) sig_win = dsp.time_window(sig, interval=[1, 3], crop='none') assert sig_win.n_samples == 10
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` must must be scalars and/or array likes of the same shape. """ # check and match the cshape cshape = _get_common_shape(frequency, amplitude, phase) frequency, amplitude, phase = _match_shape( cshape, frequency, amplitude, phase) 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