Skip to content

Commit 814581a

Browse files
committed
Add "plot input" example
1 parent 68e26a0 commit 814581a

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed

doc/examples.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ Input to Output Pass-Through
1717

1818
.. literalinclude:: ../examples/wire.py
1919

20+
Plot Microphone Signal(s) in Real-Time
21+
--------------------------------------
22+
23+
:download:`plot_input.py <../examples/plot_input.py>`
24+
25+
.. literalinclude:: ../examples/plot_input.py
26+
2027
Real-Time Text-Mode Spectrogram
2128
-------------------------------
2229

examples/plot_input.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
"""Plot the live microphone signal(s) with matplotlib."""
3+
import argparse
4+
from queue import Queue, Empty
5+
6+
7+
def int_or_str(text):
8+
"""Helper function for argument parsing."""
9+
try:
10+
return int(text)
11+
except ValueError:
12+
return text
13+
14+
15+
parser = argparse.ArgumentParser(description=__doc__)
16+
parser.add_argument(
17+
'-l', '--list-devices', action='store_true',
18+
help='show list of audio devices and exit')
19+
parser.add_argument(
20+
'-d', '--device', type=int_or_str,
21+
help='input device (numeric ID or substring)')
22+
parser.add_argument(
23+
'-w', '--window', type=float, default=200, metavar='DURATION',
24+
help='visible time slot (default: %(default)s ms)')
25+
parser.add_argument(
26+
'-i', '--interval', type=float, default=30,
27+
help='minimum time between plot updates (default: %(default)s ms)')
28+
parser.add_argument(
29+
'-b', '--blocksize', type=int, help='block size (in samples)')
30+
parser.add_argument(
31+
'-r', '--samplerate', type=float, help='sampling rate of audio device')
32+
parser.add_argument(
33+
'-n', '--downsample', type=int, default=10, metavar='N',
34+
help='display every Nth sample (default: %(default)s)')
35+
parser.add_argument(
36+
'channels', type=int, default=[1], nargs='*', metavar='CHANNEL',
37+
help='input channels to plot (default: the first)')
38+
args = parser.parse_args()
39+
if any(c < 1 for c in args.channels):
40+
parser.error('argument CHANNEL: must be >= 1')
41+
mapping = [c - 1 for c in args.channels] # Channel numbers start with 1
42+
queue = Queue()
43+
44+
45+
def audio_callback(indata, frames, time, status):
46+
"""This is called (from a separate thread) for each audio block."""
47+
if status:
48+
print(status, flush=True)
49+
# Fancy indexing with mapping creates a (necessary!) copy:
50+
queue.put(indata[::args.downsample, mapping])
51+
52+
53+
def update_plot(frame):
54+
"""This is called by matplotlib for each plot update.
55+
56+
Typically, audio callbacks happen more frequently than plot updates,
57+
therefore the queue tends to contain multiple blocks of audio data.
58+
59+
"""
60+
global plotdata
61+
block = True # The first read from the queue is blocking ...
62+
while True:
63+
try:
64+
data = queue.get(block=block)
65+
except Empty:
66+
break
67+
shift = len(data)
68+
plotdata = np.roll(plotdata, -shift, axis=0)
69+
plotdata[-shift:, :] = data
70+
block = False # ... all further reads are non-blocking
71+
for column, line in enumerate(lines):
72+
line.set_ydata(plotdata[:, column])
73+
return lines
74+
75+
76+
try:
77+
from matplotlib.animation import FuncAnimation
78+
import matplotlib.pyplot as plt
79+
import numpy as np
80+
import sounddevice as sd
81+
82+
if args.list_devices:
83+
print(sd.query_devices())
84+
parser.exit()
85+
if args.samplerate is None:
86+
device_info = sd.query_devices(args.device, 'input')
87+
args.samplerate = device_info['default_samplerate']
88+
89+
length = np.ceil(args.window * args.samplerate / (1000 * args.downsample))
90+
plotdata = np.zeros((length, len(args.channels)))
91+
92+
fig, ax = plt.subplots()
93+
lines = ax.plot(plotdata)
94+
if len(args.channels) > 1:
95+
ax.legend(['channel {}'.format(c) for c in args.channels],
96+
loc='lower left', ncol=len(args.channels))
97+
ax.axis((0, len(plotdata), -1, 1))
98+
ax.set_yticks([0])
99+
ax.yaxis.grid(True)
100+
ax.tick_params(bottom='off', top='off', labelbottom='off',
101+
right='off', left='off', labelleft='off')
102+
fig.tight_layout(pad=0)
103+
104+
stream = sd.InputStream(
105+
device=args.device, channels=max(args.channels),
106+
samplerate=args.samplerate, callback=audio_callback)
107+
ani = FuncAnimation(fig, update_plot, interval=args.interval, blit=True)
108+
with stream:
109+
plt.show()
110+
except Exception as e:
111+
parser.exit(type(e).__name__ + ': ' + str(e))

0 commit comments

Comments
 (0)