Interactive LED Music Visualizer

Introduction

Let’s face it: nowadays, most musical performances are complimented by some fancy light shows. Go to any concert, festival, club – they all have a corresponding visual performance or effects. Why not add your own home to that list? Here’s a simple yet effective project to make your very own son et lumière!

All palettes work with every visualization, but, for timeliness, not every combination is shown.

Required Materials

To follow along with this tutorial, you’ll need the following materials. The partial wishlist on the left is for the simple circuit. It does not include the potentiometer and buttons. The full wishlist on the right is for the full circut. You may not need everything though depending on what you have. Add it to your cart, read through the guide, and adjust the cart as necessary.

Any microcontroller with 3.3V and 5V pins will suffice. Any analog potentiometer and size momentary button should work. Depending on your intent, the trimpot and buttons may not be necessary.

Trimpot – The trimpot is only used to adjust the brightness threshold, so, if you want maximum brightness, you don’t have to worry about incorporating it.

Buttons – The three buttons cycle visualizations, color schemes, and shuffle mode respectively, so, if you want to do without those features (and just use shuffle mode all the time), that’s also a possibility.

If you’re compiling from the Arduino IDE or similar, you’ll want to snag the the NeoPixel Library since the code used is heavily based on it. The resistor and capacitor are not required, but they will help prevent possible damage to the LEDs. Any resistor between 300–500 Ω can be used.

Since we’re using the NeoPixel library, it may also be a good idea to get familiar with the NeoPixel Documentation.

Assembly

Depending on your setup, the project may not require any soldering! The few exceptions will probably be soldering some pins to the sound detector, and, if you’ve cut a roll of addressable LEDs in the middle, you’ll have to solder some wires to the starting LED’s pins. If you’ve never soldered before, I highly suggest taking a look at this guide to solder.

Below is also a general chart for how the pin(s) on each component should be routed and an accompanying diagram. Before you begin, here are some things to keep in mind:

Be conscious of the orientation you think would allow the sound detector to take optimal readings for your intentions. Bending the pins to hold the sound detector perpendicular to the breadboard is a recommended option.

Electrolytic capacitors are polarized, so how they are oriented is important. Make sure to place the side with a white stripe and a negative symbol into a negative current (ground) and the other into positive current.

Resistors and pushbuttons are not polarized.

Trimpots are not polarized either, however their middle pin is the analog output, so don’t power that directly.

The pins used in the diagram and the code are in parentheses. If you use a different pin, don’t forget to change it in the code as well:

Sound Detector

Addressable LED strip

Trimpot

Pushbutton

1 mF (1000 µF) Capacitor

300–500 Ω Resistor

Envelope → Analog (A0)

Digital/Analog (A5) → Resistor → DIN

5V → left or right pin

GND → Either side

Between ground and 5V

Between Digital/Analog (A5) and DIN on LED strip

3.3V → VCC

5V →5V

Middle pin → Analog (A1)

Other side → Digital (4, 5, 6)

GND → GND

GND → GND

Remaining left or right pin → GND

The circuit should look something like the diagrams below. If you just want the LEDs to react to sound using one visualizer and one color palette, you can build the simple circuit on the left. If you want to take advantage of all the visuals, palettes, and shuffle mode, you can build the full circuit on the right.

Simple Circuit

Full Circuit

Fritzing diagram for the circuit as described above. Click the images for a closer look.

After assembling the circuit, you should have something similar to the image below. Depending on your setup, you may have less components.

Sound Detector Notes: The microphone used is not a sophisticated, logarithmic sound receiver like your ear; it is only measuring compressional waves in the air. Consequently, the microphone is more likely to detect and/or prioritize lower-frequency sounds since they require more energy to propagat, and therefore oscillate the air more intensely. Also, a resistor can be placed in the "GAIN" slots to modify the gain. Standard gain should be sufficient for our purposes, but, for more info, visit this tutorial to modify the gain.

Programming

Note: This example assumes you are using the latest version of the Arduino IDE on your desktop. If this is your first time using Arduino, please review our tutorial on installing the Arduino IDE. If you have not previously installed an Arduino library, please check out our installation guide.

WS2812 Arduino Library

If you have not already, install the library by searching for NeoPixel within the Arduino IDE's library manager. You can also manually install the NeoPixel library by downloading a zip.

Simple Visualizer Program

Below is a small sample program to test if everything is connected properly with the simple and full circuit. It only contains one visualizer and one color palette to keep things concise. It doesn’t need buttons since there’s nothing to toggle, but you can still use it to test your potentiometer. Make sure to adjust the number of LEDs (i.e. LED_TOTAL) depending on the length of LED strip that you are using. Copy and paste the code in the Arduino IDE. Select your board, COM port, and press the upload button!

