2016-11-08 02:45:52 +01:00
|
|
|
from __future__ import print_function
|
|
|
|
from __future__ import division
|
|
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
from scipy.ndimage.filters import gaussian_filter1d
|
|
|
|
import config
|
|
|
|
import microphone
|
|
|
|
import dsp
|
|
|
|
import led
|
|
|
|
import gui
|
|
|
|
|
|
|
|
_time_prev = time.time() * 1000.0
|
|
|
|
"""The previous time that the frames_per_second() function was called"""
|
|
|
|
|
2016-11-13 10:11:15 +01:00
|
|
|
_fps = dsp.ExpFilter(val=config.FPS, alpha_decay=0.002, alpha_rise=0.002)
|
2016-11-08 02:45:52 +01:00
|
|
|
"""The low-pass filter used to estimate frames-per-second"""
|
|
|
|
|
|
|
|
|
|
|
|
def frames_per_second():
|
|
|
|
"""Return the estimated frames per second
|
|
|
|
|
|
|
|
Returns the current estimate for frames-per-second (FPS).
|
|
|
|
FPS is estimated by measured the amount of time that has elapsed since
|
|
|
|
this function was previously called. The FPS estimate is low-pass filtered
|
|
|
|
to reduce noise.
|
|
|
|
|
|
|
|
This function is intended to be called one time for every iteration of
|
|
|
|
the program's main loop.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
fps : float
|
|
|
|
Estimated frames-per-second. This value is low-pass filtered
|
|
|
|
to reduce noise.
|
|
|
|
"""
|
|
|
|
global _time_prev, _fps
|
|
|
|
time_now = time.time() * 1000.0
|
|
|
|
dt = time_now - _time_prev
|
|
|
|
_time_prev = time_now
|
|
|
|
if dt == 0.0:
|
|
|
|
return _fps.value
|
|
|
|
return _fps.update(1000.0 / dt)
|
|
|
|
|
|
|
|
|
|
|
|
def interpolate(y, new_length):
|
|
|
|
"""Intelligently resizes the array by linearly interpolating the values
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
y : np.array
|
|
|
|
Array that should be resized
|
|
|
|
|
|
|
|
new_length : int
|
|
|
|
The length of the new interpolated array
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
z : np.array
|
|
|
|
New array with length of new_length that contains the interpolated
|
|
|
|
values of y.
|
|
|
|
"""
|
|
|
|
if len(y) == new_length:
|
|
|
|
return y
|
|
|
|
x_old = np.linspace(0, 1, len(y))
|
|
|
|
x_new = np.linspace(0, 1, new_length)
|
|
|
|
z = np.interp(x_new, x_old, y)
|
|
|
|
return z
|
|
|
|
|
|
|
|
|
2016-11-13 10:11:15 +01:00
|
|
|
r_filt = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
|
2016-11-22 05:11:45 +01:00
|
|
|
alpha_decay=0.08, alpha_rise=0.99)
|
2016-11-13 10:11:15 +01:00
|
|
|
g_filt = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
|
2016-11-22 05:11:45 +01:00
|
|
|
alpha_decay=0.15, alpha_rise=0.99)
|
2016-11-13 10:11:15 +01:00
|
|
|
b_filt = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
|
2016-11-22 05:11:45 +01:00
|
|
|
alpha_decay=0.25, alpha_rise=0.99)
|
|
|
|
p_filt = dsp.ExpFilter(np.tile(1, (3, config.N_PIXELS // 2)),
|
|
|
|
alpha_decay=0.05, alpha_rise=0.8)
|
2016-12-29 23:51:38 +01:00
|
|
|
p = np.tile(1.0, (3, config.N_PIXELS // 2))
|
2016-11-22 05:11:45 +01:00
|
|
|
gain = dsp.ExpFilter(np.tile(0.01, config.N_FFT_BINS),
|
|
|
|
alpha_decay=0.001, alpha_rise=0.99)
|
|
|
|
|
|
|
|
|
|
|
|
def largest_indices(ary, n):
|
2016-12-29 23:51:38 +01:00
|
|
|
"""Returns indices of the n largest values in the given a numpy array"""
|
2016-11-22 05:11:45 +01:00
|
|
|
flat = ary.flatten()
|
|
|
|
indices = np.argpartition(flat, -n)[-n:]
|
|
|
|
indices = indices[np.argsort(-flat[indices])]
|
|
|
|
return np.unravel_index(indices, ary.shape)
|
|
|
|
|
2016-11-08 02:45:52 +01:00
|
|
|
|
2016-11-22 05:11:45 +01:00
|
|
|
def visualize_max(y):
|
2016-12-29 23:51:38 +01:00
|
|
|
"""Experimental sandbox effect. Not recommended for use"""
|
2016-11-13 10:11:15 +01:00
|
|
|
y = np.copy(interpolate(y, config.N_PIXELS // 2)) * 255.0
|
2016-11-22 05:11:45 +01:00
|
|
|
ind = largest_indices(y, 15)
|
2016-12-29 23:51:38 +01:00
|
|
|
y[ind] *= -1.0
|
|
|
|
y[y > 0] = 0.0
|
|
|
|
y[ind] *= -1.0
|
2016-11-08 02:45:52 +01:00
|
|
|
# Blur the color channels with different strengths
|
2016-11-22 05:11:45 +01:00
|
|
|
r = gaussian_filter1d(y, sigma=0.25)
|
|
|
|
g = gaussian_filter1d(y, sigma=0.10)
|
|
|
|
b = gaussian_filter1d(y, sigma=0.00)
|
|
|
|
b = np.roll(b, 1)
|
|
|
|
b[0] = b[1]
|
2016-11-08 02:45:52 +01:00
|
|
|
r_filt.update(r)
|
|
|
|
g_filt.update(g)
|
|
|
|
b_filt.update(b)
|
2016-11-13 10:11:15 +01:00
|
|
|
# Pixel values
|
|
|
|
pixel_r = np.concatenate((r_filt.value[::-1], r_filt.value))
|
|
|
|
pixel_g = np.concatenate((g_filt.value[::-1], g_filt.value))
|
|
|
|
pixel_b = np.concatenate((b_filt.value[::-1], b_filt.value))
|
2016-11-08 02:45:52 +01:00
|
|
|
# Update the LED strip values
|
2016-11-13 10:11:15 +01:00
|
|
|
led.pixels[0, :] = pixel_r
|
|
|
|
led.pixels[1, :] = pixel_g
|
|
|
|
led.pixels[2, :] = pixel_b
|
2016-12-29 23:51:38 +01:00
|
|
|
led.update()
|
2016-11-08 02:45:52 +01:00
|
|
|
# Update the GUI plots
|
2016-11-13 10:11:15 +01:00
|
|
|
GUI.curve[0][0].setData(y=pixel_r)
|
|
|
|
GUI.curve[0][1].setData(y=pixel_g)
|
|
|
|
GUI.curve[0][2].setData(y=pixel_b)
|
2016-11-08 02:45:52 +01:00
|
|
|
|
|
|
|
|
2016-11-22 05:11:45 +01:00
|
|
|
def visualize_scroll(y):
|
2016-12-29 23:51:38 +01:00
|
|
|
"""Effect that originates in the center and scrolls outwards"""
|
2016-11-22 05:11:45 +01:00
|
|
|
global p
|
|
|
|
y = gaussian_filter1d(y, sigma=1.0)**3.0
|
|
|
|
y = np.copy(y)
|
|
|
|
gain.update(y)
|
|
|
|
y /= gain.value
|
|
|
|
y *= 255.0
|
|
|
|
r = int(max(y[:len(y) // 3]))
|
|
|
|
g = int(max(y[len(y) // 3: 2 * len(y) // 3]))
|
|
|
|
b = int(max(y[2 * len(y) // 3:]))
|
|
|
|
p = np.roll(p, 1, axis=1)
|
2016-12-25 06:54:06 +01:00
|
|
|
p *= 0.98
|
2016-11-22 05:11:45 +01:00
|
|
|
p = gaussian_filter1d(p, sigma=0.2)
|
|
|
|
p[0, 0] = r
|
|
|
|
p[1, 0] = g
|
|
|
|
p[2, 0] = b
|
2016-12-29 23:51:38 +01:00
|
|
|
# Update the LED strip
|
2016-11-22 05:11:45 +01:00
|
|
|
led.pixels = np.concatenate((p[:, ::-1], p), axis=1)
|
|
|
|
led.update()
|
2016-12-29 23:51:38 +01:00
|
|
|
# Update the GUI plots
|
|
|
|
GUI.curve[0][0].setData(y=np.concatenate((p[0, :][::-1], p[0, :])))
|
|
|
|
GUI.curve[0][1].setData(y=np.concatenate((p[1, :][::-1], p[1, :])))
|
|
|
|
GUI.curve[0][2].setData(y=np.concatenate((p[2, :][::-1], p[2, :])))
|
2016-11-22 05:11:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
def visualize_energy(y):
|
2016-12-29 23:51:38 +01:00
|
|
|
"""Effect that expands from the center with increasing sound energy"""
|
2016-11-22 05:11:45 +01:00
|
|
|
global p
|
|
|
|
y = gaussian_filter1d(y, sigma=1.0)**3.0
|
|
|
|
gain.update(y)
|
|
|
|
y /= gain.value
|
|
|
|
y *= (config.N_PIXELS // 2) - 1
|
|
|
|
r = int(np.mean(y[:len(y) // 3]))
|
|
|
|
g = int(np.mean(y[len(y) // 3: 2 * len(y) // 3]))
|
|
|
|
b = int(np.mean(y[2 * len(y) // 3:]))
|
2016-12-29 23:51:38 +01:00
|
|
|
p[0, :r] = 255.0
|
|
|
|
p[0, r:] = 0.0
|
|
|
|
p[1, :g] = 255.0
|
|
|
|
p[1, g:] = 0.0
|
|
|
|
p[2, :b] = 255.0
|
|
|
|
p[2, b:] = 0.0
|
2016-11-22 05:11:45 +01:00
|
|
|
p_filt.update(p)
|
2017-01-01 20:14:31 +01:00
|
|
|
p = np.round(p_filt.value)
|
2016-11-22 05:11:45 +01:00
|
|
|
p[0, :] = gaussian_filter1d(p[0, :], sigma=4.0)
|
|
|
|
p[1, :] = gaussian_filter1d(p[1, :], sigma=4.0)
|
|
|
|
p[2, :] = gaussian_filter1d(p[2, :], sigma=4.0)
|
2016-12-29 23:51:38 +01:00
|
|
|
# Update LED pixel arrays
|
2016-11-22 05:11:45 +01:00
|
|
|
led.pixels = np.concatenate((p[:, ::-1], p), axis=1)
|
|
|
|
led.update()
|
2016-12-29 23:51:38 +01:00
|
|
|
# Update the GUI plots
|
|
|
|
GUI.curve[0][0].setData(y=np.concatenate((p[0, :][::-1], p[0, :])))
|
|
|
|
GUI.curve[0][1].setData(y=np.concatenate((p[1, :][::-1], p[1, :])))
|
|
|
|
GUI.curve[0][2].setData(y=np.concatenate((p[2, :][::-1], p[2, :])))
|
2016-11-22 05:11:45 +01:00
|
|
|
|
|
|
|
|
|
|
|
def visualize_spectrum(y):
|
2016-12-29 23:51:38 +01:00
|
|
|
"""Effect that maps the Mel filterbank frequencies onto the LED strip"""
|
2016-11-22 05:11:45 +01:00
|
|
|
y = np.copy(interpolate(y, config.N_PIXELS // 2)) * 255.0
|
|
|
|
# Blur the color channels with different strengths
|
|
|
|
r = gaussian_filter1d(y, sigma=0.25)
|
|
|
|
g = gaussian_filter1d(y, sigma=0.10)
|
|
|
|
b = gaussian_filter1d(y, sigma=0.00)
|
|
|
|
r_filt.update(r)
|
|
|
|
g_filt.update(g)
|
|
|
|
b_filt.update(b)
|
|
|
|
# Pixel values
|
|
|
|
pixel_r = np.concatenate((r_filt.value[::-1], r_filt.value))
|
|
|
|
pixel_g = np.concatenate((g_filt.value[::-1], g_filt.value))
|
|
|
|
pixel_b = np.concatenate((b_filt.value[::-1], b_filt.value))
|
|
|
|
# Update the LED strip values
|
|
|
|
led.pixels[0, :] = pixel_r
|
|
|
|
led.pixels[1, :] = pixel_g
|
|
|
|
led.pixels[2, :] = pixel_b
|
2016-12-29 23:51:38 +01:00
|
|
|
led.update()
|
2016-11-22 05:11:45 +01:00
|
|
|
# Update the GUI plots
|
|
|
|
GUI.curve[0][0].setData(y=pixel_r)
|
|
|
|
GUI.curve[0][1].setData(y=pixel_g)
|
|
|
|
GUI.curve[0][2].setData(y=pixel_b)
|
|
|
|
|
|
|
|
|
|
|
|
mel_gain = dsp.ExpFilter(np.tile(1e-1, config.N_FFT_BINS),
|
|
|
|
alpha_decay=0.01, alpha_rise=0.99)
|
2016-11-08 02:45:52 +01:00
|
|
|
volume = dsp.ExpFilter(config.MIN_VOLUME_THRESHOLD,
|
|
|
|
alpha_decay=0.02, alpha_rise=0.02)
|
|
|
|
|
2016-11-13 10:11:15 +01:00
|
|
|
|
2016-11-08 02:45:52 +01:00
|
|
|
def microphone_update(stream):
|
2016-11-12 00:52:57 +01:00
|
|
|
global y_roll, prev_rms, prev_exp
|
2016-12-29 23:51:38 +01:00
|
|
|
# Retrieve and normalize the new audio samples
|
2016-11-13 10:11:15 +01:00
|
|
|
y = np.fromstring(stream.read(samples_per_frame,
|
|
|
|
exception_on_overflow=False), dtype=np.int16)
|
2016-11-08 02:45:52 +01:00
|
|
|
y = y / 2.0**15
|
|
|
|
# Construct a rolling window of audio samples
|
|
|
|
y_roll = np.roll(y_roll, -1, axis=0)
|
|
|
|
y_roll[-1, :] = np.copy(y)
|
|
|
|
y_data = np.concatenate(y_roll, axis=0)
|
|
|
|
volume.update(np.nanmean(y_data ** 2))
|
|
|
|
|
|
|
|
if volume.value < config.MIN_VOLUME_THRESHOLD:
|
|
|
|
print('No audio input. Volume below threshold. Volume:', volume.value)
|
2016-11-22 05:11:45 +01:00
|
|
|
led.pixels = np.tile(0, (3, config.N_PIXELS))
|
|
|
|
led.update()
|
2016-11-08 02:45:52 +01:00
|
|
|
else:
|
2016-12-29 23:51:38 +01:00
|
|
|
# Transform audio input into the frequency domain
|
2016-11-08 02:45:52 +01:00
|
|
|
XS, YS = dsp.fft(y_data, window=np.hamming)
|
2016-12-29 23:51:38 +01:00
|
|
|
# Remove half of the FFT data because of symmetry
|
2016-11-13 10:11:15 +01:00
|
|
|
YS = YS[:len(YS) // 2]
|
|
|
|
XS = XS[:len(XS) // 2]
|
2016-12-29 23:51:38 +01:00
|
|
|
# Construct a Mel filterbank from the FFT data
|
2016-11-08 02:45:52 +01:00
|
|
|
YS = np.atleast_2d(np.abs(YS)).T * dsp.mel_y.T
|
2016-12-29 23:51:38 +01:00
|
|
|
# Scale data to values more suitable for visualization
|
2016-11-08 02:45:52 +01:00
|
|
|
YS = np.sum(YS, axis=0)**2.0
|
2016-11-22 05:11:45 +01:00
|
|
|
mel = YS**0.5
|
2016-11-13 10:11:15 +01:00
|
|
|
mel = gaussian_filter1d(mel, sigma=1.0)
|
2016-12-29 23:51:38 +01:00
|
|
|
# Normalize the Mel filterbank to make it volume independent
|
2016-11-12 00:52:57 +01:00
|
|
|
mel_gain.update(np.max(mel))
|
2016-11-08 02:45:52 +01:00
|
|
|
mel = mel / mel_gain.value
|
2016-12-29 23:51:38 +01:00
|
|
|
# Visualize the filterbank output
|
2017-01-01 20:14:31 +01:00
|
|
|
visualization_effect(mel)
|
2016-11-08 02:45:52 +01:00
|
|
|
GUI.app.processEvents()
|
2016-11-22 05:11:45 +01:00
|
|
|
print('FPS {:.0f} / {:.0f}'.format(frames_per_second(), config.FPS))
|
2016-11-08 02:45:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
# Number of audio samples to read every time frame
|
|
|
|
samples_per_frame = int(config.MIC_RATE / config.FPS)
|
|
|
|
|
|
|
|
# Array containing the rolling audio sample window
|
|
|
|
y_roll = np.random.rand(config.N_ROLLING_HISTORY, samples_per_frame) / 1e16
|
|
|
|
|
2017-01-01 20:14:31 +01:00
|
|
|
visualization_effect = visualize_spectrum
|
|
|
|
"""Visualization effect to display on the LED strip"""
|
2016-11-08 02:45:52 +01:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
import pyqtgraph as pg
|
2016-12-29 23:51:38 +01:00
|
|
|
# Create GUI plot for visualizing LED strip output
|
2016-11-08 02:45:52 +01:00
|
|
|
GUI = gui.GUI(width=800, height=400, title='Audio Visualization')
|
|
|
|
GUI.add_plot('Color Channels')
|
2016-11-12 00:52:57 +01:00
|
|
|
r_pen = pg.mkPen((255, 30, 30, 200), width=6)
|
|
|
|
g_pen = pg.mkPen((30, 255, 30, 200), width=6)
|
|
|
|
b_pen = pg.mkPen((30, 30, 255, 200), width=6)
|
2016-11-08 02:45:52 +01:00
|
|
|
GUI.add_curve(plot_index=0, pen=r_pen)
|
|
|
|
GUI.add_curve(plot_index=0, pen=g_pen)
|
|
|
|
GUI.add_curve(plot_index=0, pen=b_pen)
|
2016-11-12 00:52:57 +01:00
|
|
|
GUI.plot[0].setRange(xRange=(0, config.N_PIXELS), yRange=(-5, 275))
|
2016-11-13 10:11:15 +01:00
|
|
|
GUI.curve[0][0].setData(x=range(config.N_PIXELS))
|
|
|
|
GUI.curve[0][1].setData(x=range(config.N_PIXELS))
|
|
|
|
GUI.curve[0][2].setData(x=range(config.N_PIXELS))
|
2017-01-01 20:14:31 +01:00
|
|
|
# Add ComboBox for effect selection
|
|
|
|
effect_list = {
|
|
|
|
'Scroll effect' : visualize_scroll,
|
|
|
|
'Spectrum effect' : visualize_spectrum,
|
|
|
|
'Energy effect' : visualize_energy
|
|
|
|
}
|
|
|
|
effect_combobox = pg.ComboBox(items=effect_list)
|
|
|
|
def effect_change():
|
|
|
|
global visualization_effect
|
|
|
|
visualization_effect = effect_combobox.value()
|
2017-01-01 21:12:38 +01:00
|
|
|
effect_combobox.setValue(visualization_effect)
|
2017-01-01 20:14:31 +01:00
|
|
|
effect_combobox.currentIndexChanged.connect(effect_change)
|
|
|
|
GUI.layout.addWidget(effect_combobox)
|
2016-11-08 02:45:52 +01:00
|
|
|
# Initialize LEDs
|
|
|
|
led.update()
|
|
|
|
# Start listening to live audio stream
|
|
|
|
microphone.start_stream(microphone_update)
|