97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
|
|
# Copyright 2023 XMOS LIMITED.
|
||
|
|
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||
|
|
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
import numpy as np
|
||
|
|
import soundfile
|
||
|
|
from scipy.io import wavfile # soundfile has some issues writing high Fs files
|
||
|
|
|
||
|
|
class audio_modulator:
|
||
|
|
"""
|
||
|
|
This test helper generates a wav file with a fixed sample rate and tone frequency
|
||
|
|
of a certain length.
|
||
|
|
A method then allows sections of it to be frequency modulated by a value in Hz.
|
||
|
|
The modulated signal (which uses cumultaive phase to avoid discontinuites)
|
||
|
|
may then be plotted as an FFT to understand the SNR/THD and may also be saved
|
||
|
|
as a wav file.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, duration_s, sample_rate=48000, test_tone_hz=1000):
|
||
|
|
self.sample_rate = sample_rate
|
||
|
|
self.test_tone_hz = test_tone_hz
|
||
|
|
|
||
|
|
self.modulator = np.full(int(duration_s * sample_rate), test_tone_hz, dtype=np.float64)
|
||
|
|
|
||
|
|
def apply_frequency_deviation(self, start_s, end_s, delta_freq):
|
||
|
|
start_idx = int(start_s * self.sample_rate)
|
||
|
|
end_idx = int(end_s * self.sample_rate)
|
||
|
|
self.modulator[start_idx:end_idx] += delta_freq
|
||
|
|
|
||
|
|
def modulate_waveform(self):
|
||
|
|
# Now create the frequency modulated waveform
|
||
|
|
# this is designed to accumulate the phase so doesn't see discontinuities
|
||
|
|
# https://dsp.stackexchange.com/questions/80768/fsk-modulation-with-python
|
||
|
|
delta_phi = self.modulator * np.pi / (self.sample_rate / 2.0)
|
||
|
|
phi = np.cumsum(delta_phi)
|
||
|
|
self.waveform = np.sin(phi)
|
||
|
|
|
||
|
|
def save_modulated_wav(self, filename):
|
||
|
|
integer_output = np.int16(self.waveform * 32767)
|
||
|
|
# soundfile.write(filename, integer_output, int(self.sample_rate)) # This struggles with >768ksps
|
||
|
|
wavfile.write(filename, int(self.sample_rate), integer_output)
|
||
|
|
|
||
|
|
def plot_modulated_fft(self, filename, skip_s=None):
|
||
|
|
start_x = 0 if skip_s is None else int(skip_s * self.sample_rate) // 2 * 2
|
||
|
|
waveform = self.waveform[start_x:]
|
||
|
|
|
||
|
|
xf = np.linspace(0.0, 1.0/(2.0/self.sample_rate), waveform.size // 2)
|
||
|
|
N = xf.size
|
||
|
|
window = np.kaiser(N*2, 14)
|
||
|
|
waveform = waveform * window
|
||
|
|
yf = np.fft.fft(waveform)
|
||
|
|
fig, ax = plt.subplots()
|
||
|
|
|
||
|
|
# Plot a zoom in on the test
|
||
|
|
tone_idx = int(self.test_tone_hz / (self.sample_rate / 2) * N)
|
||
|
|
num_side_bins = 50
|
||
|
|
yf = 20 * np.log10(np.abs(yf) / N)
|
||
|
|
# ax.plot(xf[tone_idx - num_side_bins:tone_idx + num_side_bins], yf[tone_idx - num_side_bins:tone_idx + num_side_bins], marker='.')
|
||
|
|
|
||
|
|
# Plot the whole frequncy range from DC to nyquist
|
||
|
|
ax.plot(xf[:N], yf[:N], marker='.')
|
||
|
|
ax.set_xscale("log")
|
||
|
|
plt.xlim((10**1, 10**5))
|
||
|
|
plt.ylim((-200, 0))
|
||
|
|
plt.savefig(filename, dpi=150)
|
||
|
|
|
||
|
|
def load_wav(self, filename):
|
||
|
|
"""
|
||
|
|
Used for testing only - load a wav into self.waveform
|
||
|
|
"""
|
||
|
|
self.waveform, self.sample_rate = soundfile.read(filename)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
"""
|
||
|
|
This module is not intended to be run directly. This is here for internal testing only.
|
||
|
|
"""
|
||
|
|
if 0:
|
||
|
|
test_len = 10
|
||
|
|
audio = audio_modulator(test_len)
|
||
|
|
for time_s in range(test_len):
|
||
|
|
modulation_hz = 10 * (time_s - (test_len) / 2)
|
||
|
|
audio.apply_frequency_deviation(time_s, time_s + 1, modulation_hz)
|
||
|
|
|
||
|
|
audio.modulate_waveform()
|
||
|
|
audio.save_modulated_wav("modulated.wav")
|
||
|
|
audio.plot_modulated_fft("modulated_fft.png")
|
||
|
|
|
||
|
|
else:
|
||
|
|
audio = audio_modulator(1)
|
||
|
|
audio.load_wav("modulated_tone_1000Hz_sd_ds.wav")
|
||
|
|
# audio = audio_modulator(1, sample_rate=3072000)
|
||
|
|
# audio.modulate_waveform()
|
||
|
|
audio.plot_modulated_fft("modulated_tone_1000Hz_sd_ds.png")
|
||
|
|
# audio.save_modulated_wav("modulated.wav")
|
||
|
|
|