audioHandler.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. #!/usr/bin/env python3
  2. import pyaudio
  3. import struct
  4. import numpy as np
  5. class Listener:
  6. def __init__(self, dataTime = 1/20, agcTime = 10, input = False):
  7. self._dataTime = dataTime
  8. self._agcTime = agcTime
  9. self._input = input
  10. self.p = pyaudio.PyAudio()
  11. self.left = self.right = self.fft = []
  12. self._agcMaxima = [0]
  13. self._agcIndex = 0
  14. self._agcLen = 0
  15. self._beatCb = None
  16. self._sampleRate = 0
  17. self._hasNewData = False
  18. self.buffersize = None
  19. self._doFFT = False
  20. self._doAgcFFT = False
  21. def start(self, dev = None):
  22. self._device = dev or self.getDefaultOutputDeviceInfo()
  23. if self._device == None:
  24. print("no device found")
  25. return
  26. print("device name: {} channels: {} defaultSampleRate: {}".format(self._device["name"], self._device["channels"], self._device["defaultSampleRate"]))
  27. if self._sampleRate != self._device["defaultSampleRate"]:
  28. self._sampleRate = self._device["defaultSampleRate"]
  29. self.buffersize = int(self._sampleRate * self._dataTime)
  30. self.fft = self.right = self.left = np.ndarray((self.buffersize))
  31. self._agcLen = int(1 / self._dataTime * self._agcTime)
  32. self._agcMaxima = np.ndarray((self._agcLen))
  33. self._agcMaxima.fill(2**15 * 0.1)
  34. self._agcIndex = 0
  35. self._lastBeatTime = 0
  36. self.meanAmp = 2**15 * 0.1
  37. try:
  38. self._stream = self.openStream(self._device)
  39. except OSError:
  40. self._stream = None
  41. if not self._stream:
  42. print("stream open failed")
  43. return
  44. self._stream.start_stream()
  45. return self._stream.is_active()
  46. def stop(self):
  47. if not self._stream:# or not self._stream.is_active():
  48. return False
  49. self._stream.stop_stream()
  50. return True
  51. def setBeatCb(self, cb):
  52. self._beatCb = cb
  53. def getDefaultOutputDeviceInfo(self):
  54. #Set default to first in list or ask Windows
  55. try:
  56. self.p.terminate()
  57. self.p.__init__()
  58. if self._input:
  59. info = self.p.get_default_input_device_info()
  60. else:
  61. info = self.p.get_default_output_device_info()
  62. except IOError:
  63. info = None
  64. #Handle no devices available
  65. if info == None:
  66. print ("No device available.")
  67. return None
  68. if (self.p.get_host_api_info_by_index(info["hostApi"])["name"]).find("WASAPI") == -1:
  69. for i in range(0, self.p.get_device_count()):
  70. x = self.p.get_device_info_by_index(i)
  71. is_wasapi = (self.p.get_host_api_info_by_index(x["hostApi"])["name"]).find("WASAPI") != -1
  72. if x["name"].find(info["name"]) >= 0 and is_wasapi:
  73. info = x
  74. break
  75. #Handle no devices available
  76. if info == None:
  77. print ("Device doesn't support WASAPI")
  78. return None
  79. info["channels"] = info["maxInputChannels"] if (info["maxOutputChannels"] < info["maxInputChannels"]) else info["maxOutputChannels"]
  80. return info
  81. def openStream(self, dev):
  82. is_input = dev["maxInputChannels"] > 0
  83. is_wasapi = (self.p.get_host_api_info_by_index(dev["hostApi"])["name"]).find("WASAPI") != -1
  84. #print("is input: {} is wasapi: {}".format(is_input, is_wasapi))
  85. if not is_input and not is_wasapi:
  86. print ("Selection is output and does not support loopback mode.")
  87. return None
  88. if is_wasapi:
  89. stream = self.p.open(
  90. format = pyaudio.paInt16,
  91. channels = (dev["channels"] if dev["channels"] < 2 else 2),
  92. rate = int(self._sampleRate),
  93. input = True,
  94. frames_per_buffer = self.buffersize,
  95. input_device_index = dev["index"],
  96. stream_callback=self.streamCallback,
  97. as_loopback = False if is_input else is_wasapi)
  98. else:
  99. stream = self.p.open(
  100. format = pyaudio.paInt16,
  101. channels = (dev["channels"] if dev["channels"] < 2 else 2),
  102. rate = int(self._sampleRate),
  103. input = True,
  104. frames_per_buffer = self.buffersize,
  105. input_device_index = dev["index"],
  106. stream_callback=self.streamCallback)
  107. return stream
  108. def closeStream(self):
  109. if not self._stream:
  110. return False
  111. self._stream.close()
  112. return True
  113. def streamCallback(self, buf, frame_count, time_info, flag):
  114. self._buf = buf
  115. arr = np.array(struct.unpack("%dh" % (len(buf)/2), buf))
  116. mx = arr.max()
  117. self._agcIndex += 1
  118. if self._agcIndex >= self._agcLen:
  119. self._agcIndex = 0
  120. self._agcMaxima[self._agcIndex] = mx
  121. self.meanAmp = np.max(np.absolute(self._agcMaxima))
  122. if self.meanAmp > 2**15 * 0.02:
  123. amp = 1 / self.meanAmp
  124. else:
  125. amp = 1 / (2**15 * 0.02)
  126. if self._device["channels"] >= 2:
  127. self.left, self.right = arr[::2] * amp, arr[1::2] * amp
  128. if self._doFFT:
  129. self.fft = self.fftCalc((self.left+self.right)/2)
  130. else:
  131. self.left = self.right = arr * amp
  132. if self._doFFT:
  133. self.fft = self.fftCalc(self.left)
  134. self._hasNewData = True
  135. if self._doAgcFFT:
  136. self.agcFFT = np.fft.rfft(self._agcMaxima, self.beatnFFT) / self.beatnFFT
  137. if self._beatCb and mx * (time_info["current_time"] - self._lastBeatTime) > 0.5:
  138. self._lastBeatTime = time_info["current_time"]
  139. self._beatCb(self.fft)
  140. return (None, pyaudio.paContinue)
  141. def hasNewData(self):
  142. if not self._hasNewData:
  143. return False
  144. self._hasNewData = False
  145. return True
  146. def getSampleRate(self):
  147. return int(self._sampleRate)
  148. def getAgc(self):
  149. return self.meanAmp / 2**15
  150. def getVolume(self):
  151. if self._agcMaxima.sum() == 0:
  152. return 0
  153. return self._agcMaxima[self._agcIndex] / self.meanAmp
  154. def isActive(self):
  155. if not self._stream:
  156. return False
  157. return self._stream.is_active()
  158. def fftSetLimits(self, nFFT, fMin, fMax):
  159. self._doFFT = True
  160. self.nFFT = nFFT
  161. self.fftMin = int(fMin / self._sampleRate * nFFT)
  162. self.fftMax = int(fMax / self._sampleRate * nFFT)
  163. print("nFFT: {} \tfftMin: {} \tfftMax: {}".format(self.nFFT, self.fftMin, self.fftMax))
  164. def agcFftSetLimits(self, fMin, fMax):
  165. self._doAgcFFT = True
  166. self.beatnFFT = self._agcLen
  167. self.beatFftMin = int(fMin * self._dataTime * self.beatnFFT)
  168. self.beatFftMax = int(fMax * self._dataTime * self.beatnFFT)
  169. print("beat nFFT: {} \tfftMin: {} \tfftMax: {}".format(self.beatnFFT, self.beatFftMin, self.beatFftMax))
  170. def fftCalc(self, data):
  171. return abs(np.fft.rfft(data, self.nFFT)[self.fftMin:self.fftMax]) / self.nFFT
  172. def fftGroup(self, fft, limits):
  173. groups = []
  174. for freqs in zip(limits, limits[1:]):
  175. a = int(freqs[0] / self._sampleRate * self.nFFT)
  176. b = int(freqs[1] / self._sampleRate * self.nFFT)
  177. #groups.append(sum(fft[a:b]) / (b-a) if (b-a) > 0 else 0)
  178. if b != a:
  179. groups.append(max(fft[a:b]))
  180. else:
  181. groups.append(fft[a])
  182. return groups