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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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 matplotlib.pyplot as plt
  9. """---------------------------------------------------------------------------------------------------------------------
  10. In .wav-Dateien wird der Ton in absoluten Werte eingetragen. Die Standart-framerate ist 44100
  11. das heißt für jede Sekunde an Ton gibt es 44100 Werte, die die Tonwelle über die Zeit beschreiben
  12. ---------------------------------------------------------------------------------------------------------------------"""
  13. class Tinnitus: # beinhaltet alle Werte, die vom Nutzer eingestellt werden
  14. 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):
  15. self.vorname = ""
  16. self.nachname = ""
  17. self.kommentar = ""
  18. # [Alle Frequenzangaben in Hz, alle Amplitudenangaben von 0-1 (float)]
  19. self.linksFrequenz = l_freq
  20. self.rechtsFrequenz = r_freq
  21. self.linksLautstaerke = l_amp
  22. self.rechtsLautstaerke = r_amp
  23. self.linksRauschenLautstaerke = l_rausch
  24. self.rechtsRauschenLautstaerke = r_rausch
  25. self.linksRauschenUntereGrenzfrequenz = l_rausch_ug
  26. self.rechtsRauschenUntereGrenzfrequenz = r_rausch_ug
  27. self.linksRauschenObereGrenzfrequenz = l_rausch_og
  28. self.rechtsRauschenObereGrenzfrequenz = r_rausch_og
  29. def speichern(self): # speichert die Nutzerdaten in eine .csv-Datei
  30. datei = open("TinnitusDaten.csv", "w")
  31. daten = "Vorname;" + self.vorname + "\n"
  32. daten += "Nachname;" + self.nachname + "\n"
  33. daten += "linke Frequenz;" + str(self.linksFrequenz) + "\n"
  34. daten += "linke Lautstärke;" + str(self.linksLautstaerke) + "\n"
  35. daten += "linkes Rauschen Lautstärke;" + str(self.linksRauschenLautstaerke) + "\n"
  36. daten += "linkes Rauschen untere Grenzfrequenz;" + str(self.linksRauschenUntereGrenzfrequenz) + "\n"
  37. daten += "linkes Rauschen obere Grenzfrequenz;" + str(self.linksRauschenObereGrenzfrequenz) + "\n"
  38. daten += "rechte Frequenz;" + str(self.rechtsFrequenz) + "\n"
  39. daten += "rechte Lautstärke;" + str(self.rechtsLautstaerke) + "\n"
  40. daten += "rechtes Rauschen Lautstärke;" + str(self.rechtsRauschenLautstaerke) + "\n"
  41. daten += "rechtes Rauschen untere Grenzfrequenz;" + str(self.rechtsRauschenUntereGrenzfrequenz) + "\n"
  42. daten += "rechtes Rauschen obere Grenzfrequenz;" + str(self.rechtsRauschenObereGrenzfrequenz) + "\n"
  43. daten += "Kommentar;" + str(self.kommentar) + "\n"
  44. datei.write(daten)
  45. datei.close()
  46. """---------------------------------KLASSE: SOUND-----------------------------------------------------------------------
  47. Sound beinhaltet alle Variablen, die zum erstellen einer .wav-Datei benötigt werden (siehe soun.wav_speichern())
  48. Das 'sound_obj' ist für das dynamische abspielen zuständig (siehe sound.play())
  49. Beim Initialisieren muss ein Tinnitus-Objekt übergeben werden
  50. ---------------------------------------------------------------------------------------------------------------------"""
  51. class Sound:
  52. def __init__(self, tinnitus, wav_name="MeinTinnitus.wav", audio=None, nchannels=2, sampwidth=2, framerate=44100,
  53. comptype="NONE", compname="not compressed", mute=True):
  54. if audio is None:
  55. audio = []
  56. self.tinnitus = tinnitus
  57. self.wav_name = wav_name #Der Dateiname
  58. self.audio = audio # ein Array, in das die Sound-Werte geschrieben werden (von -1, bis +1)
  59. self.nchannels = nchannels # Zahl der audio channels (1:mono 2:stereo)
  60. self.sampwidth = sampwidth # Größe eines einzelnen Sound-Werts (in bytes)
  61. self.framerate = framerate # Abtastrate
  62. self.nframes = len(audio) # Anzahl der Sound-Werte -> Muss bei jeder audio-Änderung aktuallisiert werden
  63. self.comptype = comptype
  64. self.compname = compname
  65. self.mute = mute # wenn der mute boolean auf true gesetzt ist, sollte kein Ton ausgegeben werden
  66. self.sound_obj = sd.OutputStream(channels=2, callback=self.callback,
  67. samplerate=self.framerate) # Objekt fürs Abspielen (siehe sound.play())
  68. self.start_idx = 0 # wird für sound_obj benötigt
  69. self.music_samplerate = 0 # die samplerate der ausgewählten Musikdatei
  70. self.music_data = 0 # das Numpy Array der ausgewählten Musikdatei
  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. frames = self.framerate * 3 # entspricht 3 Sekunden
  76. status = "" # für den Funktionsaufruf benötigt, sonst keine Funktion
  77. audio = np.ones((frames, 2))
  78. audio = self.callback(audio, frames, self.sound_obj.time, status)
  79. # Rahmenparameter für die .wav-Datei setzen
  80. self.nframes = len(audio)
  81. wav_obj.setparams((self.nchannels, self.sampwidth, self.framerate, self.nframes, self.comptype, self.compname))
  82. #Die Audiosamples schreiben
  83. for x in range(frames):
  84. #Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  85. wav_obj.writeframes(struct.pack('h', int(audio[x][0] * 32767.0))) # Werte für links und rechts werden bei
  86. wav_obj.writeframes(struct.pack('h', int(audio[x][1] * 32767.0))) # wav abwechselnd eingetragen
  87. wav_obj.close()
  88. print("Speichern beendet.")
  89. """Die Objekt-Funktion 'start()' startet die asynchrone Soundwiedergabe. Sie ruft dabei immer wieder die Funktion
  90. sound.callback() auf. Daher können die dort genutzten Variablen dynamisch geändert werden. """
  91. def play(self):
  92. if not self.mute: # Nie abspielen, wenn die GUI auf stumm geschaltet ist
  93. self.sound_obj.start() # öffnet thread der immer wieder callback funktion aufruft und diese daten abspielt
  94. def stop(self):
  95. self.sound_obj.stop() # beendet die asynchrone Soundwiedergabe
  96. """Die Funktion callback() erzeugt bei jedem Aufruf die Audiodaten, abhängig von den aktuellen Tinnitus-Variablen.
  97. Die errechneten Werte werden in das NumPy-Array 'outdata' geschrieben """
  98. def callback(self, outdata, frames, time, status):
  99. if status: # Warnungen, wenn das Soundobj. auf Fehler stößt (hauptsächlich over/underflow wg. Timingproblemen)
  100. print(status, file=sys.stderr)
  101. # Whitenoise erzeugen
  102. for x in range(len(outdata)):
  103. rand = (np.random.rand() - 0.5) # Zufallszahl zwischen -0.5 und 0.5
  104. # links:
  105. outdata[x][0] = rand * self.tinnitus.linksRauschenLautstaerke
  106. # rechts:
  107. outdata[x][1] = rand * self.tinnitus.rechtsRauschenLautstaerke
  108. # Whitenoise durch Bandpass laufen lassen
  109. if self.tinnitus.linksRauschenLautstaerke:
  110. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  111. fGrenz = [self.tinnitus.linksRauschenUntereGrenzfrequenz,
  112. self.tinnitus.linksRauschenObereGrenzfrequenz]
  113. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  114. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  115. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  116. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  117. outdata[:, 0] = signal.sosfilt(sos, outdata[:, 0])
  118. if self.tinnitus.rechtsRauschenLautstaerke:
  119. # (-3dB Grenzen) bzw was der Bandpass durchlässt
  120. fGrenz = [self.tinnitus.rechtsRauschenUntereGrenzfrequenz,
  121. self.tinnitus.rechtsRauschenObereGrenzfrequenz]
  122. # sos (=second order sections = Filter 2. Ordnung) ist ein Array der Länge (filterOrder) und beinhaltet
  123. # die Koeffizienten der IIR Filter 2. Ordnung (b0, b1, b2 & a0, a1, a2)
  124. sos = signal.butter(5, fGrenz, 'bandpass', fs=self.framerate, output='sos')
  125. # sosfilt filtert das Signal mittels mehrerer 'second order sections' (= Filter 2. Ordnung) die über sos definiert sind
  126. outdata[:, 1] = signal.sosfilt(sos, outdata[:, 1])
  127. # Sinus addieren: f(t) = A * sin(2 * pi * f * t)
  128. for x in range(len(outdata)):
  129. # links: rauschen und sinus wieder zusammen addieren
  130. outdata[x][0] += self.tinnitus.linksLautstaerke * np.sin(2 * np.pi * self.tinnitus.linksFrequenz *
  131. ((x + self.start_idx) / self.framerate))
  132. # rechts: rauschen und sinus wieder zusammen addieren
  133. outdata[x][1] += self.tinnitus.rechtsLautstaerke * np.sin(2 * np.pi * self.tinnitus.rechtsFrequenz *
  134. ((x + self.start_idx) / self.framerate))
  135. self.start_idx += frames
  136. return outdata
  137. def musik_filtern(self):
  138. """ Die Tinnitus Frequenz aus der Musik Filtern... easy"""
  139. # Die Parameter des Tinnitus aus .csv Datei getten
  140. csvstring = open("TinnitusDaten.csv").read()
  141. #print(csvstring)
  142. x = csvstring.split("\n")
  143. #print("\nx= ", x)
  144. # linke Frequenz aus csv Datei holen
  145. lf = x[2]
  146. lf = lf.split(";")
  147. lf = float(lf[1])
  148. # rechte Frequenz aus csv Datei holen
  149. rf = x[7]
  150. rf = rf.split(";")
  151. rf = float(rf[1])
  152. # linke Lautstärke aus cvs Datei holen
  153. ll = x[3]
  154. ll = ll.split(";")
  155. ll = float(ll[1])
  156. # rechte Lautstärke aus cvs Datei holen
  157. rl = x[8]
  158. rl = rl.split(";")
  159. rl = float(rl[1])
  160. # Die Musik filtern
  161. w0 = float(lf/(self.music_samplerate/2)) # Frequency to remove from a signal. If fs is specified, this is in the same units as fs. By default, it is a normalized scalar that must satisfy 0 < w0 < 1, with w0 = 1 corresponding to half of the sampling frequency.
  162. Q = 30.0 # Quality factor. Dimensionless parameter that characterizes notch filter -3 dB bandwidth bw relative to its center frequency, Q = w0/bw.
  163. self.music_data = self.music_data/32767 # convert array from int16 to float
  164. b, a = signal.iirnotch(lf, Q, fs=self.music_samplerate)
  165. if ll != 0.0:
  166. print(self.music_data[20000:20010])
  167. musicLinks = signal.lfilter(b, a , self.music_data[:, 0]) # links
  168. else:
  169. musicLinks = self.music_data[:, 0] # ungefiltert, wenn kein Tinnitus angegeben wurde
  170. #Plot
  171. # freq, h = signal.freqz(b, a, fs=self.music_samplerate)
  172. # fig, ax = plt.subplots(2, 1, figsize=(8, 6))
  173. # ax[0].plot(freq, 20 * np.log10(abs(h)), color='blue')
  174. # ax[0].set_title("Frequency Response")
  175. # ax[0].set_ylabel("Amplitude (dB)", color='blue')
  176. # ax[0].set_xlim([0, 10000])
  177. # ax[0].set_ylim([-25, 10])
  178. # ax[0].grid()
  179. # ax[1].plot(freq, np.unwrap(np.angle(h)) * 180 / np.pi, color='green')
  180. # ax[1].set_ylabel("Angle (degrees)", color='green')
  181. # ax[1].set_xlabel("Frequency (Hz)")
  182. # ax[1].set_xlim([0, 10000])
  183. # ax[1].set_yticks([-90, -60, -30, 0, 30, 60, 90])
  184. # ax[1].set_ylim([-90, 90])
  185. # ax[1].grid()
  186. # plt.show()
  187. if rl != 0.0:
  188. musicRechts = signal.lfilter(b, a, self.music_data[:, 1]) # rechts
  189. else:
  190. musicRechts = self.music_data[:, 1] # ungefiltert, wenn kein Tinnitus angegeben wurde
  191. wav_obj = wave.open("musikTest.wav", "w")
  192. # Rahmenparameter für die .wav-Datei setzen
  193. nframes = len(self.music_data) #Gesamtanzahl der Frames in der Musikdatei
  194. wav_obj.setparams((self.nchannels, self.sampwidth, self.music_samplerate, nframes, self.comptype, self.compname))
  195. print("Maximum musicLinks: ", max(musicLinks))
  196. print("Minimum musikLinks: ", min(musicLinks))
  197. # Die Audiosamples schreiben
  198. for x in range(self.music_samplerate*5): #Kann mit nframes ersetzt werden, für den ganzen Song
  199. # Die Audiodaten müssen von float in einen passenden int-Wert umgerechnet werden
  200. wav_obj.writeframes(struct.pack('h', int(musicLinks[x] * 32767.0))) # Werte für links und rechts werden bei
  201. wav_obj.writeframes(struct.pack('h', int(musicRechts[x] * 32767.0))) # wav abwechselnd eingetragen
  202. wav_obj.close()
  203. print("Speichern beendet.")