From cd06d65e60cfccd115740274d06895c1e2d66424 Mon Sep 17 00:00:00 2001 From: not-matt <32398028+not-matt@users.noreply.github.com> Date: Tue, 27 Feb 2018 20:12:08 +0000 Subject: [PATCH] Lots of little updates behind the scenes --- python/lib/config.py | 345 +++++++++++++++++++++++++++++-------- python/lib/devices.py | 289 +++++++++++++++++++++++++++++++ python/lib/dsp.py | 31 +--- python/lib/gamma_table.npy | Bin 0 -> 1104 bytes python/lib/gui.py | 309 ++++++++++++++++++++++++++++----- python/lib/led.py | 114 ++++++------ python/lib/microphone.py | 11 +- python/lib/qfloatslider.py | 7 +- 8 files changed, 901 insertions(+), 205 deletions(-) create mode 100644 python/lib/devices.py create mode 100644 python/lib/gamma_table.npy diff --git a/python/lib/config.py b/python/lib/config.py index 09aa266..2eb9337 100644 --- a/python/lib/config.py +++ b/python/lib/config.py @@ -1,13 +1,223 @@ -"""Settings for audio reactive LED strip""" +"""Default settings and configuration for audio reactive LED strip""" from __future__ import print_function from __future__ import division import os -DEVICE = 'stripless' -"""Device used to control LED strip. Must be 'pi', 'esp8266' or 'blinkstick' +use_defaults = {"configuration": True, # See notes below for detailed explanation + "GUI_opts": False, + "devices": True, + "colors": True, + "gradients": True} + +settings = { # All settings are stored in this dict + + "configuration":{ # Program configuration + 'USE_GUI': True, # Whether to display the GUI + 'DISPLAY_FPS': False, # Whether to print the FPS when running (can reduce performance) + 'MIC_RATE': 48000, # Sampling frequency of the microphone in Hz + 'FPS': 60, # Desired refresh rate of the visualization (frames per second) + 'MIN_FREQUENCY': 20, # Frequencies below this value will be removed during audio processing + 'MAX_FREQUENCY': 18000, # Frequencies above this value will be removed during audio processing + 'MAX_BRIGHTNESS': 250, # Frequencies above this value will be removed during audio processing + 'N_ROLLING_HISTORY': 4, # Number of past audio frames to include in the rolling window + 'MIN_VOLUME_THRESHOLD': 0.01 # No music visualization displayed if recorded audio volume below threshold + #'LOGARITHMIC_SCALING': True, # Scale frequencies logarithmically to match perceived pitch of human ear + }, + + "GUI_opts":{"Graphs":True, # Which parts of the gui to show + "Reactive Effect Buttons":True, + "Non Reactive Effect Buttons":True, + "Frequency Range":True, + "Effect Options":True}, + + # All devices and their respective settings. Indexed by name, call each one what you want. + "devices":{"Desk Strip":{ + "configuration":{"TYPE": "esp8266", # Device type (see below for all supported boards) + # Required configuration for device. See below for all required keys per device + "AUTO_DETECT": True, # Set this true if you're using windows hotspot to connect (see below for more info) + "MAC_ADDR": "2c-3a-e8-2f-2c-9f", # MAC address of the ESP8266. Only used if AUTO_DETECT is True + "UDP_IP": "192.168.1.208", # IP address of the ESP8266. Must match IP in ws2812_controller.ino + "UDP_PORT": 7778, # Port number used for socket communication between Python and ESP8266 + "MAX_BRIGHTNESS": 250, # Max brightness of output (0-255) (my strip sometimes bugs out with high brightness) + # Other configuration + "N_PIXELS": 58, # Number of pixels in the LED strip (must match ESP8266 firmware) + "N_FFT_BINS": 24, # Number of frequency bins to use when transforming audio to frequency domain + "current_effect": "Single" # Currently selected effect for this board, used as default when program launches + }, + + # Configurable options for this board's effects go in this dictionary. + # Usage: config.settings["devices"][name]["effect_opts"][effect][option] + "effect_opts":{"Energy": {"blur": 1, # Amount of blur to apply + "scale":0.9, # Width of effect on strip + "r_multiplier": 1.0, # How much red + "g_multiplier": 1.0, # How much green + "b_multiplier": 1.0}, # How much blue + "Wave": {"color_wave": "Red", # Colour of moving bit + "color_flash": "White", # Colour of flashy bit + "wipe_len":5, # Initial length of colour bit after beat + "decay": 0.7, # How quickly the flash fades away + "wipe_speed":2}, # Number of pixels added to colour bit every frame + "Spectrum": {"r_multiplier": 1.0, # How much red + "g_multiplier": 1.0, # How much green + "b_multiplier": 1.0}, # How much blue + "Wavelength":{"roll_speed": 0, # How fast (if at all) to cycle colour overlay across strip + "color_mode": "Spectral", # Colour gradient to display + "mirror": False, # Reflect output down centre of strip + "reverse_grad": False, # Flip (LR) gradient + "reverse_roll": False, # Reverse movement of gradient roll + "blur": 3.0, # Amount of blur to apply + "flip_lr":False}, # Flip output left-right + "Scroll": {"decay": 0.995, # How quickly the colour fades away as it moves + "speed": 1, # Speed of scroll + "r_multiplier": 1.0, # How much red + "g_multiplier": 1.0, # How much green + "b_multiplier": 1.0, # How much blue + "blur": 0.2}, # Amount of blur to apply + "Power": {"color_mode": "Spectral", # Colour gradient to display + "s_count": 20, # Initial number of sparks + "s_color": "White", # Color of sparks + "mirror": True, # Mirror output down central axis + "flip_lr":False}, # Flip output left-right + "Single": {"color": "Purple"}, # Static color to show + "Beat": {"color": "Red", # Colour of beat flash + "decay": 0.7}, # How quickly the flash fades away + "Bars": {"resolution":4, # Number of "bars" + "color_mode":"Spectral", # Multicolour mode to use + "roll_speed":0, # How fast (if at all) to cycle colour colours across strip + "mirror": False, # Mirror down centre of strip + #"reverse_grad": False, # Flip (LR) gradient + "reverse_roll": False, # Reverse movement of gradient roll + "flip_lr":False}, # Flip output left-right + "Gradient": {"color_mode":"Spectral", # Colour gradient to display + "roll_speed": 0, # How fast (if at all) to cycle colour colours across strip + "mirror": False, # Mirror gradient down central axis + "reverse": False}, # Reverse movement of gradient + "Fade": {"color_mode":"Spectral", # Colour gradient to fade through + "roll_speed": 1, # How fast (if at all) to fade through colours + "reverse": False}, # Reverse "direction" of fade (r->g->b or r<-g<-b) + "Calibration":{"r": 100, + "g": 100, + "b": 100} + } + }, + "Main Strip":{ + "configuration":{"TYPE": "esp8266", # Device type (see below for all supported boards) + # Required configuration for device. See below for all required keys per device + "AUTO_DETECT": True, # Set this true if you're using windows hotspot to connect (see below for more info) + "MAC_ADDR": "5c-cf-7f-f0-8c-f3", # MAC address of the ESP8266. Only used if AUTO_DETECT is True + "UDP_IP": "192.168.1.208", # IP address of the ESP8266. Must match IP in ws2812_controller.ino + "UDP_PORT": 7778, # Port number used for socket communication between Python and ESP8266 + "MAX_BRIGHTNESS": 180, # Max brightness of output (0-255) (my strip sometimes bugs out with high brightness) + # Other configuration + "N_PIXELS": 226, # Number of pixels in the LED strip (must match ESP8266 firmware) + "N_FFT_BINS": 24, # Number of frequency bins to use when transforming audio to frequency domain + "current_effect": "Single" # Currently selected effect for this board, used as default when program launches + }, + + # Configurable options for this board's effects go in this dictionary. + # Usage: config.settings["devices"][name]["effect_opts"][effect][option] + "effect_opts":{"Energy": {"blur": 1, # Amount of blur to apply + "scale":0.9, # Width of effect on strip + "r_multiplier": 1.0, # How much red + "g_multiplier": 1.0, # How much green + "b_multiplier": 1.0}, # How much blue + "Wave": {"color_wave": "Red", # Colour of moving bit + "color_flash": "White", # Colour of flashy bit + "wipe_len":5, # Initial length of colour bit after beat + "decay": 0.7, # How quickly the flash fades away + "wipe_speed":2}, # Number of pixels added to colour bit every frame + "Spectrum": {"r_multiplier": 1.0, # How much red + "g_multiplier": 1.0, # How much green + "b_multiplier": 1.0}, # How much blue + "Wavelength":{"roll_speed": 0, # How fast (if at all) to cycle colour overlay across strip + "color_mode": "Spectral", # Colour gradient to display + "mirror": False, # Reflect output down centre of strip + "reverse_grad": False, # Flip (LR) gradient + "reverse_roll": False, # Reverse movement of gradient roll + "blur": 3.0, # Amount of blur to apply + "flip_lr":False}, # Flip output left-right + "Scroll": {"decay": 0.995, # How quickly the colour fades away as it moves + "speed": 1, # Speed of scroll + "r_multiplier": 1.0, # How much red + "g_multiplier": 1.0, # How much green + "b_multiplier": 1.0, # How much blue + "blur": 0.2}, # Amount of blur to apply + "Power": {"color_mode": "Spectral", # Colour gradient to display + "s_count": 20, # Initial number of sparks + "s_color": "White", # Color of sparks + "mirror": True, # Mirror output down central axis + "flip_lr":False}, # Flip output left-right + "Single": {"color": "Purple"}, # Static color to show + "Beat": {"color": "Red", # Colour of beat flash + "decay": 0.7}, # How quickly the flash fades away + "Bars": {"resolution":4, # Number of "bars" + "color_mode":"Spectral", # Multicolour mode to use + "roll_speed":0, # How fast (if at all) to cycle colour colours across strip + "mirror": False, # Mirror down centre of strip + #"reverse_grad": False, # Flip (LR) gradient + "reverse_roll": False, # Reverse movement of gradient roll + "flip_lr":False}, # Flip output left-right + "Gradient": {"color_mode":"Spectral", # Colour gradient to display + "roll_speed": 0, # How fast (if at all) to cycle colour colours across strip + "mirror": False, # Mirror gradient down central axis + "reverse": False}, # Reverse movement of gradient + "Fade": {"color_mode":"Spectral", # Colour gradient to fade through + "roll_speed": 1, # How fast (if at all) to fade through colours + "reverse": False}, # Reverse "direction" of fade (r->g->b or r<-g<-b) + "Calibration":{"r": 100, + "g": 100, + "b": 100} + } + } + }, + + + + # Collection of different colours in RGB format + "colors":{"Red":(255,0,0), + "Orange":(255,40,0), + "Yellow":(255,255,0), + "Green":(0,255,0), + "Blue":(0,0,255), + "Light blue":(1,247,161), + "Purple":(80,5,252), + "Pink":(255,0,178), + "White":(255,255,255)}, + + # Multicolour gradients. Colours must be in list above + "gradients":{"Spectral" : ["Red", "Orange", "Yellow", "Green", "Light blue", "Blue", "Purple", "Pink"], + "Dancefloor": ["Red", "Pink", "Purple", "Blue"], + "Sunset" : ["Red", "Orange", "Yellow"], + "Ocean" : ["Green", "Light blue", "Blue"], + "Jungle" : ["Green", "Red", "Orange"], + "Sunny" : ["Yellow", "Light blue", "Orange", "Blue"] + } + +} + +""" + ~~ NOTES ~~ + +[use_defaults] + +For any dicts in this file (config.py), you can add them into the use_defaults +dict to force the program to use these values over any stored in settings.ini +that you would have set using the GUI. At runtime, settings.ini is used to update +the above dicts with custom set values. + +If you're running a headless RPi, you may want to edit settings in this file, then +specify to use the dict you wrote, rather than have the program overwrite from +settings.ini at runtime. You could also run the program with the gui, set the +settings that you want, then disable the gui and the custom settings will still +be loaded. Basically it works as you would expect it to. + +[DEVICE TYPE] + +Device used to control LED strip. 'esp8266' means that you are using an ESP8266 module to control the LED strip -and commands will be sent to the ESP8266 over WiFi. +and commands will be sent to the ESP8266 over WiFi. You can have as many of +these as your computer is able to handle. 'pi' means that you are using a Raspberry Pi as a standalone unit to process audio input and control the LED strip directly. @@ -15,65 +225,45 @@ audio input and control the LED strip directly. 'blinkstick' means that a BlinkstickPro is connected to this PC which will be used to control the leds connected to it. +'fadecandy' means that a fadecandy server is running on your computer and is connected +via usb to a fadecandy board connected to LEDs + +'dotstar' creates an APA102-based output device. LMK if you have any success +getting this to work becuase i have no clue if it will. + 'stripless' means that the program will run without sending data to a strip. Useful for development etc, but doesn't look half as good ;) -""" -if DEVICE == 'esp8266': - AUTO_DETECT = False - """Set to true if the ip address of the device changes. This is the case if it's connecting - through windows hotspot for instance. If so, give the mac address of the device.""" - MAC_ADDR = "5c-cf-7f-f0-8c-f3" - """MAC address of the ESP8266.""" - UDP_IP = "192.168.1.68" - """IP address of the ESP8266. - Unless using auto detect, it must match IP in ws2812_controller.ino""" - UDP_PORT = 7778 - """Port number used for socket communication between Python and ESP8266""" - SOFTWARE_GAMMA_CORRECTION = False - """Set to False because the firmware handles gamma correction + dither""" +[REQUIRED CONFIGURATION KEYS] -elif DEVICE == 'pi': - LED_PIN = 18 - """GPIO pin connected to the LED strip pixels (must support PWM)""" - LED_FREQ_HZ = 800000 - """LED signal frequency in Hz (usually 800kHz)""" - LED_DMA = 5 - """DMA channel used for generating PWM signal (try 5)""" - BRIGHTNESS = 255 - """Brightness of LED strip between 0 and 255""" - LED_INVERT = True - """Set True if using an inverting logic level converter""" - SOFTWARE_GAMMA_CORRECTION = True - """Set to True because Raspberry Pi doesn't use hardware dithering""" +===== 'esp8266' + "AUTO_DETECT" # Set this true if you're using windows hotspot to connect (see below for more info) + "MAC_ADDR" # MAC address of the ESP8266. Only used if AUTO_DETECT is True + "UDP_IP" # IP address of the ESP8266. Must match IP in ws2812_controller.ino + "UDP_PORT" # Port number used for socket communication between Python and ESP8266 +===== 'pi' + "LED_PIN" # GPIO pin connected to the LED strip pixels (must support PWM) + "LED_FREQ_HZ" # LED signal frequency in Hz (usually 800kHz) + "LED_DMA" # DMA channel used for generating PWM signal (try 5) + "BRIGHTNESS" # Brightness of LED strip between 0 and 255 + "LED_INVERT" # Set True if using an inverting logic level converter +===== 'blinkstick' + No required configuration keys +===== 'fadecandy' + "SERVER" # Address of fadecandy server. (usually 'localhost:7890') +===== 'dotstar' + No required configuration keys +===== 'stripless' + No required configuration keys (heh) -elif DEVICE == 'blinkstick': - SOFTWARE_GAMMA_CORRECTION = True - """Set to True because blinkstick doesn't use hardware dithering""" +[AUTO_DETECT] -elif DEVICE == 'stripless': - pass +Set to true if the ip address of the device changes. This is the case if it's connecting +through windows hotspot, for instance. If so, give the mac address of the device. This +allows windows to look for the device's IP using "arp -a" and finding the matching +mac address. I haven't tested this on Linux or macOS. -else: - raise ValueError("Invalid device selected. Device {} not known.".format(DEVICE)) - -USE_GUI = True -"""Whether or not to display a PyQtGraph GUI plot of visualization""" - -DISPLAY_FPS = False -"""Whether to display the FPS when running (can reduce performance)""" - -N_PIXELS = 242 -"""Number of pixels in the LED strip (must match ESP8266 firmware)""" - -GAMMA_TABLE_PATH = os.path.join(os.path.dirname(__file__), 'gamma_table.npy') -"""Location of the gamma correction table""" - -MIC_RATE = 48000 -"""Sampling frequency of the microphone in Hz""" - -FPS = 40 -"""Desired refresh rate of the visualization (frames per second) +[FPS] FPS indicates the desired refresh rate, or frames-per-second, of the audio visualization. The actual refresh rate may be lower if the computer cannot keep @@ -87,21 +277,8 @@ appear "sluggish" or out of sync with the audio being played if it is too low. The FPS should not exceed the maximum refresh rate of the LED strip, which depends on how long the LED strip is. -""" -_max_led_FPS = int(((N_PIXELS * 30e-6) + 50e-6)**-1.0) -assert FPS <= _max_led_FPS, 'FPS must be <= {}'.format(_max_led_FPS) -MIN_FREQUENCY = 20 -"""Frequencies below this value will be removed during audio processing""" - -MAX_FREQUENCY = 18000 -"""Frequencies above this value will be removed during audio processing""" - -LOGARITHMIC_SCALING = True -"""Scale frequencies logarithmically to match perceived pitch of human ear""" - -N_FFT_BINS = 24 -"""Number of frequency bins to use when transforming audio to frequency domain +[N_FFT_BINS] Fast Fourier transforms are used to transform time-domain audio data to the frequency domain. The frequencies present in the audio signal are assigned @@ -115,8 +292,28 @@ number of bins. More bins is not always better! There is no point using more bins than there are pixels on the LED strip. """ -N_ROLLING_HISTORY = 4 -"""Number of past audio frames to include in the rolling window""" +for board in settings["devices"]: + if settings["devices"][board]["configuration"]["TYPE"] == 'esp8266': + settings["devices"][board]["configuration"]["SOFTWARE_GAMMA_CORRECTION"] = False + # Set to False because the firmware handles gamma correction + dither + elif settings["devices"][board]["configuration"]["TYPE"] == 'pi': + settings["devices"][board]["configuration"]["SOFTWARE_GAMMA_CORRECTION"] = True + # Set to True because Raspberry Pi doesn't use hardware dithering + elif settings["devices"][board]["configuration"]["TYPE"] == 'blinkstick': + settings["devices"][board]["configuration"]["SOFTWARE_GAMMA_CORRECTION"] = True + elif settings["devices"][board]["configuration"]["TYPE"] == 'dotstar': + settings["devices"][board]["configuration"]["SOFTWARE_GAMMA_CORRECTION"] = False + elif settings["devices"][board]["configuration"]["TYPE"] == 'fadecandy': + settings["devices"][board]["configuration"]["SOFTWARE_GAMMA_CORRECTION"] = False + elif settings["devices"][board]["configuration"]["TYPE"] == 'stripless': + settings["devices"][board]["configuration"]["SOFTWARE_GAMMA_CORRECTION"] = False + else: + raise ValueError("Invalid device selected. Device {} not known.".format(settings["devices"][board]["configuration"]["TYPE"])) + settings["devices"][board]["effect_opts"]["Power"]["s_count"] = settings["devices"][board]["configuration"]["N_PIXELS"]//6 + # Cheeky lil fix in case the user sets an odd number of LEDs + if settings["devices"][board]["configuration"]["N_PIXELS"] % 2: + settings["devices"][board]["configuration"]["N_PIXELS"] -= 1 + +# Ignore these +# settings["configuration"]['_max_led_FPS'] = int(((settings["configuration"]["N_PIXELS"] * 30e-6) + 50e-6)**-1.0) -MIN_VOLUME_THRESHOLD = 1e-3 -"""No music visualization displayed if recorded audio volume below threshold""" diff --git a/python/lib/devices.py b/python/lib/devices.py new file mode 100644 index 0000000..af19163 --- /dev/null +++ b/python/lib/devices.py @@ -0,0 +1,289 @@ +import time +import numpy as np +import lib.config as config + +_GAMMA_TABLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, + 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, + 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, + 18, 18, 19, 19, 20, 20, 21, 21, 22, 23, 23, 24, 24, 25, + 26, 26, 27, 28, 28, 29, 30, 30, 31, 32, 32, 33, 34, 35, + 35, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46, + 47, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 73, + 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, + 89, 91, 92, 93, 94, 95, 97, 98, 99, 100, 102, 103, 104, + 105, 107, 108, 109, 111, 112, 113, 115, 116, 117, 119, + 120, 121, 123, 124, 126, 127, 128, 130, 131, 133, 134, + 136, 137, 139, 140, 142, 143, 145, 146, 148, 149, 151, + 152, 154, 155, 157, 158, 160, 162, 163, 165, 166, 168, + 170, 171, 173, 175, 176, 178, 180, 181, 183, 185, 186, + 188, 190, 192, 193, 195, 197, 199, 200, 202, 204, 206, + 207, 209, 211, 213, 215, 217, 218, 220, 222, 224, 226, + 228, 230, 232, 233, 235, 237, 239, 241, 243, 245, 247, + 249, 251, 253, 255] +_GAMMA_TABLE = np.array(_GAMMA_TABLE) + +class LEDController: + """Base class for interfacing with hardware LED strip controllers + To add support for another hardware device, simply inherit this class + and implement the show() method. + Example usage: + import numpy as np + N_pixels = 60 + pixels = np.random.random(size=(3, N_pixels)) + device = LEDController() + device.show(pixels) + """ + + def __init__(self): + pass + + def show(self, pixels): + """Set LED pixels to the values given in the array + This function accepts an array of RGB pixel values (pixels) + and displays them on the LEDs. To add support for another + hardware device, you should create a class that inherits from + this class, and then implement this method. + Parameters + ---------- + pixels: numpy.ndarray + 2D array containing RGB pixel values for each of the LEDs. + The shape of the array is (3, n_pixels), where n_pixels is the + number of LEDs that the device has. + The array is formatted as shown below. There are three rows + (axis 0) which represent the red, green, and blue color channels. + Each column (axis 1) contains the red, green, and blue color values + for a single pixel: + np.array([ [r0, ..., rN], [g0, ..., gN], [b0, ..., bN]]) + Each value brightness value is an integer between 0 and 255. + Returns + ------- + None + """ + raise NotImplementedError('Show() was not implemented') + + def test(self, n_pixels): + pixels = np.zeros((3, n_pixels)) + pixels[0][0] = 255 + pixels[1][1] = 255 + pixels[2][2] = 255 + print('Starting LED strip test.') + print('Press CTRL+C to stop the test at any time.') + print('You should see a scrolling red, green, and blue pixel.') + while True: + self.show(pixels) + pixels = np.roll(pixels, 1, axis=1) + time.sleep(0.2) + + +class ESP8266(LEDController): + def __init__(self, auto_detect=False, + mac_addr="aa-bb-cc-dd-ee-ff", + ip='192.168.0.150', + port=7778): + """Initialize object for communicating with as ESP8266 + Parameters + ---------- + auto_detect: bool, optional + Automatically search for and find devices on windows hotspot + with given mac addresses. Windows hotspot resets the IP + addresses of any devices on reset, meaning the IP of the + ESP8266 changes every time you turn on the hotspot. This + will find the IP address of the devices for you. + mac_addr: str, optional + The MAC address of the ESP8266 on the network. Only used if + auto-detect is used + ip: str, optional + The IP address of the ESP8266 on the network. This must exactly + match the IP address of your ESP8266 device, unless using + the auto-detect feature. + port: int, optional + The port number to use when sending data to the ESP8266. This + must exactly match the port number in the ESP8266's firmware. + """ + import socket + self._mac_addr = mac_addr + self._ip = ip + self._port = port + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if auto_detect: + self.detect() + + def detect(self): + from subprocess import check_output + from time import sleep + """ Uses "arp -a" to find esp8266 on windows hotspot""" + # Find the audio strip automagically + ip_addr = False + while not ip_addr: + arp_out = check_output(['arp', '-a']).splitlines() + for i in arp_out: + if self._mac_addr in str(i): + ip_addr = i.split()[0].decode("utf-8") + break + else: + print("Device not found at physical address {}, retrying in 1s".format(self._mac_addr)) + sleep(1) + print("Found device {}, with IP address {}".format(self._mac_addr, ip_addr)) + self._ip = ip_addr + + def show(self, pixels): + """Sends UDP packets to ESP8266 to update LED strip values + The ESP8266 will receive and decode the packets to determine what values + to display on the LED strip. The communication protocol supports LED strips + with a maximum of 256 LEDs. + The packet encoding scheme is: + |i|r|g|b| + where + i (0 to 255): Index of LED to change (zero-based) + r (0 to 255): Red value of LED + g (0 to 255): Green value of LED + b (0 to 255): Blue value of LED + """ + message = pixels.T.clip(0, config.settings["configuration"]["MAX_BRIGHTNESS"]).astype(np.uint8).ravel().tostring() + self._sock.sendto(message, (self._ip, self._port)) + + +class FadeCandy(LEDController): + def __init__(self, server='localhost:7890'): + """Initializes object for communicating with a FadeCandy device + Parameters + ---------- + server: str, optional + FadeCandy server used to communicate with the FadeCandy device. + """ + try: + import audioled.opc + except ImportError as e: + print('Unable to import audioled.opc library') + print('You can install this library with `pip install opc`') + raise e + self.client = audioled.opc.Client(server) + if self.client.can_connect(): + print('Successfully connected to FadeCandy server.') + else: + print('Could not connect to FadeCandy server.') + print('Ensure that fcserver is running and try again.') + + def show(self, pixels): + self.client.put_pixels(pixels.T.clip(0, 255).astype(int).tolist()) + + +class BlinkStick(LEDController): + def __init__(self): + """Initializes a BlinkStick controller""" + try: + from blinkstick import blinkstick + except ImportError as e: + print('Unable to import the blinkstick library') + print('You can install this library with `pip install blinkstick`') + raise e + self.stick = blinkstick.find_first() + + def show(self, pixels): + """Writes new LED values to the Blinkstick. + This function updates the LED strip with new values. + """ + # Truncate values and cast to integer + n_pixels = pixels.shape[1] + pixels = pixels.clip(0, 255).astype(int) + pixels = _GAMMA_TABLE[pixels] + # Read the rgb values + r = pixels[0][:].astype(int) + g = pixels[1][:].astype(int) + b = pixels[2][:].astype(int) + + # Create array in which we will store the led states + newstrip = [None] * (n_pixels * 3) + + for i in range(n_pixels): + # Blinkstick uses GRB format + newstrip[i * 3] = g[i] + newstrip[i * 3 + 1] = r[i] + newstrip[i * 3 + 2] = b[i] + # Send the data to the blinkstick + self.stick.set_led_data(0, newstrip) + + +class RaspberryPi(LEDController): + def __init__(self, n_pixels, pin=18, invert_logic=False, + freq=800000, dma=5): + """Creates a Raspberry Pi output device + Parameters + ---------- + n_pixels: int + Number of LED strip pixels + pin: int, optional + GPIO pin used to drive the LED strip (must be a PWM pin). + Pin 18 can be used on the Raspberry Pi 2. + invert_logic: bool, optional + Whether or not to invert the driving logic. + Set this to True if you are using an inverting logic level + converter, otherwise set to False. + freq: int, optional + LED strip protocol frequency (Hz). For ws2812 this is 800000. + dma: int, optional + DMA (direct memory access) channel used to drive PWM signals. + If you aren't sure, try 5. + """ + try: + import neopixel + except ImportError as e: + url = 'learn.adafruit.com/neopixels-on-raspberry-pi/software' + print('Could not import the neopixel library') + print('For installation instructions, see {}'.format(url)) + raise e + self.strip = neopixel.Adafruit_NeoPixel(n_pixels, pin, freq, dma, + invert_logic, 255) + self.strip.begin() + + def show(self, pixels): + """Writes new LED values to the Raspberry Pi's LED strip + Raspberry Pi uses the rpi_ws281x to control the LED strip directly. + This function updates the LED strip with new values. + """ + # Truncate values and cast to integer + n_pixels = pixels.shape[1] + pixels = pixels.clip(0, 255).astype(int) + # Optional gamma correction + pixels = _GAMMA_TABLE[pixels] + # Encode 24-bit LED values in 32 bit integers + r = np.left_shift(pixels[0][:].astype(int), 8) + g = np.left_shift(pixels[1][:].astype(int), 16) + b = pixels[2][:].astype(int) + rgb = np.bitwise_or(np.bitwise_or(r, g), b) + # Update the pixels + for i in range(n_pixels): + self.strip._led_data[i] = rgb[i] + self.strip.show() + + +class DotStar(LEDController): + def __init__(self, pixels, brightness=31): + """Creates an APA102-based output device + Parameters + ---------- + pixels: int + Number of LED strip pixels + brightness: int, optional + Global brightness + """ + try: + import apa102 + except ImportError as e: + url = 'https://github.com/tinue/APA102_Pi' + print('Could not import the apa102 library') + print('For installation instructions, see {}'.format(url)) + raise e + self.strip = apa102.APA102(numLEDs=pixels, globalBrightness=brightness) # Initialize the strip + led_data = np.array(self.strip.leds, dtype=np.uint8) + # memoryview preserving the first 8 bits of LED frames (w/ global brightness) + self.strip.leds = led_data.data + # 2D view of led_data + self.led_data = led_data.reshape((pixels, 4)) # or (-1, 4) + + def show(self, pixels): + bgr = [2,1,0] + self.led_data[0:,1:4] = pixels[bgr].T.clip(0,255) + self.strip.show() \ No newline at end of file diff --git a/python/lib/dsp.py b/python/lib/dsp.py index af700ed..0c056d5 100644 --- a/python/lib/dsp.py +++ b/python/lib/dsp.py @@ -1,7 +1,7 @@ from __future__ import print_function import numpy as np -import config -import melbank +import lib.config as config +import lib.melbank as melbank class ExpFilter: @@ -24,30 +24,3 @@ class ExpFilter: self.value = alpha * value + (1.0 - alpha) * self.value return self.value - -def rfft(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.MIC_RATE) - return xs, ys - - -def fft(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.MIC_RATE) - return xs, ys - - -def create_mel_bank(): - global samples, mel_y, mel_x - samples = int(config.MIC_RATE * config.N_ROLLING_HISTORY / (2.0 * config.FPS)) - mel_y, (_, mel_x) = melbank.compute_melmat(num_mel_bands=config.N_FFT_BINS, - freq_min=config.MIN_FREQUENCY, - freq_max=config.MAX_FREQUENCY, - num_fft_bands=samples, - sample_rate=config.MIC_RATE) -samples = None -mel_y = None -mel_x = None -create_mel_bank() diff --git a/python/lib/gamma_table.npy b/python/lib/gamma_table.npy new file mode 100644 index 0000000000000000000000000000000000000000..2b9c2ef52bd95c18c32aee0811264a1952e3ca1d GIT binary patch literal 1104 zcmb8qX^2f>7{Kuv`@Zk%wcZ)VKE}Q?vdkDVm~36PDJ}gqW$M*Ml&MO zIYo(*L~QZQqC{alxX+F)ERMS`o)=pZx2JXQnU#^A5m^)YbE>5L=SlVcU&{FYH)Y)? zD`!tu-kz+2y;MbWsY>QjmCdDs9|=@VQq3Hw9;%^%nxR@6s2!@Kfx4l38mJ#?pbbNf zG|-qPp{Cj_)LdJHT52TJN?X$=)K=TkJ`~k7(&-TDs2OykGhISmwHw{(K_)%Pq8GhG zeY7wA=+6KKGKj$pVJO2G&Iqy@$tXrMhOy+3%Q*5F&jcot&m<-@g#xBBjp@u_CNX9) zn?mL=mwC)*0Sj3~5ldJ~F(oWxIV)JnDps?Wb*yIt8`;EWwy>3LY-a~M*~M=5vXA{7 z;2?)M%n^=qjN_c(B&Rsd8P0N!^IYH}m$=Lou5yj*+~6j+xXm5za*z8w;31EA%oCpS hjOV=IC9inR8{YDc_k7?ZpZLrdzVeOl{NN|Q_zkx0YJC6z literal 0 HcmV?d00001 diff --git a/python/lib/gui.py b/python/lib/gui.py index 869d5c0..0bec68c 100644 --- a/python/lib/gui.py +++ b/python/lib/gui.py @@ -1,51 +1,276 @@ -from __future__ import print_function -from __future__ import division -import time +#from __future__ import print_function +#from __future__ import division +#from scipy.ndimage.filters import gaussian_filter1d +#from collections import deque +#import time +#import sys import numpy as np -from pyqtgraph.Qt import QtGui +import lib.config as config +#import microphone +#import dsp +#import led +#import random + +from lib.qrangeslider import QRangeSlider +from lib.qfloatslider import QFloatSlider import pyqtgraph as pg -from pyqtgraph.dockarea import * +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * + +class GUI(QMainWindow): + def __init__(self): + super().__init__() + self.settings = QSettings('settings.ini', QSettings.IniFormat) + self.settings.setFallbacksEnabled(False) # File only, no fallback to registry or or. + self.initUI() + def hideGraphs(self): + print("Blah") -class GUI: - plot = [] - curve = [] + def hideOpts(self): + print("Bleh") - def __init__(self, width=800, height=450, title=''): - # Create GUI window - self.app = QtGui.QApplication([]) - self.win = pg.GraphicsWindow(title) - self.win.resize(width, height) - self.win.setWindowTitle(title) - # Create GUI layout - self.layout = QtGui.QVBoxLayout() - self.win.setLayout(self.layout) + def config.settings["configuration"]["configDialogue"](self): + self.d = QDialog(None, Qt.WindowSystemMenuHint | Qt.WindowCloseButtonHint) + b1 = QPushButton("ok",self.d) + b1.move(50,50) + self.d.setWindowTitle("Dialog") + self.d.setWindowModality(Qt.ApplicationModal) + self.d.show() + + def initUI(self): + # ==================================== Set up window and wrapping layout + self.setWindowTitle("Visualization") + wrapper = QVBoxLayout() - def add_plot(self, title): - new_plot = pg.PlotWidget() - self.layout.addWidget(new_plot) - self.plot.append(new_plot) - self.curve.append([]) + # ======================================================= Set up toolbar + #toolbar_hideGraphs.setShortcut('Ctrl+H') + toolbar_hideGraphs = QAction('GUI Properties', self) + toolbar_hideGraphs.triggered.connect(self.config.settings["configuration"]["configDialogue"]) + toolbar_hideOpts = QAction('Hide Opts', self) + toolbar_hideOpts.triggered.connect(self.hideOpts) + + self.toolbar = self.addToolBar('Toolbar') + self.toolbar.addAction(toolbar_hideGraphs) + self.toolbar.addAction(toolbar_hideOpts) - def add_curve(self, plot_index, pen=(255, 255, 255)): - self.curve[plot_index].append(self.plot[plot_index].plot(pen=pen)) + # ========================================== Set up FPS and error labels + labels_layout = QHBoxLayout() + 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) + labels_layout.addWidget(self.label_error) + labels_layout.addStretch() + labels_layout.addWidget(self.label_latency) + labels_layout.addWidget(self.label_fps) + + # ================================================== Set up graph layout + graph_view = pg.GraphicsView() + graph_layout = pg.GraphicsLayout(border=(100,100,100)) + graph_view.setCentralItem(graph_layout) + # Mel filterbank plot + fft_plot = graph_layout.addPlot(title='Filterbank Output', colspan=3) + fft_plot.setRange(yRange=[-0.1, 1.2]) + fft_plot.disableAutoRange(axis=pg.ViewBox.YAxis) + x_data = np.array(range(1, config.settings["configuration"]["N_FFT_BINS"] + 1)) + self.mel_curve = pg.PlotCurveItem() + self.mel_curve.setData(x=x_data, y=x_data*0) + fft_plot.addItem(self.mel_curve) + # Visualization plot + graph_layout.nextRow() + led_plot = graph_layout.addPlot(title='Visualization Output', colspan=3) + led_plot.setRange(yRange=[-5, 260]) + led_plot.disableAutoRange(axis=pg.ViewBox.YAxis) + # Pen for each of the color channel curves + r_pen = pg.mkPen((255, 30, 30, 200), width=4) + g_pen = pg.mkPen((30, 255, 30, 200), width=4) + b_pen = pg.mkPen((30, 30, 255, 200), width=4) + # Color channel curves + self.r_curve = pg.PlotCurveItem(pen=r_pen) + self.g_curve = pg.PlotCurveItem(pen=g_pen) + self.b_curve = pg.PlotCurveItem(pen=b_pen) + # Define x data + x_data = np.array(range(1, config.settings["configuration"]["N_PIXELS"] + 1)) + self.r_curve.setData(x=x_data, y=x_data*0) + self.g_curve.setData(x=x_data, y=x_data*0) + self.b_curve.setData(x=x_data, y=x_data*0) + # Add curves to plot + led_plot.addItem(self.r_curve) + led_plot.addItem(self.g_curve) + led_plot.addItem(self.b_curve) + # ================================================= Set up button layout + label_reactive = QLabel("Audio Reactive Effects") + label_non_reactive = QLabel("Non Reactive Effects") + reactive_button_grid = QGridLayout() + non_reactive_button_grid = QGridLayout() + buttons = {} + connecting_funcs = {} + grid_width = 4 + i = 0 + j = 0 + k = 0 + l = 0 + # Dynamically layout reactive_buttons and connect them to the visualisation effects + def connect_generator(effect): + def func(): + visualizer.current_effect = effect + buttons[effect].setDown(True) + func.__name__ = effect + return func + # Where the magic happens + for effect in visualizer.effects: + if not effect in visualizer.non_reactive_effects: + connecting_funcs[effect] = connect_generator(effect) + buttons[effect] = QPushButton(effect) + buttons[effect].clicked.connect(connecting_funcs[effect]) + reactive_button_grid.addWidget(buttons[effect], j, i) + i += 1 + if i % grid_width == 0: + i = 0 + j += 1 + else: + connecting_funcs[effect] = connect_generator(effect) + buttons[effect] = QPushButton(effect) + buttons[effect].clicked.connect(connecting_funcs[effect]) + non_reactive_button_grid.addWidget(buttons[effect], l, k) + k += 1 + if k % grid_width == 0: + k = 0 + l += 1 + + # ============================================== Set up frequency slider + # Frequency range label + label_slider = QLabel("Frequency Range") + # Frequency slider + def freq_slider_change(tick): + minf = freq_slider.tickValue(0)**2.0 * (config.settings["configuration"]["MIC_RATE"] / 2.0) + maxf = freq_slider.tickValue(1)**2.0 * (config.settings["configuration"]["MIC_RATE"] / 2.0) + t = 'Frequency range: {:.0f} - {:.0f} Hz'.format(minf, maxf) + freq_label.setText(t) + config.settings["configuration"]["MIN_FREQUENCY"] = minf + config.settings["configuration"]["MAX_FREQUENCY"] = maxf + dsp.create_mel_bank() + def set_freq_min(): + config.settings["configuration"]["MIN_FREQUENCY"] = freq_slider.start() + dsp.create_mel_bank() + def set_freq_max(): + config.settings["configuration"]["MAX_FREQUENCY"] = freq_slider.end() + dsp.create_mel_bank() + freq_slider = QRangeSlider() + freq_slider.show() + freq_slider.setMin(0) + freq_slider.setMax(20000) + freq_slider.setRange(config.settings["configuration"]["MIN_FREQUENCY"], config.settings["configuration"]["MAX_FREQUENCY"]) + freq_slider.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);') + freq_slider.setSpanStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);') + freq_slider.setDrawValues(True) + freq_slider.endValueChanged.connect(set_freq_max) + freq_slider.startValueChanged.connect(set_freq_min) + freq_slider.setStyleSheet(""" + QRangeSlider * { + border: 0px; + padding: 0px; + } + QRangeSlider > QSplitter::handle { + background: #fff; + } + QRangeSlider > QSplitter::handle:vertical { + height: 3px; + } + QRangeSlider > QSplitter::handle:pressed { + background: #ca5; + } + """) -if __name__ == '__main__': - # Example test gui - N = 48 - gui = GUI(title='Test') - # Sin plot - gui.add_plot(title='Sin Plot') - gui.add_curve(plot_index=0) - gui.win.nextRow() - # Cos plot - gui.add_plot(title='Cos Plot') - gui.add_curve(plot_index=1) - while True: - t = time.time() - x = np.linspace(t, 2 * np.pi + t, N) - gui.curve[0][0].setData(x=x, y=np.sin(x)) - gui.curve[1][0].setData(x=x, y=np.cos(x)) - gui.app.processEvents() - time.sleep(1.0 / 30.0) + # ============================================ Set up option tabs layout + label_options = QLabel("Effect Options") + opts_tabs = QTabWidget() + # Dynamically set up tabs + tabs = {} + grid_layouts = {} + self.grid_layout_widgets = {} + options = visualizer.effect_opts.keys() + for effect in visualizer.effects: + # Make the tab + self.grid_layout_widgets[effect] = {} + tabs[effect] = QWidget() + grid_layouts[effect] = QGridLayout() + tabs[effect].setLayout(grid_layouts[effect]) + opts_tabs.addTab(tabs[effect],effect) + # These functions make functions for the dynamic ui generation + # YOU WANT-A DYNAMIC I GIVE-A YOU DYNAMIC! + def gen_slider_valuechanger(effect, key): + def func(): + visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].value() + return func + def gen_float_slider_valuechanger(effect, key): + def func(): + visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].slider_value + return func + def gen_combobox_valuechanger(effect, key): + def func(): + visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].currentText() + return func + def gen_checkbox_valuechanger(effect, key): + def func(): + visualizer.effect_opts[effect][key] = self.grid_layout_widgets[effect][key].isChecked() + return func + # Dynamically generate ui for settings + if effect in visualizer.dynamic_effects_config.settings["configuration"]["config:"] + i = 0 + connecting_funcs[effect] = {} + for key, label, ui_element, *opts in visualizer.dynamic_effects_config.settings["configuration"]["config[effect"]]: + if opts: # neatest way ^^^^^ i could think of to unpack and handle an unknown number of opts (if any) + opts = opts[0] + if ui_element == "slider": + connecting_funcs[effect][key] = gen_slider_valuechanger(effect, key) + self.grid_layout_widgets[effect][key] = QSlider(Qt.Horizontal) + self.grid_layout_widgets[effect][key].setMinimum(opts[0]) + self.grid_layout_widgets[effect][key].setMaximum(opts[1]) + self.grid_layout_widgets[effect][key].setValue(visualizer.effect_opts[effect][key]) + self.grid_layout_widgets[effect][key].valueChanged.connect( + connecting_funcs[effect][key]) + elif ui_element == "float_slider": + connecting_funcs[effect][key] = gen_float_slider_valuechanger(effect, key) + self.grid_layout_widgets[effect][key] = QFloatSlider(*opts, visualizer.effect_opts[effect][key]) + self.grid_layout_widgets[effect][key].setValue(visualizer.effect_opts[effect][key]) + self.grid_layout_widgets[effect][key].valueChanged.connect( + connecting_funcs[effect][key]) + elif ui_element == "dropdown": + connecting_funcs[effect][key] = gen_combobox_valuechanger(effect, key) + self.grid_layout_widgets[effect][key] = QComboBox() + self.grid_layout_widgets[effect][key].addItems(opts) + self.grid_layout_widgets[effect][key].currentIndexChanged.connect( + connecting_funcs[effect][key]) + elif ui_element == "checkbox": + connecting_funcs[effect][key] = gen_checkbox_valuechanger(effect, key) + self.grid_layout_widgets[effect][key] = QCheckBox() + self.grid_layout_widgets[effect][key].setCheckState(visualizer.effect_opts[effect][key]) + self.grid_layout_widgets[effect][key].stateChanged.connect( + connecting_funcs[effect][key]) + grid_layouts[effect].addWidget(QLabel(label),i,0) + grid_layouts[effect].addWidget(self.grid_layout_widgets[effect][key],i,1) + i += 1 + #visualizer.effect_settings[effect] + else: + grid_layouts[effect].addWidget(QLabel("No customisable options for this effect :("),0,0) + + + + # ============================================= Add layouts into wrapper + self.setCentralWidget(QWidget(self)) + self.centralWidget().setLayout(wrapper) + wrapper.addLayout(labels_layout) + wrapper.addWidget(graph_view) + wrapper.addWidget(label_reactive) + wrapper.addLayout(reactive_button_grid) + wrapper.addWidget(label_non_reactive) + wrapper.addLayout(non_reactive_button_grid) + wrapper.addWidget(label_slider) + wrapper.addWidget(freq_slider) + wrapper.addWidget(label_options) + wrapper.addWidget(opts_tabs) + #self.show() diff --git a/python/lib/led.py b/python/lib/led.py index 7c4bbb3..9b7da37 100644 --- a/python/lib/led.py +++ b/python/lib/led.py @@ -4,45 +4,45 @@ from __future__ import division import platform import numpy as np -import config +import lib.config as config + +def detect_esp8266(): + """ Uses "arp -a" to find esp8266 on windows hotspot""" + # Find the audio strip automagically + ip_addr = False + while not ip_addr: + arp_out = check_output(['arp', '-a']).splitlines() + for i in arp_out: + if config.settings["configuration"]["MAC_ADDR"] in str(i): + ip_addr = i.split()[0].decode("utf-8") + break + else: + print("Device not found at physical address {}, retrying in 1s".format(config.settings["configuration"]["MAC_ADDR"])) + sleep(1) + print("Found device {}, with IP address {}".format(config.settings["configuration"]["MAC_ADDR"], ip_addr)) + config.settings["configuration"]["UDP_IP"] = ip_addr # ESP8266 uses WiFi communication -if config.DEVICE == 'esp8266': +if config.settings["configuration"]["DEVICE"] == 'esp8266': import socket from subprocess import check_output from time import sleep - - # Find the audio strip automagically - if config.AUTO_DETECT: - ip_addr = False - while not ip_addr: - arp_out = check_output(['arp', '-a']).splitlines() - for i in arp_out: - if config.MAC_ADDR in str(i): - ip_addr = i.split()[0].decode("utf-8") - break - else: - print("Device not found at physical address {}, retrying in 1s".format(config.MAC_ADDR)) - sleep(1) - print("Found device {}, with IP address {}".format(config.MAC_ADDR, ip_addr)) - config.UDP_IP = ip_addr - _sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) _sock.settimeout(0.005) # Raspberry Pi controls the LED strip directly -elif config.DEVICE == 'pi': +elif config.settings["configuration"]["DEVICE"] == 'pi': import neopixel - strip = neopixel.Adafruit_NeoPixel(config.N_PIXELS, config.LED_PIN, - config.LED_FREQ_HZ, config.LED_DMA, - config.LED_INVERT, config.BRIGHTNESS) + strip = neopixel.Adafruit_NeoPixel(config.settings["configuration"]["N_PIXELS"], config.settings["configuration"]["LED_PIN"], + config.settings["configuration"]["LED_FREQ_HZ"], config.settings["configuration"]["LED_DMA"], + config.settings["configuration"]["LED_INVERT"], config.settings["configuration"]["BRIGHTNESS"]) strip.begin() -elif config.DEVICE == 'blinkstick': +elif config.settings["configuration"]["DEVICE"] == 'blinkstick': from blinkstick import blinkstick import signal import sys #Will turn all leds off when invoked. def signal_handler(signal, frame): - all_off = [0]*(config.N_PIXELS*3) + all_off = [0]*(config.settings["configuration"]["N_PIXELS"]*3) stick.set_led_data(0, all_off) sys.exit(0) @@ -51,13 +51,13 @@ elif config.DEVICE == 'blinkstick': signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) -_gamma = np.load(config.GAMMA_TABLE_PATH) +_gamma = np.load(config.settings["configuration"]["GAMMA_TABLE_PATH"]) """Gamma lookup table used for nonlinear brightness correction""" -_prev_pixels = np.tile(253, (3, config.N_PIXELS)) +_prev_pixels = np.tile(253, (3, config.settings["configuration"]["N_PIXELS"])) """Pixel values that were most recently displayed on the LED strip""" -pixels = np.tile(1, (3, config.N_PIXELS)) +pixels = np.tile(1, (3, config.settings["configuration"]["N_PIXELS"])) """Pixel values for the LED strip""" _is_python_2 = int(platform.python_version_tuple()[0]) == 2 @@ -79,27 +79,37 @@ def _update_esp8266(): """ global pixels, _prev_pixels # Truncate values and cast to integer - pixels = np.clip(pixels, 0, 200).astype(int) - # Optionally apply gamma correc tio - p = _gamma[pixels] if config.SOFTWARE_GAMMA_CORRECTION else np.copy(pixels) - MAX_PIXELS_PER_PACKET = 256 + pixels = np.clip(pixels, 0, config.settings["configuration"]["MAX_BRIGHTNESS"]).astype(int) + # Optionally apply gamma correction + p = _gamma[pixels] if config.settings["configuration"]["SOFTWARE_GAMMA_CORRECTION"] else np.copy(pixels) + MAX_PIXELS_PER_PACKET = 255 # Pixel indices idx = range(pixels.shape[1]) #idx = [i for i in idx if not np.array_equal(p[:, i], _prev_pixels[:, i])] n_packets = len(idx) // MAX_PIXELS_PER_PACKET + 1 idx = np.array_split(idx, n_packets) - for packet_indices in idx: - m = '' if _is_python_2 else [] - for i in packet_indices: - if _is_python_2: - m += chr(i) + chr(pixels[0][i]) + chr(pixels[1][i]) + chr(pixels[2][i]) - else: - m.append(i) # Index of pixel to change - m.append(pixels[0][i]) # Pixel red value - m.append(pixels[1][i]) # Pixel green value - m.append(pixels[2][i]) # Pixel blue value - m = m if _is_python_2 else bytes(m) - _sock.sendto(m, (config.UDP_IP, config.UDP_PORT)) + + m = [] + for i in range(config.settings["configuration"]["N_PIXELS"]): + #m.append(i) # Index of pixel to change + m.append(pixels[0][i]) # Pixel red value + m.append(pixels[1][i]) # Pixel green value + m.append(pixels[2][i]) # Pixel blue value + m = bytes(m) + _sock.sendto(m, (config.settings["configuration"]["UDP_IP"], config.settings["configuration"]["UDP_PORT"])) + + # for packet_indices in idx: + # m = '' if _is_python_2 else [] + # for i in packet_indices: + # if _is_python_2: + # m += chr(i) + chr(pixels[0][i]) + chr(pixels[1][i]) + chr(pixels[2][i]) + # else: + # m.append(i) # Index of pixel to change + # m.append(pixels[0][i]) # Pixel red value + # m.append(pixels[1][i]) # Pixel green value + # m.append(pixels[2][i]) # Pixel blue value + # m = m if _is_python_2 else bytes(m) + # _sock.sendto(m, (config.settings["configuration"]["UDP_IP"], config.settings["configuration"]["UDP_PORT"])) _prev_pixels = np.copy(pixels) @@ -113,14 +123,14 @@ def _update_pi(): # Truncate values and cast to integer pixels = np.clip(pixels, 0, 255).astype(int) # Optional gamma correction - p = _gamma[pixels] if config.SOFTWARE_GAMMA_CORRECTION else np.copy(pixels) + p = _gamma[pixels] if config.settings["configuration"]["SOFTWARE_GAMMA_CORRECTION"] else np.copy(pixels) # Encode 24-bit LED values in 32 bit integers r = np.left_shift(p[0][:].astype(int), 8) g = np.left_shift(p[1][:].astype(int), 16) b = p[2][:].astype(int) rgb = np.bitwise_or(np.bitwise_or(r, g), b) # Update the pixels - for i in range(config.N_PIXELS): + for i in range(config.settings["configuration"]["N_PIXELS"]): # Ignore pixels if they haven't changed (saves bandwidth) if np.array_equal(p[:, i], _prev_pixels[:, i]): continue @@ -137,16 +147,16 @@ def _update_blinkstick(): # Truncate values and cast to integer pixels = np.clip(pixels, 0, 250).astype(int) # Optional gamma correction - p = _gamma[pixels] if config.SOFTWARE_GAMMA_CORRECTION else np.copy(pixels) + p = _gamma[pixels] if config.settings["configuration"]["SOFTWARE_GAMMA_CORRECTION"] else np.copy(pixels) # Read the rgb values r = p[0][:].astype(int) g = p[1][:].astype(int) b = p[2][:].astype(int) #create array in which we will store the led states - newstrip = [None]*(config.N_PIXELS*3) + newstrip = [None]*(config.settings["configuration"]["N_PIXELS"]*3) - for i in range(config.N_PIXELS): + for i in range(config.settings["configuration"]["N_PIXELS"]): # blinkstick uses GRB format newstrip[i*3] = g[i] newstrip[i*3+1] = r[i] @@ -157,13 +167,13 @@ def _update_blinkstick(): def update(): """Updates the LED strip values""" - if config.DEVICE == 'esp8266': + if config.settings["configuration"]["DEVICE"] == 'esp8266': _update_esp8266() - elif config.DEVICE == 'pi': + elif config.settings["configuration"]["DEVICE"] == 'pi': _update_pi() - elif config.DEVICE == 'blinkstick': + elif config.settings["configuration"]["DEVICE"] == 'blinkstick': _update_blinkstick() - elif config.DEVICE == 'stripless': + elif config.settings["configuration"]["DEVICE"] == 'stripless': pass # Execute this file to run a LED strand test diff --git a/python/lib/microphone.py b/python/lib/microphone.py index c39ecaa..48d292e 100644 --- a/python/lib/microphone.py +++ b/python/lib/microphone.py @@ -1,30 +1,29 @@ import time import numpy as np import pyaudio -import config +import lib.config as config def start_stream(callback): p = pyaudio.PyAudio() - frames_per_buffer = int(config.MIC_RATE / config.FPS) + frames_per_buffer = int(config.settings["configuration"]["MIC_RATE"] / config.settings["configuration"]["FPS"]) stream = p.open(format=pyaudio.paInt16, channels=1, - rate=config.MIC_RATE, + rate=config.settings["configuration"]["MIC_RATE"], input=True, frames_per_buffer=frames_per_buffer) overflows = 0 prev_ovf_time = time.time() while True: try: - y = np.fromstring(stream.read(frames_per_buffer, exception_on_overflow=False), dtype=np.int16) + y = np.fromstring(stream.read(frames_per_buffer), dtype=np.int16) y = y.astype(np.float32) - #stream.read(get_read_available(), exception_on_overflow=False) callback(y) except IOError: overflows += 1 if time.time() > prev_ovf_time + 1: prev_ovf_time = time.time() - if config.USE_GUI: + 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)) diff --git a/python/lib/qfloatslider.py b/python/lib/qfloatslider.py index 3f2ac3d..81eefda 100644 --- a/python/lib/qfloatslider.py +++ b/python/lib/qfloatslider.py @@ -15,7 +15,7 @@ class QFloatSlider(QtWidgets.QSlider): """ def __init__(self, min_value, max_value, step, default): super().__init__(QtCore.Qt.Horizontal) - self.precision = 0.001 + self.precision = 0.0001 self.min_value = min_value self.max_value = max_value self.step = step @@ -30,7 +30,10 @@ class QFloatSlider(QtWidgets.QSlider): super().setSingleStep(1) super().setValue(self._float_to_int(self.default)) super().valueChanged.connect(self._value_handler) - self.slider_value = 2.0 + #self.slider_value = 2.0 + + def setValue(self, value): + super().setValue(self._float_to_int(value)) # This is mostly disgusting python i hate floating points >:( def _float_divmod(self,a,b):