Sound-reactive LED matrix: making a WS2812 panel dance to music
- Build Logs
- 29 Jul, 2026
This is the most visually satisfying ₹350 in this whole series. It is also a much better teaching project than it looks, because getting it to work properly means confronting two things that trip people up everywhere else: what a sound sensor actually measures, and how much current addressable LEDs really draw.
What you will need
| Part | Qty | Approx. |
|---|---|---|
| WS2812 16-Bit RGB LED Matrix | 1 | ₹90 |
| Sound Sensor Module | 1 | ₹48 |
| Arduino Nano R3 (compatible) | 1 | ₹209 |
Roughly ₹347. Want it bigger? Individual WS2812B pixels are ₹10 each and chain in the same way — but read the power section before you order fifty.
The power maths nobody does first
A WS2812 is three LEDs in one package. Each colour channel pulls about 20 mA at full brightness, so one pixel showing white draws roughly 60 mA.
| Pixels | Full white | Can the Nano's 5V pin do it? |
|---|---|---|
| 16 | ~960 mA | no, but see below |
| 16 at 25% brightness | ~240 mA | yes, via USB |
| 64 | ~3.8 A | needs its own 5V supply |
| 144 (1m strip) | ~8.6 A | needs a serious supply |
For 16 pixels the honest answer is: cap the brightness in software and USB power is fine. FastLED.setBrightness(60) out of 255 keeps the whole panel under about 250 mA, which is comfortable and still bright enough to be annoying in a dark room.
If you skip that and run 16 pixels at full white off a Nano, you are asking a USB port for nearly an amp through a board whose regulator was not designed for it. It will work briefly, get hot, and behave strangely — which people then blame on the LED library.
Once you go past about 30 pixels, feed the panel from a dedicated 5V supply and tie its ground to the Arduino's. Data needs a shared reference.
Wiring
| From | To | Note |
|---|---|---|
| Matrix DIN | Nano D6 | through a 330Ω resistor |
| Matrix 5V / GND | Nano 5V / GND | at low brightness only |
| Sound sensor VCC / GND | Nano 5V / GND | |
| Sound sensor AO | Nano A0 | analogue, not DO |
The 330Ω resistor in series with the data line and a 1000µF capacitor across the panel's 5V and GND are the two things the WS2812 datasheet asks for and everyone omits. The resistor damps reflections on the data edge; the capacitor absorbs the inrush when many pixels change at once. Without them you get random flickering pixels that look like a faulty panel.
Use AO, not DO. The digital output is just a comparator against the trimpot — it tells you "louder than the threshold", one bit, and you cannot build a level meter from it. The analogue output is the actual waveform.
What a sound sensor actually gives you
This is the part that confuses people. The module does not output loudness. It outputs the microphone's instantaneous waveform, biased around roughly half the supply — about 512 on the ADC. Silence is a flat 512. Sound is a signal oscillating around 512.
So analogRead() once and you get a meaningless number: you might have sampled the peak of a waveform, or the exact moment it crossed zero. To get amplitude you must sample repeatedly over a window and measure the spread.
const int MIC = A0;
const int SAMPLE_WINDOW = 50; // ms; 50ms = 20 updates/sec
int readLevel() {
unsigned long start = millis();
int signalMax = 0;
int signalMin = 1023;
while (millis() - start < SAMPLE_WINDOW) {
int s = analogRead(MIC);
if (s > signalMax) signalMax = s;
if (s < signalMin) signalMin = s;
}
return signalMax - signalMin; // peak-to-peak amplitude
}
Auto-gain, so it works in any room
A fixed threshold fails immediately: what reads as loud in a quiet bedroom barely registers at a party. The fix is to track the loudest thing heard recently and scale against that, decaying slowly so the panel re-sensitises when the music stops.
#include <FastLED.h>
#define LED_PIN 6
#define NUM_LEDS 16
#define BRIGHTNESS 60 // keep the current sane
CRGB leds[NUM_LEDS];
float ceiling = 60; // running estimate of "loud"
const float DECAY = 0.995; // how fast it re-sensitises
void setup() {
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
}
void loop() {
int level = readLevel();
// Track the loudest recent peak, then let it sag back down.
if (level > ceiling) ceiling = level;
ceiling *= DECAY;
if (ceiling < 40) ceiling = 40; // floor, or noise fills the panel
int lit = map(level, 0, (int)ceiling, 0, NUM_LEDS);
lit = constrain(lit, 0, NUM_LEDS);
for (int i = 0; i < NUM_LEDS; i++) {
if (i < lit) {
// Green at the bottom through to red at the top, VU-meter style.
leds[i] = CHSV(map(i, 0, NUM_LEDS - 1, 96, 0), 255, 255);
} else {
leds[i].fadeToBlackBy(60); // trail, rather than a hard cut
}
}
FastLED.show();
}
Two choices worth understanding.
The floor on ceiling. Without it, a silent room decays the ceiling towards zero, then ordinary background hiss maps to the full panel and it strobes at nothing. Clamping the floor means quiet stays dark.
fadeToBlackBy instead of turning pixels off. A hard cut makes the panel flicker harshly on every beat. Fading gives each peak a short tail, which is what makes it read as "reacting to music" rather than "flashing randomly".
Things that go wrong
First pixel is a different colour to the rest. Classic WS2812 signal-integrity problem — add the 330Ω resistor, shorten the data wire, and make sure grounds are properly common.
Colours are wrong — red shows as green. Colour order. Change GRB to RGB in addLeds. Different batches genuinely differ.
Panel lights white and stays there. Usually the data line is on the wrong end — WS2812 chains are directional. Look for the arrow on the PCB; DIN is the input.
Random flickering. Missing capacitor, or the supply cannot keep up. Lower the brightness and see if it stops; if it does, it is power.
Reacts to nothing, or reacts constantly. You are on DO instead of AO, or the trimpot on the module is at an extreme. AO ignores the pot for amplitude purposes, so start there.
Only the first few pixels ever light. The ceiling is too high because something briefly clipped. Restart, or lower DECAY to 0.99 so it forgets faster.
Where to take it next
This is amplitude only — the whole panel responds to overall loudness. The genuinely interesting version splits the sound into frequency bands with an FFT so bass drives one end and treble the other. The arduinoFFT library does this on a Nano, though 16 pixels is a coarse display for it; that is the argument for chaining more WS2812B pixels into a proper bar.
Building this for a stall or an event? Tell us the pixel count and we will size the power supply for you — that is the part most people get wrong, and it is the part that catches fire.