import numpy as np
[docs]
def fm_sine_generator(xmod, fs, fc, k, spl_level, print_info=False):
"""
Creates a frequency-modulated (FM) signal of level 'spl_level' (in dB SPL)
with sinusoidal carrier of frequency 'fc', arbitrary modulating signal
'xm', frequency sensitivity 'k', and sampling frequency 'fs'. The FM signal
length is the same as the length of 'xm'.
Parameters
----------
xmod: array
Modulating signal, dim(N)
fs: float
Sampling frequency, in [Hz].
fc: float
Carrier frequency, in [Hz]. Must be less than 'fs/2'.
k: float
Frequency sensitivity of the modulator.
spl_level: float
Sound Pressure Level [dB ref 20 uPa RMS] of the modulated signal.
print_info: bool, optional
If True, the maximum frequency deviation and modulation index are printed.
Default is False
Returns
-------
y_fm: numpy.array
Frequency-modulated signal with sine carrier, dim(N) in [Pa].
inst_freq: numpy.array
Instantaneaous frequency, dim(N)
max_freq_deviation: float
Maximum frequency deviation [Hz]
FM_modulation_index: float
Modulation index
Warning
-------
spl_level must be provided in dB, ref=2e-5 Pa.
See Also
--------
.am_sine_generator : Amplitude modulation with sine wave carrier
.am_noise_generator : Amplitude modulation with broadband noise carrier
.sine_wave_generator : Sine wave generation
Notes
-----
The frequency sensitivity 'k' is equal to the frequency deviation in Hz
away from 'fc' per unit amplitude of the modulating signal 'xmod'.
Examples
--------
.. plot::
:include-source:
>>> from mosqito.utils import fm_sine_generator
>>> import matplotlib.pyplot as plt
>>> fs = 48000 # [Hz]
>>> duration = 1
>>> t = np.linspace(0, duration, int(fs*duration))
>>> dB = 60 # [dB SPL]
>>> fc = 50 # [Hz]
>>> k = fc//2 # [Hz per unit amplitude of 'xm']
>>> fm = 10 # [Hz]
>>> xmod = np.sin(2*np.pi*t*fm)
>>> y_fm, inst_freq, f_delta, m = fm_sine_generator(xmod, fs, fc, k, dB, True)
>>> fig, plots = plt.subplots(3, 1)
>>> plots[0].set_title('Frequency-modulated sine wave')
>>> plots[0].plot(t, xmod, 'C0', label='Modulating signal')
>>> plots[0].legend(loc='upper right')
>>> plots[0].grid()
>>> plots[0].set_ylabel('Amplitude')
>>> plots[0].set_xlim([0, duration])
>>> plots[1].plot(t, y_fm, '#69c3c5', label='FM signal')
>>> plots[1].legend(loc='upper right')
>>> plots[1].grid()
>>> plots[1].set_ylabel('Amplitude')
>>> plots[1].set_xlim([0, duration])
>>> plots[2].plot(t, inst_freq, '#7894cf', label='Inst Frequency')
>>> plots[2].legend(loc='upper right')
>>> plots[2].grid()
>>> plots[2].hlines(fc, -0.1, 1.2*duration, color='k', linestyle='--')
>>> plots[2].text(0.0025, fc*0.7, 'carrier freq $f_c$', fontsize=12)
>>> plots[2].set_ylim([0, 1.1*(fc+k)])
>>> plots[2].set_xlim([0, duration])
>>> plots[2].set_ylabel('Frequency')
>>> plots[2].set_xlabel('Time [s]')
>>> plt.tight_layout()
"""
assert fc < fs/2, "Carrier frequency 'fc' must be less than 'fs/2'!"
# sampling interval in seconds
dt = 1/fs
# instantaneous frequency of FM signal
inst_freq = fc + k*xmod
# unit-amplitude FM signal
y_fm = np.sin(2*np.pi * np.cumsum(inst_freq)*dt)
# max frequency deviation
f_delta = k * np.max(np.abs(xmod))
# FM modulation index
m = np.max(np.abs(2*np.pi * k * np.cumsum(xmod)*dt))
# Apply amplitude factor to obtain 'spl_level'
p_ref = 20e-6
A_rms = p_ref * 10**(spl_level/20)
y_fm *= A_rms/np.std(y_fm)
if print_info:
print(f'\tMax freq deviation: {f_delta} Hz')
print(f'\tFM modulation index: {m:.2f}')
return y_fm, inst_freq, f_delta, m