language:c
//Program by Michael Bartlett
//Libraries
#include <Adafruit_NeoPixel.h> //Library to simplify interacting with the LED strand
#ifdef __AVR__
#include <avr/power.h> //Includes the library for power reduction registers if your chip supports them.
#endif //More info: http://www.nongnu.org/avr-libc/user-manual/group__avr__power.htlm
//Constants (change these as necessary)
#define LED_PIN A5 //Pin for the pixel strand. Does not have to be analog.
#define LED_TOTAL 60 //Change this to the number of LEDs in your strand.
#define LED_HALF LED_TOTAL/2
#define AUDIO_PIN A0 //Pin for the envelope of the sound detector
#define KNOB_PIN A1 //Pin for the trimpot 10K
//////////<Globals>
// These values either need to be remembered from the last pass of loop() or
// need to be accessed by several functions in one pass, so they need to be global.
Adafruit_NeoPixel strand = Adafruit_NeoPixel(LED_TOTAL, LED_PIN, NEO_GRB + NEO_KHZ800); //LED strand objetc
uint16_t gradient = 0; //Used to iterate and loop through each color palette gradually
uint8_t volume = 0; //Holds the volume level read from the sound detector.
uint8_t last = 0; //Holds the value of volume from the previous loop() pass.
float maxVol = 15; //Holds the largest volume recorded thus far to proportionally adjust the visual's responsiveness.
float knob = 1023.0; //Holds the percentage of how twisted the trimpot is. Used for adjusting the max brightness.
float avgVol = 0; //Holds the "average" volume-level to proportionally adjust the visual experience.
float avgBump = 0; //Holds the "average" volume-change to trigger a "bump."
bool bump = false; //Used to pass if there was a "bump" in volume
//////////</Globals>
//////////<Standard Functions>
void setup() { //Like it's named, this gets ran before any other function.
Serial.begin(9600); //Sets data rate for serial data transmission.
strand.begin(); //Initialize the LED strand object.
strand.show(); //Show a blank strand, just to get the LED's ready for use.
}
void loop() { //This is where the magic happens. This loop produces each frame of the visual.
volume = analogRead(AUDIO_PIN); //Record the volume level from the sound detector
knob = analogRead(KNOB_PIN) / 1023.0; //Record how far the trimpot is twisted
avgVol = (avgVol + volume) / 2.0; //Take our "average" of volumes.
//Sets a threshold for volume.
// In practice I've found noise can get up to 15, so if it's lower, the visual thinks it's silent.
// Also if the volume is less than average volume / 2 (essentially an average with 0), it's considered silent.
if (volume < avgVol / 2.0 || volume < 15) volume = 0;
//If the current volume is larger than the loudest value recorded, overwrite
if (volume > maxVol) maxVol = volume;
//This is where "gradient" is reset to prevent overflow.
if (gradient > 1529) {
gradient %= 1530;
//Everytime a palette gets completed is a good time to readjust "maxVol," just in case
// the song gets quieter; we also don't want to lose brightness intensity permanently
// because of one stray loud sound.
maxVol = (maxVol + volume) / 2.0;
}
//If there is a decent change in volume since the last pass, average it into "avgBump"
if (volume - last > avgVol - last && avgVol - last > 0) avgBump = (avgBump + (volume - last)) / 2.0;
//if there is a notable change in volume, trigger a "bump"
bump = (volume - last) > avgBump;
Pulse(); //Calls the visual to be displayed with the globals as they are.
gradient++; //Increments gradient
last = volume; //Records current volume for next pass
delay(30); //Paces visuals so they aren't too fast to be enjoyable
}
//////////</Standard Functions>
//////////<Helper Functions>
//PULSE
//Pulse from center of the strand
void Pulse() {
fade(0.75); //Listed below, this function simply dims the colors a little bit each pass of loop()
//Advances the gradient to the next noticeable color if there is a "bump"
if (bump) gradient += 64;
//If it's silent, we want the fade effect to take over, hence this if-statement
if (volume > 0) {
uint32_t col = Rainbow(gradient); //Our retrieved 32-bit color
//These variables determine where to start and end the pulse since it starts from the middle of the strand.
// The quantities are stored in variables so they only have to be computed once.
int start = LED_HALF - (LED_HALF * (volume / maxVol));
int finish = LED_HALF + (LED_HALF * (volume / maxVol)) + strand.numPixels() % 2;
//Listed above, LED_HALF is simply half the number of LEDs on your strand. ↑ this part adjusts for an odd quantity.
for (int i = start; i < finish; i++) {
//"damp" creates the fade effect of being dimmer the farther the pixel is from the center of the strand.
// It returns a value between 0 and 1 that peaks at 1 at the center of the strand and 0 at the ends.
float damp = float(
((finish - start) / 2.0) -
abs((i - start) - ((finish - start) / 2.0))
)
/ float((finish - start) / 2.0);
//Sets the each pixel on the strand to the appropriate color and intensity
// strand.Color() takes 3 values between 0 & 255, and returns a 32-bit integer.
// Notice "knob" affecting the brightness, as in the rest of the visuals.
// Also notice split() being used to get the red, green, and blue values.
strand.setPixelColor(i, strand.Color(
split(col, 0) * pow(damp, 2.0) * knob,
split(col, 1) * pow(damp, 2.0) * knob,
split(col, 2) * pow(damp, 2.0) * knob
));
}
//Sets the max brightness of all LEDs. If it's loud, it's brighter.
// "knob" was not used here because it occasionally caused minor errors in color display.
strand.setBrightness(255.0 * pow(volume / maxVol, 2));
}
//This command actually shows the lights. If you make a new visualization, don't forget this!
strand.show();
}
//Fades lights by multiplying them by a value between 0 and 1 each pass of loop().
void fade(float damper) {
//"damper" must be between 0 and 1, or else you'll end up brightening the lights or doing nothing.
if (damper >= 1) damper = 0.99;
for (int i = 0; i < strand.numPixels(); i++) {
//Retrieve the color at the current position.
uint32_t col = (strand.getPixelColor(i)) ? strand.getPixelColor(i) : strand.Color(0, 0, 0);
//If it's black, you can't fade that any further.
if (col == 0) continue;
float colors[3]; //Array of the three RGB values
//Multiply each value by "damper"
for (int j = 0; j < 3; j++) colors[j] = split(col, j) * damper;
//Set the dampened colors back to their spot.
strand.setPixelColor(i, strand.Color(colors[0] , colors[1], colors[2]));
}
}
uint8_t split(uint32_t color, uint8_t i ) {
//0 = Red, 1 = Green, 2 = Blue
if (i == 0) return color >> 16;
if (i == 1) return color >> 8;
if (i == 2) return color >> 0;
return -1;
}
//This function simply take a value and returns a gradient color
// in the form of an unsigned 32-bit integer
//The gradient returns a different, changing color for each multiple of 255
// This is because the max value of any of the 3 LEDs is 255, so it's
// an intuitive cutoff for the next color to start appearing.
// Gradients should also loop back to their starting color so there's no jumps in color.
uint32_t Rainbow(unsigned int i) {
if (i > 1529) return Rainbow(i % 1530);
if (i > 1274) return strand.Color(255, 0, 255 - (i % 255)); //violet -> red
if (i > 1019) return strand.Color((i % 255), 0, 255); //blue -> violet
if (i > 764) return strand.Color(0, 255 - (i % 255), 255); //aqua -> blue
if (i > 509) return strand.Color(0, 255, (i % 255)); //green -> aqua
if (i > 255) return strand.Color(255 - (i % 255), 255, 0); //yellow -> green
return strand.Color(255, i, 0); //red -> yellow
}
//////////</Helper Functions>

