Intro
I recently bought a RGB LED strip and wanted to do something fun with it. It was very much like this one [link]. Except I bought it at ebay for a whopping $11 shipped. After a bunch of deliberation, I decided to paste it under my two monitors on my desk.
So what to display? I thought about maybe some linear KPIs, but I wanted to do something more fun. I opted to do a audio visualizer for the left and right channels.
Hardware
First things first. Typically I use Raspberry Pi’s for many of my projects, however this time I decided to use an arduino since I had one laying around. I figured I could connect the Arduino to the computer via USB and have the computer send what LEDs to turn on via a serial connection.
The connection from the LED strip to the Arduino is fairly simple. It uses a single wire. If you’re new to this, I suggest you follow Adafruit’s tutorial. It is super simple.
Software
Arduino
Like all good developers, I took a bunch of examples I found online and modified them until they worked for me. The code below will allow the arduino to receive LED and RGB information and then be instructed to show the values.
#include <Adafruit_NeoPixel.h> // IMPORTANT: Set pixel COUNT, PIN and TYPE #define PIXEL_PIN 7 #define PIXEL_COUNT 144 #define PIXEL_TYPE 'WS2812B' Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800); void setup() { strip.begin(); strip.setBrightness(10); strip.show(); // Initialize all pixels to 'off' // initialize serial Serial.begin(115200); } void loop() { while (Serial.available()) { uint8_t serial = Serial.read(); if (serial == 'Z') { strip.show(); } else if (serial == 'L') { int i = getNextInput(); int r = getNextInput(); int g = getNextInput(); int b = getNextInput(); strip.setPixelColor(i, strip.Color(r,g,b)); } } Serial.flush(); } uint8_t getNextInput() { while(!Serial.available()); // wait for a character return Serial.read(); }
Python
For this to work properly you need a fork of PyAudio that allows for loopback.
import pyaudio, os, serial, struct import numpy as np from time import sleep from colour import Color port = 'COM8' ard = serial.Serial(port,115200,timeout = 1) num_leds = 144 # number of LEDS in your strip defaultframes = 512 maxValue = 2**16 peakL = 0 peakR = 0 last_peakL = peakL last_peakR = peakR multiplier = 2.5 # you can play with this if you want more of the bar to light up red = Color("red") purple = Color("purple") spec_colors = list(purple.range_to(red,int(num_leds/2))) # get the gradients spec_colors_rgb = [] for spec_color in spec_colors: # convert the [0-255] for arduino color_array = spec_color.rgb spec_colors_rgb.append([int(x * 255) for x in color_array]) class textcolors: # this class is only used for the prompt of audio device if not os.name == 'nt': blue = '\033[94m' green = '\033[92m' warning = '\033[93m' fail = '\033[91m' end = '\033[0m' else: blue = '' green = '' warning = '' fail = '' end = '' #Use module p = pyaudio.PyAudio() # https://github.com/intxcc/pyaudio_portaudio try: default_device_index = p.get_default_input_device_info() #Set default to first in list or ask Windows except IOError: default_device_index = -1 print (textcolors.blue + "Available devices:\n" + textcolors.end) #Select Device for i in range(0, p.get_device_count()): info = p.get_device_info_by_index(i) print (textcolors.green + str(info["index"]) + textcolors.end + \ ": \t %s \n \t %s \n" % (info["name"], \ p.get_host_api_info_by_index(info["hostApi"])["name"])) if default_device_index == -1: default_device_index = info["index"] if default_device_index == -1: #Handle no devices available print (textcolors.fail + "No device available. Quitting." + textcolors.end) exit() device_id = int(input("Choose device [" + textcolors.blue + \ str(default_device_index) + textcolors.end + "]: ") or default_device_index)#Get input or default print ("") try: device_info = p.get_device_info_by_index(device_id) #Get device info except IOError: device_info = p.get_device_info_by_index(default_device_index) print (textcolors.warning + "Selection not available, using default." + \ textcolors.end) is_input = device_info["maxInputChannels"] > 0 #Choose between loopback or standard mode is_wasapi = (p.get_host_api_info_by_index(device_info["hostApi"])["name"]).find("WASAPI") != -1 if is_input: print (textcolors.blue + "Selection is input using standard mode.\n" + \ textcolors.end) else: if is_wasapi: useloopback = True; print (textcolors.green + \ "Selection is output. Using loopback mode.\n" + textcolors.end) else: print (textcolors.fail + \ "Selection is input and does not support " + \ "loopback mode. Quitting.\n" + textcolors.end) exit() if (device_info["maxOutputChannels"] < device_info["maxInputChannels"]): #Open stream channelcount = device_info["maxInputChannels"] else: channelcount = device_info["maxOutputChannels"] stream = p.open(format = pyaudio.paInt16, channels = channelcount, rate = int(device_info["defaultSampleRate"]), input = True, frames_per_buffer = defaultframes, input_device_index = device_info["index"], as_loopback = useloopback) while True: try: data = np.frombuffer(stream.read(1024),dtype=np.int16) # read from buffer dataL = data[0::2] dataR = data[1::2] peakL = np.abs(np.max(dataL)-np.min(dataL))/maxValue # get max value for the left channel peakR = np.abs(np.max(dataR)-np.min(dataR))/maxValue # get max value for the right channel bar_array = [[0,0,0]] * num_leds # initialize an array for all of the LEDS on the off position to_fill_L = int(min(num_leds / 2 * peakL * multiplier, num_leds/2)) # how many LEDs are we going to fill up for the left channel for i in range(0, to_fill_L): bar_array[i] = spec_colors_rgb[i] # color them based on the spectrum we generated earlier to_fill_R = int(min(num_leds / 2 * peakR * multiplier, num_leds/2)) # how many LEDs are we going to fill up for the left channel for i in range(0, to_fill_R): pos = num_leds - i - 1 # turn them on right to left bar_array[pos] = spec_colors_rgb[i] # color them based on the spectrum we generated earlier ### The following is to make the peak the color red and to persist it last_peakL = last_peakL - 1 # move the red led further down for a "falling" effect last_peakR = last_peakR - 1 # move the red led further down for a "falling" effect if last_peakL <= to_fill_L: # if the red LED is less than the peak: last_peakL = to_fill_L # make the red move up to the new peak if last_peakR <= to_fill_R: last_peakR = to_fill_R ### after how many percent of the bar do we want to start drawing the Red LED if last_peakL > int(0.25 * num_leds / 2): bar_array[last_peakL] = [255,0,0] if last_peakR > int(0.25 * num_leds / 2): bar_array[num_leds - last_peakR - 1] = [255,0,0] ## send to arduino for i in range(0,num_leds): ard.write(b'L') # 'L' # the arduino is expecting an L to begin receiving LED info ard.write(bytes([i,bar_array[i][0],bar_array[i][1],bar_array[i][2]])) # send the LED info in byte form ard.write(b'Z') # 'Z' # tell it to draw the LEDs ard.flushInput() # don't know why, but without this, it does not work. ard.flush() # don't know why, but without this, it does not work. except Exception as e: print(e) if e == 'Write timeout': # if we timeout, attempt to open the connection again print("restarting connection") ard.close() ard.open() sleep(0.005) # time between samples
Considerations
- For some weird reason I still get write timeouts sometimes, I’ll keep looking to see why.
- I am writing the WHOLE strip every sample, it would be faster to check in python which LEDs need updating and just send those to the arduino.
Conclusion
This was a lot of fun! It’s been a while since I’ve played with my arduino. Let me know what sort of other projects you come up with! Also, if you have any suggestions for cleaner/faster code, please send them my way.