Introduction: Raspberry Pi Spectrum Analyzer With RGB LED Strip and Python

About: Bitcoin, electronics, music, robots
Looking for a first project to try out on a Raspberry Pi, I though what better than a Spectrum Analyzer? (Sometimes this display is erroneously referred to as a graphic equalizer--that let's you change the sound, not display it)


I was able to get it doing 2048pt FFTs and decoding mp3s in real time, and while that shouldn't be hard on a Pi considering plenty of 8bit uCs have been made to do real time FFTs, everything is done in Python, which makes it convenient (for me) to eventually add control via a web browser, sms, and other things. There's not much room to spare, though, it chokes when I move the mouse.

Most of the code to do this is already available, my goal here is just to roughly document the steps to get this working, from the perspective of a first time Pi user. I also made some tweaks to the FFT analysis chunk to speed things up a bit.

Hardware: (thanks for the toys Adafruit!)
  • RasPi -- I think mine is running wheezy, Raspbian, ver 3.10.19
  • 15ft (1m) RGB LED strip, $125 (~160 leds) controllable via SPI, built in PWM control, you just send updates--very nice. I'm just using one strip wrapped around to form 5 columns, and writing to different segments of the strip. This way I just have three wires connected to the Pi: ground, SPI Clock and SPI Data.
  • 10A 5V power supply to drive the LEDs, $25, you could probably power the Pi with this, too.
  • Those are the essentials, but you'll probably want a bunch of other stuff:
    • USB WiFi adapter $7.61(RTL8188CUS chipset drivers are built in to wheezy OS!). I used these instructions to get it working.
    • SD card (I got an 8GB one)
    • USB Power speakers $10
    • Power USB Hub to plug in keyboard, mouse, wifi,
    • Some cell phone charger to power the Pi via it's USB power connector
    • ethernet cable to connect to internet thru a laptop pre-wifi
    • USB keyboard, mouse, HDMI monitor
    • wire, some female-to-female jumper wires
Software:
  • I mostly just used this awesome Pi-based xmas lights controller code from Chris Usey,Todd Giles and Ryan Jennings. It's a full command center for orchestrating xmas lights to audio (wav, mp3, etc). Their code lets you setup playlists and turn on and off 120VAC power based on frequency bands. You can even vote on songs through SMS messages! Their code looks at frequency bands in the music, and if the sound crosses a threshold, it turns a GPIO pin on. I changed the code to display the actual frequency band level on an RGB LED strip, rather than just having an on / off threshold. Based on similarities in the code, I suspect they got their FFT processing code from this python real-time FFT demo.
  • Python control of LPD8806 RGB LED strip via SPI.
  • This isn't related to this project, but I used the Geany IDE for coding.

Step 1: Connect LED Strip and Setup RGB LED Software

Rather than deal with separate LED strips for each column, I just looped a single 152 LED strip back and forth, securing it with zip ties to a baby gate. This wastes some LEDs as I'm not displaying anything on the turns, but you could avoid the LED waste by cutting the strip and soldering wires between the columns.

A good diagram for connecting the RGB LED strip to the RasPi can be found at adafruit's site.

Solder the 5V, ground, Clock and Data lines to the Input end of the LED strip, and connect to the Pi as shown at the link or in my pics. Be sure to connect the ground of the 5V supply to the ground of the Pi!

Grab the software and follow the instructions for getting the Pi able to output to SPI. It's important that you use the hardware SPI because any bit-banging approach won't be fast enough.

sudo raspi-config
to enable hardware SPI (follow instructions at git page).

I added the install directory to my PYTHONPATH in bashrc so I could call the functions from anywhere.
inside .bashrc:

export PYTHONPATH = $PYTHONPATH:/home/pi/RPi-LPD8806-master

test out that the strip works by running the example code:

python example.py

The xmas light code we're going to download later wants to run as root, and when you run things with a sudo in front, the environment variables, specifically, PYTHONPATH aren't transferred.

I had to edit /etc/sudoers by typing

sudo visudo

and then added at the bottom

Defaults env_keep=SYNCHRONIZED_LIGHTS_HOME
Defaults env_keep+=PYTHONPATH


the first line is something we'll need for the xmas light package to be installed later. These make sure those environment variables stick around when you run things as sudo.

To test that you have it setup right, close the terminal and re-open, then type

sudo python
from bootstrap import *
led.fill(Color(50,50,50),0,10)
led.update()

that should turn on the first 10 LEDs.

A final step is to make some modifications to speed up writing to the strip.

Inside ledstrip.py, make sure use_py_spi = True in the def __init__ line

def __init__(self, leds, use_py_spi = True, dev="/dev/spidev0.0", driver="LPD8806"):

Now inside LPD8806.py, we're going to change the SPI speed to 16MHz

if self.use_py_spi:
import spidev
self.spi = spidev.SpiDev()
self.spi.open(0,0)
self.spi.max_speed_hz = 16000000
print 'py-spidev MHz: %d' % (self.spi.max_speed_hz / 1000000.0 )

That print statement is there just to make sure everything gets set correctly.

One final change to the LPD8806.py file is in the update() function. For whatever reason, I noticed led.update() was taking a long time, upwords of 25ms. To get a good visual effect, I wanted my entire analysis and display loop to run at 20Hz, or 50ms per loop, and burning half that time waiting for the LED strip wouldn't work. And strangely, at 16MHz, it should have taken less than a ms. 3 bytes per led, 152 LEDs, (3 * 152 * 8 bits / 16M) = .2ms! (not 25ms) When I put a scope up to the SPI port, each byte was coming out at 16MHz, but there was a 160uS pause after each call to self.spi.xfer2(). My solution was to collect the entire string of bytes into a buffer and only call self.spi.xfer2() once:

def update(self, buffer):
temp_buffer = []
if self.use_py_spi:
for x in range(self.leds):
temp_buffer = temp_buffer + [i for i in buffer[x]]
#self.spi.xfer2([i for i in buffer[x]])
self.spi.xfer2(temp_buffer)
self.spi.xfer2([0x00,0x00,0x00]) #zero fill the last to prevent stray colors at the end
self.spi.xfer2([0x00]) #once more with feeling - this helps :) time2 = time.time()

Writing out to 152 LEDs at 16MHz should take no time--you should see the last LED change at the same time the first one does

Some other people have struggled with this and got around it, check out the amazing POV video effect with a single vertical LED strip (sending data at 8MHz thru 20ft of cat 5!):


Step 2: Install LightShow Pi and Configure Environment Vars and Sound

Now that we've got writing to the LED strip fast, and accessible from python running as root from anywhere, it's time to install the fantastic xmas light orchestration software, and update it to control the LED strip.

Once you download the code from bitbucket, follow the instructions to get it installed.

Before running sudo ./install.sh, you should edit the file and change the INSTALL_DIR. Then run

sudo ./install.sh

I sadly ran into all sorts of problems. Most of the software installed (which takes a while), but I had to manually set environment stuff. The end of the bash script adds the install dir to /etc/environment. I also added it to my .bashrc at the end like so:

export SYNCHRONIZED_LIGHTS_HOME="/home/pi/xmas2"
export PYTHONPATH=$PYTHONPATH:/home/pi/RPi-LPD8806-master


that second line is for the LEDs. The script tries to add the path variable to /etc/environment, but I had to add it to .bashrc. I really don't know much about linux, so you should visit the google+ community to ask for help with the install. Make sure sudoers also has this line to make sure the environment variable sticks around when you run things as sudo.

Defaults env_keep=SYNCHRONIZED_LIGHTS_HOME

As a first step, try to run a song and see if the code throws any errors:

sudo py/synchronized_lights.py --file /home/pi/some_random_music_file.mp3

Don't hear any sound? I had to change my audio out from the default HDMI to the onboard 1/8" jack:

amixer cset numid=3 1

I also had to turn up the volume

amixer set PCM 1

Step 3: Edit Defaults.cfg and Run Custom Synchronized_lights_LED_strip.py

I made the following changes to /config/defaults.cfg:

gpio_pins = 7,0,1,2,3

I only have 5 columns, so I just truncated the list of outputs to 5 numbers.

If you want to use the playlist, change the playlist_path

I also changed the following

min_frequency = 50
max_frequency = 150000


comment out custom_channel_frequencies. I just want to split the min and max frequencies evenly.

Finally, run the customized script:

sudo py/synchronized_lights.py --file /home/pi/some_random_music_file.mp3

For debugging, consider adding -v 2 --readcache 0

I was able to decode mp3s and play them at the same time with only a few glitches right at the beginning of the song. The base code has a feature where it writes out all the levels to a cache file so when it plays the song again it doesn't have to run the FFTs. readcache 0 turns this off if you don't want to have to delete the .gz cache it generates every time.

--------------------
Some highlights in the changes I made to the original
--------------------

The main changes consist of stripping out the GPIO code and replacing it with code to send levels out to the RGB LED strip. The better way to do this would be to modify the hardware_controller.py file, but no matter what some changes to the original would be needed since I'm not just using on/off signals anymore.

I spent most of my time trying to optimize the speed of the calculate_levels() function. I found out that the line

data = np.array(data, dtype='h')

, which simply creates a numpy array from a python array, took 10ms, which is as much as all the FFT processing! This was greatly sped up by replacing it with a function that loads a binary memory array directly:

np.frombuffer(data...

The rest of my changes only really affected things when I was doing testing with pure sine waves (see attached test files). The dynamic adjustments on the original code made it work well despite everything below. Pretty great!

Since the audio is stereo, I throw out the even numbers since those represent the right channel. The original code was analyzing the stereo signal as if it were mono, which probably added a bit of energy to the lowest frequency band.

data[:] = data_stereo[::2]

The fft was also running on a non-windowed chunk of audio. When you run an FFT on a chunk of audio carved out of the middle of a song, the edges of that will look like steep drops to the FFT algorithm. This adds a bunch of energy across all the bands. The solution is to taper down each chunk, or "window" it. You can see a picture of this attached to this page of before and after windowing an audio chunk.

window = np.hanning(len(data))
data = data * window


Finally, when the power of sound goes up by a factor of 10, we hear that as a doubling, so I sum all the bins in each frequency band and take the log10 of those so the LEDs bounce twice as high when we hear the sound double.

To test things out, try running the attached sound files. The pink noise file should report the same total power per frequency band. This is interesting, because there will be only 5 or so bins in the first band, and hundreds in the last, but if you add up all the energies, they should be the same, despite the difference in number of bins. Pink noise sounds like it's the same loudness across the entire spectrum. The pink noise sweep file has a bandpass filter being swept from low to high, and you can see this demo in the video.

Raspberry Pi Contest

Participated in the
Raspberry Pi Contest

Make It Glow Contest

Participated in the
Make It Glow Contest