import math
import wave  # bearbeiten von .wav-Dateien
import struct
import sounddevice as sd
import numpy as np
import sys  # für Fehlermeldungen
import time as tm

"""---------------------------------------------------------------------------------------------------------------------
In .wav-Dateien wird der Ton in absoluten Werte eingetragen. Die Standart-framerate ist 44100
das heißt für jede Sekunde an Ton gibt es 44100 Werte, die die Tonwelle über die Zeit beschreiben
---------------------------------------------------------------------------------------------------------------------"""


class Tinnitus:  # beinhaltet alle Werte, die vom Nutzer eingestellt werden
    def __init__(self, l_freq=0, r_freq=0, l_amp=0, r_amp=0, l_rausch=0, r_rausch=0, ohr=0):
        self.vorname = ""
        self.nachname = ""
        self.kommentar = ""
        self.linksFrequenz = l_freq
        self.rechtsFrequenz = r_freq
        self.linksLautstaerke = l_amp
        self.rechtsLautstaerke = r_amp
        self.linksRauschenLautstaerke = l_rausch  # float von 0-1 (0 ist aus)
        self.rechtsRauschenLautstaerke = r_rausch
        self.ohr = ohr  # 0:both 1:left 2:right 3:links/rechts unterschiedlich

    def speichern(self):  # speichert die Nutzerdaten in eine .csv-Datei
        datei = open("TinnitusDaten.csv", "w")

        daten = "Vorname;" + self.vorname + "\n"
        daten += "Nachname;" + self.nachname + "\n"
        daten += "linke Frequenz;" + str(self.linksFrequenz) + "\n"
        daten += "linke Lautstärke;" + str(self.linksLautstaerke) + "\n"
        daten += "linkes Rauschen;" + str(self.linksRauschenLautstaerke) + "\n"
        daten += "rechte Frequenz;" + str(self.rechtsFrequenz) + "\n"
        daten += "rechte Lautstärke;" + str(self.rechtsLautstaerke) + "\n"
        daten += "rechtes Rauschen;" + str(self.rechtsRauschenLautstaerke) + "\n"
        daten += "Kommentar;" + str(self.kommentar) + "\n"

        datei.write(daten)
        datei.close()


"""---------------------------------KLASSE: SOUND-----------------------------------------------------------------------
Sound beinhaltet alle Variablen, die zum erstellen einer .wav-Datei benötigt werden (siehe soun.wav_speichern())
   Das 'sound_obj' ist für das dynamische abspielen zuständig (siehe sound.play())
   Beim Initialisieren muss eine Tinnitus-Objekt übergeben werden
---------------------------------------------------------------------------------------------------------------------"""


class Sound:
    def __init__(self, tinnitus, name="MeinTinnitus.wav", audio=None, nchannels=1, sampwidth=2, framerate=44100,
                 comptype="NONE", compname="not compressed", mute=True):
        if audio is None:
            audio = []
        self.tinnitus = tinnitus
        self.name = name
        self.audio = audio  # ein Array, in das die Sound-Werte geschrieben werden (von -1, bis +1)
        self.nchannels = nchannels  # Zahl der audio channels (1:mono 2:stereo)
        self.sampwidth = sampwidth  # Größe eines einzelnen Sound-Werts (in bytes)
        self.framerate = framerate  # Abtastrate
        self.nframes = len(audio)  # Anzahl der Sound-Werte -> Muss bei jeder audio-Änderung aktuallisiert werden
        self.comptype = comptype
        self.compname = compname
        self.mute = mute  # wenn der mute boolean auf true gesetzt ist, sollte kein Ton ausgegeben werden
        self.sound_obj = sd.OutputStream(channels=2, callback=self.callback,
                                         samplerate=self.framerate)  # Objekt fürs Abspielen (siehe sound.play())
        self.start_idx = 0  # wird für sound_obj benötigt

    def wav_speichern(self):  # ezeugt/aktuallisiert die .wav-Datei
        wav_file = wave.open(self.name, "w")
        print("Sound wird als .wav-Datei gespeichert. Bitte warten...\nDer Vorgang kann ca. 30 Sekunden dauern")
        # zuerst muss ein Array mit Audiodaten gefüllt werden
        audio = []
        freq = self.tinnitus.linksFrequenz
        dauer_ms = 1000.0  # 10 Sekunden
        amp = self.tinnitus.linksLautstaerke
        rauschen = self.tinnitus.linksRauschenLautstaerke

        num_samples = dauer_ms * (self.framerate / 1000.0)  # framerate -pro Sekunde- umgerechnet in -pro Millisekunde-

        for x in range(int(num_samples)):  # einen einfachen Sinus ins array schreiben
            audio.append(amp * math.sin(2 * math.pi * freq * (x / self.framerate)))

        if rauschen:  # das Rauschen addieren
            for x in range(int(num_samples)):
                audio[x] += (np.random.rand() - 0.5) * rauschen

        # erst werden die Rahmen-Parameter gesetzt
        self.nframes = len(audio)  # Anpassen des nframes-Parameters
        wav_file.setparams((self.nchannels, self.sampwidth, self.framerate, self.nframes, self.comptype, self.compname))

        # dann wird das audio-Array an die Datei übertragen
        for sample in audio:
            wav_file.writeframes(struct.pack('h', int(sample * 32767.0)))

        wav_file.close()
        print("Speichern beendet.")

    """Die Objekt-Funktion 'start()' startet die asynchrone Soundwiedergabe. Sie ruft dabei immer wieder die Funktion
       sound.callback() auf. Daher können die dort genutzten Variablen dynamisch geändert werden. """

    def play(self):
        if not self.mute:  # Nie abspielen, wenn die GUI auf stumm geschaltet ist
            self.sound_obj.start()

    def stop(self):
        self.sound_obj.stop()  # beendet die asynchrone Soundwiedergabe

    """Die Funktion callback() erzeugt bei jedem Aufruf die Audiodaten, abhängig von den aktuellen Tinnitus-Variablen.
       Die errechneten  Werte werden in das NumPy-Array 'outdata' geschrieben """

    def callback(self, outdata, frames, time, status):
        if status:  # Warnungen, wenn das Soundobj. auf Fehler stößt (hauptsächlich over/underflow wg. Timingproblemen)
            print(status, file=sys.stderr)

        # Sinus ins Array schreiben: f(t) = A * sin(2 * pi * f * t)
        for x in range(frames):
            # links:
            outdata[x][0] = self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz *
                                                                    ((x + self.start_idx) / self.framerate))
            # rechts:
            outdata[x][1] = self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz *
                                                                     ((x + self.start_idx) / self.framerate))
        # Rauschen addieren
        for x in range(frames):
            rand = (np.random.rand() - 0.5)  # Zufallszahl zwischen -0.5 und 0.5
            # links:
            if self.tinnitus.linksRauschenLautstaerke:
                outdata[x][0] += rand * self.tinnitus.linksRauschenLautstaerke
            # rechts:
            if self.tinnitus.rechtsRauschenLautstaerke:
                outdata[x][1] += rand * self.tinnitus.rechtsRauschenLautstaerke

        self.start_idx += frames
