# Copyright 2023-2025 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")

