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 pyaudio import numpy as np import lib.config as config #import lib.microphone as microphone import lib.dsp as dsp #import lib.led as led import lib.melbank as melbank import lib.devices as devices import random from PyQt5.QtCore import * from PyQt5.QtWidgets import * if config.settings["configuration"]["USE_GUI"]: from lib.qrangeslider import QRangeSlider from lib.qfloatslider import QFloatSlider import pyqtgraph as pg from PyQt5.QtGui import QColor, QIcon class BoardManager(): """ Class that manages all the boards, with their respective Visualisations, GUI tabs, and DSPs """ def __init__(self): self.visualizers = {} self.boards = {} self.signal_processers = {} def addBoard(self, board, config_exists=False, req_config=None, gen_config=None): if not config_exists: self.addConfig(board, req_config, gen_config) # Initialise Visualiser self.visualizers[board] = Visualizer(board) # Initialise DSP self.signal_processers[board] = DSP(board) # Initialise Device if config.settings["devices"][board]["configuration"]["TYPE"] == 'ESP8266': self.boards[board] = devices.ESP8266( auto_detect=config.settings["devices"][board]["configuration"]["AUTO_DETECT"], mac_addr=config.settings["devices"][board]["configuration"]["MAC_ADDR"], ip=config.settings["devices"][board]["configuration"]["UDP_IP"], port=config.settings["devices"][board]["configuration"]["UDP_PORT"]) if config.settings["devices"][board]["configuration"]["TYPE"] == 'PxMatrix': self.boards[board] = devices.PxMatrix( auto_detect=config.settings["devices"][board]["configuration"]["AUTO_DETECT"], mac_addr=config.settings["devices"][board]["configuration"]["MAC_ADDR"], ip=config.settings["devices"][board]["configuration"]["UDP_IP"], port=config.settings["devices"][board]["configuration"]["UDP_PORT"]) elif config.settings["devices"][board]["configuration"]["TYPE"] == 'RaspberryPi': self.boards[board] = devices.RaspberryPi( n_pixels=config.settings["devices"][board]["configuration"]["N_PIXELS"], pin=config.settings["devices"][board]["configuration"]["LED_PIN"], invert_logic=config.settings["devices"][board]["configuration"]["LED_INVERT"], freq=config.settings["devices"][board]["configuration"]["LED_FREQ_HZ"], dma=config.settings["devices"][board]["configuration"]["LED_DMA"]) elif config.settings["devices"][board]["configuration"]["TYPE"] == 'Fadecandy': self.boards[board] = devices.FadeCandy( server=config.settings["devices"][board]["configuration"]["SERVER"]) elif config.settings["devices"][board]["configuration"]["TYPE"] == 'BlinkStick': self.boards[board] = devices.BlinkStick() elif config.settings["devices"][board]["configuration"]["TYPE"] == 'DotStar': self.boards[board] = devices.DotStar() elif config.settings["devices"][board]["configuration"]["TYPE"] == 'sACNClient': self.boards[board] = devices.sACNClient( ip = config.settings["devices"][board]["configuration"]["IP"], start_universe = config.settings["devices"][board]["configuration"]["START_UNIVERSE"], start_channel = config.settings["devices"][board]["configuration"]["START_CHANNEL"], universe_size = config.settings["devices"][board]["configuration"]["UNIVERSE_SIZE"], channel_count = config.settings["devices"][board]["configuration"]["N_PIXELS"] * 3, fps = config.settings["configuration"]["FPS"]) elif config.settings["devices"][board]["configuration"]["TYPE"] == 'Stripless': self.boards[board] = devices.Stripless() if config.settings["configuration"]["USE_GUI"]: gui.addBoard(board) def delBoard(self, board): #print("deleting board {}".format(board)) del self.visualizers[board] del self.signal_processers[board] del self.boards[board] del config.settings["devices"][board] gui.delBoard(board) def addConfig(self, board, req_config, gen_config): if board in self.boards: raise ValueError('Device already exists under name: {}\nPlease use a different name.'.format(board)) config.settings["devices"][board] = {} # Update missing values from defaults merged_general_config = {**config.default_general_config, **gen_config} # combine into one configuration dict merged_config = {**req_config, **merged_general_config} # Generate device config dict config.settings["devices"][board]["configuration"] = {} for configuration in merged_config: config.settings["devices"][board]["configuration"][configuration] = merged_config[configuration] # Generate device effect opts dict config.settings["devices"][board]["effect_opts"] = config.default_effect_opts class BeatDetector(): def __init__(self): pass def update(audio_data): pass class Visualizer(BoardManager): def __init__(self, board): # Name of board this for which this visualizer instance is visualising self.board = board # 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, "Bars":self.visualize_bars, #"Pulse":self.visualize_pulse, #"Pulse":self.visualize_pulse, #"Auto":self.visualize_auto, "Single":self.visualize_single, "Fade":self.visualize_fade, "Gradient":self.visualize_gradient, "Calibration": self.visualize_calibration} # 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", "Calibration"] # 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.settings["devices"][self.board]["configuration"]["N_FFT_BINS"])] self.prev_output = np.array([[0 for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])] for i in range(3)]) self.output = np.array([[0 for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])] for i in range(3)]) self.prev_spectrum = np.array([config.settings["devices"][self.board]["configuration"]["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,int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.11)), "low":(int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.13), int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.4)), "mid":(int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.4), int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.7)), "high":(int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.8), int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]))} self.min_detect_amplitude = {"beat":0.7, "low":0.5, "mid":0.3, "high":0.3} self.min_percent_diff = {"beat":70, "low":100, "mid":50, "high":30} # Setup for fps counter self.frame_counter = 0 self.start_time = time.time() # Setup for "Wave" (don't change these) self.wave_wipe_count = 0 # Setup for "Power" (don't change these) self.power_indexes = [] self.power_brightness = 0 def get_vis(self, y, audio_input): self.update_freq_channels(y) self.detect_freqs() if config.settings["devices"][self.board]["configuration"]["current_effect"] in self.non_reactive_effects: self.prev_output = self.effects[config.settings["devices"][self.board]["configuration"]["current_effect"]]() elif audio_input: self.prev_output = self.effects[config.settings["devices"][self.board]["configuration"]["current_effect"]](y) else: self.prev_output = np.multiply(self.prev_output, 0.95) self.frame_counter += 1 elapsed = time.time() - self.start_time if elapsed >= 1.0: self.start_time = time.time() fps = self.frame_counter//elapsed latency = elapsed/self.frame_counter self.frame_counter = 0 if config.settings["configuration"]["USE_GUI"]: gui.label_latency.setText("{:0.3f} ms Processing Latency ".format(latency)) gui.label_fps.setText('{:.0f} / {:.0f} FPS '.format(fps, config.settings["configuration"]["FPS"])) 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.settings["devices"][self.board]["configuration"]["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] >= self.min_percent_diff[i]\ 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.1)\ and len(self.freq_channels[0]) == self.freq_channel_history: self.prev_freq_detects[i] = time.time() self.current_freq_detects[i] = True else: self.current_freq_detects[i] = False def visualize_scroll(self, y): # Effect that scrolls colours corresponding to frequencies across the strip #y = y**1.5 n_pixels = config.settings["devices"][self.board]["configuration"]["N_PIXELS"] y = np.copy(interpolate(y, n_pixels // 2)) board_manager.signal_processers[self.board].common_mode.update(y) diff = y - self.prev_spectrum self.prev_spectrum = np.copy(y) y = np.clip(y, 0, 1) lows = y[:len(y) // 6] mids = y[len(y) // 6: 2 * len(y) // 5] high = y[2 * len(y) // 5:] # max values lows_max = np.max(lows)#*config.settings["devices"][self.board]["effect_opts"]["Scroll"]["lows_multiplier"]) mids_max = float(np.max(mids))#*config.settings["devices"][self.board]["effect_opts"]["Scroll"]["mids_multiplier"]) high_max = float(np.max(high))#*config.settings["devices"][self.board]["effect_opts"]["Scroll"]["high_multiplier"]) # indexes of max values # map to colour gradient lows_val = (np.array(colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Scroll"]["lows_color"])) * lows_max).astype(int) mids_val = (np.array(colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Scroll"]["mids_color"])) * mids_max).astype(int) high_val = (np.array(colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Scroll"]["high_color"])) * high_max).astype(int) # Scrolling effect window speed = config.settings["devices"][self.board]["effect_opts"]["Scroll"]["speed"] self.output[:, speed:] = self.output[:, :-speed] self.output = (self.output * config.settings["devices"][self.board]["effect_opts"]["Scroll"]["decay"]).astype(int) self.output = gaussian_filter1d(self.output, sigma=config.settings["devices"][self.board]["effect_opts"]["Scroll"]["blur"]) # Create new color originating at the center self.output[0, :speed] = lows_val[0] + mids_val[0] + high_val[0] self.output[1, :speed] = lows_val[1] + mids_val[1] + high_val[1] self.output[2, :speed] = lows_val[2] + mids_val[2] + high_val[2] # Update the LED strip #return np.concatenate((self.prev_spectrum[:, ::-speed], self.prev_spectrum), axis=1) if config.settings["devices"][self.board]["effect_opts"]["Scroll"]["mirror"]: p = np.concatenate((self.output[:, ::-2], self.output[:, ::2]), axis=1) else: p = self.output return p def visualize_energy(self, y): """Effect that expands from the center with increasing sound energy""" y = np.copy(y) board_manager.signal_processers[self.board].gain.update(y) y /= board_manager.signal_processers[self.board].gain.value scale = config.settings["devices"][self.board]["effect_opts"]["Energy"]["scale"] # Scale by the width of the LED strip y *= float((config.settings["devices"][self.board]["configuration"]["N_PIXELS"] * scale) - 1) y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) # Map color channels according to energy in the different freq bands #y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) diff = y - self.prev_spectrum self.prev_spectrum = np.copy(y) spectrum = np.copy(self.prev_spectrum) spectrum = np.array([j for i in zip(spectrum,spectrum) for j in i]) # Color channel mappings r = int(np.mean(spectrum[:len(spectrum) // 3]**scale)*config.settings["devices"][self.board]["effect_opts"]["Energy"]["r_multiplier"]) g = int(np.mean(spectrum[len(spectrum) // 3: 2 * len(spectrum) // 3]**scale)*config.settings["devices"][self.board]["effect_opts"]["Energy"]["g_multiplier"]) b = int(np.mean(spectrum[2 * len(spectrum) // 3:]**scale)*config.settings["devices"][self.board]["effect_opts"]["Energy"]["b_multiplier"]) # Assign color to different frequency regions self.output[0, :r] = 255 self.output[0, r:] = 0 self.output[1, :g] = 255 self.output[1, g:] = 0 self.output[2, :b] = 255 self.output[2, b:] = 0 # Apply blur to smooth the edges self.output[0, :] = gaussian_filter1d(self.output[0, :], sigma=config.settings["devices"][self.board]["effect_opts"]["Energy"]["blur"]) self.output[1, :] = gaussian_filter1d(self.output[1, :], sigma=config.settings["devices"][self.board]["effect_opts"]["Energy"]["blur"]) self.output[2, :] = gaussian_filter1d(self.output[2, :], sigma=config.settings["devices"][self.board]["effect_opts"]["Energy"]["blur"]) if config.settings["devices"][self.board]["effect_opts"]["Energy"]["mirror"]: p = np.concatenate((self.output[:, ::-2], self.output[:, ::2]), axis=1) else: p = self.output return p def visualize_wavelength(self, y): y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) board_manager.signal_processers[self.board].common_mode.update(y) diff = y - self.prev_spectrum self.prev_spectrum = np.copy(y) # Color channel mappings r = board_manager.signal_processers[self.board].r_filt.update(y - board_manager.signal_processers[self.board].common_mode.value) g = np.abs(diff) b = board_manager.signal_processers[self.board].b_filt.update(np.copy(y)) r = np.array([j for i in zip(r,r) for j in i]) output = np.array([colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["color_mode"]][0][ (config.settings["devices"][self.board]["configuration"]["N_PIXELS"] if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_grad"] else 0): (None if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_grad"] else config.settings["devices"][self.board]["configuration"]["N_PIXELS"]):]*r, colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["color_mode"]][1][ (config.settings["devices"][self.board]["configuration"]["N_PIXELS"] if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_grad"] else 0): (None if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_grad"] else config.settings["devices"][self.board]["configuration"]["N_PIXELS"]):]*r, colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["color_mode"]][2][ (config.settings["devices"][self.board]["configuration"]["N_PIXELS"] if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_grad"] else 0): (None if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_grad"] else config.settings["devices"][self.board]["configuration"]["N_PIXELS"]):]*r]) #self.prev_spectrum = y colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["color_mode"]] = np.roll( colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["color_mode"]], config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["roll_speed"]*(-1 if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["reverse_roll"] else 1), axis=1) output[0] = gaussian_filter1d(output[0], sigma=config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["blur"]) output[1] = gaussian_filter1d(output[1], sigma=config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["blur"]) output[2] = gaussian_filter1d(output[2], sigma=config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["blur"]) if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["flip_lr"]: output = np.fliplr(output) if config.settings["devices"][self.board]["effect_opts"]["Wavelength"]["mirror"]: output = np.concatenate((output[:, ::-2], output[:, ::2]), axis=1) return output def visualize_spectrum(self, y): """Effect that maps the Mel filterbank frequencies onto the LED strip""" #print(len(y)) #print(y) y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) board_manager.signal_processers[self.board].common_mode.update(y) diff = y - self.prev_spectrum self.prev_spectrum = np.copy(y) # Color channel mappings r = board_manager.signal_processers[self.board].r_filt.update(y - board_manager.signal_processers[self.board].common_mode.value) g = np.abs(diff) b = board_manager.signal_processers[self.board].b_filt.update(np.copy(y)) r *= config.settings["devices"][self.board]["effect_opts"]["Spectrum"]["r_multiplier"] g *= config.settings["devices"][self.board]["effect_opts"]["Spectrum"]["g_multiplier"] b *= config.settings["devices"][self.board]["effect_opts"]["Spectrum"]["b_multiplier"] # 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.settings["devices"][self.board]["configuration"]["N_PIXELS"])) output[0][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_flash"])[0] output[1][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_flash"])[1] output[2][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_flash"])[2] self.wave_wipe_count = config.settings["devices"][self.board]["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,config.settings["devices"][self.board]["effect_opts"]["Wave"]["decay"]) for i in range(self.wave_wipe_count): output[0][i]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_wave"])[0] output[0][-i]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_wave"])[0] output[1][i]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_wave"])[1] output[1][-i]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_wave"])[1] output[2][i]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_wave"])[2] output[2][-i]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Wave"]["color_wave"])[2] #output = np.concatenate([output,np.fliplr(output)], axis=1) if self.wave_wipe_count > config.settings["devices"][self.board]["configuration"]["N_PIXELS"]//2: self.wave_wipe_count = config.settings["devices"][self.board]["configuration"]["N_PIXELS"]//2 self.wave_wipe_count += config.settings["devices"][self.board]["effect_opts"]["Wave"]["wipe_speed"] return output def visualize_beat(self, y): """Effect that flashes to the beat""" if self.current_freq_detects["beat"]: output = np.zeros((3,config.settings["devices"][self.board]["configuration"]["N_PIXELS"])) output[0][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Beat"]["color"])[0] output[1][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Beat"]["color"])[1] output[2][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Beat"]["color"])[2] else: output = np.copy(self.prev_output) output = np.multiply(self.prev_output,config.settings["devices"][self.board]["effect_opts"]["Beat"]["decay"]) return output def visualize_bars(self, y): # Bit of fiddling with the y values y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) board_manager.signal_processers[self.board].common_mode.update(y) self.prev_spectrum = np.copy(y) # Color channel mappings r = board_manager.signal_processers[self.board].r_filt.update(y - board_manager.signal_processers[self.board].common_mode.value) r = np.array([j for i in zip(r,r) for j in i]) # Split y into [resulution] chunks and calculate the average of each max_values = np.array([max(i) for i in np.array_split(r, config.settings["devices"][self.board]["effect_opts"]["Bars"]["resolution"])]) max_values = np.clip(max_values, 0, 1) color_sets = [] for i in range(config.settings["devices"][self.board]["effect_opts"]["Bars"]["resolution"]): # [r,g,b] values from a multicolour gradient array at [resulution] equally spaced intervals color_sets.append([colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Bars"]["color_mode"]]\ [j][i*(config.settings["devices"][self.board]["configuration"]["N_PIXELS"]//config.settings["devices"][self.board]["effect_opts"]["Bars"]["resolution"])] for j in range(3)]) output = np.zeros((3,config.settings["devices"][self.board]["configuration"]["N_PIXELS"])) chunks = np.array_split(output[0], config.settings["devices"][self.board]["effect_opts"]["Bars"]["resolution"]) n = 0 # Assign blocks with heights corresponding to max_values and colours from color_sets for i in range(len(chunks)): m = len(chunks[i]) for j in range(3): output[j][n:n+m] = color_sets[i][j]*max_values[i] n += m colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Bars"]["color_mode"]] = np.roll( colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Bars"]["color_mode"]], config.settings["devices"][self.board]["effect_opts"]["Bars"]["roll_speed"]*(-1 if config.settings["devices"][self.board]["effect_opts"]["Bars"]["reverse_roll"] else 1), axis=1) if config.settings["devices"][self.board]["effect_opts"]["Bars"]["flip_lr"]: output = np.fliplr(output) if config.settings["devices"][self.board]["effect_opts"]["Bars"]["mirror"]: output = np.concatenate((output[:, ::-2], output[:, ::2]), axis=1) return output def visualize_power(self, y): #config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"] # Bit of fiddling with the y values y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) board_manager.signal_processers[self.board].common_mode.update(y) self.prev_spectrum = np.copy(y) # Color channel mappings r = board_manager.signal_processers[self.board].r_filt.update(y - board_manager.signal_processers[self.board].common_mode.value) r = np.array([j for i in zip(r,r) for j in i]) output = np.array([colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"]][0, :config.settings["devices"][self.board]["configuration"]["N_PIXELS"]]*r, colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"]][1, :config.settings["devices"][self.board]["configuration"]["N_PIXELS"]]*r, colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"]][2, :config.settings["devices"][self.board]["configuration"]["N_PIXELS"]]*r]) # if there's a high (eg clap): if self.current_freq_detects["high"]: self.power_brightness = 1.0 # Generate random indexes self.power_indexes = random.sample(range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"]), config.settings["devices"][self.board]["effect_opts"]["Power"]["s_count"]) #print("ye") # Assign colour to the random indexes for index in self.power_indexes: output[0, index] = int(colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Power"]["s_color"])[0]*self.power_brightness) output[1, index] = int(colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Power"]["s_color"])[1]*self.power_brightness) output[2, index] = int(colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Power"]["s_color"])[2]*self.power_brightness) # Remove some of the indexes for next time self.power_indexes = [i for i in self.power_indexes if i not in random.sample(self.power_indexes, len(self.power_indexes)//4)] if len(self.power_indexes) <= 4: self.power_indexes = [] # Fade the colour of the sparks out a bit for next time if self.power_brightness > 0: self.power_brightness -= 0.05 # Calculate length of bass bar based on max bass frequency volume and length of strip strip_len = int((config.settings["devices"][self.board]["configuration"]["N_PIXELS"]//3)*max(y[:int(config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]*0.2)])) # Add the bass bars into the output. Colour proportional to length output[0][:strip_len] = colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"]][0][strip_len] output[1][:strip_len] = colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"]][1][strip_len] output[2][:strip_len] = colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Power"]["color_mode"]][2][strip_len] if config.settings["devices"][self.board]["effect_opts"]["Power"]["flip_lr"]: output = np.fliplr(output) if config.settings["devices"][self.board]["effect_opts"]["Power"]["mirror"]: output = np.concatenate((output[:, ::-2], output[:, ::2]), axis=1) return output def visualize_pulse(self, y): """dope ass visuals that's what""" config.settings["devices"][self.board]["effect_opts"]["Pulse"]["bar_color"] config.settings["devices"][self.board]["effect_opts"]["Pulse"]["bar_speed"] config.settings["devices"][self.board]["effect_opts"]["Pulse"]["bar_length"] config.settings["devices"][self.board]["effect_opts"]["Pulse"]["color_mode"] y = np.copy(interpolate(y, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) common_mode.update(y) # i honestly have no idea what this is but i just work with it rather than trying to figure it out self.prev_spectrum = np.copy(y) # Color channel mappings r = r_filt.update(y - common_mode.value) # same with this, no flippin clue r = np.array([j for i in zip(r,r) for j in i]) output = np.array([colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Pulse"]["color_mode"]][0][:config.settings["devices"][self.board]["configuration"]["N_PIXELS"]], colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Pulse"]["color_mode"]][1][:config.settings["devices"][self.board]["configuration"]["N_PIXELS"]], colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Pulse"]["color_mode"]][2][:config.settings["devices"][self.board]["configuration"]["N_PIXELS"]]]) def visualize_single(self): "Displays a single colour, non audio reactive" output = np.zeros((3,config.settings["devices"][self.board]["configuration"]["N_PIXELS"])) output[0][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Single"]["color"])[0] output[1][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Single"]["color"])[1] output[2][:]=colour_manager.colour(config.settings["devices"][self.board]["effect_opts"]["Single"]["color"])[2] return output def visualize_gradient(self): "Displays a multicolour gradient, non audio reactive" output = np.array([colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Gradient"]["color_mode"]][0][:config.settings["devices"][self.board]["configuration"]["N_PIXELS"]], colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Gradient"]["color_mode"]][1][:config.settings["devices"][self.board]["configuration"]["N_PIXELS"]], colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Gradient"]["color_mode"]][2][:config.settings["devices"][self.board]["configuration"]["N_PIXELS"]]]) colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Gradient"]["color_mode"]] = np.roll( colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Gradient"]["color_mode"]], config.settings["devices"][self.board]["effect_opts"]["Gradient"]["roll_speed"]*(-1 if config.settings["devices"][self.board]["effect_opts"]["Gradient"]["reverse"] else 1), axis=1) if config.settings["devices"][self.board]["effect_opts"]["Gradient"]["mirror"]: output = np.concatenate((output[:, ::-2], output[:, ::2]), axis=1) return output def visualize_fade(self): "Fades through a multicolour gradient, non audio reactive" output = np.array([[colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Fade"]["color_mode"]][0][0] for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])], [colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Fade"]["color_mode"]][1][0] for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])], [colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Fade"]["color_mode"]][2][0] for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])]]) colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Fade"]["color_mode"]] = np.roll( colour_manager.full_gradients[self.board][config.settings["devices"][self.board]["effect_opts"]["Fade"]["color_mode"]], config.settings["devices"][self.board]["effect_opts"]["Fade"]["roll_speed"]*(-1 if config.settings["devices"][self.board]["effect_opts"]["Fade"]["reverse"] else 1), axis=1) return output def visualize_calibration(self): "Custom values for RGB" output = np.array([[config.settings["devices"][self.board]["effect_opts"]["Calibration"]["r"] for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])], [config.settings["devices"][self.board]["effect_opts"]["Calibration"]["g"] for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])], [config.settings["devices"][self.board]["effect_opts"]["Calibration"]["b"] for i in range(config.settings["devices"][self.board]["configuration"]["N_PIXELS"])]]) return output class DSP(BoardManager): def __init__(self, board): # Name of board for which this dsp instance is processing audio self.board = board # Initialise filters etc self.fft_plot_filter = dsp.ExpFilter(np.tile(1e-1, config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]), alpha_decay=0.99, alpha_rise=0.99) self.mel_gain = dsp.ExpFilter(np.tile(1e-1, config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]), alpha_decay=0.01, alpha_rise=0.99) self.mel_smoothing = dsp.ExpFilter(np.tile(1e-1, config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]), alpha_decay=0.2, alpha_rise=0.99) self.gain = dsp.ExpFilter(np.tile(0.01, config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"]), alpha_decay=0.001, alpha_rise=0.99) self.r_filt = dsp.ExpFilter(np.tile(0.01, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2), alpha_decay=0.2, alpha_rise=0.99) self.g_filt = dsp.ExpFilter(np.tile(0.01, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2), alpha_decay=0.05, alpha_rise=0.3) self.b_filt = dsp.ExpFilter(np.tile(0.01, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2), alpha_decay=0.1, alpha_rise=0.5) self.common_mode = dsp.ExpFilter(np.tile(0.01, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2), alpha_decay=0.99, alpha_rise=0.01) self.p_filt = dsp.ExpFilter(np.tile(1, (3, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)), alpha_decay=0.1, alpha_rise=0.99) self.volume = dsp.ExpFilter(config.settings["configuration"]["MIN_VOLUME_THRESHOLD"], alpha_decay=0.9, alpha_rise=0.02) self.p = np.tile(1.0, (3, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] // 2)) # Number of audio samples to read every time frame self.samples_per_frame = int(config.settings["mic_config"]["MIC_RATE"] / config.settings["configuration"]["FPS"]) # Array containing the rolling audio sample window self.y_roll = np.random.rand(config.settings["configuration"]["N_ROLLING_HISTORY"], self.samples_per_frame) / 1e16 self.fft_window = np.hamming(int(config.settings["mic_config"]["MIC_RATE"] / config.settings["configuration"]["FPS"])\ * config.settings["configuration"]["N_ROLLING_HISTORY"]) self.samples = None self.mel_y = None self.mel_x = None self.create_mel_bank() def update(self, audio_samples): """ Return processed audio data Returns mel curve, x/y data This is called every time there is a microphone update Returns ------- audio_data : dict Dict containinng "mel", "vol", "x", and "y" """ audio_data = {} # Normalize samples between 0 and 1 y = audio_samples / 2.0**15 # Construct a rolling window of audio samples self.y_roll[:-1] = self.y_roll[1:] self.y_roll[-1, :] = np.copy(y) y_data = np.concatenate(self.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 *= self.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 * self.mel_y.T # Scale data to values more suitable for visualization mel = np.sum(mel, axis=0) mel = mel**2 # Gain normalization self.mel_gain.update(np.max(gaussian_filter1d(mel, sigma=0.1))) mel /= self.mel_gain.value mel = self.mel_smoothing.update(mel) x = np.linspace(config.settings["devices"][self.board]["configuration"]["MIN_FREQUENCY"], config.settings["devices"][self.board]["configuration"]["MAX_FREQUENCY"], len(mel)) y = self.fft_plot_filter.update(mel) audio_data["mel"] = mel audio_data["vol"] = vol audio_data["x"] = x audio_data["y"] = y return audio_data def rfft(self, data, window=None): window = 1.0 if window is None else window(len(data)) ys = np.abs(np.fft.rfft(data * window)) xs = np.fft.rfftfreq(len(data), 1.0 / config.settings["mic_config"]["MIC_RATE"]) return xs, ys def fft(self, data, window=None): window = 1.0 if window is None else window(len(data)) ys = np.fft.fft(data * window) xs = np.fft.fftfreq(len(data), 1.0 / config.settings["mic_config"]["MIC_RATE"]) return xs, ys def create_mel_bank(self): samples = int(config.settings["mic_config"]["MIC_RATE"] * config.settings["configuration"]["N_ROLLING_HISTORY"]\ / (2.0 * config.settings["configuration"]["FPS"])) self.mel_y, (_, self.mel_x) = melbank.compute_melmat(num_mel_bands=config.settings["devices"][self.board]["configuration"]["N_FFT_BINS"], freq_min=config.settings["devices"][self.board]["configuration"]["MIN_FREQUENCY"], freq_max=config.settings["devices"][self.board]["configuration"]["MAX_FREQUENCY"], num_fft_bands=samples, sample_rate=config.settings["mic_config"]["MIC_RATE"]) class ColourManager(): """ Controls all colours and gradients, both user set and default Colours and gradients are stored in two dicts, one for defaults and one for user set Format for colours: "Red":(255,0,0) Format for gradients: "Ocean":[(0, 255, 0), (0, 247, 161), (0, 0, 255)] """ def __init__(self): self.colours_storage = QSettings('./lib/colours.ini', QSettings.IniFormat) self.gradients_storage = QSettings('./lib/gradients.ini', QSettings.IniFormat) self.colours_storage.setFallbacksEnabled(False) self.gradients_storage.setFallbacksEnabled(False) self.effects_using_colours = [] self.effects_using_gradients = [] # load user set colours and gradients self.loadFromINI(self.colours_storage, "colours") self.loadFromINI(self.gradients_storage, "gradients") # default colours and gradients self.loadDefaultColours() self.loadDefaultGradients() self.buildGradients() # find and save which effects have settings for colours or gradients for board in config.settings["devices"]: for effect in config.dynamic_effects_config: for setting in config.dynamic_effects_config[effect]: if setting[2] == "dropdown": if setting[3] == "colours": self.effects_using_colours.append((board, effect, setting[0])) elif setting[3] == "gradients": self.effects_using_gradients.append((board, effect, setting[0])) def buildGradients(self): # generate full gradients for each device self.full_gradients = {} for board in config.settings["devices"]: self.full_gradients[board] = {} for i in ["user", "default"]: for gradient in config.colour_manager[i+"_gradients"]: self.full_gradients[board][gradient] = self._easing_gradient_generator(config.colour_manager[i+"_gradients"][gradient], config.settings["devices"][board]["configuration"]["N_PIXELS"]) self.full_gradients[board][gradient] = np.concatenate((self.full_gradients[board][gradient][:, ::-1], self.full_gradients[board][gradient]), axis=1) def colour(self, colour): # returns the values of a given colour. use this function to get colour values. if colour in config.colour_manager["user_colours"]: return config.colour_manager["user_colours"][colour] elif colour in config.colour_manager["default_colours"]: return config.colour_manager["default_colours"][colour] else: print("colour {} has not been defined".format(colour)) return (0,0,0) def getColours(self, group): # "user" returns user colours # "default" returns default colours # "all" returns a list of both if group == "user": return config.colour_manager["user_colours"] elif group == "default": return config.colour_manager["default_colours"] elif group == "all": return {**config.colour_manager["user_colours"], **config.colour_manager["default_colours"]} def getGradients(self, group): if group == "user": return config.colour_manager["user_gradients"] elif group == "default": return config.colour_manager["default_gradients"] elif group == "all": return {**config.colour_manager["user_gradients"], **config.colour_manager["default_gradients"]} def _easing_gradient_generator(self, colors, length): """ returns np.array of given length that eases between specified colours parameters: colors - list, colours must be in config.colour_manager["colours"] eg. ["Red", "Orange", "Blue", "Purple"] length - int, length of array to return. should be from config.settings eg. config.settings["devices"]["my strip"]["configuration"]["N_PIXELS"] """ def _easing_func(x, length, slope=2.5): # returns a nice eased curve with defined length and curve xa = (x/length)**slope return xa / (xa + (1 - (x/length))**slope) colors = colors[::-1] # needs to be reversed, makes it easier to deal with n_transitions = len(colors) - 1 ease_length = length // n_transitions pad = length - (n_transitions * ease_length) output = np.zeros((3, length)) ease = np.array([_easing_func(i, ease_length, slope=2.5) for i in range(ease_length)]) # for r,g,b for i in range(3): # for each transition for j in range(n_transitions): # Starting ease value start_value = colors[j][i] # Ending ease value end_value = colors[j+1][i] # Difference between start and end diff = end_value - start_value # Make array of all start value base = np.empty(ease_length) base.fill(start_value) # Make array of the difference between start and end diffs = np.empty(ease_length) diffs.fill(diff) # run diffs through easing function to make smooth curve eased_diffs = diffs * ease # add transition to base values to produce curve from start to end value base += eased_diffs # append this to the output array output[i, j*ease_length:(j+1)*ease_length] = base # cast to int output = np.asarray(output, dtype=int) # pad out the ends (bit messy but it works and looks good) if pad: for i in range(3): output[i, -pad:] = output[i, -pad-1] return output def loadFromINI(self, settings, toLoad, overwrite=True): # loads colours or gradients from INI save file # toLoad: "colours" or "gradients" # settings is the QSettings object # overwrite kwarg determines if duplicates are overwritten from file or not. storage = settings.value(toLoad) if storage: try: config.colour_manager["user_"+toLoad] = {**config.colour_manager["user_"+toLoad], **settings.value(toLoad)} if overwrite else\ {**settings.value(toLoad), **config.colour_manager["user_"+toLoad]} except TypeError: print("Error parsing {} from file: {}".format(toLoad, storage)) pass else: print("No user {} found".format(toLoad)) def addColour(self, group, colour_name, colour_value): # can be used to add a new colour, or modify an existing value assert(group in ["user", "default"]), "invalic group: {}".format(group) assert(colour_name not in self.getColours("all")), "Colour {} already exists".format(colour_name) assert(type(colour_value) == tuple), "Colour_value {} not tuple format".format(colour_value) config.colour_manager[group+"_colours"][colour_name] = colour_value def editColour(self, group, old_name, colour_value, new_name=None): # changes the name and/or value of a colour assert(group in ["user", "default"]), "Invalic group: {}".format(group) assert(old_name in self.getColours("all")), "Colour {} does not exist".format(old_name) assert(type(colour_value) == tuple), "Colour_value {} not tuple format".format(colour_value) if group == "default": assert(not new_name), "Not allowed to edit default colour names" if new_name: self.delColour(group, old_name) old_name = new_name config.colour_manager[group+"_colours"][old_name] = colour_value def delColour(self, group, colour_name): # delete a saved colour. assert(group in ["user", "default"]), "Invalic group: {}".format(group) assert(group in ["user"]), "Deleting default colours not allowed" del config.colour_manager[group+"_colours"][colour_name] self.removeReferences("colour", colour_name) def addGradient(self, gradient_name, gradient_colours): # can be used to add a new gradient, or modify an existing one config.colour_manager["user_gradients"][gradient_name] = gradient_colours def delGradient(self, gradient_name): # delete a saved gradient del config.colour_manager["user_gradients"][gradient_name] def loadDefaultColours(self): # Loads default colours. config.colour_manager["default_colours"] = config.default_colours.copy() def loadDefaultGradients(self): # Loads default gradients. config.colour_manager["default_gradients"] = config.default_gradients def saveColours(self): self.colours_storage.setValue("colours", config.colour_manager["user_colours"]) self.colours_storage.sync() def saveGradients(self): self.gradients_storage.setValue("gradients", config.colour_manager["user_gradients"]) self.gradients_storage.sync() def removeReferences(self, to_remove, name, new_name=None): # cleans settings of a colour/gradient # eg if "red" is deleted, all usages of "red" will be changed to "black" # to_remove: "colour" or "gradient" # name: name of thing to remove references of # new_name: optional, change name to something else null_colour = "Black" null_gradient = "Spectral" if to_remove == "colour": if new_name in colour_manager.getColours("all"): null_colour = new_name elif to_remove == "gradient": if new_name in colour_manager.getGradients("all"): null_gradient = new_name if to_remove == "colour": for effect in self.effects_using_colours: if config.settings["devices"][effect[0]]["effect_opts"][effect[1]][effect[2]] == name: print(effect, "set to", null_colour) config.settings["devices"][effect[0]]["effect_opts"][effect[1]][effect[2]] = null_colour elif to_remove == "gradient": for effect in self.effects_using_gradients: if config.settings["devices"][effect[0]]["effect_opts"][effect[1]][effect[2]] == name: config.settings["devices"][effect[0]]["effect_opts"][effect[1]][effect[2]] = null_gradient class Microphone(): """Controls the audio input, allowing device selection and streaming""" def __init__(self, callback_func): # in this class, "device" is used to refer to the audio device, not "device" in the context of the led strips self.callback_func = callback_func self.numdevices = py_audio.get_device_count() self.default_device_id = py_audio.get_default_input_device_info()['index'] self.devices = [] #for each audio device, add to list of devices for i in range(0,self.numdevices): device_info = py_audio.get_device_info_by_host_api_device_index(0,i) if device_info["maxInputChannels"] > 1: self.devices.append(device_info) if not "MIC_ID" in config.settings["mic_config"]: self.setDevice(self.default_device_id) else: self.setDevice(config.settings["mic_config"]["MIC_ID"]) def getDevices(self): return self.devices def setDevice(self, device_id): # set device to stream from by the id of the device if not device_id in range(0,self.numdevices): raise ValueError("No device with id {}".format(device_id)) self.device_id = self.devices[device_id]["index"] self.device_name = self.devices[device_id]["name"] self.device_rate = int(self.devices[device_id]["defaultSampleRate"]) self.frames_per_buffer = self.device_rate // config.settings["configuration"]["FPS"] config.settings["mic_config"]["MIC_ID"] = self.device_id config.settings["mic_config"]["MIC_NAME"] = self.device_name config.settings["mic_config"]["MIC_RATE"] = self.device_rate def startStream(self): self.stream = py_audio.open(format = pyaudio.paInt16, channels = 1, rate = self.device_rate, input = True, input_device_index = self.device_id, frames_per_buffer = self.frames_per_buffer) # overflows = 0 # prev_ovf_time = time.time() while True: try: y = np.fromstring(self.stream.read(self.frames_per_buffer), dtype=np.int16) y = y.astype(np.float32) self.callback_func(y) except IOError: pass # overflows += 1 # if time.time() > prev_ovf_time + 1: # prev_ovf_time = time.time() # if config.settings["configuration"]["USE_GUI"]: # gui.label_error.setText('Audio buffer has overflowed {} times'.format(overflows)) # else: # print('Audio buffer has overflowed {} times'.format(overflows)) def stopStream(self): self.stream.stop_stream() self.stream.close() class GUI(QMainWindow): """The graphical interface of the application""" def __init__(self): super().__init__() self.initMainWindow() self.updateUIVisibleItems() def initMainWindow(self): # Set up window and wrapping layout self.setWindowTitle("Visualization") # Initial window size/pos last saved if available settings.beginGroup("MainWindow") if settings.value("geometry"): self.restoreGeometry(settings.value("geometry")) else: self.setGeometry(100,100,500,300) if settings.value("state"): self.restoreState(settings.value("state")) settings.endGroup() self.main_wrapper = QVBoxLayout() # Set up toolbar #toolbar_guiDialogue.setShortcut('Ctrl+H') toolbar_deviceDialogue = QAction('LED Strip Manager', self) toolbar_deviceDialogue.triggered.connect(self.deviceDialogue) toolbar_micDialogue = QAction('Microphone Setup', self) toolbar_micDialogue.triggered.connect(self.micDialogue) toolbar_colourDialogue = QAction('Colour Control', self) toolbar_colourDialogue.triggered.connect(self.colourDialogue) toolbar_guiDialogue = QAction('GUI Properties', self) toolbar_guiDialogue.triggered.connect(self.guiDialogue) toolbar_saveDialogue = QAction('Save Settings', self) toolbar_saveDialogue.triggered.connect(self.saveDialogue) self.toolbar = self.addToolBar('top_toolbar') self.toolbar.setObjectName('top_toolbar') self.toolbar.addAction(toolbar_deviceDialogue) self.toolbar.addAction(toolbar_micDialogue) self.toolbar.addAction(toolbar_colourDialogue) self.toolbar.addAction(toolbar_guiDialogue) self.toolbar.addAction(toolbar_saveDialogue) # Set up FPS and error labels self.statusbar = QStatusBar() self.setStatusBar(self.statusbar) self.label_error = QLabel("") self.label_fps = QLabel("") self.label_latency = QLabel("") self.label_fps.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.label_latency.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.statusbar.addPermanentWidget(self.label_error, stretch=1) self.statusbar.addPermanentWidget(self.label_latency) self.statusbar.addPermanentWidget(self.label_fps) # Set up board tabs widget self.label_boards = QLabel("LED Strips") self.boardsTabWidget = QTabWidget() # Dynamically set up boards tabs self.gui_widgets = {} # contains references to areas of gui for visibility settings self.gui_widgets["Graphs"] = [] self.gui_widgets["Reactive Effect Buttons"] = [] self.gui_widgets["Non Reactive Effect Buttons"] = [] self.gui_widgets["Frequency Range"] = [] self.gui_widgets["Effect Options"] = [] self.board_tabs = {} # contains all the tabs, one for each board self.board_tabs_widgets = {} # contains all the widgets for each tab self.main_wrapper.addWidget(self.label_boards) self.main_wrapper.addWidget(self.boardsTabWidget) #self.setLayout(self.main_wrapper) # Set up setupHelper self.initSetupHelper() # Set wrapper as main widget self.setCentralWidget(QWidget(self)) self.centralWidget().setLayout(self.main_wrapper) self.show() def initSetupHelper(self): helpstring = """ Looks like you need to connect an LED strip!\n\n 1: Open the 'LED Strip Manager' on the toolbar above\n 2: Choose the type of device (eg ESP8266) from the dropdown menu\n 3: Fill in the boxes with connection and settings info\n 4: Add the device\n 5: Party time!\n\n If you have any questions, feel free to open an issue on the GitHub page. """ self.setupHelper = QWidget() self.setupHelperLayout = QVBoxLayout() self.setupHelper.setLayout(self.setupHelperLayout) self.setupHelperText = QLabel(helpstring) self.setupHelperText.setWordWrap(True) self.setupHelperLayout.addWidget(self.setupHelperText) self.setupHelperLayout.addStretch() def showSetupHelper(self): self.boardsTabWidget.addTab(self.setupHelper, "Setup Helper") def hideSetupHelper(self): idx = self.boardsTabWidget.indexOf(self.setupHelper) self.boardsTabWidget.removeTab(idx) def addBoard(self, board): self.board_tabs_widgets[board] = {} self.board_tabs[board] = QWidget() self.initBoardUI(board) self.boardsTabWidget.addTab(self.board_tabs[board],board) self.board_tabs[board].setLayout(self.board_tabs_widgets[board]["wrapper"]) self.gui_widgets["Graphs"].append([self.board_tabs_widgets[board]["graph_view"]]) self.gui_widgets["Reactive Effect Buttons"].append([self.board_tabs_widgets[board]["label_reactive"], self.board_tabs_widgets[board]["reactive_button_grid_wrap"]]) self.gui_widgets["Non Reactive Effect Buttons"].append([self.board_tabs_widgets[board]["label_non_reactive"], self.board_tabs_widgets[board]["non_reactive_button_grid_wrap"]]) self.gui_widgets["Frequency Range"].append([self.board_tabs_widgets[board]["label_slider"], self.board_tabs_widgets[board]["freq_slider"]]) self.gui_widgets["Effect Options"].append([self.board_tabs_widgets[board]["label_options"], self.board_tabs_widgets[board]["opts_tabs"]]) self.updateUIVisibleItems() def delBoard(self, board): idx = self.boardsTabWidget.indexOf(self.board_tabs[board]) self.boardsTabWidget.removeTab(idx) #self.gui_widgets["Graphs"].remove([self.board_tabs_widgets[board]["graph_view"]]) #self.gui_widgets["Reactive Effect Buttons"].remove([self.board_tabs_widgets[board]["label_reactive"], self.board_tabs_widgets[board]["reactive_button_grid_wrap"]]) #self.gui_widgets["Non Reactive Effect Buttons"].remove([self.board_tabs_widgets[board]["label_non_reactive"], self.board_tabs_widgets[board]["non_reactive_button_grid_wrap"]]) #self.gui_widgets["Frequency Range"].remove([self.board_tabs_widgets[board]["label_slider"], self.board_tabs_widgets[board]["freq_slider"]]) #self.gui_widgets["Effect Options"].remove([self.board_tabs_widgets[board]["label_options"], self.board_tabs_widgets[board]["opts_tabs"]]) #del self.board_tabs_widgets[board] self.board_tabs[board].deleteLater() self.updateUIVisibleItems() def closeEvent(self, event): # executed when the window is being closed quit_msg = "Are you sure you want to exit?" reply = QMessageBox.question(self, 'Exit', quit_msg, QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: # Save window state settings.beginGroup("MainWindow") settings.setValue("geometry", self.saveGeometry()) settings.setValue('state', self.saveState()) settings.endGroup() # save all settings settings.setValue("settings_dict", config.settings) # save and close settings.sync() colour_manager.saveColours() colour_manager.saveGradients() event.accept() sys.exit(0) else: event.ignore() def updateUIVisibleItems(self): for section in self.gui_widgets: for widgets in self.gui_widgets[section]: for widget in widgets: widget.setVisible(config.settings["GUI_opts"][section]) if len(config.settings["devices"]) == 0: self.showSetupHelper() else: self.hideSetupHelper() def colourDialogue(self): import re def cleanColourGrid(): # cleanup old colour widgets referring to ones that no longer exist for widget in self.colourDialogueWidgets: widget.deleteLater() def updateColourGrid(): def delFuncGenerator(name, group): def func(): colour_manager.delColour(group, name) cleanColourGrid() gui.updateColourDropdowns() updateColourGrid() func.__name__ = name return func def editFuncGenerator(name, group): def func(): value = colour_manager.colour(name) addColourDialogue(group=group, name=name, value=value, edit=True) cleanColourGrid() gui.updateColourDropdowns() updateColourGrid() func.__name__ = name return func self.colourDialogueWidgets = [] self.colourDialogueGridEditFuncs = {} self.colourDialogueGridDeleteFuncs = {} del_enabled = {"user": True, "default": False} for i in ["user", "default"]: colours = colour_manager.getColours(i) count = 0 for colour in colours: self.colourDialogueGridEditFuncs[colour] = editFuncGenerator(colour, i) self.colourDialogueGridDeleteFuncs[colour] = delFuncGenerator(colour, i) label = QLabel(colour) colourBox = QWidget() delButton = QPushButton("") delButton.setIcon(QIcon('./lib/bin.png')) delButton.setIconSize(QSize(14,14)) delButton.clicked.connect(self.colourDialogueGridDeleteFuncs[colour]) delButton.setEnabled(del_enabled[i]) editButton = QPushButton("") editButton.setIcon(QIcon('./lib/edit.png')) editButton.clicked.connect(self.colourDialogueGridEditFuncs[colour]) editButton.setIconSize(QSize(14,14)) p = colourBox.palette() p.setColor(colourBox.backgroundRole(), QColor(*colours[colour])) colourBox.setPalette(p) colourBox.setAutoFillBackground(True) self.pallettes[i]["grid"].setColumnStretch(0,1) self.pallettes[i]["grid"].setColumnStretch(1,1) self.pallettes[i]["grid"].addWidget(label,count,0) self.pallettes[i]["grid"].addWidget(colourBox,count,1) self.pallettes[i]["grid"].addWidget(editButton,count,2) self.pallettes[i]["grid"].addWidget(delButton,count,3) self.colourDialogueWidgets.extend([label, colourBox, editButton, delButton]) count += 1 def restoreDefaultColours(): colour_manager.loadDefaultColours() cleanColourGrid() gui.updateColourDropdowns() updateColourGrid() def addColourDialogue(name=None, value=None, edit=False, group="user"): self.initial_value = value def addColour(): rgb = self.addColourPreviewBoxPalette.color(self.addColourPreviewBox.backgroundRole()).getRgb() if edit: colour_manager.editColour(group, name, rgb, new_name=(self.addColourEditName.text() if group=="user" else None)) else: colour_manager.addColour(group, self.addColourEditName.text(), rgb) cleanColourGrid() gui.updateColourDropdowns() updateColourGrid() self.addColourDialogue.accept() def cancelChanges(): setPreviewColour(QColor(*self.initial_value if self.initial_value else (255,255,255))) def acceptChanges(): self.initial_value = self.addColourPreviewBoxPalette.color(self.addColourPreviewBox.backgroundRole()).getRgb() setPreviewColour(QColor(*self.initial_value if self.initial_value else (255,255,255))) def showColourDialogue(): self.colourEditDialogue = QColorDialog(QColor(*self.initial_value if self.initial_value else (255,255,255))) self.colourEditDialogue.setWindowModality(Qt.ApplicationModal) self.colourEditDialogue.show() self.colourEditDialogue.accepted.connect(acceptChanges) self.colourEditDialogue.rejected.connect(cancelChanges) self.colourEditDialogue.currentColorChanged.connect(setPreviewColour) def setPreviewColour(colour): self.addColourPreviewBoxPalette.setColor(self.addColourPreviewBox.backgroundRole(), colour) self.addColourPreviewBox.setPalette(self.addColourPreviewBoxPalette) if edit: colour_manager.editColour(group, name, colour.getRgb(), new_name=(self.addColourEditName.text() if group=="user" else None)) def validColourName(name): styles = ["", "border: 1px solid #3be820;", "border: 1px solid red;"] if name: if re.match("\w+$", name): self.addColourEditName.setStyleSheet(styles[1]) buttons.button(QDialogButtonBox.Ok).setEnabled(True) else: self.addColourEditName.setStyleSheet(styles[2]) buttons.button(QDialogButtonBox.Ok).setEnabled(False) else: self.addColourEditName.setStyleSheet(styles[0]) buttons.button(QDialogButtonBox.Ok).setEnabled(False) # Set up window and layout self.addColourDialogue = QDialog(None, Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) self.addColourDialogue.setWindowTitle("{} Colour".format("Edit" if edit else "Add")) self.addColourDialogue.setWindowModality(Qt.ApplicationModal) layout = QGridLayout() self.addColourDialogue.setLayout(layout) # Colour name input self.addColourLabelName = QLabel("Name") self.addColourEditName = QLineEdit() self.addColourEditName.setEnabled(True if group == "user" else False) self.addColourEditName.textChanged.connect(validColourName) layout.addWidget(self.addColourLabelName, 0, 0) layout.addWidget(self.addColourEditName, 0, 1, 1, 2) # Colour value input self.addColourLabelColour = QLabel("Colour") self.addColourPreviewBox = QWidget() self.addColourPreviewBoxPalette = self.addColourPreviewBox.palette() self.addColourPreviewBoxPalette.setColor(self.addColourPreviewBox.backgroundRole(), QColor(*self.initial_value if self.initial_value else (255,255,255))) self.addColourPreviewBox.setPalette(self.addColourPreviewBoxPalette) self.addColourPreviewBox.setAutoFillBackground(True) self.colourEdit = QPushButton("Edit Colour") self.colourEdit.clicked.connect(showColourDialogue) layout.addWidget(self.addColourLabelColour, 1, 0) layout.addWidget(self.addColourPreviewBox, 1, 1) layout.addWidget(self.colourEdit, 1, 2) # Set up dialogue buttons buttons = QDialogButtonBox(Qt.Horizontal, self) buttons.addButton(QDialogButtonBox.Ok) buttons.button(QDialogButtonBox.Ok).setEnabled(False) buttons.accepted.connect(addColour) layout.addWidget(buttons, 2, 0, 1, 3) layout.setColumnStretch(1,1) if name: self.addColourEditName.setText(name) self.addColourDialogue.show() # Set up window and layout self.colour_dialogue = QDialog(None, Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) self.colour_dialogue.setWindowTitle("Colour Control") self.colour_dialogue.setWindowModality(Qt.ApplicationModal) layout = QVBoxLayout() self.colour_dialogue.setLayout(layout) # Set up colour display self.pallettes = {} self.pallettes["user"] = {} self.pallettes["default"] = {} self.pallettes["buttons"] = {} for i in ["user", "default"]: self.pallettes[i]["groupbox"] = QGroupBox() self.pallettes[i]["wrapper_layout"] = QVBoxLayout() self.pallettes[i]["grid"] = QGridLayout() self.pallettes[i]["groupbox"].setLayout(self.pallettes[i]["wrapper_layout"]) self.pallettes[i]["wrapper_layout"].addLayout(self.pallettes[i]["grid"], stretch=1) restore = QPushButton("Reset Defaults") add = QPushButton("Add Colour") restore.clicked.connect(restoreDefaultColours) add.clicked.connect(addColourDialogue) self.pallettes["buttons"]["wrap"] = QWidget() self.pallettes["buttons"]["layout"] = QHBoxLayout() self.pallettes["buttons"]["layout"].addStretch() self.pallettes["buttons"]["layout"].addWidget(restore) self.pallettes["buttons"]["layout"].addWidget(add) self.pallettes["buttons"]["wrap"].setLayout(self.pallettes["buttons"]["layout"]) self.pallettes["user"]["groupbox"].setTitle("Custom colours") self.pallettes["default"]["groupbox"].setTitle("Default colours") layout.addWidget(self.pallettes["default"]["groupbox"]) layout.addWidget(self.pallettes["user"]["groupbox"]) layout.addWidget(self.pallettes["buttons"]["wrap"]) # Load and show colours updateColourGrid() # Set up dialogue buttons self.buttons = QDialogButtonBox(Qt.Horizontal, self) self.buttons.addButton(QDialogButtonBox.Ok) self.buttons.accepted.connect(self.colour_dialogue.accept) layout.addWidget(self.buttons) self.colour_dialogue.show() def micDialogue(self): def set_mic(): microphone.setDevice(mic_button_group.checkedId()) microphone.startStream() # Set up window and layout self.mic_dialogue = QDialog(None, Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) self.mic_dialogue.setWindowTitle("Mic Setup") self.mic_dialogue.setWindowModality(Qt.ApplicationModal) layout = QVBoxLayout() self.mic_dialogue.setLayout(layout) # Set up buttons for each mic mic_button_group = QButtonGroup(self.mic_dialogue) mic_buttons = {} mics = microphone.getDevices() for mic in mics: mic_id = mic["index"] mic_buttons[mic_id] = QRadioButton(mic["name"]) mic_button_group.addButton(mic_buttons[mic_id]) mic_button_group.setId(mic_buttons[mic_id], mic_id) mic_buttons[mic_id].clicked.connect(set_mic) if config.settings["mic_config"]["MIC_ID"] == mic_id: mic_buttons[mic_id].setChecked(Qt.Checked) layout.addWidget(mic_buttons[mic_id]) # Set up ok/cancel buttons self.buttons = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal, self) self.buttons.accepted.connect(self.mic_dialogue.accept) layout.addWidget(self.buttons) self.mic_dialogue.show() def deviceDialogue(self): def addBoard_to_manager(): general_config = {} required_config = {} current_device = device_type_cbox.currentText() for gen_config_setting in config.device_gen_config: if config.device_gen_config[gen_config_setting][2] == "textbox": general_config[gen_config_setting] = gen_widgets[gen_config_setting][1].text() elif config.device_gen_config[gen_config_setting][2] == "textbox-int": general_config[gen_config_setting] = int(gen_widgets[gen_config_setting][1].text()) elif config.device_gen_config[gen_config_setting][2] == "checkbox": general_config[gen_config_setting] = gen_widgets[gen_config_setting][1].isChecked() if config.device_req_config[current_device]: for req_config_setting in config.device_req_config[current_device]: if config.device_req_config[current_device][req_config_setting][2] == "textbox": required_config[req_config_setting] = req_widgets[current_device][req_config_setting][1].text() elif config.device_req_config[current_device][req_config_setting][2] == "textbox-int": required_config[req_config_setting] = int(req_widgets[current_device][req_config_setting][1].text()) elif config.device_req_config[current_device][req_config_setting][2] == "checkbox": required_config[req_config_setting] = req_widgets[current_device][req_config_setting][1].isChecked() required_config["TYPE"] = current_device board_name = general_config["NAME"] del general_config["NAME"] board_manager.addBoard(board_name, config_exists=False, req_config=required_config, gen_config=general_config) def remBoard_from_manager(): for board in self.to_delete: board_manager.delBoard(board) for widget in self.del_widgets: self.del_widgets[widget][0].deleteLater() self.del_widgets[widget][1].deleteLater() populate_remove_device_list() def populate_remove_device_list(): self.del_widgets = {} i = 0 for board in config.settings["devices"]: # Make widgets wLabel = QLabel(board) wEdit = QCheckBox() wEdit.setCheckState(Qt.Unchecked) wEdit.stateChanged.connect(validate_rem_device_checks) self.del_widgets[board] = [wLabel, wEdit] # Add to layout remDeviceTabButtonLayout.addWidget(self.del_widgets[board][0], i, 0) remDeviceTabButtonLayout.addWidget(self.del_widgets[board][1], i, 1) i += 1 def validate_rem_device_checks(): self.to_delete = [] for board in config.settings["devices"]: if self.del_widgets[board][1].isChecked(): self.to_delete.append(board) self.rem_device_button.setEnabled(True if self.to_delete else False) def show_hide_addBoard_interface(): current_device = device_type_cbox.currentText() for device in config.device_req_config: for req_config_setting in req_widgets[device]: if req_config_setting is not "no_config": for widget in req_widgets[device][req_config_setting]: widget.setVisible(device == current_device) else: req_widgets[device][req_config_setting].setVisible(device == current_device) def validate_inputs(): # Checks all inputs are ok, before setting "add device" to usable import re current_device = device_type_cbox.currentText() req_valid_inputs = {} gen_valid_inputs = {} styles = ["", "border: 1px solid #3be820;", "border: 1px solid red;"] def valid_mac(x): return True if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", test.lower()) else False def valid_ip(x): try: pieces = x.split('.') if len(pieces) != 4: return False return all(0<=int(i)<256 for i in pieces) except: return False def valid_int(x): try: x = int(x) return x > 0 except: return False def validate_address_port(x): try: pieces = x.split(":") if len(pieces) != 2: return False return (((pieces[0] == "localhost") or (valid_ip(pieces[0]))) and valid_int(pieces[1])) is True except: return False def update_req_box_highlight(setting, val): req_widgets[current_device][setting][1].setStyleSheet(styles[val]) def update_gen_box_highlight(setting, val): gen_widgets[setting][1].setStyleSheet(styles[val]) def update_add_device_button(): req_value = all(req_valid_inputs[setting] for setting in config.device_req_config[current_device]) if config.device_req_config[current_device] else True gen_value = all(gen_valid_inputs[setting] for setting in config.device_gen_config) if config.device_gen_config else True self.add_device_button.setEnabled(req_value and gen_value) # Validate the inputs, highlight invalid boxes # ESP8266 if current_device == "ESP8266": for req_config_setting in config.device_req_config[current_device]: test = req_widgets[current_device][req_config_setting][1].text() # Validate MAC if req_config_setting == "MAC_ADDR": valid = valid_mac(test) req_widgets[current_device][req_config_setting][1].setText(test.replace(":", "-")) # Validate IP elif req_config_setting == "UDP_IP": valid = valid_ip(test) # Validate port elif req_config_setting == "UDP_PORT": valid = valid_int(test) else: valid = True req_valid_inputs[req_config_setting] = valid update_req_box_highlight(req_config_setting, (1 if valid else 2) if test else 0) # PxMatrix if current_device == "PxMatrix": for req_config_setting in config.device_req_config[current_device]: test = req_widgets[current_device][req_config_setting][1].text() # Validate MAC if req_config_setting == "MAC_ADDR": valid = valid_mac(test) req_widgets[current_device][req_config_setting][1].setText(test.replace(":", "-")) # Validate IP elif req_config_setting == "UDP_IP": valid = valid_ip(test) # Validate port elif req_config_setting == "UDP_PORT": valid = valid_int(test) else: valid = True req_valid_inputs[req_config_setting] = valid update_req_box_highlight(req_config_setting, (1 if valid else 2) if test else 0) # Raspberry Pi elif current_device == "RaspberryPi": for req_config_setting in config.device_req_config[current_device]: test = req_widgets[current_device][req_config_setting][1].text() # Validate LED Pin if req_config_setting == "LED_PIN": valid = valid_int(test) # Validate LED Freq elif req_config_setting == "LED_FREQ_HZ": valid = valid_int(test) # Validate LED DMA elif req_config_setting == "LED_DMA": valid = valid_int(test) else: valid = True req_valid_inputs[req_config_setting] = valid update_req_box_highlight(req_config_setting, (1 if valid else 2) if test else 0) # Fadecandy elif current_device == "Fadecandy": for req_config_setting in config.device_req_config[current_device]: test = req_widgets[current_device][req_config_setting][1].text() # Validate Server if req_config_setting == "SERVER": valid = validate_address_port(test) else: valid = True req_valid_inputs[req_config_setting] = valid update_req_box_highlight(req_config_setting, (1 if valid else 2) if test else 0) # Other devices without required config elif not config.device_req_config[current_device]: pass for gen_config_setting in config.device_gen_config: test = gen_widgets[gen_config_setting][1].text() # Validate Server if gen_config_setting in ["N_PIXELS", "N_FFT_BINS", "MIN_FREQUENCY", "MAX_FREQUENCY"]: valid = valid_int(test) else: valid = True gen_valid_inputs[gen_config_setting] = valid update_gen_box_highlight(gen_config_setting, (1 if valid else 2) if test else 0) update_add_device_button() # Set up window and layout self.device_dialogue = QDialog(None, Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) self.device_dialogue.setWindowTitle("LED Strip Manager") self.device_dialogue.setWindowModality(Qt.ApplicationModal) layout = QVBoxLayout() self.device_dialogue.setLayout(layout) # Set up tab layouts tabs = QTabWidget() layout.addWidget(tabs) addDeviceTab = QWidget() remDeviceTab = QWidget() addDeviceTabLayout = QVBoxLayout() remDeviceTabLayout = QVBoxLayout() addDeviceReqGroupBox = QGroupBox("Device Setup") addDeviceGenGroupBox = QGroupBox("Configuration") remDeviceGroupBox = QGroupBox("Devices") addDeviceTabReqButtonLayout = QGridLayout() addDeviceTabGenButtonLayout = QGridLayout() remDeviceTabButtonLayout = QGridLayout() addDeviceReqGroupBox.setLayout(addDeviceTabReqButtonLayout) addDeviceGenGroupBox.setLayout(addDeviceTabGenButtonLayout) remDeviceGroupBox.setLayout(remDeviceTabButtonLayout) addDeviceTab.setLayout(addDeviceTabLayout) remDeviceTab.setLayout(remDeviceTabLayout) tabs.addTab(addDeviceTab, "Add Device") tabs.addTab(remDeviceTab, "Remove Device") # Set up "Add Device" tab device_type_cbox = QComboBox() device_type_cbox.addItems(config.device_req_config.keys()) device_type_cbox.currentIndexChanged.connect(show_hide_addBoard_interface) device_type_cbox.currentIndexChanged.connect(validate_inputs) addDeviceTabLayout.addWidget(device_type_cbox) # Set up "Add Device" required settings widgets req_widgets = {} addDeviceTabLayout.addWidget(addDeviceReqGroupBox) # if the new board has required config for device in config.device_req_config: # Make the req_widgets req_widgets[device] = {} if config.device_req_config[device]: for req_config_setting in config.device_req_config[device]: label = config.device_req_config[device][req_config_setting][0] guide = config.device_req_config[device][req_config_setting][1] wType = config.device_req_config[device][req_config_setting][2] deflt = config.device_req_config[device][req_config_setting][3] wLabel = QLabel(label) #wGuide = QLabel(guide) if wType in ["textbox", "textbox-int"]: wEdit = QLineEdit() wEdit.setPlaceholderText(deflt) wEdit.textChanged.connect(validate_inputs) elif wType == "checkbox": wEdit = QCheckBox() wEdit.setCheckState(Qt.Checked if deflt else Qt.Unchecked) req_widgets[device][req_config_setting] = [wLabel, wEdit] # Add req_widgets to layout i = 0 for req_config in req_widgets[device]: addDeviceTabReqButtonLayout.addWidget(req_widgets[device][req_config][0], i, 0) addDeviceTabReqButtonLayout.addWidget(req_widgets[device][req_config][1], i, 1) #addDeviceTabReqButtonLayout.addWidget(widget_set[2], i+1, 0, 1, 2) i += 1 else: no_setup = QLabel("Device requires no additional setup here! :)") req_widgets[device]["no_config"] = no_setup addDeviceTabReqButtonLayout.addWidget(no_setup, 0, 0) # Set up "Add Device" general settings widgets gen_widgets = {} addDeviceTabLayout.addWidget(addDeviceGenGroupBox) addDeviceTabLayout.addStretch(1) for gen_config_setting in config.device_gen_config: label = config.device_gen_config[gen_config_setting][0] guide = config.device_gen_config[gen_config_setting][1] wType = config.device_gen_config[gen_config_setting][2] deflt = config.device_gen_config[gen_config_setting][3] wLabel = QLabel(label) #wGuide = QLabel(guide) if wType in ["textbox", "textbox-int"]: wEdit = QLineEdit() wEdit.setPlaceholderText(deflt) wEdit.textChanged.connect(validate_inputs) elif wType == "checkbox": wEdit = QCheckBox() wEdit.setCheckState(Qt.Checked if deflt else Qt.Unchecked) gen_widgets[gen_config_setting] = [wLabel, wEdit] # Add gen_widgets to layout i = 0 for req_config in gen_widgets: addDeviceTabGenButtonLayout.addWidget(gen_widgets[req_config][0], i, 0) addDeviceTabGenButtonLayout.addWidget(gen_widgets[req_config][1], i, 1) #addDeviceTabGenButtonLayout.addWidget(widget_set[2], i+1, 0, 1, 2) i += 1 # Show appropriate req_widgets show_hide_addBoard_interface() self.add_device_button = QPushButton("Add Device") self.add_device_button.setEnabled(False) self.add_device_button.clicked.connect(addBoard_to_manager) addDeviceTabLayout.addWidget(self.add_device_button) # Set up "Remove Device" tab remDeviceTabLayout.addWidget(remDeviceGroupBox) remDeviceTabLayout.addStretch(1) # Show devices available to delete populate_remove_device_list() self.rem_device_button = QPushButton("Delete Device") self.rem_device_button.setEnabled(False) self.rem_device_button.clicked.connect(remBoard_from_manager) remDeviceTabLayout.addWidget(self.rem_device_button) # Set up ok/cancel buttons self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self) self.buttons.accepted.connect(self.device_dialogue.accept) self.buttons.rejected.connect(self.device_dialogue.reject) layout.addWidget(self.buttons) # Set button states and show dialogue validate_inputs() self.device_dialogue.show() def saveDialogue(self): # Save window state settings.beginGroup("MainWindow") settings.setValue("geometry", self.saveGeometry()) settings.setValue('state', self.saveState()) settings.endGroup() # save all settings settings.setValue("settings_dict", config.settings) # save and close settings.sync() # Confirmation message self.conf_dialogue = QMessageBox() self.conf_dialogue.setText("Settings saved.\nSettings are also automatically saved when program closes.") self.conf_dialogue.show() def guiDialogue(self): def update_visibilty_dict(): for checkbox in self.gui_vis_checkboxes: config.settings["GUI_opts"][checkbox] = self.gui_vis_checkboxes[checkbox].isChecked() self.updateUIVisibleItems() self.gui_dialogue = QDialog(None, Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) self.gui_dialogue.setWindowTitle("Show/hide Sections") self.gui_dialogue.setWindowModality(Qt.ApplicationModal) layout = QGridLayout() self.gui_dialogue.setLayout(layout) # OK button self.buttons = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal, self) self.buttons.accepted.connect(self.gui_dialogue.accept) self.gui_vis_checkboxes = {} for section in self.gui_widgets: self.gui_vis_checkboxes[section] = QCheckBox(section) self.gui_vis_checkboxes[section].setCheckState( Qt.Checked if config.settings["GUI_opts"][section] else Qt.Unchecked) self.gui_vis_checkboxes[section].stateChanged.connect(update_visibilty_dict) layout.addWidget(self.gui_vis_checkboxes[section]) layout.addWidget(self.buttons) self.gui_dialogue.show() def updateColourDropdowns(self): colours = colour_manager.getColours("all") # dd, dropdown for dd in self.colour_dropdowns: current = config.settings["devices"][dd[0]]["effect_opts"][dd[1]][dd[2]] self.board_tabs_widgets[dd[0]]["grid_layout_widgets"][dd[1]][dd[2]].clear() self.board_tabs_widgets[dd[0]]["grid_layout_widgets"][dd[1]][dd[2]].addItems(colours) self.board_tabs_widgets[dd[0]]["grid_layout_widgets"][dd[1]][dd[2]].setCurrentIndex(list(colours).index(current)) def updateGradientDropdowns(self): gradients = colour_manager.getGradients() for dd in self.gradient_dropdowns: current = config.settings["devices"][dd[0]]["effect_opts"][dd[1]][dd[2]] self.board_tabs_widgets[dd[0]]["grid_layout_widgets"][dd[1]][dd[2]].clear() self.board_tabs_widgets[dd[0]]["grid_layout_widgets"][dd[1]][dd[2]].addItems(gradients) self.board_tabs_widgets[dd[0]]["grid_layout_widgets"][dd[1]][dd[2]].setCurrentIndex(list(gradients).index(current)) def initBoardUI(self, board): self.board = board # Set up wrapping layout self.board_tabs_widgets[board]["wrapper"] = QVBoxLayout() # Set up graph layout self.board_tabs_widgets[board]["graph_view"] = pg.GraphicsView() graph_layout = pg.GraphicsLayout(border=(100,100,100)) self.board_tabs_widgets[board]["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.settings["devices"][self.board]["configuration"]["N_FFT_BINS"] + 1)) self.board_tabs_widgets[board]["mel_curve"] = pg.PlotCurveItem() self.board_tabs_widgets[board]["mel_curve"].setData(x=x_data, y=x_data*0) fft_plot.addItem(self.board_tabs_widgets[board]["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.board_tabs_widgets[board]["r_curve"] = pg.PlotCurveItem(pen=r_pen) self.board_tabs_widgets[board]["g_curve"] = pg.PlotCurveItem(pen=g_pen) self.board_tabs_widgets[board]["b_curve"] = pg.PlotCurveItem(pen=b_pen) # Define x data x_data = np.array(range(1, config.settings["devices"][self.board]["configuration"]["N_PIXELS"] + 1)) self.board_tabs_widgets[board]["r_curve"].setData(x=x_data, y=x_data*0) self.board_tabs_widgets[board]["g_curve"].setData(x=x_data, y=x_data*0) self.board_tabs_widgets[board]["b_curve"].setData(x=x_data, y=x_data*0) # Add curves to plot led_plot.addItem(self.board_tabs_widgets[board]["r_curve"]) led_plot.addItem(self.board_tabs_widgets[board]["g_curve"]) led_plot.addItem(self.board_tabs_widgets[board]["b_curve"]) # Set up button layout self.board_tabs_widgets[board]["label_reactive"] = QLabel("Audio Reactive Effects") self.board_tabs_widgets[board]["label_non_reactive"] = QLabel("Non Reactive Effects") self.board_tabs_widgets[board]["reactive_button_grid_wrap"] = QWidget() self.board_tabs_widgets[board]["non_reactive_button_grid_wrap"] = QWidget() self.board_tabs_widgets[board]["reactive_button_grid"] = QGridLayout() self.board_tabs_widgets[board]["non_reactive_button_grid"] = QGridLayout() self.board_tabs_widgets[board]["reactive_button_grid_wrap"].setLayout(self.board_tabs_widgets[board]["reactive_button_grid"]) self.board_tabs_widgets[board]["non_reactive_button_grid_wrap"].setLayout(self.board_tabs_widgets[board]["non_reactive_button_grid"]) 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(): config.settings["devices"][board]["configuration"]["current_effect"] = effect buttons[effect].setDown(True) func.__name__ = effect return func # Where the magic happens for effect in board_manager.visualizers[board].effects: if not effect in board_manager.visualizers[board].non_reactive_effects: connecting_funcs[effect] = connect_generator(effect) buttons[effect] = QPushButton(effect) buttons[effect].clicked.connect(connecting_funcs[effect]) self.board_tabs_widgets[board]["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]) self.board_tabs_widgets[board]["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 self.board_tabs_widgets[board]["label_slider"] = QLabel("Frequency Range") # Frequency slider def freq_slider_change(tick): minf = self.board_tabs_widgets[board]["freq_slider"].tickValue(0)**2.0 * (config.settings["mic_config"]["MIC_RATE"] / 2.0) maxf = self.board_tabs_widgets[board]["freq_slider"].tickValue(1)**2.0 * (config.settings["mic_config"]["MIC_RATE"] / 2.0) t = 'Frequency range: {:.0f} - {:.0f} Hz'.format(minf, maxf) freq_label.setText(t) config.settings["devices"][self.board]["configuration"]["MIN_FREQUENCY"] = minf config.settings["devices"][self.board]["configuration"]["MAX_FREQUENCY"] = maxf board_manager.signal_processers[self.board].create_mel_bank() def set_freq_min(): config.settings["devices"][board]["configuration"]["MIN_FREQUENCY"] = self.board_tabs_widgets[board]["freq_slider"].start() board_manager.signal_processers[board].create_mel_bank() def set_freq_max(): config.settings["devices"][board]["configuration"]["MAX_FREQUENCY"] = self.board_tabs_widgets[board]["freq_slider"].end() board_manager.signal_processers[board].create_mel_bank() self.board_tabs_widgets[board]["freq_slider"] = QRangeSlider() self.board_tabs_widgets[board]["freq_slider"].show() self.board_tabs_widgets[board]["freq_slider"].setMin(0) self.board_tabs_widgets[board]["freq_slider"].setMax(20000) self.board_tabs_widgets[board]["freq_slider"].setRange(config.settings["devices"][board]["configuration"]["MIN_FREQUENCY"], config.settings["devices"][board]["configuration"]["MAX_FREQUENCY"]) self.board_tabs_widgets[board]["freq_slider"].setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);') self.board_tabs_widgets[board]["freq_slider"].setSpanStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);') self.board_tabs_widgets[board]["freq_slider"].setDrawValues(True) self.board_tabs_widgets[board]["freq_slider"].endValueChanged.connect(set_freq_max) self.board_tabs_widgets[board]["freq_slider"].startValueChanged.connect(set_freq_min) self.board_tabs_widgets[board]["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 self.board_tabs_widgets[board]["label_options"] = QLabel("Effect Options") self.board_tabs_widgets[board]["opts_tabs"] = QTabWidget() # Dynamically set up tabs tabs = {} grid_layouts = {} self.board_tabs_widgets[board]["grid_layout_widgets"] = {} # easy access to dropdowns of colours or gradients. they often need updating. # contains tuples (board, effect, key) to allow access in self.board_tabs_widgets self.colour_dropdowns = [] self.gradient_dropdowns = [] options = config.settings["devices"][board]["effect_opts"].keys() for effect in board_manager.visualizers[self.board].effects: # Make the tab self.board_tabs_widgets[board]["grid_layout_widgets"][effect] = {} tabs[effect] = QWidget() grid_layouts[effect] = QGridLayout() tabs[effect].setLayout(grid_layouts[effect]) self.board_tabs_widgets[board]["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(): config.settings["devices"][board]["effect_opts"][effect][key] = self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].value() return func def gen_float_slider_valuechanger(effect, key): def func(): config.settings["devices"][board]["effect_opts"][effect][key] = self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].slider_value return func def gen_combobox_valuechanger(effect, key): def func(): config.settings["devices"][board]["effect_opts"][effect][key] = self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].currentText() return func def gen_checkbox_valuechanger(effect, key): def func(): config.settings["devices"][board]["effect_opts"][effect][key] = self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].isChecked() return func # Dynamically generate ui for settings if effect in config.dynamic_effects_config: i = 0 connecting_funcs[effect] = {} for key, label, ui_element, *opts in config.dynamic_effects_config[effect]: if opts: # neatest way ^^^^^ i could think of to unpack and handle an unknown number of opts (if any) NOTE only works with py >=3.6 if opts[0] not in ["colours", "gradients"]: opts = list(opts[0]) if ui_element == "slider": connecting_funcs[effect][key] = gen_slider_valuechanger(effect, key) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key] = QSlider(Qt.Horizontal) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].setMinimum(opts[0]) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].setMaximum(opts[1]) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].setValue(config.settings["devices"][board]["effect_opts"][effect][key]) self.board_tabs_widgets[board]["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.board_tabs_widgets[board]["grid_layout_widgets"][effect][key] = QFloatSlider(*opts, config.settings["devices"][board]["effect_opts"][effect][key]) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].setValue(config.settings["devices"][board]["effect_opts"][effect][key]) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].valueChanged.connect(connecting_funcs[effect][key]) elif ui_element == "dropdown": if opts[0] == "colours": self.colour_dropdowns.append((board, effect, key)) opts = list(colour_manager.getColours("all")) elif opts[0] == "gradients": self.gradient_dropdowns.append((board, effect, key)) opts = list(colour_manager.getGradients("all")) connecting_funcs[effect][key] = gen_combobox_valuechanger(effect, key) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key] = QComboBox() self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].addItems(opts) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].setCurrentIndex(opts.index(config.settings["devices"][board]["effect_opts"][effect][key])) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].currentTextChanged.connect(connecting_funcs[effect][key]) elif ui_element == "checkbox": connecting_funcs[effect][key] = gen_checkbox_valuechanger(effect, key) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key] = QCheckBox() self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].stateChanged.connect( connecting_funcs[effect][key]) self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key].setCheckState( Qt.Checked if config.settings["devices"][board]["effect_opts"][effect][key] else Qt.Unchecked) grid_layouts[effect].addWidget(QLabel(label),i,0) grid_layouts[effect].addWidget(self.board_tabs_widgets[board]["grid_layout_widgets"][effect][key],i,1) i += 1 else: grid_layouts[effect].addWidget(QLabel("No customisable options for this effect :("),0,0) # Add layouts into self.board_tabs_widgets[board]["wrapper"] self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["graph_view"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["label_reactive"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["reactive_button_grid_wrap"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["label_non_reactive"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["non_reactive_button_grid_wrap"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["label_slider"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["freq_slider"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["label_options"]) self.board_tabs_widgets[board]["wrapper"].addWidget(self.board_tabs_widgets[board]["opts_tabs"]) def update_config_dicts(): # Updates config.settings with any values stored in settings.ini if settings.value("settings_dict"): for settings_dict in settings.value("settings_dict"): if not config.use_defaults[settings_dict]: try: config.settings[settings_dict] = {**config.settings[settings_dict], **settings.value("settings_dict")[settings_dict]} except TypeError: print("Error parsing settings dictionary {}".format(settings_dict)) pass else: print("Could not find settings.ini") def save_config_dicts(): # saves config.settings settings.setValue("settings_dict", config.settings) settings.sync() 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 audio_datas = {} outputs = {} audio_input = True # Visualization for each board for board in board_manager.boards: # Get processed audio data for each device audio_datas[board] = board_manager.signal_processers[board].update(audio_samples) # Get visualization output for each board audio_input = audio_datas[board]["vol"] > config.settings["configuration"]["MIN_VOLUME_THRESHOLD"] outputs[board] = board_manager.visualizers[board].get_vis(audio_datas[board]["mel"], audio_input) # Map filterbank output onto LED strip(s) board_manager.boards[board].show(outputs[board]) if config.settings["configuration"]["USE_GUI"]: # Plot filterbank output gui.board_tabs_widgets[board]["mel_curve"].setData(x=audio_datas[board]["x"], y=audio_datas[board]["y"]) # Plot visualizer output gui.board_tabs_widgets[board]["r_curve"].setData(y=outputs[board][0]) gui.board_tabs_widgets[board]["g_curve"].setData(y=outputs[board][1]) gui.board_tabs_widgets[board]["b_curve"].setData(y=outputs[board][2]) # FPS update fps = frames_per_second() if time.time() - 0.5 > prev_fps_update: prev_fps_update = time.time() # Various GUI updates if config.settings["configuration"]["USE_GUI"]: # Update error label if audio_input: gui.label_error.setText("") else: gui.label_error.setText("No audio input. Volume below threshold.") app.processEvents() # Left in just in case prople dont use the gui elif audio_datas[board]["vol"] < config.settings["configuration"]["MIN_VOLUME_THRESHOLD"]: print("No audio input. Volume below threshold. Volume: {}".format(audio_datas[board]["vol"])) if config.settings["configuration"]["DISPLAY_FPS"]: print('FPS {:.0f} / {:.0f}'.format(fps, config.settings["configuration"]["FPS"])) # Load and update configuration from settings.ini settings = QSettings('./lib/settings.ini', QSettings.IniFormat) settings.setFallbacksEnabled(False) # File only, no fallback to registry update_config_dicts() # Initialise board(s) board_manager = BoardManager() # Initialise Colour Manager colour_manager = ColourManager() # Initialise GUI if config.settings["configuration"]["USE_GUI"]: # Create GUI window app = QApplication([]) app.setApplicationName('Visualization') gui = GUI() app.processEvents() # Populate board manager with boards for board in config.settings["devices"]: board_manager.addBoard(board, config_exists=True) # FPS prev_fps_update = time.time() # 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.settings["configuration"]["FPS"], alpha_decay=0.2, alpha_rise=0.2) # Start listening to live audio stream py_audio = pyaudio.PyAudio() microphone = Microphone(microphone_update) try: microphone.startStream() finally: save_config_dicts() colour_manager.saveColours() colour_manager.saveGradients() py_audio.terminate()