changeset 357:b087642a456f

audiolevels now saves multiple frequency bands Ignore-this: 933a5d32f4d97ee148f15a3c6b12235e
author drewp@bigasterisk.com
date Wed, 05 Sep 2018 06:06:25 -0700
parents c1d38b884a2e
children 7096dad37074
files service/audioInputLevels/audioInputLevelsPulse.py service/audioInputLevels/makefile service/audioInputLevels/requirements.txt
diffstat 3 files changed, 83 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/service/audioInputLevels/audioInputLevelsPulse.py	Wed Sep 05 01:51:14 2018 -0700
+++ b/service/audioInputLevels/audioInputLevelsPulse.py	Wed Sep 05 06:06:25 2018 -0700
@@ -1,18 +1,19 @@
 # based on http://freshfoo.com/blog/pulseaudio_monitoring
+#
+# https://github.com/swharden/Python-GUI-examples/blob/master/2016-07-37_qt_audio_monitor/SWHear.py is similar
 from __future__ import division
 import socket, time, logging, os
 from Queue import Queue
-from ctypes import POINTER, c_ubyte, c_void_p, c_ulong, cast
+from ctypes import c_void_p, c_ulong, string_at
 from docopt import docopt
 from influxdb import InfluxDBClient
+import numpy
 
-# From https://github.com/Valodim/python-pulseaudio
 from pulseaudio import lib_pulseaudio as P
 
 logging.basicConfig(level=logging.INFO)
 log = logging.getLogger()
 
-METER_RATE = 1
 
 class PeakMonitor(object):
 
@@ -20,6 +21,9 @@
         self.source_name = source_name
         self.rate = rate
 
+        self.bufs = []
+        self.buf_samples = 0
+        
         # Wrap callback methods in appropriate ctypefunc instances so
         # that the Pulseaudio C API can call them
         self._context_notify_cb = P.pa_context_notify_cb_t(self.context_notify_cb)
@@ -78,7 +82,7 @@
             
             samplespec = P.pa_sample_spec()
             samplespec.channels = 1
-            samplespec.format = P.PA_SAMPLE_U8
+            samplespec.format = P.PA_SAMPLE_S32LE
             samplespec.rate = self.rate
             pa_stream = P.pa_stream_new(context, "audioInputLevels", samplespec, None)
             
@@ -93,18 +97,73 @@
     def stream_read_cb(self, stream, length, index_incr):
         data = c_void_p()
         P.pa_stream_peek(stream, data, c_ulong(length))
-        data = cast(data, POINTER(c_ubyte))
         try:
