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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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. if self.tinnitus.rechtsRauschenLautstaerke:
  124. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  125. fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz,
  126. self.tinnitus.rechtsRauschenObereGrenzfrequenz]
  127. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  128. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  129. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  130. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  131. outdata[:, 1] = signal.sosfilt(sos, outdata[:, 1])
  132. # Sinus addieren: f(t) = A * sin(2 * pi * f * t)
  133. for x in range(len(outdata)):
  134. # links: rauschen und sinus wieder zusammen addieren
  135. outdata[x][0] += self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz *
  136. ((x + self.start_idx) / self.framerate))
  137. # rechts: rauschen und sinus wieder zusammen addieren
  138. outdata[x][1] += self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz *
  139. ((x + self.start_idx) / self.framerate))
  140. self.start_idx += frames
  141. return outdata
  142. def musik_filtern(self):
  143. """
  144. Diese Funktion filtert die Tinnitus Frequenz aus einer gewählten Musikdatei. Dabei geht sie in 3 großen
  145. Schritten vor:
  146. 1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen
  147. 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden"
  148. 3. Die fertigen Audiodatei als .wav Datei speichern
  149. """
  150. nframes = len(self.music_data) # Gesamtanzahl der Frames in der Musikdatei
  151. # ------------1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen---------------------
  152. self.filterfortschritt = 1, 0 # der erste schritt
  153. csvstring = open("TinnitusDaten.csv").read()
  154. tinnitus_data = csvstring.split("\n")
  155. # linke Frequenz aus csv Datei holen
  156. lf = tinnitus_data[2]
  157. lf = lf.split(";")
  158. lf = float(lf[1])
  159. # rechte Frequenz aus csv Datei holen
  160. rf = tinnitus_data[7]
  161. rf = rf.split(";")
  162. rf = float(rf[1])
  163. # linke Lautstärke aus cvs Datei holen
  164. ll = tinnitus_data[3]
  165. ll = ll.split(";")
  166. ll = float(ll[1])
  167. # rechte Lautstärke aus cvs Datei holen
  168. rl = tinnitus_data[8]
  169. rl = rl.split(";")
  170. rl = float(rl[1])
  171. # -------- 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden------
  172. self.filterfortschritt = 2, 0 # der zweite schritt
  173. start_time = time.time() # einen Timer laufen lassen um zu sehen wie lange Filterung dauert
  174. self.music_data = self.music_data/32767 # convert array from int16 to float
  175. """ OLD IIR Notch Filter 2nd Order----------------------------------------------------------------------
  176. w0 = float(lf / (self.music_samplerate / 2)) # Frequency to remove from a signal. If fs is specified, this is
  177. in the same units as fs. By default, it is a normalized scalar that must satisfy 0 < w0 < 1, with w0 = 1
  178. corresponding to half of the sampling frequency.
  179. Q = 30.0 # Quality factor. Dimensionless parameter that characterizes notch filter -3 dB bandwidth bw relative
  180. to its center frequency, Q = w0/bw.
  181. b, a = signal.iirnotch(lf, Q, fs=self.music_samplerate)
  182. ----------------------------------------------------------------------------------------------------------------
  183. """
  184. """ New IIR Notch Filter 5th order--------------------------------------------------------------------------"""
  185. # ------------------------------------------LEFT EAR FILTERING-------------------------------------------------
  186. # Filterparameter festlegen------------
  187. order = 5 # Filterordnung
  188. bandwidth = 175 # Bandbreite des Sperrbereichs in Hz
  189. #stop_attenuation = 100 # minimum Attenuation (Damping, Reduction) in stop Band [only for elliptic filter necessary]
  190. cutoff_frequencies = [(lf - (bandwidth / 2)),(lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper)
  191. max_ripple_passband = 50 # Maximal erlaubte Welligkeit im Passbereich
  192. # -------------------------------------
  193. if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  194. b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  195. fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (links)
  196. music_links = signal.lfilter(b, a, self.music_data[:, 0]) # diese Funktion filtert die Audiodaten
  197. else:
  198. music_links = self.music_data[:, 0] # ungefiltert, wenn kein Tinnitus angegeben wurde
  199. # ------------------------------------- RIGHT EAR FILTERING ------------------------------------------------
  200. if rl != 0.0: # nur wenn die Lautstärke des rechten Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  201. cutoff_frequencies = [(rf - (bandwidth / 2)), (
  202. rf + (bandwidth / 2))] # change the cutoff frequencies to the tinnitus of the RIGHT EAR
  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 (rechts)
  205. music_rechts = signal.lfilter(b, a, self.music_data[:, 1]) # rechts
  206. else:
  207. music_rechts = self.music_data[:, 1] # diese Funktion filtert die Audiodaten(die Tinnitusfreq wird entfernt)
  208. endTimeFiltering = time.time()
  209. print("benötigte Zeit zum Filtern rechts Ohr =", endTimeFiltering - start_time, "s")
  210. #----------------------- 3. Maxima finden und Samples auf 1 normieren ----------------------------
  211. self.filterfortschritt = 3, 0 # der dritte schritt
  212. #Maximum finden (Funktion max(...) ist minimal schneller, macht aber Probleme beim Feedback)
  213. start_time = time.time()
  214. max_ges = 0
  215. for i in range(nframes):
  216. if max_ges < abs(music_links[i]):
  217. max_ges = abs(music_links[i])
  218. if max_ges < abs(music_rechts[i]):
  219. max_ges = abs(music_rechts[i])
  220. if i % 50000 == 0:
  221. fortschritt = i / nframes * 100
  222. self.filterfortschritt = 3, round(fortschritt, 1)
  223. print(" max: ", self.filterfortschritt[1], "%")
  224. end_time = time.time()
  225. print("Zeitaufwand Maxima-Suche: ", end_time - start_time)
  226. #auf 4 Nachkommastellen aufrunden
  227. start_time = time.time()
  228. max_ges = int(max_ges * 10000)
  229. max_ges += 1
  230. max_ges /= 10000
  231. end_time = time.time()
  232. print("Zeitaufwand Maximum runden: ", end_time - start_time)
  233. #Alle samples normieren
  234. start_time = time.time()
  235. music_links /= max_ges
  236. music_rechts /= max_ges
  237. end_time = time.time()
  238. print("Zeitaufwand samples normieren: ", end_time - start_time)
  239. # ------------------------- 4. Die fertigen Audiodatei als .wav Datei speichern --------------------------------
  240. self.filterfortschritt = [4, 0] # der vierte Schritt
  241. start_time = time.time()
  242. wav_obj = wave.open("MyTinnitusFreeSong.wav", "w")
  243. # Rahmenparameter für die .wav-Datei setzen
  244. wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname))
  245. """The values are stored in a temporary list, and when the process is finished, they are joined together into
  246. an string which is then sent to the output with the traditional writeframes method."""
  247. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  248. # Die Audiosamples schreiben
  249. print("Musikdatei wird erstellt...")
  250. for tinnitus_data in range(nframes): #geht jeden Sample-Wert der Musikdatei einzeln durch
  251. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  252. packedMusic.append(struct.pack('h', int(music_links[tinnitus_data] * 32767.0)))
  253. packedMusic.append(struct.pack('h', int(music_rechts[tinnitus_data] * 32767.0)))
  254. # wav_obj.writeframes(struct.pack('h', int(music_links[x] * 32767.0))) # Werte für links und rechts werden bei
  255. # wav_obj.writeframes(struct.pack('h', int(musicRechts[x] * 32767.0))) # wav abwechselnd eingetragen
  256. if tinnitus_data % 50000 == 0:
  257. fortschritt = tinnitus_data/nframes*100
  258. self.filterfortschritt = 4, round(fortschritt, 1)
  259. print(" samples: ", self.filterfortschritt[1], "%")
  260. end_time = time.time()
  261. print("Zeitaufwand für das packen der einzelnen Samples =", end_time - start_time, "s")
  262. value_str = b"".join(packedMusic)
  263. start = time.time()
  264. wav_obj.writeframes(value_str)
  265. end = time.time()
  266. print("Zeitaufwand für das schreiben aller Frames in die wav Datei =", end - start, "s")
  267. wav_obj.close()
  268. print("Speichern beendet.")
  269. self.filterfortschritt = 5, 0 #Nach erfolgreichem Filtern Fortschritt zur Bestätigung auf 5 setzen
  270. # # Plot (hilfreich für Filterentwurf)
  271. # freq, h = signal.freqz(b, a, fs=self.music_samplerate)
  272. # fig, ax = plt.subplots(2, 1, figsize=(8, 6))
  273. # ax[0].plot(freq, 20 * np.log10(abs(h)), color='blue')
  274. # ax[0].set_title("Frequency Response")
  275. # ax[0].set_ylabel("Amplitude (dB)", color='blue')
  276. # ax[0].set_xlim([0, 10000])
  277. # ax[0].set_ylim([-120, 10])
  278. # ax[0].grid()
  279. # ax[1].plot(freq, np.unwrap(np.angle(h)) * 180 / np.pi, color='green')
  280. # ax[1].set_ylabel("Angle (degrees)", color='green')
  281. # ax[1].set_xlabel("Frequency (Hz)")
  282. # ax[1].set_xlim([0, 10000])
  283. # ax[1].set_yticks([-90, -60, -30, 0, 30, 60, 90])
  284. # ax[1].set_ylim([-90, 90])
  285. # ax[1].grid()
  286. # plt.show()