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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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 # siehe musik_filtern(), zwischen 0 und 100, wird für die feedback-fkt gebraucht
  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][0] * 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. # ------------1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen---------------------
  151. csvstring = open("TinnitusDaten.csv").read()
  152. tinnitus_data = csvstring.split("\n")
  153. # linke Frequenz aus csv Datei holen
  154. lf = tinnitus_data[2]
  155. lf = lf.split(";")
  156. lf = float(lf[1])
  157. # rechte Frequenz aus csv Datei holen
  158. rf = tinnitus_data[7]
  159. rf = rf.split(";")
  160. rf = float(rf[1])
  161. # linke Lautstärke aus cvs Datei holen
  162. ll = tinnitus_data[3]
  163. ll = ll.split(";")
  164. ll = float(ll[1])
  165. # rechte Lautstärke aus cvs Datei holen
  166. rl = tinnitus_data[8]
  167. rl = rl.split(";")
  168. rl = float(rl[1])
  169. # -------- 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden------
  170. start_time = time.time() # einen Timer laufen lassen um zu sehen wie lange Filterung dauert
  171. self.music_data = self.music_data/32767 * 0.8 # convert array from int16 to float
  172. """ OLD IIR Notch Filter 2nd Order----------------------------------------------------------------------
  173. w0 = float(lf / (self.music_samplerate / 2)) # Frequency to remove from a signal. If fs is specified, this is
  174. in the same units as fs. By default, it is a normalized scalar that must satisfy 0 < w0 < 1, with w0 = 1
  175. corresponding to half of the sampling frequency.
  176. Q = 30.0 # Quality factor. Dimensionless parameter that characterizes notch filter -3 dB bandwidth bw relative
  177. to its center frequency, Q = w0/bw.
  178. b, a = signal.iirnotch(lf, Q, fs=self.music_samplerate)
  179. ----------------------------------------------------------------------------------------------------------------
  180. """
  181. """ New IIR Notch Filter 5th order--------------------------------------------------------------------------"""
  182. # ------------------------------------------LEFT EAR FILTERING-------------------------------------------------
  183. # Filterparameter festlegen------------
  184. order = 5 # Filterordnung
  185. bandwidth = 175 # Bandbreite des Sperrbereichs in Hz
  186. #stop_attenuation = 100 # minimum Attenuation (Damping, Reduction) in stop Band [only for elliptic filter necessary]
  187. cutoff_frequencies = [(lf - (bandwidth / 2)),(lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper)
  188. max_ripple_passband = 50 # Maximal erlaubte Welligkeit im Passbereich
  189. # -------------------------------------
  190. if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  191. b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  192. fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (links)
  193. music_links = signal.lfilter(b, a, self.music_data[:, 0]) # diese Funktion filtert die Audiodaten
  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 = [(rf - (bandwidth / 2)), (
  199. rf + (bandwidth / 2))] # change the cutoff frequencies to the tinnitus of the RIGHT EAR
  200. b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  201. fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (rechts)
  202. music_rechts = signal.lfilter(b, a, self.music_data[:, 1]) # rechts
  203. else:
  204. music_rechts = self.music_data[:, 1] # diese Funktion filtert die Audiodaten(die Tinnitusfreq wird entfernt)
  205. endTimeFiltering = time.time()
  206. print("benötigte Zeit zum Filtern rechts Ohr =", endTimeFiltering - start_time, "s")
  207. # ------------------------- 3. Die fertigen Audiodatei als .wav Datei speichern --------------------------------
  208. start_time = time.time()
  209. wav_obj = wave.open("MyTinnitusFreeSong.wav", "w")
  210. # Rahmenparameter für die .wav-Datei setzen
  211. nframes = len(self.music_data) #Gesamtanzahl der Frames in der Musikdatei
  212. wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname))
  213. """The values are stored in a temporary list, and when the process is finished, they are joined together into
  214. an string which is then sent to the output with the traditional writeframes method."""
  215. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  216. # Die Audiosamples schreiben
  217. print("Musikdatei wird erstellt...")
  218. for tinnitus_data in range(nframes): #geht jeden Sample-Wert der Musikdatei einzeln durch
  219. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  220. packedMusic.append(struct.pack('h', int(music_links[tinnitus_data] * 32767.0)))
  221. packedMusic.append(struct.pack('h', int(music_rechts[tinnitus_data] * 32767.0)))
  222. # wav_obj.writeframes(struct.pack('h', int(music_links[x] * 32767.0))) # Werte für links und rechts werden bei
  223. # wav_obj.writeframes(struct.pack('h', int(musicRechts[x] * 32767.0))) # wav abwechselnd eingetragen
  224. if tinnitus_data % 10000 == 0:
  225. fortschritt = tinnitus_data/nframes*100
  226. self.filterfortschritt = round(fortschritt, 1)
  227. print(" ", self.filterfortschritt, "%")
  228. end_time = time.time()
  229. print("Zeitaufwand für das packen der einzelnen Samples =", end_time - start_time, "s")
  230. value_str = b"".join(packedMusic)
  231. start = time.time()
  232. wav_obj.writeframes(value_str)
  233. end = time.time()
  234. print("Zeitaufwand für das schreiben aller Frames in die wav Datei =", end - start, "s")
  235. wav_obj.close()
  236. print("Speichern beendet.")
  237. # # Plot (hilfreich für Filterentwurf)
  238. # freq, h = signal.freqz(b, a, fs=self.music_samplerate)
  239. # fig, ax = plt.subplots(2, 1, figsize=(8, 6))
  240. # ax[0].plot(freq, 20 * np.log10(abs(h)), color='blue')
  241. # ax[0].set_title("Frequency Response")
  242. # ax[0].set_ylabel("Amplitude (dB)", color='blue')
  243. # ax[0].set_xlim([0, 10000])
  244. # ax[0].set_ylim([-120, 10])
  245. # ax[0].grid()
  246. # ax[1].plot(freq, np.unwrap(np.angle(h)) * 180 / np.pi, color='green')
  247. # ax[1].set_ylabel("Angle (degrees)", color='green')
  248. # ax[1].set_xlabel("Frequency (Hz)")
  249. # ax[1].set_xlim([0, 10000])
  250. # ax[1].set_yticks([-90, -60, -30, 0, 30, 60, 90])
  251. # ax[1].set_ylim([-90, 90])
  252. # ax[1].grid()
  253. # plt.show()