import wave # bearbeiten von .wav-Dateien import struct import sounddevice as sd import numpy as np import sys # für Fehlermeldungen from scipy import signal import csv import time import matplotlib.pyplot as plt """--------------------------------------------------------------------------------------------------------------------- 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, l_rausch_ug=10, r_rausch_ug=10, l_rausch_og=20000, r_rausch_og=20000): self.vorname = "" self.nachname = "" self.kommentar = "" # [Alle Frequenzangaben in Hz, alle Amplitudenangaben von 0-1 (float)] self.linksFrequenz = l_freq self.rechtsFrequenz = r_freq self.linksLautstaerke = l_amp self.rechtsLautstaerke = r_amp self.linksRauschenLautstaerke = l_rausch self.rechtsRauschenLautstaerke = r_rausch self.linksRauschenUntereGrenzfrequenz = l_rausch_ug self.rechtsRauschenUntereGrenzfrequenz = r_rausch_ug self.linksRauschenObereGrenzfrequenz = l_rausch_og self.rechtsRauschenObereGrenzfrequenz = r_rausch_og 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 Lautstärke;" + str(self.linksRauschenLautstaerke) + "\n" daten += "linkes Rauschen untere Grenzfrequenz;" + str(self.linksRauschenUntereGrenzfrequenz) + "\n" daten += "linkes Rauschen obere Grenzfrequenz;" + str(self.linksRauschenObereGrenzfrequenz) + "\n" daten += "rechte Frequenz;" + str(self.rechtsFrequenz) + "\n" daten += "rechte Lautstärke;" + str(self.rechtsLautstaerke) + "\n" daten += "rechtes Rauschen Lautstärke;" + str(self.rechtsRauschenLautstaerke) + "\n" daten += "rechtes Rauschen untere Grenzfrequenz;" + str(self.rechtsRauschenUntereGrenzfrequenz) + "\n" daten += "rechtes Rauschen obere Grenzfrequenz;" + str(self.rechtsRauschenObereGrenzfrequenz) + "\n" daten += "Kommentar;" + str(self.kommentar) + "\n" datei.write(daten) datei.close() """---------------------------------KLASSE: SOUND----------------------------------------------------------------------- Sound beinhaltet alle Variablen und Funktionen zum bearbeiten der wav-Dateien und zum abspielen des Sounds in Echtzeit Beim Initialisieren muss ein Tinnitus-Objekt übergeben werden ---------------------------------------------------------------------------------------------------------------------""" class Sound: def __init__(self, tinnitus): self.tinnitus = tinnitus # Variablen für das Abspeichern und Abspielen des Tinnitus-Geräuschs: self.wav_name = "MeinTinnitus.wav" #Der Dateiname self.audio = [] # ein Array, in das die Sound-Werte geschrieben werden (von -1, bis +1) self.nchannels = 2 # Zahl der audio channels (1:mono 2:stereo) self.sampwidth = 2 # Größe eines einzelnen Sound-Werts (in bytes) self.framerate = 44100 # Abtastrate self.nframes = len(self.audio) # Anzahl der Sound-Werte -> Muss bei jeder audio-Änderung aktuallisiert werden self.comptype = "NONE" self.compname = "not compressed" self.mute = True # 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 #Variablen für das Filtern der Musikdatei: self.music_samplerate = 0 # die samplerate der ausgewählten Musikdatei self.music_data = 0 # das Numpy Array der ausgewählten Musikdatei self.filterfortschritt = 0, 0. # für feedback-fkt, 1.Position: Abschnitt-Nmr, 2.Positon: Speicherfortschritt def wav_speichern(self): # ezeugt/aktuallisiert die .wav-Datei print("Sound wird als .wav-Datei gespeichert. Bitte warten...\nDer Vorgang kann ca. 10 Sekunden dauern") wav_obj = wave.open(self.wav_name, "w") # Die Callback-Funktion aufrufen, um die Audiodaten zu bekommen nframes = self.framerate * 10 # entspricht 10 Sekunden status = "" # für den Funktionsaufruf benötigt, sonst keine Funktion audio = np.ones((nframes, 2)) # Audio-Array initialisieren audio = self.callback(audio, nframes, self.sound_obj.time, status) #Audio-Samples auf 1 normieren. Samples müssen zum speichern zwischen -1 und 1 liegen #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback) max_ges = 1 for i in range(nframes): if max_ges < abs(audio[i][0]): max_ges = abs(audio[i][0]) if max_ges < abs(audio[i][1]): max_ges = abs(audio[i][1]) #auf 4 Nachkommastellen aufrunden max_ges = int(max_ges * 10000) max_ges += 1 max_ges /= 10000 print("X_GES: ", max_ges) #Alle samples normieren audio[:, 0] /= max_ges audio[:, 1] /= max_ges # Rahmenparameter für die .wav-Datei setzen self.nframes = len(audio) wav_obj.setparams((self.nchannels, self.sampwidth, self.framerate, self.nframes, self.comptype, self.compname)) packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen #Die Audiosamples schreiben for x in range(self.nframes): # geht jeden Sample-Wert der Musikdatei einzeln durch # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden packedMusic.append(struct.pack('h', int(audio[x][0] * 32767.0))) packedMusic.append(struct.pack('h', int(audio[x][1] * 32767.0))) value_str = b"".join(packedMusic) wav_obj.writeframes(value_str) wav_obj.close() print("Speichern beendet.") def play(self): """sound.play()' 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. """ if not self.mute: # Nie abspielen, wenn die GUI auf stumm geschaltet ist self.sound_obj.start() # öffnet thread der immer wieder callback funktion aufruft und diese daten abspielt def stop(self): self.sound_obj.stop() # beendet die asynchrone Soundwiedergabe def callback(self, outdata, frames, time, status): """erzeugt bei jedem Aufruf die Audiodaten, abhängig von den aktuellen Tinnitus-Variablen. Die errechneten Werte werden in das NumPy-Array 'outdata' geschrieben """ if status: # Warnungen, wenn das Soundobj. auf Fehler stößt (hauptsächlich over/underflow wg. Timingproblemen) print(status, file=sys.stderr) # Whitenoise erzeugen # Wird auch durchlaufen, wenn es kein Rauschen gibt, damit die alten Daten mit 0 überschrieben werden for x in range(len(outdata)): rand = (np.random.rand() - 0.5) # Zufallszahl zwischen -0.5 und 0.5 # links: outdata[x][0] = rand * self.tinnitus.linksRauschenLautstaerke # rechts: outdata[x][1] = rand * self.tinnitus.rechtsRauschenLautstaerke # Whitenoise durch Bandpass laufen lassen if self.tinnitus.linksRauschenLautstaerke: # (-3dB Grenzen) bzw was der Bandpass durchlässt fGrenz = [self.tinnitus.linksRauschenUntereGrenzfrequenz, self.tinnitus.linksRauschenObereGrenzfrequenz] sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos') # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind outdata[:, 0] = signal.sosfilt(sos, outdata[:, 0]) if self.tinnitus.rechtsRauschenLautstaerke: # (-3dB Grenzen) bzw was der Bandpass durchlässt fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz, self.tinnitus.rechtsRauschenObereGrenzfrequenz] sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos') # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind outdata[:, 1] = signal.sosfilt(sos, outdata[:, 1]) # Sinus addieren: f(t) = A * sin(2 * pi * f * t) for x in range(len(outdata)): # links: rauschen und sinus wieder zusammen addieren outdata[x][0] += self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz * ((x + self.start_idx) / self.framerate)) # rechts: rauschen und sinus wieder zusammen addieren outdata[x][1] += self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz * ((x + self.start_idx) / self.framerate)) self.start_idx += frames return outdata def musik_filtern(self): """ Diese Funktion filtert die Tinnitus Frequenz aus einer gewählten Musikdatei. Dabei geht sie in 4 großen Schritten vor: 1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden" 3. Überschwinger finden und alle Audiosamples wieder auf 1 normieren 4. Die fertigen Audiodatei als .wav Datei speichern """ nframes = len(self.music_data) # Gesamtanzahl der Frames in der Musikdatei # ------------1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen--------------------- self.filterfortschritt = 1, 0 # der erste schritt csvstring = open("TinnitusDaten.csv").read() tinnitus_data = csvstring.split("\n") # linke Frequenz aus csv Datei holen lf = tinnitus_data[2] lf = lf.split(";") lf = float(lf[1]) # rechte Frequenz aus csv Datei holen rf = tinnitus_data[7] rf = rf.split(";") rf = float(rf[1]) # linke Lautstärke aus cvs Datei holen ll = tinnitus_data[3] ll = ll.split(";") ll = float(ll[1]) # rechte Lautstärke aus cvs Datei holen rl = tinnitus_data[8] rl = rl.split(";") rl = float(rl[1]) # -------- 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden------ self.filterfortschritt = 2, 0 # der zweite schritt im Feedback start_time = time.time() # einen Timer laufen lassen um zu sehen wie lange Filterung dauert #Parameter festlegen order = 501 # Filterordnung bandwidth = 1000 # Bandbreite des Sperrbereichs in Hz self.music_data = self.music_data/32767 # convert array from int16 to float # ------------------------------------------LEFT EAR FILTERING------------------------------------------------- if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert cutoff_frequencies = [(lf - (bandwidth / 2)), (lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper) h = signal.firwin(order, [cutoff_frequencies[0], cutoff_frequencies[1]], fs=self.music_samplerate) music_links = signal.lfilter(h, 1.0, self.music_data[:, 0]) else: music_links = self.music_data[:, 0] # ungefiltert, wenn kein Tinnitus angegeben wurde # ------------------------------------- RIGHT EAR FILTERING ------------------------------------------------ if rl != 0.0: # nur wenn die Lautstärke des rechten Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert cutoff_frequencies = [(lf - (bandwidth / 2)), (lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper) h = signal.firwin(order, [cutoff_frequencies[0], cutoff_frequencies[1]], fs=self.music_samplerate) music_rechts = signal.lfilter(h, 1.0, self.music_data[:, 1]) else: music_rechts = self.music_data[:, 1] # ungefiltert, wenn kein Tinnitus angegeben wurde endTimeFiltering = time.time() print("benötigte Zeit zum Filtern rechts Ohr =", endTimeFiltering - start_time, "s") #----------------------- 3. Maxima finden und Samples auf 1 normieren ---------------------------- self.filterfortschritt = 3, 0 # der dritte schritt #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback) start_time = time.time() max_ges = 1 fortschritt = 0 for i in range(nframes): if max_ges < abs(music_links[i]): max_ges = abs(music_links[i]) if max_ges < abs(music_rechts[i]): max_ges = abs(music_rechts[i]) if i % int(nframes/10) == 0: # gibt Fortschritt in 10%-Schritten an fortschritt += 10 self.filterfortschritt = 3, round(fortschritt, 1) print(" max: ", self.filterfortschritt[1], "%") end_time = time.time() print("Zeitaufwand Maxima-Suche: ", end_time - start_time) #auf 4 Nachkommastellen aufrunden start_time = time.time() max_ges = int(max_ges * 10000) max_ges += 1 max_ges /= 10000 end_time = time.time() print("Zeitaufwand Maximum runden: ", end_time - start_time) #Alle samples normieren start_time = time.time() music_links /= max_ges music_rechts /= max_ges end_time = time.time() print("Zeitaufwand samples normieren: ", end_time - start_time) # ------------------------- 4. Die fertigen Audiodatei als .wav Datei speichern -------------------------------- self.filterfortschritt = [4, 0] # der vierte Schritt start_time = time.time() wav_obj = wave.open("MyTinnitusFreeSong.wav", "w") # Rahmenparameter für die .wav-Datei setzen wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname)) """The values are stored in a temporary list, and when the process is finished, they are joined together into an string which is then sent to the output with the traditional writeframes method.""" packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen # Die Audiosamples schreiben print("Musikdatei wird erstellt...") fortschritt = 0 for tinnitus_data in range(nframes): #geht jeden Sample-Wert der Musikdatei einzeln durch # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden packedMusic.append(struct.pack('h', int(music_links[tinnitus_data] * 32767.0))) packedMusic.append(struct.pack('h', int(music_rechts[tinnitus_data] * 32767.0))) if tinnitus_data % int(nframes/10) == 0: # gibt Fortschritt in 10%-Schritten an fortschritt += 10 self.filterfortschritt = 4, round(fortschritt, 1) print(" samples: ", self.filterfortschritt[1], "%") end_time = time.time() print("Zeitaufwand für das packen der einzelnen Samples =", end_time - start_time, "s") value_str = b"".join(packedMusic) start = time.time() wav_obj.writeframes(value_str) end = time.time() print("Zeitaufwand für das schreiben aller Frames in die wav Datei =", end - start, "s") wav_obj.close() print("Speichern beendet.") self.filterfortschritt = 5, 0 #Nach erfolgreichem Filtern Fortschritt zur Bestätigung auf 5 setzen