Browse Source

Code aufgeräumt + kleine Verbesserungen

master
Heiko Ommert 3 years ago
parent
commit
c32668034f
2 changed files with 93 additions and 137 deletions
  1. 65
    112
      TinnitusAnalyse/SoundGenerator.py
  2. 28
    25
      TinnitusAnalyse/TinnitusAnalyse_GUI.py

+ 65
- 112
TinnitusAnalyse/SoundGenerator.py View File

@@ -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

self.music_data = self.music_data/32767 # convert array from int16 to float

# ------------------------------------------LEFT EAR FILTERING-------------------------------------------------
# Filterparameter festlegen------------
#Parameter 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
# -------------------------------------
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
# 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()

+ 28
- 25
TinnitusAnalyse/TinnitusAnalyse_GUI.py View File

@@ -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)

Loading…
Cancel
Save