Mercurial > code > home > repos > homeauto
annotate service/audioInputLevels/audioInputLevelsPulse.py @ 1754:92999dfbf321 default tip
add shelly support
author | drewp@bigasterisk.com |
---|---|
date | Tue, 04 Jun 2024 13:03:43 -0700 |
parents | 4fa5c6a61282 |
children |
rev | line source |
---|---|
82 | 1 # based on http://freshfoo.com/blog/pulseaudio_monitoring |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
2 # |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
3 # https://github.com/swharden/Python-GUI-examples/blob/master/2016-07-37_qt_audio_monitor/SWHear.py is similar |
82 | 4 from __future__ import division |
479 | 5 import socket, time, logging, os, subprocess |
82 | 6 from Queue import Queue |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
7 from ctypes import c_void_p, c_ulong, string_at |
356 | 8 from docopt import docopt |
307 | 9 from influxdb import InfluxDBClient |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
10 import numpy |
82 | 11 |
12 from pulseaudio import lib_pulseaudio as P | |
13 | |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
14 logging.basicConfig(level=logging.INFO) |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
15 log = logging.getLogger() |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
16 |
82 | 17 |
18 class PeakMonitor(object): | |
19 | |
20 def __init__(self, source_name, rate): | |
21 self.source_name = source_name | |
22 self.rate = rate | |
23 | |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
24 self.bufs = [] |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
25 self.buf_samples = 0 |
721 | 26 |
82 | 27 # Wrap callback methods in appropriate ctypefunc instances so |
28 # that the Pulseaudio C API can call them | |
29 self._context_notify_cb = P.pa_context_notify_cb_t(self.context_notify_cb) | |
30 self._source_info_cb = P.pa_source_info_cb_t(self.source_info_cb) | |
31 self._stream_read_cb = P.pa_stream_request_cb_t(self.stream_read_cb) | |
32 | |
33 # stream_read_cb() puts peak samples into this Queue instance | |
34 self._samples = Queue() | |
35 | |
36 # Create the mainloop thread and set our context_notify_cb | |
37 # method to be called when there's updates relating to the | |
38 # connection to Pulseaudio | |
39 _mainloop = P.pa_threaded_mainloop_new() | |
40 _mainloop_api = P.pa_threaded_mainloop_get_api(_mainloop) | |
41 context = P.pa_context_new(_mainloop_api, 'peak_demo') | |
42 P.pa_context_set_state_callback(context, self._context_notify_cb, None) | |
43 P.pa_context_connect(context, None, 0, None) | |
44 P.pa_threaded_mainloop_start(_mainloop) | |
45 | |
46 def __iter__(self): | |
47 while True: | |
48 yield self._samples.get() | |
49 | |
50 def context_notify_cb(self, context, _): | |
51 state = P.pa_context_get_state(context) | |
52 | |
53 if state == P.PA_CONTEXT_READY: | |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
54 log.info("Pulseaudio connection ready...") |
82 | 55 # Connected to Pulseaudio. Now request that source_info_cb |
56 # be called with information about the available sources. | |
57 o = P.pa_context_get_source_info_list(context, self._source_info_cb, None) | |
58 P.pa_operation_unref(o) | |
59 | |
60 elif state == P.PA_CONTEXT_FAILED : | |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
61 log.error("Connection failed") |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
62 os.abort() |
82 | 63 |
64 elif state == P.PA_CONTEXT_TERMINATED: | |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
65 log.error("Connection terminated") |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
66 os.abort() |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
67 |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
68 else: |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
69 log.info('context_notify_cb state=%r', state) |
82 | 70 |
71 def source_info_cb(self, context, source_info_p, _, __): | |
72 if not source_info_p: | |
73 return | |
74 | |
75 source_info = source_info_p.contents | |
76 | |
77 if source_info.name == self.source_name: | |
78 # Found the source we want to monitor for peak levels. | |
79 # Tell PA to call stream_read_cb with peak samples. | |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
80 log.info('setting up peak recording using %s', source_info.name) |
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
81 log.info('description: %r', source_info.description) |
721 | 82 |
82 | 83 samplespec = P.pa_sample_spec() |
84 samplespec.channels = 1 | |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
85 samplespec.format = P.PA_SAMPLE_S32LE |
82 | 86 samplespec.rate = self.rate |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
87 pa_stream = P.pa_stream_new(context, "audioInputLevels", samplespec, None) |
721 | 88 |
82 | 89 P.pa_stream_set_read_callback(pa_stream, |
90 self._stream_read_cb, | |
91 source_info.index) | |
92 P.pa_stream_connect_record(pa_stream, | |
93 source_info.name, | |
94 None, | |
95 P.PA_STREAM_PEAK_DETECT) | |
96 | |
97 def stream_read_cb(self, stream, length, index_incr): | |
98 data = c_void_p() | |
99 P.pa_stream_peek(stream, data, c_ulong(length)) | |
339
30cd6cb833e3
audioInputLevels robustness and cleanup
drewp@bigasterisk.com
parents:
307
diff
changeset
|
100 try: |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
101 buf = string_at(data, length) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
102 arr = numpy.fromstring(buf, dtype=numpy.dtype('<i4')) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
103 self.bufs.append(arr) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
104 self.buf_samples += arr.shape[0] |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
105 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
106 if self.buf_samples > self.rate * 1.0: |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
107 self.onChunk(numpy.concatenate(self.bufs)) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
108 self.bufs = [] |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
109 self.buf_samples = 0 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
110 finally: |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
111 P.pa_stream_drop(stream) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
112 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
113 def fft(self, arr): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
114 t1 = time.time() |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
115 # if this is slow, try |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
116 # https://hgomersall.github.io/pyFFTW/sphinx/tutorial.html#the-workhorse-pyfftw-fftw-class |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
117 # . But, it seems to take 1-10ms per second of audio, so who |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
118 # cares. |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
119 mags = numpy.abs(numpy.fft.fft(arr)) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
120 ft = time.time() - t1 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
121 return mags, ft |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
122 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
123 def timeSinceLastChunk(self): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
124 now = time.time() |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
125 if hasattr(self, 'lastRead'): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
126 dt = now - self.lastRead |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
127 else: |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
128 dt = 1 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
129 self.lastRead = now |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
130 return dt |
721 | 131 |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
132 def onChunk(self, arr): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
133 dt = self.timeSinceLastChunk() |
721 | 134 |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
135 n = 8192 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
136 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
137 mags, ft = self.fft(arr[:n]) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
138 freqs = numpy.fft.fftfreq(n, d=1.0/self.rate) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
139 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
140 def freq_range(lo, hi): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
141 mask = (lo < freqs) & (freqs < hi) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
142 return numpy.sum(mags * mask) / numpy.count_nonzero(mask) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
143 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
144 scl = 1000000000 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
145 bands = {'hi': freq_range(500, 8192) / scl, |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
146 'mid': freq_range(300, 500) / scl, |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
147 'lo': freq_range(90, 300) / scl, |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
148 'value': freq_range(90, 8192) / scl, |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
149 } |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
150 log.debug('%r', bands) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
151 #import ipdb;ipdb.set_trace() |
721 | 152 |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
153 if log.isEnabledFor(logging.DEBUG): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
154 self.dumpFreqs(n, dt, ft, scl, arr, freqs, mags) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
155 self._samples.put(bands) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
156 |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
157 def dumpFreqs(self, n, dt, ft, scl, arr, freqs, mags): |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
158 log.debug( |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
159 'new chunk of %s samples, dt ~%.4f; ~%.1f hz; max %.1f; fft took %.2fms', |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
160 n, dt, n / dt, arr.max() / scl, ft * 1000, |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
161 ) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
162 rows = zip(freqs, mags) |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
163 for fr,v in rows[1:8] + rows[8:n//8:30]: |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
164 log.debug('%6.1f %6.3f %s', |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
165 fr, v / scl, '*' * int(min(80, 80 * int(v) / scl))) |
721 | 166 |
82 | 167 |
168 def main(): | |
356 | 169 arg = docopt(""" |
170 Usage: audioInputLevelsPulse.py [-v] --source=<name> | |
82 | 171 |
356 | 172 --source=<name> pulseaudio source name (use `pactl list sources | grep Name`) |
173 -v Verbose | |
174 """) | |
175 | |
176 log.setLevel(logging.DEBUG if arg['-v'] else logging.INFO) | |
82 | 177 |
479 | 178 # todo move this into the PeakMonitor part |
179 subprocess.check_output(['pactl', | |
180 'set-source-volume', arg['--source'], '94900']) | |
721 | 181 |
182 influx = InfluxDBClient('bang5, 9060, 'root', 'root', 'main') | |
307 | 183 |
184 hostname = socket.gethostname() | |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
185 METER_RATE = 8192 |
356 | 186 monitor = PeakMonitor(arg['--source'], METER_RATE) |
82 | 187 for sample in monitor: |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
188 log.debug(' %6.3f %s', sample['value'], '>' * int(min(80, sample['value'] * 80))) |
307 | 189 influx.write_points([{'measurement': 'audioLevel', |
357
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
190 "fields": sample, |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
191 "time": int(time.time())}], |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
192 tags=dict(location=hostname), |
b087642a456f
audiolevels now saves multiple frequency bands
drewp@bigasterisk.com
parents:
356
diff
changeset
|
193 time_precision='s') |
721 | 194 |
82 | 195 if __name__ == '__main__': |
196 main() |