-            for i in xrange(length):
-                # When PA_SAMPLE_U8 is used, samples values range from 128
-                # to 255 because the underlying audio data is signed but
-                # it doesn't make sense to return signed peaks.
-                self._samples.put(data[i] - 128)
-        except ValueError:
-            # "data will be NULL and nbytes will contain the length of the hole"
-            log.info("skipping hole of length %s" % length)
-            # This seems to happen at startup for a while.
-        P.pa_stream_drop(stream)
+            buf = string_at(data, length)
+            arr = numpy.fromstring(buf, dtype=numpy.dtype('<i4'))
+            self.bufs.append(arr)
+            self.buf_samples += arr.shape[0]
+
+            if self.buf_samples > self.rate * 1.0:
+                self.onChunk(numpy.concatenate(self.bufs))
+                self.bufs = []
+                self.buf_samples = 0
+        finally:
+            P.pa_stream_drop(stream)
+
+    def fft(self, arr):
+        t1 = time.time()
+        # if this is slow, try
+        # https://hgomersall.github.io/pyFFTW/sphinx/tutorial.html#the-workhorse-pyfftw-fftw-class
+        # . But, it seems to take 1-10ms per second of audio, so who
+        # cares.
+        mags = numpy.abs(numpy.fft.fft(arr))
+        ft = time.time() - t1
+        return mags, ft
+
+    def timeSinceLastChunk(self):
+        now = time.time()
+        if hasattr(self, 'lastRead'):
+            dt = now - self.lastRead
+        else:
+            dt = 1
+        self.lastRead = now
+        return dt
+        
+    def onChunk(self, arr):
+        dt = self.timeSinceLastChunk()
+        
+        n = 8192
+
+        mags, ft = self.fft(arr[:n])
+        freqs = numpy.fft.fftfreq(n, d=1.0/self.rate)
+
+        def freq_range(lo, hi):
+            mask = (lo < freqs) & (freqs < hi)
+            return numpy.sum(mags * mask) / numpy.count_nonzero(mask)
+
+        scl = 1000000000
+        bands = {'hi': freq_range(500, 8192) / scl,
+                 'mid': freq_range(300, 500) / scl,
+                 'lo': freq_range(90, 300) / scl,
+                 'value': freq_range(90, 8192) / scl,
+                 }
+        log.debug('%r', bands)
+        #import ipdb;ipdb.set_trace()
+        
+        if log.isEnabledFor(logging.DEBUG):
+            self.dumpFreqs(n, dt, ft, scl, arr, freqs, mags)
+        self._samples.put(bands)
+
+    def dumpFreqs(self, n, dt, ft, scl, arr, freqs, mags):
+        log.debug(
+            'new chunk of %s samples, dt ~%.4f; ~%.1f hz; max %.1f; fft took %.2fms',
+            n, dt, n / dt, arr.max() / scl, ft * 1000,
+        )
+        rows = zip(freqs, mags)
+        for fr,v in rows[1:8] + rows[8:n//8:30]:
+            log.debug('%6.1f %6.3f %s',
+                      fr, v / scl, '*' * int(min(80, 80 * int(v) / scl)))
+        
 
 def main():
     arg = docopt("""
@@ -119,13 +178,15 @@
     influx = InfluxDBClient('bang6', 9060, 'root', 'root', 'main')
 
     hostname = socket.gethostname()
+    METER_RATE = 8192
     monitor = PeakMonitor(arg['--source'], METER_RATE)
     for sample in monitor:
-        log.debug(' %3d %s', sample, '>' * sample)
+        log.debug(' %6.3f %s', sample['value'], '>' * int(min(80, sample['value'] * 80)))
         influx.write_points([{'measurement': 'audioLevel',
-                              "tags": dict(stat='max', location=hostname),
-                              "fields": {"value": sample / 128},
-                              "time": int(time.time())}], time_precision='s')
+                              "fields": sample,
+                              "time": int(time.time())}],
+                            tags=dict(location=hostname),
+                            time_precision='s')
         
 if __name__ == '__main__':
     main()
--- a/service/audioInputLevels/makefile	Wed Sep 05 01:51:14 2018 -0700
+++ b/service/audioInputLevels/makefile	Wed Sep 05 06:06:25 2018 -0700
@@ -19,3 +19,5 @@
           bang6:5000/audio_input_levels_x86:latest \
           python /mnt/audioInputLevelsPulse.py --source alsa_input.pci-0000_00_1f.3.analog-stereo -v
 
+local_run_pi:
+	docker run --rm -it  -v /etc/pulse:/etc/pulse  -v /tmp/pulseaudio:/tmp/pulseaudio --net=host  bang6:5000/audio_input_levels_pi:latest python ./audioInputLevelsPulse.py --source alsa_input.usb-C-Media_Electronics_Inc._USB_PnP_Sound_Device-00.analog-mono -v
--- a/service/audioInputLevels/requirements.txt	Wed Sep 05 01:51:14 2018 -0700
+++ b/service/audioInputLevels/requirements.txt	Wed Sep 05 06:06:25 2018 -0700
@@ -1,3 +1,4 @@
 git+http://github.com/dvarrazzo/python-pulseaudio.git@53f10e5cf9ac3d7b49d7859af6006850d63b6d6a#egg=python-pulseaudio
 influxdb==3.0.0
 docopt
+numpy