From c32668034ff39553c1416e2655be5689464905ae Mon Sep 17 00:00:00 2001 From: ommerthe69431 Date: Mon, 5 Oct 2020 18:00:09 +0200 Subject: [PATCH] =?UTF-8?q?Code=20aufger=C3=A4umt=20+=20kleine=20Verbesser?= =?UTF-8?q?ungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TinnitusAnalyse/SoundGenerator.py | 177 +++++++++---------------- TinnitusAnalyse/TinnitusAnalyse_GUI.py | 53 ++++---- 2 files changed, 93 insertions(+), 137 deletions(-) diff --git a/TinnitusAnalyse/SoundGenerator.py b/TinnitusAnalyse/SoundGenerator.py index ec58e17..c8adda3 100644 --- a/TinnitusAnalyse/SoundGenerator.py +++ b/TinnitusAnalyse/SoundGenerator.py @@ -15,7 +15,7 @@ das heißt für jede Sekunde an Ton gibt es 44100 Werte, die die Tonwelle über 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, l_rausch_ug=10, r_rausch_ug=10, l_rausch_og=20000, r_rausch_og=20000): + 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 = "" @@ -55,30 +55,31 @@ class Tinnitus: # beinhaltet alle Werte, die vom Nutzer eingestellt werden """---------------------------------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()) +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, wav_name="MeinTinnitus.wav", audio=None, nchannels=2, sampwidth=2, framerate=44100, - comptype="NONE", compname="not compressed", mute=True): - if audio is None: - audio = [] + def __init__(self, tinnitus): + self.tinnitus = tinnitus - self.wav_name = wav_name #Der Dateiname - 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 + + # 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 @@ -90,10 +91,31 @@ class Sound: wav_obj = wave.open(self.wav_name, "w") # Die Callback-Funktion aufrufen, um die Audiodaten zu bekommen - frames = self.framerate * 10 # entspricht 10 Sekunden + nframes = self.framerate * 10 # entspricht 10 Sekunden status = "" # für den Funktionsaufruf benötigt, sonst keine Funktion - audio = np.ones((frames, 2)) - audio = self.callback(audio, frames, self.sound_obj.time, status) + 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) @@ -113,10 +135,10 @@ class Sound: wav_obj.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): + """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 @@ -124,15 +146,16 @@ class Sound: 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): + """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: @@ -143,39 +166,14 @@ class Sound: # 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 (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet - # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2) + 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]) - # Plotten des Filters für Filterentwicklung und Dokumentation nützlich--------- - # w, h = signal.sosfreqz(sos, worN=1500) - # plt.subplot(2, 1, 1) - # db = 20 * np.log10(np.maximum(np.abs(h), 1e-5)) - # plt.plot(w / np.pi, db) - # plt.ylim(-75, 5) - # plt.grid(True) - # plt.yticks([0, -20, -40, -60]) - # plt.ylabel('Gain [dB]') - # plt.title('Frequency Response') - # plt.subplot(2, 1, 2) - # plt.plot(w / np.pi, np.angle(h)) - # plt.grid(True) - # plt.yticks([-np.pi, -0.5 * np.pi, 0, 0.5 * np.pi, np.pi], - # [r'$-\pi$', r'$-\pi/2$', '0', r'$\pi/2$', r'$\pi$']) - # plt.ylabel('Phase [rad]') - # plt.xlabel('Normalized frequency (1.0 = Nyquist)') - # plt.show() - # ------------------------------------------------------------------------------- if self.tinnitus.rechtsRauschenLautstaerke: # (-3dB Grenzen) bzw was der Bandpass durchlässt - fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz, - self.tinnitus.rechtsRauschenObereGrenzfrequenz] - # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet - # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2) + 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]) @@ -191,16 +189,17 @@ class Sound: ((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 3 großen + 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. Die fertigen Audiodatei als .wav Datei speichern + 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 @@ -235,53 +234,28 @@ class Sound: 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------------------------------------------------- - # Filterparameter festlegen------------ - order = 501 # Filterordnung - bandwidth = 1000 # Bandbreite des Sperrbereichs in Hz - #stop_attenuation = 100 # minimum Attenuation (Damping, Reduction) in stop Band [only for elliptic filter necessary] - cutoff_frequencies = [(lf - (bandwidth / 2)), (lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper) - max_ripple_passband = 50 # Maximal erlaubte Welligkeit im Passbereich - # ------------------------------------- - if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert - # b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter', - # fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (links) - # - # music_links = signal.lfilter(b, a, self.music_data[:, 0]) # diese Funktion wendet den Filter an - # - # print("b=", b) - # print("a=", a) - - # FIR Filterversuch - #h = signal.firwin(order, cutoff_frequencies, pass_zero="bandstop", fs=self.music_samplerate, width=bandwidth, - # window="hamming") + 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) - print("h= ", h) 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 = [(rf - (bandwidth / 2)), ( - rf + (bandwidth / 2))] # change the cutoff frequencies to the tinnitus of the RIGHT EAR - - # h = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter', - # fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (rechts) - # - # music_rechts = signal.lfilter(b, a, self.music_data[:, 1]) # rechts - - # FIR Filterversuch - print("UG Freq = ", cutoff_frequencies[0]/(self.music_samplerate/2)) + 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]) + music_rechts = signal.lfilter(h, 1.0, self.music_data[:, 1]) else: - music_rechts = self.music_data[:, 1] # diese Funktion filtert die Audiodaten(die Tinnitusfreq wird entfernt) + 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") @@ -291,14 +265,14 @@ class Sound: #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback) start_time = time.time() - max_ges = 0 + 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: # glaub hier stand 10000 davor oder 50000 + 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], "%") @@ -320,9 +294,9 @@ class Sound: 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") @@ -341,10 +315,7 @@ class Sound: # 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))) - - # wav_obj.writeframes(struct.pack('h', int(music_links[x] * 32767.0))) # Werte für links und rechts werden bei - # wav_obj.writeframes(struct.pack('h', int(musicRechts[x] * 32767.0))) # wav abwechselnd eingetragen - if tinnitus_data % int(nframes/10) == 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], "%") @@ -352,7 +323,6 @@ class Sound: 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() @@ -363,20 +333,3 @@ class Sound: print("Speichern beendet.") self.filterfortschritt = 5, 0 #Nach erfolgreichem Filtern Fortschritt zur Bestätigung auf 5 setzen - # Plot (hilfreich für Filterentwurf) - # freq, h = signal.freqz(b, a, fs=self.music_samplerate) - # fig, ax = plt.subplots(2, 1, figsize=(8, 6)) - # ax[0].plot(freq, 20 * np.log10(abs(h)), color='blue') - # ax[0].set_title("Frequency Response") - # ax[0].set_ylabel("Amplitude (dB)", color='blue') - # ax[0].set_xlim([0, 10000]) - # ax[0].set_ylim([-120, 10]) - # ax[0].grid() - # ax[1].plot(freq, np.unwrap(np.angle(h)) * 180 / np.pi, color='green') - # ax[1].set_ylabel("Angle (degrees)", color='green') - # ax[1].set_xlabel("Frequency (Hz)") - # ax[1].set_xlim([0, 10000]) - # ax[1].set_yticks([-90, -60, -30, 0, 30, 60, 90]) - # ax[1].set_ylim([-90, 90]) - # ax[1].grid() - # plt.show() diff --git a/TinnitusAnalyse/TinnitusAnalyse_GUI.py b/TinnitusAnalyse/TinnitusAnalyse_GUI.py index d089ce2..5124075 100644 --- a/TinnitusAnalyse/TinnitusAnalyse_GUI.py +++ b/TinnitusAnalyse/TinnitusAnalyse_GUI.py @@ -134,7 +134,7 @@ def unten_button_speichern_press(): sound.wav_speichern() feedback("Daten erfolgreich gespeichert. Siehe: " + sound.wav_name, "white", "green") except: - feedback("Fehlgeschlagener Speicherversuch! Bitte schließe Microsoft Excel.", "white", "red") + feedback("Fehlgeschlagener Speicherversuch! Bitte schließe Microsoft Excel.", "white", "red") def unten_button_play_press(): @@ -151,28 +151,6 @@ def unten_button_stop_press(): sound.stop() -def feedback(text, fontcolor="black", backgroundcolor="lightsteelblue"): - """ This is a helper function. You can give it a string text and it will display it in the feedback frame (bottom - right of the GUI) in the text widget. The parameter color is also a string and defines the font color. Same with - background. Honestly this function is way too complicated, but Tkinter has no nicer/easier builtin way of doing the - coloring nicely """ - feedback.lineCounter += 1 # in order to color the texts nicely we need to count the lines of text we add - untenFeedbackText.config(state=NORMAL) # activate text field (otherwise it is readonly) - - if feedback.lineCounter == 12: # if we reached the end of the text box - untenFeedbackText.delete("1.0", END) # just delete everything - feedback.lineCounter = 1 # and start at line 1 again - - untenFeedbackText.insert(INSERT, text + "\n") # insert the text - # these 2 lines just color the text nicely, but Tkinter forces your to first "tag_add" mark it and specify the - # line number and char number you want to mark. And then "tag_config" change the color of this marked region - untenFeedbackText.tag_add("Line"+str(feedback.lineCounter), str(feedback.lineCounter)+".0", str(float(len(text)))) - untenFeedbackText.tag_config("Line"+str(feedback.lineCounter), foreground=fontcolor, background=backgroundcolor) - - untenFeedbackText.config(state=DISABLED) # set the text field back to readonly - root.update() #Damit der Text sofort ausgegeben wird, auch wenn das Programm erst noch was anderes macht - - def unten_button_musikdatei_laden_press(): """ This function opends a window that lets you select .mp3 and .wav files. The user is supposed to select their music files here""" @@ -241,12 +219,37 @@ def unten_button_filtere_tinnitus_aus_musik(): "gestellt ist. Sonst gehen wir davon aus, dass auf diesem Ohr kein Tinnitus vorliegt.", "red", "white") -""" Initialisierungen """ + +"""--------------Feedback Funktion------------------""" + +def feedback(text, fontcolor="black", backgroundcolor="lightsteelblue"): + """ This is a helper function. You can give it a string text and it will display it in the feedback frame (bottom + right of the GUI) in the text widget. The parameter color is also a string and defines the font color. Same with + background. Honestly this function is way too complicated, but Tkinter has no nicer/easier builtin way of doing the + coloring nicely """ + feedback.lineCounter += 1 # in order to color the texts nicely we need to count the lines of text we add + untenFeedbackText.config(state=NORMAL) # activate text field (otherwise it is readonly) + + if feedback.lineCounter == 12: # if we reached the end of the text box + untenFeedbackText.delete("1.0", END) # just delete everything + feedback.lineCounter = 1 # and start at line 1 again + + untenFeedbackText.insert(INSERT, text + "\n") # insert the text + # these 2 lines just color the text nicely, but Tkinter forces your to first "tag_add" mark it and specify the + # line number and char number you want to mark. And then "tag_config" change the color of this marked region + untenFeedbackText.tag_add("Line"+str(feedback.lineCounter), str(feedback.lineCounter)+".0", str(float(len(text)))) + untenFeedbackText.tag_config("Line"+str(feedback.lineCounter), foreground=fontcolor, background=backgroundcolor) + + untenFeedbackText.config(state=DISABLED) # set the text field back to readonly + root.update() #Damit der Text sofort ausgegeben wird, auch wenn das Programm erst noch was anderes macht + +"""------------------ Initialisierungen --------------------------""" tinnitus = Tinnitus() # siehe SoundGenerator.py sound = Sound(tinnitus) # siehe SoundGenerator.py feedback.lineCounter = 0 # Funktionsvariable der Feedback funktion. Ein Funktionsaufruf Counter -"""------------------------------------------ AUFBAU DES ROOT WINDOWS -----------------------------------------------""" + +"""---------------------------------- AUFBAU DES ROOT WINDOWS -----------------------------------------""" root = Tk() # build the main window root.title("Tinnitus Analyse") root.minsize(width=800, height=500) # set windowsize (width an height in pixels)