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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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. def wav_speichern(self): # ezeugt/aktuallisiert die .wav-Datei
  73. print("Sound wird als .wav-Datei gespeichert. Bitte warten...\nDer Vorgang kann ca. 10 Sekunden dauern")
  74. wav_obj = wave.open(self.wav_name, "w")
  75. # Die Callback-Funktion aufrufen, um die Audiodaten zu bekommen
  76. frames = self.framerate * 3 # entspricht 3 Sekunden
  77. status = "" # für den Funktionsaufruf benötigt, sonst keine Funktion
  78. audio = np.ones((frames, 2))
  79. audio = self.callback(audio, frames, self.sound_obj.time, status)
  80. # Rahmenparameter für die .wav-Datei setzen
  81. self.nframes = len(audio)
  82. wav_obj.setparams((self.nchannels, self.sampwidth, self.framerate, self.nframes, self.comptype, self.compname))
  83. #Die Audiosamples schreiben
  84. for x in range(frames):
  85. #Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  86. wav_obj.writeframes(struct.pack('h', int(audio[x][0] * 32767.0))) # Werte für links und rechts werden bei
  87. wav_obj.writeframes(struct.pack('h', int(audio[x][1] * 32767.0))) # wav abwechselnd eingetragen
  88. wav_obj.close()
  89. print("Speichern beendet.")
  90. """Die Objekt-Funktion 'start()' startet die asynchrone Soundwiedergabe. Sie ruft dabei immer wieder die Funktion
  91. sound.callback() auf. Daher können die dort genutzten Variablen dynamisch geändert werden. """
  92. def play(self):
  93. if not self.mute: # Nie abspielen, wenn die GUI auf stumm geschaltet ist
  94. self.sound_obj.start() # öffnet thread der immer wieder callback funktion aufruft und diese daten abspielt
  95. def stop(self):
  96. self.sound_obj.stop() # beendet die asynchrone Soundwiedergabe
  97. """Die Funktion callback() erzeugt bei jedem Aufruf die Audiodaten, abhängig von den aktuellen Tinnitus-Variablen.
  98. Die errechneten Werte werden in das NumPy-Array 'outdata' geschrieben """
  99. def callback(self, outdata, frames, time, status):
  100. if status: # Warnungen, wenn das Soundobj. auf Fehler stößt (hauptsächlich over/underflow wg. Timingproblemen)
  101. print(status, file=sys.stderr)
  102. # Whitenoise erzeugen
  103. for x in range(len(outdata)):
  104. rand = (np.random.rand() - 0.5) # Zufallszahl zwischen -0.5 und 0.5
  105. # links:
  106. outdata[x][0] = rand * self.tinnitus.linksRauschenLautstaerke
  107. # rechts:
  108. outdata[x][1] = rand * self.tinnitus.rechtsRauschenLautstaerke
  109. # Whitenoise durch Bandpass laufen lassen
  110. if self.tinnitus.linksRauschenLautstaerke:
  111. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  112. fGrenz = [self.tinnitus.linksRauschenUntereGrenzfrequenz,
  113. self.tinnitus.linksRauschenObereGrenzfrequenz]
  114. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  115. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  116. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  117. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  118. outdata[:, 0] = signal.sosfilt(sos, outdata[:, 0])
  119. if self.tinnitus.rechtsRauschenLautstaerke:
  120. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  121. fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz,
  122. self.tinnitus.rechtsRauschenObereGrenzfrequenz]
  123. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  124. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  125. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  126. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  127. outdata[:, 1] = signal.sosfilt(sos, outdata[:, 1])
  128. # Sinus addieren: f(t) = A * sin(2 * pi * f * t)
  129. for x in range(len(outdata)):
  130. # links: rauschen und sinus wieder zusammen addieren
  131. outdata[x][0] += self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz *
  132. ((x + self.start_idx) / self.framerate))
  133. # rechts: rauschen und sinus wieder zusammen addieren
  134. outdata[x][1] += self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz *
  135. ((x + self.start_idx) / self.framerate))
  136. self.start_idx += frames
  137. return outdata
  138. def musik_filtern(self):
  139. """
  140. Diese Funktion filtert die Tinnitus Frequenz aus einer gewählten Musikdatei. Dabei geht sie in 3 großen
  141. Schritten vor:
  142. 1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen
  143. 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden"
  144. 3. Die fertigen Audiodatei als .wav Datei speichern
  145. """
  146. # ------------1. Die nötigen Informationen über den Tinnitus aus der .csv Datei herausholen---------------------
  147. csvstring = open("TinnitusDaten.csv").read()
  148. tinnitus_data = csvstring.split("\n")
  149. # linke Frequenz aus csv Datei holen
  150. lf = tinnitus_data[2]
  151. lf = lf.split(";")
  152. lf = float(lf[1])
  153. # rechte Frequenz aus csv Datei holen
  154. rf = tinnitus_data[7]
  155. rf = rf.split(";")
  156. rf = float(rf[1])
  157. # linke Lautstärke aus cvs Datei holen
  158. ll = tinnitus_data[3]
  159. ll = ll.split(";")
  160. ll = float(ll[1])
  161. # rechte Lautstärke aus cvs Datei holen
  162. rl = tinnitus_data[8]
  163. rl = rl.split(";")
  164. rl = float(rl[1])
  165. # -------- 2. Die digitalen Filter erstellen und die Tinnitus Frequenz aus der Audiodatei "herausschneiden------
  166. start_time = time.time() # einen Timer laufen lassen um zu sehen wie lange Filterung dauert
  167. self.music_data = self.music_data/32767 * 0.8 # convert array from int16 to float
  168. """ OLD IIR Notch Filter 2nd Order----------------------------------------------------------------------
  169. w0 = float(lf / (self.music_samplerate / 2)) # Frequency to remove from a signal. If fs is specified, this is
  170. in the same units as fs. By default, it is a normalized scalar that must satisfy 0 < w0 < 1, with w0 = 1
  171. corresponding to half of the sampling frequency.
  172. Q = 30.0 # Quality factor. Dimensionless parameter that characterizes notch filter -3 dB bandwidth bw relative
  173. to its center frequency, Q = w0/bw.
  174. b, a = signal.iirnotch(lf, Q, fs=self.music_samplerate)
  175. ----------------------------------------------------------------------------------------------------------------
  176. """
  177. """ New IIR Notch Filter 5th order--------------------------------------------------------------------------"""
  178. # ------------------------------------------LEFT EAR FILTERING-------------------------------------------------
  179. # Filterparameter festlegen------------
  180. order = 5 # Filterordnung
  181. bandwidth = 175 # Bandbreite des Sperrbereichs in Hz
  182. #stop_attenuation = 100 # minimum Attenuation (Damping, Reduction) in stop Band [only for elliptic filter necessary]
  183. cutoff_frequencies = [(lf - (bandwidth / 2)),(lf + (bandwidth / 2))] # the cutoff frequencies (lower and upper)
  184. max_ripple_passband = 50 # Maximal erlaubte Welligkeit im Passbereich
  185. # -------------------------------------
  186. if ll != 0.0: # nur wenn die Lautstärke des linken Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  187. b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  188. fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (links)
  189. music_links = signal.lfilter(b, a, self.music_data[:, 0]) # diese Funktion filtert die Audiodaten
  190. else:
  191. music_links = self.music_data[:, 0] # ungefiltert, wenn kein Tinnitus angegeben wurde
  192. # ------------------------------------- RIGHT EAR FILTERING ------------------------------------------------
  193. if rl != 0.0: # nur wenn die Lautstärke des rechten Tinnitus ungleich 0 ist, wird auf diesem Ohr auch gefiltert
  194. cutoff_frequencies = [(rf - (bandwidth / 2)), (
  195. rf + (bandwidth / 2))] # change the cutoff frequencies to the tinnitus of the RIGHT EAR
  196. b, a = signal.iirfilter(order, cutoff_frequencies, rp=max_ripple_passband, btype='bandstop', ftype='butter',
  197. fs=self.music_samplerate) # Diese Funktion erstellt den IIR-Bandpassfilter (rechts)
  198. music_rechts = signal.lfilter(b, a, self.music_data[:, 1]) # rechts
  199. else:
  200. music_rechts = self.music_data[:, 1] # diese Funktion filtert die Audiodaten(die Tinnitusfreq wird entfernt)
  201. endTimeFiltering = time.time()
  202. print("benötigte Zeit zum Filtern rechts Ohr =", endTimeFiltering - start_time, "s")
  203. # ------------------------- 3. Die fertigen Audiodatei als .wav Datei speichern --------------------------------
  204. start_time = time.time()
  205. wav_obj = wave.open("MyTinnitusFreeSong.wav", "w")
  206. # Rahmenparameter für die .wav-Datei setzen
  207. nframes = len(self.music_data) #Gesamtanzahl der Frames in der Musikdatei
  208. wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname))
  209. """The values are stored in a temporary list, and when the process is finished, they are joined together into
  210. an string which is then sent to the output with the traditional writeframes method."""
  211. packedMusic = [] # Liste an die wir die einzelnen frames bereits in binär umgewandelt aneinanderreihen
  212. # Die Audiosamples schreiben
  213. print("Musikdatei wird erstellt...")
  214. for tinnitus_data in range(self.music_samplerate * 60): #Kann mit nframes ersetzt werden, für den ganzen Song
  215. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  216. packedMusic.append(struct.pack('h', int(music_links[tinnitus_data] * 32767.0)))
  217. packedMusic.append(struct.pack('h', int(music_rechts[tinnitus_data] * 32767.0)))
  218. # wav_obj.writeframes(struct.pack('h', int(music_links[x] * 32767.0))) # Werte für links und rechts werden bei
  219. # wav_obj.writeframes(struct.pack('h', int(musicRechts[x] * 32767.0))) # wav abwechselnd eingetragen
  220. if tinnitus_data % 10000 == 0:
  221. fortschritt = tinnitus_data/(self.music_samplerate*20)*100
  222. print(" ", round(fortschritt, 1), "%")
  223. end_time = time.time()
  224. print("Zeitaufwand für das packen der einzelnen Samples =", end_time - start_time, "s")
  225. value_str = b"".join(packedMusic)
  226. start = time.time()
  227. wav_obj.writeframes(value_str)
  228. end = time.time()
  229. print("Zeitaufwand für das schreiben aller Frames in die wav Datei =", end - start, "s")
  230. wav_obj.close()
  231. print("Speichern beendet.")
  232. # # Plot (hilfreich für Filterentwurf)
  233. # freq, h = signal.freqz(b, a, fs=self.music_samplerate)
  234. # fig, ax = plt.subplots(2, 1, figsize=(8, 6))
  235. # ax[0].plot(freq, 20 * np.log10(abs(h)), color='blue')
  236. # ax[0].set_title("Frequency Response")
  237. # ax[0].set_ylabel("Amplitude (dB)", color='blue')
  238. # ax[0].set_xlim([0, 10000])
  239. # ax[0].set_ylim([-120, 10])
  240. # ax[0].grid()
  241. # ax[1].plot(freq, np.unwrap(np.angle(h)) * 180 / np.pi, color='green')
  242. # ax[1].set_ylabel("Angle (degrees)", color='green')
  243. # ax[1].set_xlabel("Frequency (Hz)")
  244. # ax[1].set_xlim([0, 10000])
  245. # ax[1].set_yticks([-90, -60, -30, 0, 30, 60, 90])
  246. # ax[1].set_ylim([-90, 90])
  247. # ax[1].grid()
  248. # plt.show()