"""Module for generating stochastic signals as part of pyfar.signals."""
import numpy as np
import pyfar
from pyfar.dsp import fft
[docs]
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 bandwidth. The default is ``white``.
rms : double, array like, optional
The root 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
[docs]
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