audio-reactive-led-strip/python/visualization.py
not-matt 61f4defaee
Add files via upload
Added static effects, added new colour modes, improved gradient scrolling and mirroring, cleaned up stuff behind the scenes, added lots more options for different effects
2017-12-27 01:58:55 +00:00

857 lines
43 KiB
Python

from __future__ import print_function
from __future__ import division
from scipy.ndimage.filters import gaussian_filter1d
from collections import deque
import time
import sys
import numpy as np
import config
import microphone
import dsp
import led
if config.USE_GUI:
from qrangeslider import QRangeSlider
from qfloatslider import QFloatSlider
import pyqtgraph as pg
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Visualizer():
def __init__(self):
# Dictionary linking names of effects to their respective functions
self.effects = {"Scroll":self.visualize_scroll,
"Energy":self.visualize_energy,
"Spectrum":self.visualize_spectrum,
#"Power":self.visualize_power,
"Wavelength":self.visualize_wavelength,
"Beat":self.visualize_beat,
"Wave":self.visualize_wave,
"Single":self.visualize_single,
"Fade":self.visualize_fade,
"Gradient":self.visualize_gradient}
#"Auto":self.visualize_auto}
# Collection of different colour in RGB format
self.colors = {"Red":(255,0,0),
"Orange":(255,40,0),
"Yellow":(255,255,0),
"Green":(0,255,0),
"Blue":(0,0,255),
"Light blue":(1,247,161),
"Purple":(80,5,252),
"Pink":(255,0,178),
"White":(255,255,255)}
# List of all the visualisation effects that aren't audio reactive.
# These will still display when no music is playing.
self.non_reactive_effects = ["Single", "Gradient", "Fade"]
# List of names of multicolour gradients, used in various effects
self.multicolor_mode_names = ["Spectral",
"Dancefloor",
"Brilliance",
"Jungle",
"Sky",
"Acid",
"Ocean"]
# The currently selected effect
self.current_effect = "Wavelength"
# Setup for frequency detection algorithm
self.freq_channel_history = 40
self.beat_count = 0
self.freq_channels = [deque(maxlen=self.freq_channel_history) for i in range(config.N_FFT_BINS)]
self.prev_output = np.array([[0 for i in range(config.N_PIXELS)] for i in range(3)])
self.prev_spectrum = [0 for i in range(config.N_PIXELS//2)]
self.current_freq_detects = {"beat":False,
"low":False,
"mid":False,
"high":False}
self.prev_freq_detects = {"beat":0,
"low":0,
"mid":0,
"high":0}
self.detection_ranges = {"beat":(0,1),
"low":(1,int(config.N_FFT_BINS*0.2)),
"mid":(int(config.N_FFT_BINS*0.4),int(config.N_FFT_BINS*0.6)),
"high":(int(config.N_FFT_BINS*0.7),int(config.N_FFT_BINS))}
self.min_detect_amplitude = {"beat":0.7,
"low":0.5,
"mid":0.3,
"high":0.05}
# Configurable options for effects go in this dictionary.
# Usage: self.effect_opts[effect][option]
self.effect_opts = {"Energy":{"blur": 1, # Amount of blur to apply
"scale":0.9}, # Width of effect on strip
"Wave":{"color_wave": "Red", # Colour of moving bit
"color_flash": "White", # Colour of flashy bit
"wipe_len":5, # Initial length of colour bit after beat
"decay": 0.7, # How quickly the flash fades away
"wipe_speed":2}, # Number of pixels added to colour bit every frame
"Wavelength":{"roll_speed": 0, # How fast (if at all) to cycle colour overlay across strip
"color_mode": "Spectral", # Colour mode of overlay
"mirror": False, # Reflect output down centre of strip
"reverse_grad": False, # Flip (LR) gradient
"reverse_roll": False, # Reverse movement of gradient
"blur": 3.0}, # Amount of blur to apply
"Scroll":{"decay": 0.95, # How quickly the colour fades away as it moves
"blur": 0.2}, # Amount of blur to apply
"Power":{"blur": 3.0}, # Amount of blur to apply
"Single":{"color": "Red"}, # Static color to show
"Beat":{"color": "Red", # Colour of beat flash
"decay": 0.7}, # How quickly the flash fades away
"Gradient":{"color_mode":"Spectral", # Colour gradient to display
"roll_speed": 0, # How fast (if at all) to cycle colour overlay across strip
"mirror": False, # Mirror gradient down central axis
"reverse": False}, # Reverse movement of gradient
"Fade":{"color_mode":"Spectral", # Colour gradient to fade through
"roll_speed": 1, # How fast (if at all) to fade through colours
"reverse": False} # Reverse "direction" of fade (r->g->b or r<-g<-b)
}
# Configurations for dynamic ui generation. Effect options can be changed by widgets created at runtime,
# meaning that you don't need to worry about the user interface - it's all done for you. All you need to
# do is add items to this dict below.
#
# First line of code below explained (as an example):
# "Energy" is the visualization we're doing options for
# "blur" is the key in the options dict (self.effect_opts["Energy"]["blur"])
# "Blur" is the string we show on the GUI next to the slider
# "float_slider" is the GUI element we want to use
# (0.1,4.0,0.1) is a tuple containing all the details for setting up the slider (see above)
#
# Each effect key points to a list. Each list contains lists giving config for each option.
# Syntax: effect:[key, label_text, ui_element, opts]
# effect - the effect which you want to change options for. MUST have a key in self.effect_opts
# key - the key of thing you want to be changed. MUST be in self.effect_opts[effect], otherwise it won't work.
# label - the text displayed on the ui
# ui_element - how you want the variable to be changed
# opts - options for the ui element. Must be a tuple.
# UI Elements + opts:
# slider, (min, max, interval) (for integer values in a given range)
# float_slider, (min, max, interval) (for floating point values in a given range)
# checkbox, () (for True/False values)
# dropdown, (dict or list) (dict/list, example see below. Keys will be displayed in the dropdown if dict, otherwise just list items)
#
# Hope this clears things up a bit for you! GUI has never been easier..? The reason for doing this is
# 1 - To make it easy to add options to your effects for the user
# 2 - To give a consistent GUI for the user. If every options page was set out differently it would all be a mess
self.dynamic_effects_config = {"Energy":[["blur", "Blur", "float_slider", (0.1,4.0,0.1)],
["scale", "Scale", "float_slider", (0.4,1.0,0.05)]],
"Wave":[["color_flash", "Flash Color", "dropdown", self.colors],
["color_wave", "Wave Color", "dropdown", self.colors],
["wipe_len", "Wave Start Length", "slider", (0,config.N_PIXELS//4,1)],
["wipe_speed", "Wave Speed", "slider", (1,10,1)],
["decay", "Flash Decay", "float_slider", (0.1,1.0,0.05)]],
"Wavelength":[["color_mode", "Color Mode", "dropdown", self.multicolor_mode_names],
["roll_speed", "Roll Speed", "slider", (0,8,1)],
["blur", "Blur", "float_slider", (0.1,4.0,0.1)],
["mirror", "Mirror", "checkbox"],
["reverse_grad", "Reverse Gradient", "checkbox"],
["reverse_roll", "Reverse Roll", "checkbox"]],
"Scroll":[["blur", "Blur", "float_slider", (0.05,4.0,0.05)],
["decay", "Decay", "float_slider", (0.95,1.0,0.005)]],
"Power":[["blur", "Blur", "float_slider", (0.1,4.0,0.1)]],
"Single":[["color", "Color", "dropdown", self.colors]],
"Beat":[["color", "Color", "dropdown", self.colors],
["decay", "Flash Decay", "float_slider", (0.3,0.98,0.005)]],
"Gradient":[["color_mode", "Color Mode", "dropdown", self.multicolor_mode_names],
["roll_speed", "Roll Speed", "slider", (0,8,1)],
["mirror", "Mirror", "checkbox"],
["reverse", "Reverse", "checkbox"]],
"Fade":[["color_mode", "Color Mode", "dropdown", self.multicolor_mode_names],
["roll_speed", "Fade Speed", "slider", (0,8,1)],
["reverse", "Reverse", "checkbox"]]
}
# Setup for "Wave" (don't change these)
self.wave_wipe_count = 0
# Setup for multicolour modes (don't mess with this either unless you want to add in your own multicolour modes)
# If there's a multicolour mode you would like to see, let me know on GitHub!
self.multicolor_modes = {}
# chunks of colour gradients
_blank_overlay = np.zeros((3,config.N_PIXELS))
# used to construct rgb overlay. [0-255,255...] whole length of strip
_gradient_whole = [int(i*255/(config.N_PIXELS//2)) for i in range(config.N_PIXELS//2)] +\
[255 for i in range(config.N_PIXELS//2)]
# also used to make bits and pieces. [0-255], 1/2 length of strip
_alt_gradient_half = [int(i*255/(config.N_PIXELS//2)) for i in range(config.N_PIXELS//2)]
# used to construct rgb overlay. [0-255,255...] 1/2 length of strip
_gradient_half = _gradient_whole[::2]
# Spectral colour mode
self.multicolor_modes["Spectral"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Spectral"][0, :config.N_PIXELS//2] = _gradient_half[::-1]
self.multicolor_modes["Spectral"][1, :] = _gradient_half + _gradient_half[::-1]
self.multicolor_modes["Spectral"][2, :] = np.flipud(self.multicolor_modes["Spectral"][0])
# Dancefloor colour mode
self.multicolor_modes["Dancefloor"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Dancefloor"][0, :] = _gradient_whole[::-1]
self.multicolor_modes["Dancefloor"][2, :] = _gradient_whole
# Brilliance colour mode
self.multicolor_modes["Brilliance"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Brilliance"][0, :] = _gradient_whole[::-1]
self.multicolor_modes["Brilliance"][1, :] = 255
self.multicolor_modes["Brilliance"][2, :] = _gradient_whole
# Jungle colour mode
self.multicolor_modes["Jungle"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Jungle"][0, :] = _gradient_whole[::-1]
self.multicolor_modes["Jungle"][1, :] = _gradient_whole
# Sky colour mode
self.multicolor_modes["Sky"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Sky"][0, :config.N_PIXELS//2] = _alt_gradient_half[::-1]
self.multicolor_modes["Sky"][1, config.N_PIXELS//2:] = _alt_gradient_half
self.multicolor_modes["Sky"][2, :] = 255
# Acid colour mode
self.multicolor_modes["Acid"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Acid"][0, :config.N_PIXELS//2] = _alt_gradient_half[::-1]
self.multicolor_modes["Acid"][1, :] = 255
self.multicolor_modes["Acid"][2, config.N_PIXELS//2:] = _alt_gradient_half
# Ocean colour mode
self.multicolor_modes["Ocean"] = np.zeros((3,config.N_PIXELS))
self.multicolor_modes["Ocean"][1, :] = _gradient_whole
self.multicolor_modes["Ocean"][2, :] = _gradient_whole[::-1]
for i in self.multicolor_modes:
self.multicolor_modes[i] = np.concatenate((self.multicolor_modes[i][:, ::-1],
self.multicolor_modes[i]), axis=1)
def get_vis(self, y, audio_input):
self.update_freq_channels(y)
self.detect_freqs()
if audio_input:
self.prev_output = np.copy(self.effects[self.current_effect](y))
elif self.current_effect in self.non_reactive_effects:
self.prev_output = np.copy(self.effects[self.current_effect](y))
else:
self.prev_output = np.multiply(self.prev_output, 0.95)
return self.prev_output
def _split_equal(self, value, parts):
value = float(value)
return [int(round(i*value/parts)) for i in range(1,parts+1)]
def update_freq_channels(self, y):
for i in range(len(y)):
self.freq_channels[i].appendleft(y[i])
def detect_freqs(self):
"""
Function that updates current_freq_detects. Any visualisation algorithm can check if
there is currently a beat, low, mid, or high by querying the self.current_freq_detects dict.
"""
channel_avgs = []
differences = []
for i in range(config.N_FFT_BINS):
channel_avgs.append(sum(self.freq_channels[i])/len(self.freq_channels[i]))
differences.append(((self.freq_channels[i][0]-channel_avgs[i])*100)//channel_avgs[i])
for i in ["beat", "low", "mid", "high"]:
if any(differences[j] >= 100 and self.freq_channels[j][0] >= self.min_detect_amplitude[i]\
for j in range(*self.detection_ranges[i]))\
and (time.time() - self.prev_freq_detects[i] > 0.15)\
and len(self.freq_channels[0]) == self.freq_channel_history:
self.prev_freq_detects[i] = time.time()
self.current_freq_detects[i] = True
#print(i)
else:
self.current_freq_detects[i] = False
def visualize_scroll(self, y):
"""Effect that originates in the center and scrolls outwards"""
global p
y = y**2.0
gain.update(y)
y /= gain.value
y *= 255.0
r = int(np.max(y[:len(y) // 3]))
g = int(np.max(y[len(y) // 3: 2 * len(y) // 3]))
b = int(np.max(y[2 * len(y) // 3:]))
# Scrolling effect window
p[:, 1:] = p[:, :-1]
p *= self.effect_opts["Scroll"]["decay"]
p = gaussian_filter1d(p, sigma=self.effect_opts["Scroll"]["blur"])
# Create new color originating at the center
p[0, 0] = r
p[1, 0] = g
p[2, 0] = b
# Update the LED strip
return np.concatenate((p[:, ::-1], p), axis=1)
def visualize_energy(self, y):
"""Effect that expands from the center with increasing sound energy"""
global p
y = np.copy(y)
gain.update(y)
y /= gain.value
scale = self.effect_opts["Energy"]["scale"]
# Scale by the width of the LED strip
y *= float((config.N_PIXELS * scale) - 1)
# Map color channels according to energy in the different freq bands
r = int(np.mean(y[:len(y) // 3]**scale))
g = int(np.mean(y[len(y) // 3: 2 * len(y) // 3]**scale))
b = int(np.mean(y[2 * len(y) // 3:]**scale))
# Assign color to different frequency regions
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
p_filt.update(p)
p = np.round(p_filt.value)
# Apply blur to smooth the edges
p[0, :] = gaussian_filter1d(p[0, :], sigma=self.effect_opts["Energy"]["blur"])
p[1, :] = gaussian_filter1d(p[1, :], sigma=self.effect_opts["Energy"]["blur"])
p[2, :] = gaussian_filter1d(p[2, :], sigma=self.effect_opts["Energy"]["blur"])
# Set the new pixel value
return np.concatenate((p[:, ::-1], p), axis=1)
def visualize_wavelength(self, y):
y = np.copy(interpolate(y, config.N_PIXELS // 2))
common_mode.update(y)
diff = y - self.prev_spectrum
self.prev_spectrum = np.copy(y)
# Color channel mappings
r = r_filt.update(y - common_mode.value)
#g = np.abs(diff)
b = b_filt.update(np.copy(y))
r = np.array([j for i in zip(r,r) for j in i])
#b = np.array([j for i in zip(b,b) for j in i])
output = np.array([self.multicolor_modes[self.effect_opts["Wavelength"]["color_mode"]][0][
(config.N_PIXELS if not self.effect_opts["Wavelength"]["reverse_grad"] else 0):
(None if not self.effect_opts["Wavelength"]["reverse_grad"] else config.N_PIXELS):]*r,
self.multicolor_modes[self.effect_opts["Wavelength"]["color_mode"]][1][
(config.N_PIXELS if not self.effect_opts["Wavelength"]["reverse_grad"] else 0):
(None if not self.effect_opts["Wavelength"]["reverse_grad"] else config.N_PIXELS):]*r,
self.multicolor_modes[self.effect_opts["Wavelength"]["color_mode"]][2][
(config.N_PIXELS if not self.effect_opts["Wavelength"]["reverse_grad"] else 0):
(None if not self.effect_opts["Wavelength"]["reverse_grad"] else config.N_PIXELS):]*r])
self.prev_spectrum = y
self.multicolor_modes[self.effect_opts["Wavelength"]["color_mode"]] = np.roll(
self.multicolor_modes[self.effect_opts["Wavelength"]["color_mode"]],
self.effect_opts["Wavelength"]["roll_speed"]*(-1 if self.effect_opts["Wavelength"]["reverse_roll"] else 1),
axis=1)
output[0] = gaussian_filter1d(output[0], sigma=self.effect_opts["Wavelength"]["blur"])
output[1] = gaussian_filter1d(output[1], sigma=self.effect_opts["Wavelength"]["blur"])
output[2] = gaussian_filter1d(output[2], sigma=self.effect_opts["Wavelength"]["blur"])
if self.effect_opts["Wavelength"]["mirror"]:
output = np.concatenate((output[:, ::-2], output[:, ::2]), axis=1)
return output
def visualize_power(self, y):
"""Effect that pulses different reqions of the strip increasing sound energy"""
global p
_p = np.copy(p)
y = np.copy(interpolate(y, config.N_PIXELS // 2))
common_mode.update(y)
diff = y - self.prev_spectrum
self.prev_spectrum = np.copy(y)
# Color channel mappings
r = r_filt.update(y - common_mode.value)
g = np.abs(diff)
b = b_filt.update(np.copy(y))
# I have no idea what any of this does but it looks kinda cool
r = [int(i*255) for i in r[::3]]
g = [int(i*255) for i in g[::3]]
b = [int(i*255) for i in b[::3]]
_p[0, 0:len(r)] = r
_p[1, len(r):len(r)+len(g)] = g
_p[2, len(r)+len(g):config.N_PIXELS] = b[:39] # this needs to be fixed
p_filt.update(_p)
# Clip it into range
_p = np.clip(p, 0, 255).astype(int)
# Apply substantial blur to smooth the edges
_p[0, :] = gaussian_filter1d(_p[0, :], sigma=self.effect_opts["Power"]["blur"])
_p[1, :] = gaussian_filter1d(_p[1, :], sigma=self.effect_opts["Power"]["blur"])
_p[2, :] = gaussian_filter1d(_p[2, :], sigma=self.effect_opts["Power"]["blur"])
self.prev_spectrum = y
return np.concatenate((_p[:, ::-1], _p), axis=1)
def visualize_spectrum(self, y):
"""Effect that maps the Mel filterbank frequencies onto the LED strip"""
global p
#print(len(y))
#print(y)
y = np.copy(interpolate(y, config.N_PIXELS // 2))
common_mode.update(y)
diff = y - self.prev_spectrum
self.prev_spectrum = np.copy(y)
# Color channel mappings
r = r_filt.update(y - common_mode.value)
g = np.abs(diff)
b = b_filt.update(np.copy(y))
# Mirror the color channels for symmetric output
r = np.concatenate((r[::-1], r))
g = np.concatenate((g[::-1], g))
b = np.concatenate((b[::-1], b))
output = np.array([r, g,b]) * 255
self.prev_spectrum = y
return output
def visualize_auto(self,y):
"""Automatically (intelligently?) cycle through effects"""
return self.visualize_beat(y) # real intelligent
def visualize_wave(self, y):
"""Effect that flashes to the beat with scrolling coloured bits"""
if self.current_freq_detects["beat"]:
output = np.zeros((3,config.N_PIXELS))
output[0][:]=self.colors[self.effect_opts["Wave"]["color_flash"]][0]
output[1][:]=self.colors[self.effect_opts["Wave"]["color_flash"]][1]
output[2][:]=self.colors[self.effect_opts["Wave"]["color_flash"]][2]
self.wave_wipe_count = self.effect_opts["Wave"]["wipe_len"]
else:
output = np.copy(self.prev_output)
#for i in range(len(self.prev_output)):
# output[i] = np.hsplit(self.prev_output[i],2)[0]
output = np.multiply(self.prev_output,self.effect_opts["Wave"]["decay"])
for i in range(self.wave_wipe_count):
output[0][i]=self.colors[self.effect_opts["Wave"]["color_wave"]][0]
output[0][-i]=self.colors[self.effect_opts["Wave"]["color_wave"]][0]
output[1][i]=self.colors[self.effect_opts["Wave"]["color_wave"]][1]
output[1][-i]=self.colors[self.effect_opts["Wave"]["color_wave"]][1]
output[2][i]=self.colors[self.effect_opts["Wave"]["color_wave"]][2]
output[2][-i]=self.colors[self.effect_opts["Wave"]["color_wave"]][2]
#output = np.concatenate([output,np.fliplr(output)], axis=1)
self.wave_wipe_count += self.effect_opts["Wave"]["wipe_speed"]
if self.wave_wipe_count > config.N_PIXELS//2:
self.wave_wipe_count = config.N_PIXELS//2
return output
def visualize_beat(self, y):
"""Effect that flashes to the beat"""
if self.current_freq_detects["beat"]:
output = np.zeros((3,config.N_PIXELS))
output[0][:]=self.colors[self.effect_opts["Beat"]["color"]][0]
output[1][:]=self.colors[self.effect_opts["Beat"]["color"]][1]
output[2][:]=self.colors[self.effect_opts["Beat"]["color"]][2]
else:
output = np.copy(self.prev_output)
output = np.multiply(self.prev_output,self.effect_opts["Beat"]["decay"])
return output
def visualize_single(self, y):
output = np.zeros((3,config.N_PIXELS))
output[0][:]=self.colors[self.effect_opts["Single"]["color"]][0]
output[1][:]=self.colors[self.effect_opts["Single"]["color"]][1]
output[2][:]=self.colors[self.effect_opts["Single"]["color"]][2]
return output
def visualize_gradient(self, y):
output = np.array([self.multicolor_modes[self.effect_opts["Gradient"]["color_mode"]][0][:config.N_PIXELS],
self.multicolor_modes[self.effect_opts["Gradient"]["color_mode"]][1][:config.N_PIXELS],
self.multicolor_modes[self.effect_opts["Gradient"]["color_mode"]][2][:config.N_PIXELS]])
self.multicolor_modes[self.effect_opts["Gradient"]["color_mode"]] = np.roll(
self.multicolor_modes[self.effect_opts["Gradient"]["color_mode"]],
self.effect_opts["Gradient"]["roll_speed"]*(-1 if self.effect_opts["Gradient"]["reverse"] else 1),
axis=1)
if self.effect_opts["Gradient"]["mirror"]:
output = np.concatenate((output[:, ::-2], output[:, ::2]), axis=1)
return output
def visualize_fade(self, y):
output = [[self.multicolor_modes[self.effect_opts["Fade"]["color_mode"]][0][0] for i in range(config.N_PIXELS)],
[self.multicolor_modes[self.effect_opts["Fade"]["color_mode"]][1][0] for i in range(config.N_PIXELS)],
[self.multicolor_modes[self.effect_opts["Fade"]["color_mode"]][2][0] for i in range(config.N_PIXELS)]]
self.multicolor_modes[self.effect_opts["Fade"]["color_mode"]] = np.roll(
self.multicolor_modes[self.effect_opts["Fade"]["color_mode"]],
self.effect_opts["Fade"]["roll_speed"]*(-1 if self.effect_opts["Fade"]["reverse"] else 1),
axis=1)
return output
class GUI(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# ==================================== Set up window and wrapping layout
self.setWindowTitle("Visualization")
wrapper = QVBoxLayout()
# ========================================== Set up FPS and error labels
labels_layout = QHBoxLayout()
self.label_error = QLabel("")
self.label_fps = QLabel("")
self.label_fps.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
labels_layout.addWidget(self.label_error)
labels_layout.addStretch()
labels_layout.addWidget(self.label_fps)
# ================================================== Set up graph layout
graph_view = pg.GraphicsView()
graph_layout = pg.GraphicsLayout(border=(100,100,100))
graph_view.setCentralItem(graph_layout)
# Mel filterbank plot
fft_plot = graph_layout.addPlot(title='Filterbank Output', colspan=3)
fft_plot.setRange(yRange=[-0.1, 1.2])
fft_plot.disableAutoRange(axis=pg.ViewBox.YAxis)
x_data = np.array(range(1, config.N_FFT_BINS + 1))
self.mel_curve = pg.PlotCurveItem()
self.mel_curve.setData(x=x_data, y=x_data*0)
fft_plot.addItem(self.mel_curve)
# Visualization plot
graph_layout.nextRow()
led_plot = graph_layout.addPlot(title='Visualization Output', colspan=3)
led_plot.setRange(yRange=[-5, 260])
led_plot.disableAutoRange(axis=pg.ViewBox.YAxis)
# Pen for each of the color channel curves
r_pen = pg.mkPen((255, 30, 30, 200), width=4)
g_pen = pg.mkPen((30, 255, 30, 200), width=4)
b_pen = pg.mkPen((30, 30, 255, 200), width=4)
# Color channel curves
self.r_curve = pg.PlotCurveItem(pen=r_pen)
self.g_curve = pg.PlotCurveItem(pen=g_pen)
self.b_curve = pg.PlotCurveItem(pen=b_pen)
# Define x data
x_data = np.array(range(1, config.N_PIXELS + 1))
self.r_curve.setData(x=x_data, y=x_data*0)
self.g_curve.setData(x=x_data, y=x_data*0)
self.b_curve.setData(x=x_data, y=x_data*0)
# Add curves to plot
led_plot.addItem(self.r_curve)
led_plot.addItem(self.g_curve)
led_plot.addItem(self.b_curve)
# ================================================= Set up button layout
label_reactive = QLabel("Audio Reactive Effects")
label_non_reactive = QLabel("Non Reactive Effects")
reactive_button_grid = QGridLayout()
non_reactive_button_grid = QGridLayout()
buttons = {}
connecting_funcs = {}
grid_width = 4
i = 0
j = 0
k = 0
l = 0
# Dynamically layout reactive_buttons and connect them to the visualisation effects
def connect_generator(effect):
def func():
visualizer.current_effect = effect
buttons[effect].setDown(True)
func.__name__ = effect
return func
# Where the magic happens
for effect in visualizer.effects:
if not effect in visualizer.non_reactive_effects:
connecting_funcs[effect] = connect_generator(effect)
buttons[effect] = QPushButton(effect)
buttons[effect].clicked.connect(connecting_funcs[effect])
reactive_button_grid.addWidget(buttons[effect], j, i)
i += 1
if i % grid_width == 0:
i = 0
j += 1
else:
connecting_funcs[effect] = connect_generator(effect)
buttons[effect] = QPushButton(effect)
buttons[effect].clicked.connect(connecting_funcs[effect])
non_reactive_button_grid.addWidget(buttons[effect], l, k)
k += 1
if k % grid_width == 0:
k = 0
l += 1
# ============================================== Set up frequency slider
# Frequency range label
label_slider = QLabel("Frequency Range")
# Frequency slider
def freq_slider_change(tick):
minf = freq_slider.tickValue(0)**2.0 * (config.MIC_RATE / 2.0)
maxf = freq_slider.tickValue(1)**2.0 * (config.MIC_RATE / 2.0)
t = 'Frequency range: {:.0f} - {:.0f} Hz'.format(minf, maxf)
freq_label.setText(t)
config.MIN_FREQUENCY = minf
config.MAX_FREQUENCY = maxf
dsp.create_mel_bank()
def set_freq_min():
config.MIN_FREQUENCY = freq_slider.start()
dsp.create_mel_bank()
def set_freq_max():
config.MAX_FREQUENCY = freq_slider.end()
dsp.create_mel_bank()
freq_slider = QRangeSlider()
freq_slider.show()
freq_slider.setMin(0)
freq_slider.setMax(20000)
freq_slider.setRange(config.MIN_FREQUENCY, config.MAX_FREQUENCY)
freq_slider.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
freq_slider.setSpanStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);')
freq_slider.setDrawValues(True)
freq_slider.endValueChanged.connect(set_freq_max)
freq_slider.startValueChanged.connect(set_freq_min)
freq_slider.setStyleSheet("""
QRangeSlider * {
border: 0px;
padding: 0px;
}
QRangeSlider > QSplitter::handle {
background: #fff;
}
QRangeSlider > QSplitter::handle:vertical {
height: 3px;
}
QRangeSlider > QSplitter::handle:pressed {
background: #ca5;
}
""")
# ============================================ Set up option tabs layout
label_options = QLabel("Effect Options")
opts_tabs = QTabWidget()
# Dynamically set up tabs
tabs = {}
grid_layouts = {}
self.grid_layout_widgets = {}
options = visualizer.effect_opts.keys()
for effect in visualizer.effects:
# Make the tab
self.grid_layout_widgets[effect] = {}
tabs[effect] = QWidget()
grid_layouts[effect] = QGridLayout()
tabs[effect].setLayout(grid_layouts[effect])
opts_tabs.addTab(tabs[effect],effect)
# These functions make functions for the dynamic ui generation
# YOU WANT-A DYNAMIC I GIVE-A YOU DYNAMIC!
def gen_slider_valuechanger(effect, key):
def func():
visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].value()
return func
def gen_float_slider_valuechanger(effect, key):
def func():
visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].slider_value
return func
def gen_combobox_valuechanger(effect, key):
def func():
visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].currentText()
return func
def gen_checkbox_valuechanger(effect, key):
def func():
visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].isChecked()
return func
# Dynamically generate ui for settings
if effect in visualizer.dynamic_effects_config:
i = 0
connecting_funcs[effect] = {}
for key, label, ui_element, *opts in visualizer.dynamic_effects_config[effect]:
if opts: # neatest way ^^^^^ i could think of to unpack and handle an unknown number of opts (if any)
opts = opts[0]
if ui_element == "slider":
connecting_funcs[effect][key] = gen_slider_valuechanger(effect, key)
self.grid_layout_widgets[effect][key] = QSlider(Qt.Horizontal)
self.grid_layout_widgets[effect][key].setMinimum(opts[0])
self.grid_layout_widgets[effect][key].setMaximum(opts[1])
self.grid_layout_widgets[effect][key].setValue(visualizer.effect_opts[effect][key])
self.grid_layout_widgets[effect][key].valueChanged.connect(
connecting_funcs[effect][key])
elif ui_element == "float_slider":
connecting_funcs[effect][key] = gen_float_slider_valuechanger(effect, key)
self.grid_layout_widgets[effect][key] = QFloatSlider(*opts, visualizer.effect_opts[effect][key])
self.grid_layout_widgets[effect][key].setValue(visualizer.effect_opts[effect][key])
self.grid_layout_widgets[effect][key].valueChanged.connect(
connecting_funcs[effect][key])
elif ui_element == "dropdown":
connecting_funcs[effect][key] = gen_combobox_valuechanger(effect, key)
self.grid_layout_widgets[effect][key] = QComboBox()
self.grid_layout_widgets[effect][key].addItems(opts)
self.grid_layout_widgets[effect][key].currentIndexChanged.connect(
connecting_funcs[effect][key])
elif ui_element == "checkbox":
connecting_funcs[effect][key] = gen_checkbox_valuechanger(effect, key)
self.grid_layout_widgets[effect][key] = QCheckBox()
self.grid_layout_widgets[effect][key].setCheckState(visualizer.effect_opts[effect][key])
self.grid_layout_widgets[effect][key].stateChanged.connect(
connecting_funcs[effect][key])
grid_layouts[effect].addWidget(QLabel(label),i,0)
grid_layouts[effect].addWidget(self.grid_layout_widgets[effect][key],i,1)
i += 1
#visualizer.effect_settings[effect]
else:
grid_layouts[effect].addWidget(QLabel("No customisable options for this effect :("),0,0)
# ============================================= Add layouts into wrapper
self.setLayout(wrapper)
wrapper.addLayout(labels_layout)
wrapper.addWidget(graph_view)
wrapper.addWidget(label_reactive)
wrapper.addLayout(reactive_button_grid)
wrapper.addWidget(label_non_reactive)
wrapper.addLayout(non_reactive_button_grid)
wrapper.addWidget(label_slider)
wrapper.addWidget(freq_slider)
wrapper.addWidget(label_options)
wrapper.addWidget(opts_tabs)
self.show()
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 memoize(function):
"""Provides a decorator for memoizing functions"""
from functools import wraps
memo = {}
@wraps(function)
def wrapper(*args):
if args in memo:
return memo[args]
else:
rv = function(*args)
memo[args] = rv
return rv
return wrapper
@memoize
def _normalized_linspace(size):
return np.linspace(0, 1, size)
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 = _normalized_linspace(len(y))
x_new = _normalized_linspace(new_length)
z = np.interp(x_new, x_old, y)
return z
def microphone_update(audio_samples):
global y_roll, prev_rms, prev_exp, prev_fps_update
# Normalize samples between 0 and 1
y = audio_samples / 2.0**15
# Construct a rolling window of audio samples
y_roll[:-1] = y_roll[1:]
y_roll[-1, :] = np.copy(y)
y_data = np.concatenate(y_roll, axis=0).astype(np.float32)
vol = np.max(np.abs(y_data))
# Transform audio input into the frequency domain
N = len(y_data)
N_zeros = 2**int(np.ceil(np.log2(N))) - N
# Pad with zeros until the next power of two
y_data *= fft_window
y_padded = np.pad(y_data, (0, N_zeros), mode='constant')
YS = np.abs(np.fft.rfft(y_padded)[:N // 2])
# Construct a Mel filterbank from the FFT data
mel = np.atleast_2d(YS).T * dsp.mel_y.T
# Scale data to values more suitable for visualization
# mel = np.sum(mel, axis=0)
mel = np.sum(mel, axis=0)
mel = mel**2.0
# Gain normalization
mel_gain.update(np.max(gaussian_filter1d(mel, sigma=1.0)))
mel /= mel_gain.value
mel = mel_smoothing.update(mel)
# Map filterbank output onto LED strip
led.pixels = visualizer.get_vis(mel, audio_input = True if vol > config.MIN_VOLUME_THRESHOLD else False)
led.update()
if config.USE_GUI:
x = np.linspace(config.MIN_FREQUENCY, config.MAX_FREQUENCY, len(mel))
if vol < config.MIN_VOLUME_THRESHOLD:
gui.label_error.setText("No audio input. Volume below threshold.")
gui.mel_curve.setData(x=x, y=[0 for i in range(config.N_FFT_BINS)])
else:
# Plot filterbank output
gui.mel_curve.setData(x=x, y=fft_plot_filter.update(mel))
gui.label_error.setText("")
fps = frames_per_second()
if time.time() - 0.5 > prev_fps_update:
prev_fps_update = time.time()
app.processEvents()
# Plot the color channels
gui.r_curve.setData(y=led.pixels[0])
gui.g_curve.setData(y=led.pixels[1])
gui.b_curve.setData(y=led.pixels[2])
# Update fps counter
gui.label_fps.setText('{:.0f} / {:.0f} FPS'.format(fps, config.FPS))
elif vol < config.MIN_VOLUME_THRESHOLD:
print("No audio input. Volume below threshold. Volume: {}".format(vol))
if config.DISPLAY_FPS:
print('FPS {:.0f} / {:.0f}'.format(fps, config.FPS))
# Initialise visualiser and GUI
visualizer = Visualizer()
if config.USE_GUI:
# Create GUI window
app = QApplication([])
app.setApplicationName('Visualization')
gui = GUI()
app.processEvents()
# Initialise filter stuff
fft_plot_filter = dsp.ExpFilter(np.tile(1e-1, config.N_FFT_BINS),
alpha_decay=0.5, alpha_rise=0.99)
mel_gain = dsp.ExpFilter(np.tile(1e-1, config.N_FFT_BINS),
alpha_decay=0.01, alpha_rise=0.99)
mel_smoothing = dsp.ExpFilter(np.tile(1e-1, config.N_FFT_BINS),
alpha_decay=0.5, alpha_rise=0.99)
volume = dsp.ExpFilter(config.MIN_VOLUME_THRESHOLD,
alpha_decay=0.02, alpha_rise=0.02)
fft_window = np.hamming(int(config.MIC_RATE / config.FPS) * config.N_ROLLING_HISTORY)
prev_fps_update = time.time()
# Initialise more filter stuff
r_filt = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
alpha_decay=0.2, alpha_rise=0.99)
g_filt = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
alpha_decay=0.05, alpha_rise=0.3)
b_filt = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
alpha_decay=0.1, alpha_rise=0.5)
common_mode = dsp.ExpFilter(np.tile(0.01, config.N_PIXELS // 2),
alpha_decay=0.99, alpha_rise=0.01)
p_filt = dsp.ExpFilter(np.tile(1, (3, config.N_PIXELS // 2)),
alpha_decay=0.1, alpha_rise=0.99)
p = np.tile(1.0, (3, config.N_PIXELS // 2))
gain = dsp.ExpFilter(np.tile(0.01, config.N_FFT_BINS),
alpha_decay=0.001, alpha_rise=0.99)
# The previous time that the frames_per_second() function was called
_time_prev = time.time() * 1000.0
# The low-pass filter used to estimate frames-per-second
_fps = dsp.ExpFilter(val=config.FPS, alpha_decay=0.2, alpha_rise=0.2)
# 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
# Initialize LEDs
led.update()
# Start listening to live audio stream
microphone.start_stream(microphone_update)