|
@@ -0,0 +1,228 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+
|
|
|
+import pyaudio
|
|
|
+import struct
|
|
|
+
|
|
|
+import numpy as np
|
|
|
+
|
|
|
+class Listener:
|
|
|
+ def __init__(self, dataTime = 1/20, agcTime = 10, input = False):
|
|
|
+ self._dataTime = dataTime
|
|
|
+ self._agcTime = agcTime
|
|
|
+ self._input = input
|
|
|
+ self.p = pyaudio.PyAudio()
|
|
|
+
|
|
|
+ self.left = self.right = self.fft = []
|
|
|
+
|
|
|
+ self._agcMaxima = [0]
|
|
|
+ self._agcIndex = 0
|
|
|
+ self._agcLen = 0
|
|
|
+ self._beatCb = None
|
|
|
+ self._sampleRate = 0
|
|
|
+ self._hasNewData = False
|
|
|
+ self.buffersize = None
|
|
|
+
|
|
|
+ self._doFFT = False
|
|
|
+ self._doAgcFFT = False
|
|
|
+
|
|
|
+ def start(self, dev = None):
|
|
|
+ self._device = dev or self.getDefaultOutputDeviceInfo()
|
|
|
+
|
|
|
+ if self._device == None:
|
|
|
+ print("no device found")
|
|
|
+ return
|
|
|
+
|
|
|
+ print("device name: {} channels: {} defaultSampleRate: {}".format(self._device["name"], self._device["channels"], self._device["defaultSampleRate"]))
|
|
|
+
|
|
|
+ if self._sampleRate != self._device["defaultSampleRate"]:
|
|
|
+ self._sampleRate = self._device["defaultSampleRate"]
|
|
|
+ self.buffersize = int(self._sampleRate * self._dataTime)
|
|
|
+ self.fft = self.right = self.left = np.ndarray((self.buffersize))
|
|
|
+
|
|
|
+ self._agcLen = int(1 / self._dataTime * self._agcTime)
|
|
|
+ self._agcMaxima = np.ndarray((self._agcLen))
|
|
|
+ self._agcMaxima.fill(2**15 * 0.1)
|
|
|
+ self._agcIndex = 0
|
|
|
+ self._lastBeatTime = 0
|
|
|
+ self.meanAmp = 2**15 * 0.1
|
|
|
+
|
|
|
+ try:
|
|
|
+ self._stream = self.openStream(self._device)
|
|
|
+ except OSError:
|
|
|
+ self._stream = None
|
|
|
+
|
|
|
+ if not self._stream:
|
|
|
+ print("stream open failed")
|
|
|
+ return
|
|
|
+
|
|
|
+ self._stream.start_stream()
|
|
|
+
|
|
|
+ return self._stream.is_active()
|
|
|
+
|
|
|
+ def stop(self):
|
|
|
+ if not self._stream:# or not self._stream.is_active():
|
|
|
+ return False
|
|
|
+ self._stream.stop_stream()
|
|
|
+ return True
|
|
|
+
|
|
|
+ def setBeatCb(self, cb):
|
|
|
+ self._beatCb = cb
|
|
|
+
|
|
|
+ def getDefaultOutputDeviceInfo(self):
|
|
|
+ #Set default to first in list or ask Windows
|
|
|
+ try:
|
|
|
+ self.p.terminate()
|
|
|
+ self.p.__init__()
|
|
|
+ if self._input:
|
|
|
+ info = self.p.get_default_input_device_info()
|
|
|
+ else:
|
|
|
+ info = self.p.get_default_output_device_info()
|
|
|
+ except IOError:
|
|
|
+ info = None
|
|
|
+
|
|
|
+ #Handle no devices available
|
|
|
+ if info == None:
|
|
|
+ print ("No device available.")
|
|
|
+ return None
|
|
|
+
|
|
|
+ if (self.p.get_host_api_info_by_index(info["hostApi"])["name"]).find("WASAPI") == -1:
|
|
|
+ for i in range(0, self.p.get_device_count()):
|
|
|
+ x = self.p.get_device_info_by_index(i)
|
|
|
+ is_wasapi = (self.p.get_host_api_info_by_index(x["hostApi"])["name"]).find("WASAPI") != -1
|
|
|
+ if x["name"].find(info["name"]) >= 0 and is_wasapi:
|
|
|
+ info = x
|
|
|
+ break
|
|
|
+
|
|
|
+ #Handle no devices available
|
|
|
+ if info == None:
|
|
|
+ print ("Device doesn't support WASAPI")
|
|
|
+ return None
|
|
|
+
|
|
|
+ info["channels"] = info["maxInputChannels"] if (info["maxOutputChannels"] < info["maxInputChannels"]) else info["maxOutputChannels"]
|
|
|
+
|
|
|
+ return info
|
|
|
+
|
|
|
+ def openStream(self, dev):
|
|
|
+
|
|
|
+ is_input = dev["maxInputChannels"] > 0
|
|
|
+ is_wasapi = (self.p.get_host_api_info_by_index(dev["hostApi"])["name"]).find("WASAPI") != -1
|
|
|
+
|
|
|
+ #print("is input: {} is wasapi: {}".format(is_input, is_wasapi))
|
|
|
+
|
|
|
+ if not is_input and not is_wasapi:
|
|
|
+ print ("Selection is output and does not support loopback mode.")
|
|
|
+ return None
|
|
|
+ if is_wasapi:
|
|
|
+ stream = self.p.open(
|
|
|
+ format = pyaudio.paInt16,
|
|
|
+ channels = (dev["channels"] if dev["channels"] < 2 else 2),
|
|
|
+ rate = int(self._sampleRate),
|
|
|
+ input = True,
|
|
|
+ frames_per_buffer = self.buffersize,
|
|
|
+ input_device_index = dev["index"],
|
|
|
+ stream_callback=self.streamCallback,
|
|
|
+ as_loopback = False if is_input else is_wasapi)
|
|
|
+ else:
|
|
|
+ stream = self.p.open(
|
|
|
+ format = pyaudio.paInt16,
|
|
|
+ channels = (dev["channels"] if dev["channels"] < 2 else 2),
|
|
|
+ rate = int(self._sampleRate),
|
|
|
+ input = True,
|
|
|
+ frames_per_buffer = self.buffersize,
|
|
|
+ input_device_index = dev["index"],
|
|
|
+ stream_callback=self.streamCallback)
|
|
|
+ return stream
|
|
|
+
|
|
|
+ def closeStream(self):
|
|
|
+ if not self._stream:
|
|
|
+ return False
|
|
|
+ self._stream.close()
|
|
|
+ return True
|
|
|
+
|
|
|
+ def streamCallback(self, buf, frame_count, time_info, flag):
|
|
|
+ self._buf = buf
|
|
|
+ arr = np.array(struct.unpack("%dh" % (len(buf)/2), buf))
|
|
|
+
|
|
|
+ mx = arr.max()
|
|
|
+ self._agcIndex += 1
|
|
|
+ if self._agcIndex >= self._agcLen:
|
|
|
+ self._agcIndex = 0
|
|
|
+ self._agcMaxima[self._agcIndex] = mx
|
|
|
+
|
|
|
+ self.meanAmp = np.max(np.absolute(self._agcMaxima))
|
|
|
+
|
|
|
+ if self.meanAmp > 2**15 * 0.02:
|
|
|
+ amp = 1 / self.meanAmp
|
|
|
+ else:
|
|
|
+ amp = 1 / (2**15 * 0.02)
|
|
|
+
|
|
|
+ if self._device["channels"] >= 2:
|
|
|
+ self.left, self.right = arr[::2] * amp, arr[1::2] * amp
|
|
|
+ if self._doFFT:
|
|
|
+ self.fft = self.fftCalc((self.left+self.right)/2)
|
|
|
+ else:
|
|
|
+ self.left = self.right = arr * amp
|
|
|
+ if self._doFFT:
|
|
|
+ self.fft = self.fftCalc(self.left)
|
|
|
+
|
|
|
+ self._hasNewData = True
|
|
|
+
|
|
|
+ if self._doAgcFFT:
|
|
|
+ self.agcFFT = np.fft.rfft(self._agcMaxima, self.beatnFFT) / self.beatnFFT
|
|
|
+
|
|
|
+ if self._beatCb and mx * (time_info["current_time"] - self._lastBeatTime) > 0.5:
|
|
|
+ self._lastBeatTime = time_info["current_time"]
|
|
|
+ self._beatCb(self.fft)
|
|
|
+
|
|
|
+ return (None, pyaudio.paContinue)
|
|
|
+
|
|
|
+ def hasNewData(self):
|
|
|
+ if not self._hasNewData:
|
|
|
+ return False
|
|
|
+ self._hasNewData = False
|
|
|
+ return True
|
|
|
+
|
|
|
+ def getSampleRate(self):
|
|
|
+ return int(self._sampleRate)
|
|
|
+
|
|
|
+ def getAgc(self):
|
|
|
+ return self.meanAmp / 2**15
|
|
|
+
|
|
|
+ def getVolume(self):
|
|
|
+ if self._agcMaxima.sum() == 0:
|
|
|
+ return 0
|
|
|
+ return self._agcMaxima[self._agcIndex] / self.meanAmp
|
|
|
+
|
|
|
+ def isActive(self):
|
|
|
+ if not self._stream:
|
|
|
+ return False
|
|
|
+ return self._stream.is_active()
|
|
|
+
|
|
|
+ def fftSetLimits(self, nFFT, fMin, fMax):
|
|
|
+ self._doFFT = True
|
|
|
+ self.nFFT = nFFT
|
|
|
+ self.fftMin = int(fMin / self._sampleRate * nFFT)
|
|
|
+ self.fftMax = int(fMax / self._sampleRate * nFFT)
|
|
|
+ print("nFFT: {} \tfftMin: {} \tfftMax: {}".format(self.nFFT, self.fftMin, self.fftMax))
|
|
|
+
|
|
|
+ def agcFftSetLimits(self, fMin, fMax):
|
|
|
+ self._doAgcFFT = True
|
|
|
+ self.beatnFFT = self._agcLen
|
|
|
+ self.beatFftMin = int(fMin * self._dataTime * self.beatnFFT)
|
|
|
+ self.beatFftMax = int(fMax * self._dataTime * self.beatnFFT)
|
|
|
+ print("beat nFFT: {} \tfftMin: {} \tfftMax: {}".format(self.beatnFFT, self.beatFftMin, self.beatFftMax))
|
|
|
+
|
|
|
+ def fftCalc(self, data):
|
|
|
+ return abs(np.fft.rfft(data, self.nFFT)[self.fftMin:self.fftMax]) / self.nFFT
|
|
|
+
|
|
|
+ def fftGroup(self, fft, limits):
|
|
|
+ groups = []
|
|
|
+ for freqs in zip(limits, limits[1:]):
|
|
|
+ a = int(freqs[0] / self._sampleRate * self.nFFT)
|
|
|
+ b = int(freqs[1] / self._sampleRate * self.nFFT)
|
|
|
+ #groups.append(sum(fft[a:b]) / (b-a) if (b-a) > 0 else 0)
|
|
|
+ if b != a:
|
|
|
+ groups.append(max(fft[a:b]))
|
|
|
+ else:
|
|
|
+ groups.append(fft[a])
|
|
|
+ return groups
|