Adjust the number of LEDs (LED_TOTAL) that you are using in the strip.

If you didn’t use a potentiometer, don’t forget to remove all references to the variable knob in the code (ctrl+F will come in handy for that). Otherwise, the program will think you still have a potentiometer that is set to a very low value (i.e. everything will be very dim).

If you didn’t use buttons, change the initialization bool shuffle = false; to bool shuffle = true;. The code should compile and run properly, but for good practice you should remove all blocks the code says to delete since they reference the BUTTON constants.

Well Done! For those curious about the math used to process the audio, check out the notes and graphs linked in the code math.md! There's also some notes about how each visualization is calculated in each function.

Final Touches

With the electronics and the code working, you can now add your visualizer to a variety of enclosures or art pieces. For the final touches on this project, an elk was laser etched on a piece of acrylic. The LED strip was then wrapped around the outer perimeter of the piece.

Add some music, and you have yourself a beautiful piece of interactive art.

In 2003, CU student Nate Seidle fried a power supply in his dorm room and, in lieu of a way to order easy replacements, decided to start his own company. Since then, SparkFun has been committed to sustainably helping our world achieve electronics literacy from our headquarters in Boulder, Colorado.

No matter your vision, SparkFun's products and resources are designed to make the world of electronics more accessible. In addition to over 2,000 open source components and widgets, SparkFun offers curriculum, training and online tutorials designed to help demystify the wonderful world of embedded electronics. We're here to help you start something.