Skip to content

Instantly share code, notes, and snippets.

@maurisvh
Last active August 9, 2022 09:00
Show Gist options
  • Star 35 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save maurisvh/df919538bcef391bc89f to your computer and use it in GitHub Desktop.
Save maurisvh/df919538bcef391bc89f to your computer and use it in GitHub Desktop.
ANSI art spectrogram viewer that reads audio from a microphone
#!/usr/bin/python
import numpy
import pyaudio
import re
import sys
WIDTH = 79
BOOST = 1.0
# Create a nice output gradient using ANSI escape sequences.
cols = [30, 34, 35, 91, 93, 97]
chars = [(' ', False), (':', False), ('%', False), ('#', False),
('#', True), ('%', True), (':', True)]
gradient = []
for bg, fg in zip(cols, cols[1:]):
for char, invert in chars:
if invert:
bg, fg = fg, bg
gradient.append('\x1b[{};{}m{}'.format(fg, bg + 10, char))
class Spectrogram(object):
def __init__(self):
self.audio = pyaudio.PyAudio()
def __enter__(self):
"""Open the microphone stream."""
device_index = self.find_input_device()
device_info = self.audio.get_device_info_by_index(device_index)
rate = int(device_info['defaultSampleRate'])
self.buffer_size = int(rate * 0.02)
self.stream = self.audio.open(format=pyaudio.paInt16,
channels=1, rate=rate, input=True,
input_device_index=device_index,
frames_per_buffer=self.buffer_size)
return self
def __exit__(self, *ignored):
"""Close the microphone stream."""
self.stream.close()
def find_input_device(self):
"""
Find a microphone input device. Return None if no preferred
deveice was found, and the default should be used.
"""
for i in range(self.audio.get_device_count()):
name = self.audio.get_device_info_by_index(i)['name']
if re.match('mic|input', name, re.I):
return i
return None
def color(self, x):
"""
Given 0 <= x <= 1 (input is clamped), return a string of ANSI
escape sequences representing a gradient color.
"""
x = max(0.0, min(1.0, x))
return gradient[int(x * (len(gradient) - 1))]
def listen(self):
"""Listen for one buffer of audio and print a gradient."""
block_string = self.stream.read(self.buffer_size)
block = numpy.fromstring(block_string, dtype='h') / 32768.0
nbands = 30 * WIDTH
fft = abs(numpy.fft.fft(block, n=nbands))
pos, neg = numpy.split(fft, 2)
bands = (pos + neg[::-1]) / float(nbands) * BOOST
line = (self.color(x) for x in bands[:WIDTH])
print ''.join(line) + '\x1b[0m'
sys.stdout.flush()
if __name__ == '__main__':
with Spectrogram() as s:
while True:
s.listen()
@wolever
Copy link

wolever commented Sep 28, 2015

Very cool!

I've fixed an issue I was having on the mac where the input device wasn't found by find_input_device here: https://gist.github.com/wolever/47a5aaa9ce9886297814

@mgeier
Copy link

mgeier commented Oct 3, 2015

There's a bug in the color inversion logic: bg and fg get switched each time invert is True, even if they are already switched.

As a result of this, the second % ends up un-inverted.

@mgeier
Copy link

mgeier commented Oct 4, 2015

Here's an alternative implementation:

colors = 30, 34, 35, 91, 93, 97
chars = ' :%#\t#%:'
gradient = []
for bg, fg in zip(colors, colors[1:]):
    for char in chars:
        if char == '\t':
            bg, fg = fg, bg
        else:
            gradient.append('\x1b[{};{}m{}'.format(fg, bg + 10, char))

@mgeier
Copy link

mgeier commented Oct 8, 2015

I took the liberty of turning this into an example application for the sounddevice module: spectrogram.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment