Using a RGB LED strip as an audio visualizer

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

  1. For some weird reason I still get write timeouts sometimes, I’ll keep looking to see why.
  2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *