def fft_norm(self, value): """ The normalization for the Fourier Transform. See pyfar.fft.normalization for more information. """ # check input if value not in self._VALID_FFT_NORMS: raise ValueError(("Invalid FFT normalization. Has to be " f"{', '.join(self._VALID_FFT_NORMS)}, but found " f"'{value}'")) # apply new normalization if Signal is in frequency domain if self._fft_norm != value and self._domain == 'freq': # de-normalize self._data = fft.normalization(self._data, self._n_samples, self._sampling_rate, self._fft_norm, inverse=True) # normalize self._data = fft.normalization(self._data, self._n_samples, self._sampling_rate, value, inverse=False) self._fft_norm = value
def test_normalization_exceptions(): # Call without numpy array with raises(ValueError): fft.normalization(1, 1, 44100, 'rms') # Invalid normalization with raises(ValueError): fft.normalization(np.array([1]), 1, 44100, 'goofy')
def test_normalization_with_window_value_error(): """ Test if normalization throws a ValueError if the window has the wrong length. """ with raises(ValueError): # n_samples=5, and len(window)=5 fft.normalization(np.array([.5, 1, .5]), 4, 44100, 'amplitude', window=[1, 1, 1, 1, 1])
def test_normalization_none(impulse): spec_out = fft.normalization( impulse.freq.copy(), impulse.n_samples, impulse.sampling_rate, impulse.fft_norm, inverse=False) npt.assert_allclose(spec_out, impulse.freq, atol=10*np.finfo(float).eps) spec_out = fft.normalization( impulse.freq.copy(), impulse.n_samples, impulse.sampling_rate, impulse.fft_norm, inverse=True) npt.assert_allclose(spec_out, impulse.freq, atol=10*np.finfo(float).eps)
def _arithmetic(data: tuple, domain: str, operation: Callable): """Apply arithmetic operations.""" # check input and obtain meta data of new signal sampling_rate, n_samples, fft_norm, times, frequencies, audio_type = \ _assert_match_for_arithmetic(data, domain) # apply arithmetic operation result = _get_arithmetic_data(data[0], n_samples, domain) for d in range(1, len(data)): result = operation(result, _get_arithmetic_data(data[d], n_samples, domain)) # check if to return an audio object if audio_type == Signal: # apply desired fft normalization if domain == 'freq': result = fft.normalization(result, n_samples, sampling_rate, fft_norm) result = Signal(result, sampling_rate, n_samples, domain, fft_norm=fft_norm) elif audio_type == TimeData: result = TimeData(result, times) elif audio_type == FrequencyData: result = FrequencyData(result, frequencies, fft_norm) return result
def test_normalization_with_window(): """ Test if the window cancels out if applying the normalization and inverse normalization. """ # test with window as list and numpy array windows = [[1, 1, 1, 1], np.array([1, 1, 1, 1])] fft_norms = ['unitary', 'amplitude', 'rms', 'power', 'psd'] for window in windows: for fft_norm in fft_norms: print(f"testing: {window}, {fft_norm}") spec = fft.normalization(np.array([.5, 1, .5]), 4, 44100, fft_norm, window=window) spec = fft.normalization(spec, 4, 44100, fft_norm, inverse=True, window=window) npt.assert_allclose(spec, np.array([.5, 1, .5]), atol=1e-15)
def test_normalization_single_sided_multi_channel_even_samples(): # single sided test spectrum v = 1/3 + 1/3j vsq = v * np.abs(v) tile = (4, 2, 1) spec_single = np.tile(np.array([v, v, v]), tile) # valid number of samples of time signal corresponding to spec_single N = 4 # time signal with even number of samples Nsq = N**2 # factor for power and psd normalization fs = 40 # arbitrary sampling frequency for psd normalization # expected results for even number of samples sqrt2 = np.sqrt(2) truth = { 'unitary': np.array([v, v * 2, v]), 'amplitude': np.array([v / N, v / N * 2, v / N]), 'rms': np.array([v / N, v / N / sqrt2 * 2, v / N]), 'power': np.array([vsq / Nsq, vsq / Nsq * 2, vsq / Nsq]), 'psd': np.array([vsq / N / fs, vsq / N / fs * 2, vsq / N / fs]) } for normalization in truth: print(f"Assesing normalization: '{normalization}'") spec_out = fft.normalization(spec_single.copy(), N, fs, normalization, inverse=False) npt.assert_allclose(spec_out, np.tile(truth[normalization], tile), atol=1e-15) print(f"Assesing normalization: '{normalization}' (inverse)") spec_out_inv = fft.normalization(spec_out, N, fs, normalization, inverse=True) npt.assert_allclose(spec_out_inv, spec_single, atol=1e-15)
def test_normalization_both_sided_single_channel(): # single sided test spectrum v = 1/3 + 1/3j vsq = v * np.abs(v) spec_single = np.array([v, v, v]) # valid number of samples of time signal corresponding to spec_single N = 3 # time signal with even number of samples Nsq = N**2 # factor for power and psd normalization fs = 30 # arbitrary sampling frequency for psd normalization # expected results for even number of samples truth = { 'unitary': np.array([v, v, v]), 'amplitude': np.array([v / N, v / N, v / N]), 'power': np.array([vsq / Nsq, vsq / Nsq, vsq / Nsq]), 'psd': np.array([vsq / N / fs, vsq / N / fs, vsq / N / fs]) } for normalization in truth: print(f"Assesing normalization: '{normalization}'") spec_out = fft.normalization(spec_single.copy(), N, fs, normalization, inverse=False, single_sided=False) npt.assert_allclose(spec_out, truth[normalization], atol=1e-15) print(f"Assesing normalization: '{normalization}' (inverse)") spec_out_inv = fft.normalization(spec_out, N, fs, normalization, inverse=True, single_sided=False) npt.assert_allclose(spec_out_inv, spec_single, atol=1e-15)
def _get_arithmetic_data(data, n_samples, domain): """ Return data in desired domain without any fft normalization. Parameters ---------- data : Signal, array like, number Input data n_samples : Number of samples of data if data is a Signal (required for fft normalization). domain : 'time', 'freq' Domain in which the data is returned Returns ------- data_out : numpy array Data in desired domain without any fft normalization if data is a Signal. `np.asarray(data)` otherwise. """ if isinstance(data, (Signal, TimeData, FrequencyData)): # get signal in correct domain if domain == "time": data_out = data.time.copy() elif domain == "freq": data_out = data.freq.copy() if isinstance(data, Signal): if data.fft_norm != 'none': # remove current fft normalization data_out = fft.normalization(data_out, n_samples, data.sampling_rate, data.fft_norm, inverse=True) else: raise ValueError( f"domain must be 'time' or 'freq' but found {domain}") else: data_out = np.asarray(data) return data_out
def group_delay(signal, frequencies=None, method='fft'): """Returns the group delay of a signal in samples. Parameters ---------- signal : Signal object An audio signal object from the pyfar signal class frequencies : number array like Frequency or frequencies in Hz at which the group delay is calculated. The default is None, in which case signal.frequencies is used. method : 'scipy', 'fft', optional Method to calculate the group delay of a Signal. Both methods calculate the group delay using the method presented in [1]_ avoiding issues due to discontinuities in the unwrapped phase. Note that the scipy version additionally allows to specify frequencies for which the group delay is evaluated. The default is 'fft', which is faster. Returns ------- group_delay : numpy array Frequency dependent group delay in samples. The array is flattened if a single channel signal was passed to the function. References ---------- .. [1] https://www.dsprelated.com/showarticle/69.php """ # check input and default values if not isinstance(signal, Signal): raise TypeError('Input data has to be of type: Signal.') if frequencies is not None and method == 'fft': raise ValueError( "Specifying frequencies is not supported for the 'fft' method.") frequencies = signal.frequencies if frequencies is None \ else np.asarray(frequencies, dtype=float) if method == 'scipy': # get time signal and reshape for easy looping time = signal.time time = time.reshape((-1, signal.n_samples)) # initialize group delay group_delay = np.zeros((np.prod(signal.cshape), frequencies.size)) # calculate the group delay for cc in range(time.shape[0]): group_delay[cc] = sgn.group_delay((time[cc], 1), frequencies, fs=signal.sampling_rate)[1] # reshape to match signal group_delay = group_delay.reshape(signal.cshape + (-1, )) elif method == 'fft': freq_k = fft.rfft(signal.time * np.arange(signal.n_samples), signal.n_samples, signal.sampling_rate, fft_norm='none') freq = fft.normalization(signal.freq, signal.n_samples, signal.sampling_rate, signal.fft_norm, inverse=True) group_delay = np.real(freq_k / freq) # catch zeros in the denominator group_delay[np.abs(freq) < 1e-15] = 0 else: raise ValueError( "Invalid method, needs to be either 'scipy' or 'fft'.") # flatten in numpy fashion if a single channel is returned if signal.cshape == (1, ): group_delay = np.squeeze(group_delay) return group_delay
def spectrogram(signal, dB=True, log_prefix=20, log_reference=1, window='hann', window_length=1024, window_overlap_fct=0.5): """Compute the magnitude spectrum versus time. This is a wrapper for scipy.signal.spectogram with two differences. First, the returned times refer to the start of the FFT blocks, i.e., the first time is always 0 whereas it is window_length/2 in scipy. Second, the returned spectrogram is normalized accroding to `signal.signal_type` and `signal.fft_norm`. Parameters ---------- signal : Signal pyfar Signal object. db : Boolean Falg to plot the logarithmic magnitude specturm. The default is True. log_prefix : integer, float Prefix for calculating the logarithmic time data. The default is 20. log_reference : integer Reference for calculating the logarithmic time data. The default is 1. window : str Specifies the window (See scipy.signal.get_window). The default is 'hann'. window_length : integer Specifies the window length in samples. The default ist 1024. window_overlap_fct : double Ratio of points to overlap between fft segments [0...1]. The default is 0.5 Returns ------- frequencies : numpy array Frequencies in Hz at which the magnitude spectrum was computed times : numpy array Times in seconds at which the magnitude spectrum was computed spectrogram : numpy array """ # check input if not isinstance(signal, Signal): raise TypeError('Input data has to be of type: Signal.') if window_length > signal.n_samples: raise ValueError("window_length exceeds signal length") # get spectrogram from scipy.signal window_overlap = int(window_length * window_overlap_fct) window = sgn.get_window(window, window_length) frequencies, times, spectrogram = sgn.spectrogram(x=signal.time.squeeze(), fs=signal.sampling_rate, window=window, noverlap=window_overlap, mode='magnitude', scaling='spectrum') # remove normalization from scipy.signal.spectrogram spectrogram /= np.sqrt(1 / window.sum()**2) # apply normalization from signal spectrogram = fft.normalization(spectrogram, window_length, signal.sampling_rate, signal.fft_norm, window=window) # scipy.signal takes the center of the DFT blocks as time stamp we take the # beginning (looks nicer in plots, both conventions are used) times -= times[0] return frequencies, times, spectrogram