Datenablage für Tinnitus Therapie Projektarbeit von Julian Seyffer und Heiko Ommert SS2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SoundGenerator.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import wave # bearbeiten von .wav-Dateien
  2. import struct
  3. import sounddevice as sd
  4. import numpy as np
  5. import sys # für Fehlermeldungen
  6. from scipy import signal
  7. import csv
  8. import time
  9. import matplotlib.pyplot as plt
  10. """---------------------------------------------------------------------------------------------------------------------
  11. In .wav-Dateien wird der Ton in absoluten Werte eingetragen. Die Standart-framerate ist 44100
  12. das heißt für jede Sekunde an Ton gibt es 44100 Werte, die die Tonwelle über die Zeit beschreiben
  13. ---------------------------------------------------------------------------------------------------------------------"""
  14. class Tinnitus: # beinhaltet alle Werte, die vom Nutzer eingestellt werden
  15. 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):
  16. self.vorname = ""
  17. self.nachname = ""
  18. self.kommentar = ""
  19. # [Alle Frequenzangaben in Hz, alle Amplitudenangaben von 0-1 (float)]
  20. self.linksFrequenz = l_freq
  21. self.rechtsFrequenz = r_freq
  22. self.linksLautstaerke = l_amp
  23. self.rechtsLautstaerke = r_amp
  24. self.linksRauschenLautstaerke = l_rausch
  25. self.rechtsRauschenLautstaerke = r_rausch
  26. self.linksRauschenUntereGrenzfrequenz = l_rausch_ug
  27. self.rechtsRauschenUntereGrenzfrequenz = r_rausch_ug
  28. self.linksRauschenObereGrenzfrequenz = l_rausch_og
  29. self.rechtsRauschenObereGrenzfrequenz = r_rausch_og
  30. def speichern(self): # speichert die Nutzerdaten in eine .csv-Datei
  31. datei = open("TinnitusDaten.csv", "w")
  32. daten = "Vorname;" + self.vorname + "\n"
  33. daten += "Nachname;" + self.nachname + "\n"
  34. daten += "linke Frequenz;" + str(self.linksFrequenz) + "\n"
  35. daten += "linke Lautstärke;" + str(self.linksLautstaerke) + "\n"
  36. daten += "linkes Rauschen Lautstärke;" + str(self.linksRauschenLautstaerke) + "\n"
  37. daten += "linkes Rauschen untere Grenzfrequenz;" + str(self.linksRauschenUntereGrenzfrequenz) + "\n"
  38. daten += "linkes Rauschen obere Grenzfrequenz;" + str(self.linksRauschenObereGrenzfrequenz) + "\n"
  39. daten += "rechte Frequenz;" + str(self.rechtsFrequenz) + "\n"
  40. daten += "rechte Lautstärke;" + str(self.rechtsLautstaerke) + "\n"
  41. daten += "rechtes Rauschen Lautstärke;" + str(self.rechtsRauschenLautstaerke) + "\n"
  42. daten += "rechtes Rauschen untere Grenzfrequenz;" + str(self.rechtsRauschenUntereGrenzfrequenz) + "\n"
  43. daten += "rechtes Rauschen obere Grenzfrequenz;" + str(self.rechtsRauschenObereGrenzfrequenz) + "\n"
  44. daten += "Kommentar;" + str(self.kommentar) + "\n"
  45. datei.write(daten)
  46. datei.close()
  47. """---------------------------------KLASSE: SOUND-----------------------------------------------------------------------
  48. Sound beinhaltet alle Variablen und Funktionen zum bearbeiten der wav-Dateien und zum abspielen des Sounds in Echtzeit
  49. Beim Initialisieren muss ein Tinnitus-Objekt übergeben werden
  50. ---------------------------------------------------------------------------------------------------------------------"""
  51. class Sound:
  52. def __init__(self, tinnitus):
  53. self.tinnitus = tinnitus
  54. # Variablen für das Abspeichern und Abspielen des Tinnitus-Geräuschs:
  55. self.wav_name = "MeinTinnitus.wav" #Der Dateiname
  56. self.audio = [] # ein Array, in das die Sound-Werte geschrieben werden (von -1, bis +1)
  57. self.nchannels = 2 # Zahl der audio channels (1:mono 2:stereo)
  58. self.sampwidth = 2 # Größe eines einzelnen Sound-Werts (in bytes)
  59. self.framerate = 44100 # Abtastrate
  60. self.nframes = len(self.audio) # Anzahl der Sound-Werte -> Muss bei jeder audio-Änderung aktuallisiert werden
  61. self.comptype = "NONE"
  62. self.compname = "not compressed"
  63. self.mute = True # wenn der mute boolean auf true gesetzt ist, sollte kein Ton ausgegeben werden
  64. self.sound_obj = sd.OutputStream(channels=2, callback=self.callback,
  65. samplerate=self.framerate) # Objekt fürs Abspielen (siehe sound.play())
  66. self.start_idx = 0 # wird für sound_obj benötigt
  67. #Variablen für das Filtern der Musikdatei:
  68. self.music_samplerate = 0 # die samplerate der ausgewählten Musikdatei
  69. self.music_data = 0 # das Numpy Array der ausgewählten Musikdatei
  70. self.filterfortschritt = 0, 0. # für feedback-fkt, 1.Position: Abschnitt-Nmr, 2.Positon: Speicherfortschritt
  71. def wav_speichern(self): # ezeugt/aktuallisiert die .wav-Datei
  72. print("Sound wird als .wav-Datei gespeichert. Bitte warten...\nDer Vorgang kann ca. 10 Sekunden dauern")
  73. wav_obj = wave.open(self.wav_name, "w")
  74. # Die Callback-Funktion aufrufen, um die Audiodaten zu bekommen
  75. nframes = self.framerate * 10 # entspricht 10 Sekunden
  76. status = "" # für den Funktionsaufruf benötigt, sonst keine Funktion
  77. audio = np.ones((nframes, 2)) # Audio-Array initialisieren
  78. audio = self.callback(audio, nframes, self.sound_obj.time, status)
  79. #Audio-Samples auf 1 normieren. Samples müssen zum speichern zwischen -1 und 1 liegen
  80. #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback)
  81. max_ges = 1
  82. for i in range(nframes):
  83. if max_ges < abs(audio[i][0]):
  84. max_ges = abs(audio[i][0])
  85. if max_ges < abs(audio[i][1]):
  86. max_ges = abs(audio[i][1])
  87. #auf 4 Nachkommastellen aufrunden
  88. max_ges = int(max_ges * 10000)
  89. max_ges += 1
  90. max_ges /= 10000
  91. print("X_GES: ", max_ges)
  92. #Alle samples normieren
  93. audio[:, 0] /= max_ges
  94. audio[:, 1] /= max_ges
  95. # Rahmenparameter für die .wav-Datei setzen
  96. self.nframes = len(audio)
  97. wav_obj.setparams((self.nchannels, self.sampwidth, self.framerate, self.nframes, self.comptype, self.compname))
  98. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  99. #Die Audiosamples schreiben
  100. for x in range(self.nframes): # geht jeden Sample-Wert der Musikdatei einzeln durch
  101. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  102. packedMusic.append(struct.pack('h', int(audio[x][0] * 32767.0)))
  103. packedMusic.append(struct.pack('h', int(audio[x][1] * 32767.0)))
  104. value_str = b"".join(packedMusic)
  105. wav_obj.writeframes(value_str)
  106. wav_obj.close()
  107. print("Speichern beendet.")
  108. def play(self):
  109. """sound.play()' startet die asynchrone Soundwiedergabe. Sie ruft dabei immer wieder die Funktion
  110. sound.callback() auf. Daher können die dort genutzten Variablen dynamisch geändert werden. """
  111. if not self.mute: # Nie abspielen, wenn die GUI auf stumm geschaltet ist
  112. self.sound_obj.start() # öffnet thread der immer wieder callback funktion aufruft und diese daten abspielt
  113. def stop(self):
  114. self.sound_obj.stop() # beendet die asynchrone Soundwiedergabe
  115. def callback(self, outdata, frames, time, status):
  116. """erzeugt bei jedem Aufruf die Audiodaten, abhängig von den aktuellen Tinnitus-Variablen.
  117. Die errechneten Werte werden in das NumPy-Array 'outdata' geschrieben """
  118. if status: # Warnungen, wenn das Soundobj. auf Fehler stößt (hauptsächlich over/underflow wg. Timingproblemen)
  119. print(status, file=sys.stderr)
  120. # Whitenoise erzeugen
  121. # Wird auch durchlaufen, wenn es kein Rauschen gibt, damit die alten Daten mit 0 überschrieben werden
  122. for x in range(len(outdata)):
  123. rand = (np.random.rand() - 0.5) # Zufallszahl zwischen -0.5 und 0.5
  124. # links:
  125. outdata[x][0] = rand * self.tinnitus.linksRauschenLautstaerke
  126. # rechts:
  127. outdata[x][1] = rand * self.tinnitus.rechtsRauschenLautstaerke
  128. # Whitenoise durch Bandpass laufen lassen
  129. if self.tinnitus.linksRauschenLautstaerke:
  130. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  131. fGrenz = [self.tinnitus.linksRauschenUntereGrenzfrequenz, self.tinnitus.linksRauschenObereGrenzfrequenz]
  132. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  133. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  134. outdata[:, 0] = signal.sosfilt(sos, outdata[:, 0])
  135. if self.tinnitus.rechtsRauschenLautstaerke:
  136. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  137. fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz, self.tinnitus.rechtsRauschenObereGrenzfrequenz]
  138. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  139. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  140. outdata[:, 1] = signal.sosfilt(sos, outdata[:, 1])
  141. # Sinus addieren: f(t) = A * sin(2 * pi * f * t)
  142. for x in range(len(outdata)):
  143. # links: rauschen und sinus wieder zusammen addieren
  144. outdata[x][0] += self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz *
  145. ((x + self.start_idx) / self.framerate))
  146. # rechts: rauschen und sinus wieder zusammen addieren
  147. outdata[x][1] += self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz *
  148. ((x + self.start_idx) / self.framerate))
  149. self.start_idx += frames
  150. return outdata
  151. def musik_filtern(self):
  152. """
  153. Diese Funktion filtert die Tinnitus Frequenz aus einer gewählten Musikdatei. Dabei geht sie in 4 großen
  154. Schritten vor:
  155. 1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen
  156. 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden"
  157. 3. Überschwinger finden und alle Audiosamples wieder auf 1 normieren
  158. 4. Die fertigen Audiodatei als .wav Datei speichern
  159. """
  160. nframes = len(self.music_data) # Gesamtanzahl der Frames in der Musikdatei
  161. # ------------1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen---------------------
  162. self.filterfortschritt = 1, 0 # der erste schritt
  163. csvstring = open("TinnitusDaten.csv").read()
  164. tinnitus_data = csvstring.split("\n")
  165. # linke Frequenz aus csv Datei holen
  166. lf = tinnitus_data[2]
  167. lf = lf.split(";")
  168. lf = float(lf[1])
  169. # rechte Frequenz aus csv Datei holen
  170. rf = tinnitus_data[7]
  171. rf = rf.split(";")
  172. rf = float(rf[1])
  173. # linke Lautstärke aus cvs Datei holen
  174. ll = tinnitus_data[3]
  175. ll = ll.split(";")
  176. ll = float(ll[1])
  177. # rechte Lautstärke aus cvs Datei holen
  178. rl = tinnitus_data[8]
  179. rl = rl.split(";")
  180. rl = float(rl[1])
  181. # -------- 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden------
  182. self.filterfortschritt = 2, 0 # der zweite schritt im Feedback
  183. start_time = time.time() # einen Timer laufen lassen um zu sehen wie lange Filterung dauert
  184. #Parameter festlegen
  185. order = 501 # Filterordnung
  186. bandwidth = 1000 # Bandbreite des Sperrbereichs in Hz
  187. self.music_data = self.music_data/32767 # convert array from int16 to float
  188. # ------------------------------------------LEFT EAR FILTERING-------------------------------------------------
  189. if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  190. cutoff_frequencies = [(lf - (bandwidth / 2)),
  191. (lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper)
  192. h = signal.firwin(order, [cutoff_frequencies[0], cutoff_frequencies[1]], fs=self.music_samplerate)
  193. music_links = signal.lfilter(h, 1.0, self.music_data[:, 0])
  194. else:
  195. music_links = self.music_data[:, 0] # ungefiltert, wenn kein Tinnitus angegeben wurde
  196. # ------------------------------------- RIGHT EAR FILTERING ------------------------------------------------
  197. if rl != 0.0: # nur wenn die Lautstärke des rechten Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  198. cutoff_frequencies = [(lf - (bandwidth / 2)),
  199. (lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper)
  200. h = signal.firwin(order, [cutoff_frequencies[0], cutoff_frequencies[1]], fs=self.music_samplerate)
  201. music_rechts = signal.lfilter(h, 1.0, self.music_data[:, 1])
  202. else:
  203. music_rechts = self.music_data[:, 1] # ungefiltert, wenn kein Tinnitus angegeben wurde
  204. endTimeFiltering = time.time()
  205. print("benötigte Zeit zum Filtern rechts Ohr =", endTimeFiltering - start_time, "s")
  206. #----------------------- 3. Maxima finden und Samples auf 1 normieren ----------------------------
  207. self.filterfortschritt = 3, 0 # der dritte schritt
  208. #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback)
  209. start_time = time.time()
  210. max_ges = 1
  211. fortschritt = 0
  212. for i in range(nframes):
  213. if max_ges < abs(music_links[i]):
  214. max_ges = abs(music_links[i])
  215. if max_ges < abs(music_rechts[i]):
  216. max_ges = abs(music_rechts[i])
  217. if i % int(nframes/10) == 0: # gibt Fortschritt in 10%-Schritten an
  218. fortschritt += 10
  219. self.filterfortschritt = 3, round(fortschritt, 1)
  220. print(" max: ", self.filterfortschritt[1], "%")
  221. end_time = time.time()
  222. print("Zeitaufwand Maxima-Suche: ", end_time - start_time)
  223. #auf 4 Nachkommastellen aufrunden
  224. start_time = time.time()
  225. max_ges = int(max_ges * 10000)
  226. max_ges += 1
  227. max_ges /= 10000
  228. end_time = time.time()
  229. print("Zeitaufwand Maximum runden: ", end_time - start_time)
  230. #Alle samples normieren
  231. start_time = time.time()
  232. music_links /= max_ges
  233. music_rechts /= max_ges
  234. end_time = time.time()
  235. print("Zeitaufwand samples normieren: ", end_time - start_time)
  236. # ------------------------- 4. Die fertigen Audiodatei als .wav Datei speichern --------------------------------
  237. self.filterfortschritt = [4, 0] # der vierte Schritt
  238. start_time = time.time()
  239. wav_obj = wave.open("MyTinnitusFreeSong.wav", "w")
  240. # Rahmenparameter für die .wav-Datei setzen
  241. wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname))
  242. """The values are stored in a temporary list, and when the process is finished, they are joined together into
  243. an string which is then sent to the output with the traditional writeframes method."""
  244. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  245. # Die Audiosamples schreiben
  246. print("Musikdatei wird erstellt...")
  247. fortschritt = 0
  248. for tinnitus_data in range(nframes): #geht jeden Sample-Wert der Musikdatei einzeln durch
  249. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  250. packedMusic.append(struct.pack('h', int(music_links[tinnitus_data] * 32767.0)))
  251. packedMusic.append(struct.pack('h', int(music_rechts[tinnitus_data] * 32767.0)))
  252. if tinnitus_data % int(nframes/10) == 0: # gibt Fortschritt in 10%-Schritten an
  253. fortschritt += 10
  254. self.filterfortschritt = 4, round(fortschritt, 1)
  255. print(" samples: ", self.filterfortschritt[1], "%")
  256. end_time = time.time()
  257. print("Zeitaufwand für das packen der einzelnen Samples =", end_time - start_time, "s")
  258. value_str = b"".join(packedMusic)
  259. start = time.time()
  260. wav_obj.writeframes(value_str)
  261. end = time.time()
  262. print("Zeitaufwand für das schreiben aller Frames in die wav Datei =", end - start, "s")
  263. wav_obj.close()
  264. print("Speichern beendet.")
  265. self.filterfortschritt = 5, 0 #Nach erfolgreichem Filtern Fortschritt zur Bestätigung auf 5 setzen