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 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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, ohr=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, die zum erstellen einer .wav-Datei benötigt werden (siehe soun.wav_speichern())
  49. Das 'sound_obj' ist für das dynamische abspielen zuständig (siehe sound.play())
  50. Beim Initialisieren muss ein Tinnitus-Objekt übergeben werden
  51. ---------------------------------------------------------------------------------------------------------------------"""
  52. class Sound:
  53. def __init__(self, tinnitus, wav_name="MeinTinnitus.wav", audio=None, nchannels=2, sampwidth=2, framerate=44100,
  54. comptype="NONE", compname="not compressed", mute=True):
  55. if audio is None:
  56. audio = []
  57. self.tinnitus = tinnitus
  58. self.wav_name = wav_name #Der Dateiname
  59. self.audio = audio # ein Array, in das die Sound-Werte geschrieben werden (von -1, bis +1)
  60. self.nchannels = nchannels # Zahl der audio channels (1:mono 2:stereo)
  61. self.sampwidth = sampwidth # Größe eines einzelnen Sound-Werts (in bytes)
  62. self.framerate = framerate # Abtastrate
  63. self.nframes = len(audio) # Anzahl der Sound-Werte -> Muss bei jeder audio-Änderung aktuallisiert werden
  64. self.comptype = comptype
  65. self.compname = compname
  66. self.mute = mute # wenn der mute boolean auf true gesetzt ist, sollte kein Ton ausgegeben werden
  67. self.sound_obj = sd.OutputStream(channels=2, callback=self.callback,
  68. samplerate=self.framerate) # Objekt fürs Abspielen (siehe sound.play())
  69. self.start_idx = 0 # wird für sound_obj benötigt
  70. self.music_samplerate = 0 # die samplerate der ausgewählten Musikdatei
  71. self.music_data = 0 # das Numpy Array der ausgewählten Musikdatei
  72. self.filterfortschritt = 0, 0. # für feedback-fkt, 1.Position: Abschnitt-Nmr, 2.Positon: Speicherfortschritt
  73. def wav_speichern(self): # ezeugt/aktuallisiert die .wav-Datei
  74. print("Sound wird als .wav-Datei gespeichert. Bitte warten...\nDer Vorgang kann ca. 10 Sekunden dauern")
  75. wav_obj = wave.open(self.wav_name, "w")
  76. # Die Callback-Funktion aufrufen, um die Audiodaten zu bekommen
  77. frames = self.framerate * 10 # entspricht 10 Sekunden
  78. status = "" # für den Funktionsaufruf benötigt, sonst keine Funktion
  79. audio = np.ones((frames, 2))
  80. audio = self.callback(audio, frames, self.sound_obj.time, status)
  81. # Rahmenparameter für die .wav-Datei setzen
  82. self.nframes = len(audio)
  83. wav_obj.setparams((self.nchannels, self.sampwidth, self.framerate, self.nframes, self.comptype, self.compname))
  84. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  85. #Die Audiosamples schreiben
  86. for x in range(self.nframes): # geht jeden Sample-Wert der Musikdatei einzeln durch
  87. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  88. packedMusic.append(struct.pack('h', int(audio[x][0] * 32767.0)))
  89. packedMusic.append(struct.pack('h', int(audio[x][1] * 32767.0)))
  90. value_str = b"".join(packedMusic)
  91. wav_obj.writeframes(value_str)
  92. wav_obj.close()
  93. print("Speichern beendet.")
  94. """Die Objekt-Funktion 'start()' startet die asynchrone Soundwiedergabe. Sie ruft dabei immer wieder die Funktion
  95. sound.callback() auf. Daher können die dort genutzten Variablen dynamisch geändert werden. """
  96. def play(self):
  97. if not self.mute: # Nie abspielen, wenn die GUI auf stumm geschaltet ist
  98. self.sound_obj.start() # öffnet thread der immer wieder callback funktion aufruft und diese daten abspielt
  99. def stop(self):
  100. self.sound_obj.stop() # beendet die asynchrone Soundwiedergabe
  101. """Die Funktion callback() erzeugt bei jedem Aufruf die Audiodaten, abhängig von den aktuellen Tinnitus-Variablen.
  102. Die errechneten Werte werden in das NumPy-Array 'outdata' geschrieben """
  103. def callback(self, outdata, frames, time, status):
  104. if status: # Warnungen, wenn das Soundobj. auf Fehler stößt (hauptsächlich over/underflow wg. Timingproblemen)
  105. print(status, file=sys.stderr)
  106. # Whitenoise erzeugen
  107. for x in range(len(outdata)):
  108. rand = (np.random.rand() - 0.5) # Zufallszahl zwischen -0.5 und 0.5
  109. # links:
  110. outdata[x][0] = rand * self.tinnitus.linksRauschenLautstaerke
  111. # rechts:
  112. outdata[x][1] = rand * self.tinnitus.rechtsRauschenLautstaerke
  113. # Whitenoise durch Bandpass laufen lassen
  114. if self.tinnitus.linksRauschenLautstaerke:
  115. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  116. fGrenz = [self.tinnitus.linksRauschenUntereGrenzfrequenz,
  117. self.tinnitus.linksRauschenObereGrenzfrequenz]
  118. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  119. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  120. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  121. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  122. outdata[:, 0] = signal.sosfilt(sos, outdata[:, 0])
  123. # Plotten des Filters für Filterentwicklung und Dokumentation nützlich---------
  124. # w, h = signal.sosfreqz(sos, worN=1500)
  125. # plt.subplot(2, 1, 1)
  126. # db = 20 * np.log10(np.maximum(np.abs(h), 1e-5))
  127. # plt.plot(w / np.pi, db)
  128. # plt.ylim(-75, 5)
  129. # plt.grid(True)
  130. # plt.yticks([0, -20, -40, -60])
  131. # plt.ylabel('Gain [dB]')
  132. # plt.title('Frequency Response')
  133. # plt.subplot(2, 1, 2)
  134. # plt.plot(w / np.pi, np.angle(h))
  135. # plt.grid(True)
  136. # plt.yticks([-np.pi, -0.5 * np.pi, 0, 0.5 * np.pi, np.pi],
  137. # [r'$-\pi$', r'$-\pi/2$', '0', r'$\pi/2$', r'$\pi$'])
  138. # plt.ylabel('Phase [rad]')
  139. # plt.xlabel('Normalized frequency (1.0 = Nyquist)')
  140. # plt.show()
  141. # -------------------------------------------------------------------------------
  142. if self.tinnitus.rechtsRauschenLautstaerke:
  143. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  144. fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz,
  145. self.tinnitus.rechtsRauschenObereGrenzfrequenz]
  146. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  147. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  148. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  149. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  150. outdata[:, 1] = signal.sosfilt(sos, outdata[:, 1])
  151. # Sinus addieren: f(t) = A * sin(2 * pi * f * t)
  152. for x in range(len(outdata)):
  153. # links: rauschen und sinus wieder zusammen addieren
  154. outdata[x][0] += self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz *
  155. ((x + self.start_idx) / self.framerate))
  156. # rechts: rauschen und sinus wieder zusammen addieren
  157. outdata[x][1] += self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz *
  158. ((x + self.start_idx) / self.framerate))
  159. self.start_idx += frames
  160. return outdata
  161. def musik_filtern(self):
  162. """
  163. Diese Funktion filtert die Tinnitus Frequenz aus einer gewählten Musikdatei. Dabei geht sie in 3 großen
  164. Schritten vor:
  165. 1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen
  166. 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden"
  167. 3. Die fertigen Audiodatei als .wav Datei speichern
  168. """
  169. nframes = len(self.music_data) # Gesamtanzahl der Frames in der Musikdatei
  170. # ------------1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen---------------------
  171. self.filterfortschritt = 1, 0 # der erste schritt
  172. csvstring = open("TinnitusDaten.csv").read()
  173. tinnitus_data = csvstring.split("\n")
  174. # linke Frequenz aus csv Datei holen
  175. lf = tinnitus_data[2]
  176. lf = lf.split(";")
  177. lf = float(lf[1])
  178. # rechte Frequenz aus csv Datei holen
  179. rf = tinnitus_data[7]
  180. rf = rf.split(";")
  181. rf = float(rf[1])
  182. # linke Lautstärke aus cvs Datei holen
  183. ll = tinnitus_data[3]
  184. ll = ll.split(";")
  185. ll = float(ll[1])
  186. # rechte Lautstärke aus cvs Datei holen
  187. rl = tinnitus_data[8]
  188. rl = rl.split(";")
  189. rl = float(rl[1])
  190. # -------- 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden------
  191. self.filterfortschritt = 2, 0 # der zweite schritt im Feedback
  192. start_time = time.time() # einen Timer laufen lassen um zu sehen wie lange Filterung dauert
  193. self.music_data = self.music_data/32767 # convert array from int16 to float
  194. # ------------------------------------------LEFT EAR FILTERING-------------------------------------------------
  195. # Filterparameter festlegen------------
  196. order = 501 # Filterordnung
  197. bandwidth = 1000 # Bandbreite des Sperrbereichs in Hz
  198. #stop_attenuation = 100 # minimum Attenuation (Damping, Reduction) in stop Band [only for elliptic filter necessary]
  199. cutoff_frequencies = [(lf - (bandwidth / 2)), (lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper)
  200. max_ripple_passband = 50 # Maximal erlaubte Welligkeit im Passbereich
  201. # -------------------------------------
  202. if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  203. # b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  204. # fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (links)
  205. #
  206. # music_links = signal.lfilter(b, a, self.music_data[:, 0]) # diese Funktion wendet den Filter an
  207. #
  208. # print("b=", b)
  209. # print("a=", a)
  210. # FIR Filterversuch
  211. #h = signal.firwin(order, cutoff_frequencies, pass_zero="bandstop", fs=self.music_samplerate, width=bandwidth,
  212. # window="hamming")
  213. h = signal.firwin(order, [cutoff_frequencies[0], cutoff_frequencies[1]], fs=self.music_samplerate)
  214. print("h= ", h)
  215. music_links = signal.lfilter(h, 1.0, self.music_data[:, 0])
  216. else:
  217. music_links = self.music_data[:, 0] # ungefiltert, wenn kein Tinnitus angegeben wurde
  218. # ------------------------------------- RIGHT EAR FILTERING ------------------------------------------------
  219. if rl != 0.0: # nur wenn die Lautstärke des rechten Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  220. cutoff_frequencies = [(rf - (bandwidth / 2)), (
  221. rf + (bandwidth / 2))] # change the cutoff frequencies to the tinnitus of the RIGHT EAR
  222. # h = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  223. # fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (rechts)
  224. #
  225. # music_rechts = signal.lfilter(b, a, self.music_data[:, 1]) # rechts
  226. # FIR Filterversuch
  227. print("UG Freq = ", cutoff_frequencies[0]/(self.music_samplerate/2))
  228. h = signal.firwin(order, [cutoff_frequencies[0], cutoff_frequencies[1]], fs=self.music_samplerate)
  229. music_rechts = signal.lfilter(h, [1.0], self.music_data[:, 1])
  230. else:
  231. music_rechts = self.music_data[:, 1] # diese Funktion filtert die Audiodaten(die Tinnitusfreq wird entfernt)
  232. endTimeFiltering = time.time()
  233. print("benötigte Zeit zum Filtern rechts Ohr =", endTimeFiltering - start_time, "s")
  234. #----------------------- 3. Maxima finden und Samples auf 1 normieren ----------------------------
  235. self.filterfortschritt = 3, 0 # der dritte schritt
  236. #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback)
  237. start_time = time.time()
  238. max_ges = 0
  239. fortschritt = 0
  240. for i in range(nframes):
  241. if max_ges < abs(music_links[i]):
  242. max_ges = abs(music_links[i])
  243. if max_ges < abs(music_rechts[i]):
  244. max_ges = abs(music_rechts[i])
  245. if i % int(nframes/10) == 0: # glaub hier stand 10000 davor oder 50000
  246. fortschritt += 10
  247. self.filterfortschritt = 3, round(fortschritt, 1)
  248. print(" max: ", self.filterfortschritt[1], "%")
  249. end_time = time.time()
  250. print("Zeitaufwand Maxima-Suche: ", end_time - start_time)
  251. #auf 4 Nachkommastellen aufrunden
  252. start_time = time.time()
  253. max_ges = int(max_ges * 10000)
  254. max_ges += 1
  255. max_ges /= 10000
  256. end_time = time.time()
  257. print("Zeitaufwand Maximum runden: ", end_time - start_time)
  258. #Alle samples normieren
  259. start_time = time.time()
  260. music_links /= max_ges
  261. music_rechts /= max_ges
  262. end_time = time.time()
  263. print("Zeitaufwand samples normieren: ", end_time - start_time)
  264. # ------------------------- 4. Die fertigen Audiodatei als .wav Datei speichern --------------------------------
  265. self.filterfortschritt = [4, 0] # der vierte Schritt
  266. start_time = time.time()
  267. wav_obj = wave.open("MyTinnitusFreeSong.wav", "w")
  268. # Rahmenparameter für die .wav-Datei setzen
  269. wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname))
  270. """The values are stored in a temporary list, and when the process is finished, they are joined together into
  271. an string which is then sent to the output with the traditional writeframes method."""
  272. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  273. # Die Audiosamples schreiben
  274. print("Musikdatei wird erstellt...")
  275. fortschritt = 0
  276. for tinnitus_data in range(nframes): #geht jeden Sample-Wert der Musikdatei einzeln durch
  277. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  278. packedMusic.append(struct.pack('h', int(music_links[tinnitus_data] * 32767.0)))
  279. packedMusic.append(struct.pack('h', int(music_rechts[tinnitus_data] * 32767.0)))
  280. # wav_obj.writeframes(struct.pack('h', int(music_links[x] * 32767.0))) # Werte für links und rechts werden bei
  281. # wav_obj.writeframes(struct.pack('h', int(musicRechts[x] * 32767.0))) # wav abwechselnd eingetragen
  282. if tinnitus_data % int(nframes/10) == 0:
  283. fortschritt += 10
  284. self.filterfortschritt = 4, round(fortschritt, 1)
  285. print(" samples: ", self.filterfortschritt[1], "%")
  286. end_time = time.time()
  287. print("Zeitaufwand für das packen der einzelnen Samples =", end_time - start_time, "s")
  288. value_str = b"".join(packedMusic)
  289. start = time.time()
  290. wav_obj.writeframes(value_str)
  291. end = time.time()
  292. print("Zeitaufwand für das schreiben aller Frames in die wav Datei =", end - start, "s")
  293. wav_obj.close()
  294. print("Speichern beendet.")
  295. self.filterfortschritt = 5, 0 #Nach erfolgreichem Filtern Fortschritt zur Bestätigung auf 5 setzen
  296. # Plot (hilfreich für Filterentwurf)
  297. # freq, h = signal.freqz(b, a, fs=self.music_samplerate)
  298. # fig, ax = plt.subplots(2, 1, figsize=(8, 6))
  299. # ax[0].plot(freq, 20 * np.log10(abs(h)), color='blue')
  300. # ax[0].set_title("Frequency Response")
  301. # ax[0].set_ylabel("Amplitude (dB)", color='blue')
  302. # ax[0].set_xlim([0, 10000])
  303. # ax[0].set_ylim([-120, 10])
  304. # ax[0].grid()
  305. # ax[1].plot(freq, np.unwrap(np.angle(h)) * 180 / np.pi, color='green')
  306. # ax[1].set_ylabel("Angle (degrees)", color='green')
  307. # ax[1].set_xlabel("Frequency (Hz)")
  308. # ax[1].set_xlim([0, 10000])
  309. # ax[1].set_yticks([-90, -60, -30, 0, 30, 60, 90])
  310. # ax[1].set_ylim([-90, 90])
  311. # ax[1].grid()
  312. # plt.show()