Skip to content

Commit da2ed11

Browse files
committed
1 parent ce3b3f4 commit da2ed11

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

doc/examples.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,10 @@ Input to Output Pass-Through
1616
:download:`wire.py <../examples/wire.py>`
1717

1818
.. literalinclude:: ../examples/wire.py
19+
20+
Real-Time Text-Mode Spectrogram
21+
-------------------------------
22+
23+
:download:`spectrogram.py <../examples/spectrogram.py>`
24+
25+
.. literalinclude:: ../examples/spectrogram.py

examples/spectrogram.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
"""Show a text-mode spectrogram using live microphone data."""
3+
import argparse
4+
import logging
5+
import numpy as np
6+
import shutil
7+
8+
usage_line = ' press <enter> to quit, +<enter> or -<enter> to change scaling '
9+
10+
try:
11+
columns, _ = shutil.get_terminal_size()
12+
except AttributeError:
13+
columns = 80
14+
15+
parser = argparse.ArgumentParser(description=__doc__)
16+
parser.add_argument('-l', '--list-devices', action='store_true',
17+
help='list audio devices and exit')
18+
parser.add_argument('-b', '--block-duration', type=float,
19+
metavar='DURATION', default=50,
20+
help='block size (default %(default)s milliseconds)')
21+
parser.add_argument('-c', '--columns', type=int, default=columns,
22+
help='width of spectrogram')
23+
parser.add_argument('-d', '--device', type=int, help='input device ID')
24+
parser.add_argument('-g', '--gain', type=float, default=10,
25+
help='initial gain factor (default %(default)s)')
26+
parser.add_argument('-r', '--range', type=float, nargs=2,
27+
metavar=('LOW', 'HIGH'), default=[100, 2000],
28+
help='frequency range (default %(default)s Hz)')
29+
args = parser.parse_args()
30+
31+
low, high = args.range
32+
if high <= low:
33+
parser.error("HIGH must be greater than LOW")
34+
35+
# Create a nice output gradient using ANSI escape sequences.
36+
# Stolen from https://gist.github.com/maurisvh/df919538bcef391bc89f
37+
colors = 30, 34, 35, 91, 93, 97
38+
chars = ' :%#\t#%:'
39+
gradient = []
40+
for bg, fg in zip(colors, colors[1:]):
41+
for char in chars:
42+
if char == '\t':
43+
bg, fg = fg, bg
44+
else:
45+
gradient.append('\x1b[{};{}m{}'.format(fg, bg + 10, char))
46+
47+
try:
48+
import sounddevice as sd
49+
50+
if args.list_devices:
51+
sd.print_devices()
52+
parser.exit()
53+
54+
if args.device is None:
55+
args.device = sd.default.device['input']
56+
samplerate = sd.query_devices(args.device)['default_samplerate']
57+
58+
delta_f = (high - low) / (args.columns - 1)
59+
fftsize = np.ceil(samplerate / delta_f).astype(int)
60+
low_bin = np.floor(low / delta_f)
61+
62+
statuses = sd.CallbackFlags()
63+
64+
def callback(indata, frames, time, status):
65+
global statuses
66+
statuses |= status
67+
if any(indata):
68+
magnitude = np.abs(np.fft.rfft(indata[:, 0], n=fftsize))
69+
magnitude *= args.gain / fftsize
70+
line = (gradient[int(np.clip(x, 0, 1) * (len(gradient) - 1))]
71+
for x in magnitude[low_bin:low_bin + args.columns])
72+
print(*line, sep='', end='\x1b[0m\n', flush=True)
73+
else:
74+
print('no input', flush=True)
75+
76+
with sd.InputStream(device=args.device, channels=1, callback=callback,
77+
blocksize=int(samplerate * args.block_duration / 1000),
78+
samplerate=samplerate):
79+
while True:
80+
response = input()
81+
if response in ('', 'q', 'Q'):
82+
break
83+
for ch in response:
84+
if ch == '+':
85+
args.gain *= 2
86+
elif ch == '-':
87+
args.gain /= 2
88+
else:
89+
print('\x1b[31;40m', usage_line.center(args.columns, '#'),
90+
'\x1b[0m', sep='')
91+
break
92+
if statuses:
93+
logging.warning(str(statuses))
94+
except KeyboardInterrupt:
95+
parser.exit('Interrupted by user')
96+
except Exception as e:
97+
parser.exit(str(e))

0 commit comments

Comments
 (0)