Overview
This project builds a music-reactive VU (Volume Unit) meter using a strip of WS2812B NeoPixel LEDs and a MAX4466 electret microphone amplifier. The ESP32 reads the microphone's analogue output, measures the peak audio level in each sampling window, maps it to a number of lit LEDs, and uses the FastLED library to colour the strip from green (quiet) through yellow (moderate) to red (loud).
WS2812B LEDs (commonly sold as NeoPixels) contain a red, green, and blue LED plus a tiny control chip inside each 5 mm package. They chain together using a single data wire with a proprietary timed serial protocol. The FastLED library handles all timing automatically and provides a rich API for colour manipulation, animations, and brightness control.
You will learn: analogue audio sampling on the ESP32, peak detection over a time window, HSV colour space for smooth colour transitions, and driving addressable RGB LED strips with FastLED. This project is crowd-pleasingly visual and teaches core concepts that apply to any LED art installation.
Components
- 1× ESP32 DevKit V1
- 1× WS2812B LED Strip (30 or 60 LEDs/m) — IP30 indoor strip, 1 meter minimum
- 1× MAX4466 Microphone Amplifier Module — Adjustable gain — much better than KY-038
- 1× 5 V 2 A Power Supply — USB charger works for 30 LEDs; longer strips need dedicated 5 V PSU
- 1× 470 Ω Resistor — Series resistor on LED data line — prevents ringing
- 1× 1000 µF 6.3 V Capacitor — Across LED power supply to absorb current spikes
- 1× Jumper wires + breadboard
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| MAX4466 OUT | GPIO 34 | Analogue audio signal — ADC1, no Wi-Fi noise |
| MAX4466 VCC | 3.3 V | |
| MAX4466 GND | GND | |
| LED Strip DATA IN | GPIO 5 (via 470 Ω) | Series 470 Ω protects data line from ringing |
| LED Strip 5V | 5 V PSU positive | NEVER power more than 5 LEDs from ESP32 VIN — use dedicated PSU |
| LED Strip GND | PSU GND + ESP32 GND | Common ground is essential |
| Capacitor | Across PSU 5V and GND | Absorbs current spikes when many LEDs change at once |
Arduino Code
/*
* ESP32 NeoPixel Music Visualizer — Beginner (VU Meter)
* Reads MAX4466 microphone, maps volume to LED count and colour.
* Green (quiet) → Yellow (moderate) → Red (loud)
*
* Library: FastLED by Daniel Garcia (Library Manager)
*
* Power note: 1 white LED at full brightness = 60 mA.
* 30 LEDs = 1.8 A peak. Use a 5V 2A+ power supply, not the ESP32.
*/
#include <FastLED.h>
#define MIC_PIN 34
#define LED_PIN 5
#define NUM_LEDS 30
#define BRIGHTNESS 80 // 0–255 (keep low for power budget)
#define SAMPLE_MS 20 // Audio sampling window (ms)
#define DC_OFFSET 2048 // ADC midpoint at 12-bit = 4096/2
CRGB leds[NUM_LEDS];
// Collect peak audio level over SAMPLE_MS milliseconds
int sampleAudio() {
unsigned long start = millis();
int peak = 0;
while (millis() - start < SAMPLE_MS) {
int raw = analogRead(MIC_PIN);
int level = abs(raw - DC_OFFSET);
if (level > peak) peak = level;
}
return peak;
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.clear(); FastLED.show();
Serial.println("Music Visualizer started. Speak or play music near mic.");
}
void loop() {
int peak = sampleAudio();
// Map peak level (0–2047) to number of lit LEDs
// Adjust the 30 (upper bound) based on your mic sensitivity
int litCount = map(peak, 0, 600, 0, NUM_LEDS);
litCount = constrain(litCount, 0, NUM_LEDS);
// Colour: HSV hue 96=green → 64=yellow → 0=red as level increases
FastLED.clear();
for (int i = 0; i < litCount; i++) {
// Map LED position to hue: bottom=green, top=red
uint8_t hue = map(i, 0, NUM_LEDS - 1, 96, 0);
leds[i] = CHSV(hue, 255, 255);
}
FastLED.show();
// Optional: print peak to Serial Plotter for mic calibration
Serial.println(peak);
}How It Works
Peak Detection: The inner while loop runs for SAMPLE_MS milliseconds, reading the ADC as fast as possible (~50,000 samples/second on ESP32). It tracks the maximum deviation from DC_OFFSET (2048 at 12-bit). Audio signals are AC — they swing above and below the DC midpoint. Taking abs() and keeping the max captures the amplitude of the loudest moment in the window.
DC Offset Correction: The MAX4466 biases the microphone signal around VCC/2 (about 1.65 V) so it fits in the 0–3.3 V ADC range. At silence, analogRead() returns ~2048 (half of 4096 at 12-bit). Subtracting DC_OFFSET centres the signal at 0, so quiet audio gives values near 0 and loud audio gives values up to ±2047.
FastLED HSV Colour Mapping: FastLED's CHSV(hue, saturation, value) lets you specify colour as hue angle, colour purity, and brightness. Hue 96 is green, 64 is yellow, 0 is red (HSV wraps at 255). By mapping the LED position index to the hue, shorter bars are entirely green while tall bars add yellow and red at the top — exactly like a professional VU meter.
GRB Colour Order: WS2812B LEDs use GRB byte order (Green, Red, Blue) instead of RGB. FastLED's <WS2812B, LED_PIN, GRB> template parameter handles this automatically. If colours look wrong (red when you expect green), the byte order is incorrect — try RGB or BGR.
Applications
- Party room or studio lighting reactive to music
- Desk ambient lighting that pulses to music while working
- Sound-sensitive art installation
- Quiet-zone indicator (goes red when room gets too loud)
- Baby monitor visual alert (silent light show instead of speaker noise)
Troubleshooting
LEDs are always at full brightness regardless of sound
The peak is always high, suggesting DC_OFFSET is wrong. Print analogRead(MIC_PIN) to Serial Plotter during silence. The resting value should be near 2048. If it is very different, update DC_OFFSET to match. Also check MAX4466 gain pot — turn it fully counter-clockwise to minimum.
LEDs flicker randomly even in silence
Electrical noise on the ADC. 1) Add a 100 nF capacitor between GPIO 34 and GND. 2) Move MAX4466 away from the ESP32 if possible. 3) Increase SAMPLE_MS to 50 ms to average more samples per frame.
LEDs show wrong colours (purple instead of red, etc.)
The colour order is wrong. Try changing GRB to RGB, BGR, or GRBW in the FastLED.addLeds line until colours match. Different WS2812B clones use different byte orders.
Upgrades
- Add a sensitivity pot (10 kΩ) to adjust the peak-to-LED mapping without reflashing
- Implement a "peak hold" dot that lingers at the highest recent level for 1-2 seconds
- Add a falling-bar animation where LEDs decay smoothly rather than dropping instantly
FAQ
You need an ESP32 DevKit, TODO: sensor, MAX4466 OUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to LED Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
At the intermediate level you implement a real-time FFT (Fast Fourier Transform) frequency analyser. Instead of just measuring total volume, the FFT decomposes the audio into 16 frequency bins — bass, mid, treble — and lights a separate column of LEDs for each bin. The result looks like a classic equaliser display: bass frequencies on the left, treble on the right, each column bouncing independently to the music.
You will use the ESP32's I2S interface with an INMP441 digital microphone for much lower noise than the MAX4466 analogue approach, and the arduinoFFT library to process 256 audio samples per frame at ~60 fps. This introduces digital signal processing concepts directly applicable to audio analysis, vibration sensing, and wireless signal processing.
Components
- 1× ESP32 DevKit V1
- 1× INMP441 I2S MEMS Microphone — Better noise floor than MAX4466
- 1× WS2812B LED Matrix (8×8 or 8×16) — Or arrange a strip into columns
- 1× 5 V 3 A Power Supply — 64-LED matrix can draw 3.8 A peak
- 1× 470 Ω resistor + 1000 µF cap — Same as beginner level
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| INMP441 SCK | GPIO 14 | I2S serial clock |
| INMP441 WS | GPIO 15 | I2S word select (L/R) |
| INMP441 SD | GPIO 32 | I2S serial data |
| INMP441 L/R | GND | Select left channel (set to 3.3V for right) |
| INMP441 VDD | 3.3 V | |
| LED Matrix DATA | GPIO 5 (via 470 Ω) |
Arduino Code
/*
* ESP32 NeoPixel Music Visualizer — Intermediate (FFT Spectrum)
* INMP441 I2S mic + arduinoFFT + WS2812B 8×16 matrix
* Libraries: FastLED, arduinoFFT
*/
#include <FastLED.h>
#include <driver/i2s.h>
#include "arduinoFFT.h"
#define LED_PIN 5
#define NUM_LEDS 128 // 8 columns × 16 rows
#define COLS 8
#define ROWS 16
#define BRIGHTNESS 40
#define I2S_SCK 14
#define I2S_WS 15
#define I2S_SD 32
#define SAMPLES 256
#define SAMPLE_RATE 44100
CRGB leds[NUM_LEDS];
double vReal[SAMPLES], vImag[SAMPLES];
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, SAMPLES, SAMPLE_RATE);
// Column heights (decaying)
float colHeight[COLS] = {0};
void i2sSetup(){
i2s_config_t cfg = {
.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_RX),
.sample_rate=SAMPLE_RATE,
.bits_per_sample=I2S_BITS_PER_SAMPLE_32BIT,
.channel_format=I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format=I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1,
.dma_buf_count=4,.dma_buf_len=64,.use_apll=false
};
i2s_pin_config_t pins = {
.bck_io_num=I2S_SCK,.ws_io_num=I2S_WS,
.data_out_num=I2S_PIN_NO_CHANGE,.data_in_num=I2S_SD
};
i2s_driver_install(I2S_NUM_0,&cfg,0,NULL);
i2s_set_pin(I2S_NUM_0,&pins);
}
void readI2S(){
int32_t raw[SAMPLES]; size_t bytes=0;
i2s_read(I2S_NUM_0,raw,sizeof(raw),&bytes,portMAX_DELAY);
for(int i=0;i<SAMPLES;i++){
vReal[i]=(double)(raw[i]>>8)/8388608.0; // Normalise 24-bit
vImag[i]=0;
}
}
void drawSpectrum(){
FastLED.clear();
for(int c=0;c<COLS;c++){
int h=(int)colHeight[c];
for(int r=0;r<h;r++){
int ledIdx=c*ROWS+r;
uint8_t hue=map(r,0,ROWS-1,96,0); // Green at bottom, red at top
leds[ledIdx]=CHSV(hue,255,255);
}
}
FastLED.show();
}
void setup(){
Serial.begin(115200);
FastLED.addLeds<WS2812B,LED_PIN,GRB>(leds,NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.clear(); FastLED.show();
i2sSetup();
}
void loop(){
readI2S();
FFT.windowing(FFTWindow::Hamming,FFTDirection::Forward);
FFT.compute(FFTDirection::Forward);
FFT.complexToMagnitude();
// Map FFT bins to columns (logarithmic frequency distribution)
int binStarts[COLS+1]={1,2,3,5,8,13,21,34,55}; // Fibonacci-ish spread
for(int c=0;c<COLS;c++){
double mag=0;
for(int b=binStarts[c];b<binStarts[c+1];b++) mag=max(mag,vReal[b]);
float target=constrain((float)(mag*20.0),0,ROWS);
// Smooth: fast attack, slow decay
if(target>colHeight[c]) colHeight[c]=target;
else colHeight[c]=max(0.0f, colHeight[c]-0.8f);
}
drawSpectrum();
}How It Works
I2S Digital Microphone: The INMP441 outputs audio as a 24-bit I2S (Inter-IC Sound) stream. I2S is a standard digital audio interface: SCK is the bit clock, WS (word select) toggles at the sample rate to separate left/right channels, SD carries the data. The ESP32 has two hardware I2S peripherals that handle all timing without CPU intervention.
FFT Processing: The Fast Fourier Transform converts 256 time-domain audio samples into 128 frequency-domain magnitude values (frequency bins). Each bin represents a frequency range: bin 0 = 0 Hz, bin 128 = 22050 Hz (Nyquist at 44100 Hz sample rate). The bin resolution is 44100/256 ≈ 172 Hz per bin.
Logarithmic Bin Grouping: Human hearing is logarithmic — an octave (doubling of frequency) sounds the same width at 200 Hz as at 2000 Hz. The binStarts array groups linear FFT bins into logarithmically-spaced columns using a Fibonacci-like spread, giving each display column perceptually equal frequency width.
Attack/Decay Animation: Column heights use asymmetric smoothing: instant attack (target > current → jump immediately) and gradual decay (target < current → subtract 0.8 per frame). This gives the characteristic "fast up, slow fall" VU meter behavior that makes the display feel musical rather than twitchy.
Applications
- Field trial with visible OLED feedback
- Manual override for maintenance or testing
- Calibrated setup for daily use
Troubleshooting
All columns show equal height (flat spectrum)
I2S not receiving audio. Check SCK, WS, SD pin connections and L/R pin state (must be GND for left channel). Verify i2s_read returns bytes > 0. Print vReal[10] to confirm non-zero magnitudes after FFT.
FFT shows strong DC spike in bin 0
Apply a DC-removal high-pass filter before FFT: subtract the running average from each sample. The Hamming window helps but does not fully remove DC offset from I2S data.
Upgrades
- Add a push button to cycle through colour themes (rainbow, fire, ice, party)
- Use PSRAM (if your ESP32 module has it) for larger FFT sizes (512 or 1024) for higher frequency resolution
- Sync multiple LED strips via ESP-NOW for a whole-room installation
FAQ
You need an ESP32 DevKit, TODO: sensor, MAX4466 OUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to LED Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The advanced level adds a web configuration panel where you can switch between 6 visualisation modes, adjust sensitivity, change colour palettes, and set beat-sync brightness pulses — all from a browser without reflashing. The ESP32 also receives audio wirelessly via Bluetooth A2DP, letting you visualise music from your phone's Spotify app without any wires or microphone required.
Bluetooth A2DP (Advanced Audio Distribution Profile) receives stereo audio streams from any Bluetooth audio source. The ESP32-A2DP library connects as a Bluetooth speaker and delivers audio samples to a callback function for processing. Combined with a web UI for configuration, this creates a professional-grade music visualiser comparable to commercial products.
Components
- 1× ESP32 DevKit V1 — Bluetooth built-in
- 1× WS2812B LED Strip / Matrix — 60+ LEDs recommended for visual impact
- 1× 5 V 5 A Power Supply — For 60+ LEDs at full brightness
- 1× 470 Ω + 1000 µF
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| LED Strip DATA | GPIO 5 (via 470 Ω) | |
| LED 5V / GND | External 5V PSU | Common GND with ESP32 |
Arduino Code
/*
* ESP32 NeoPixel Music Visualizer — Advanced
* Bluetooth A2DP audio input + Web config panel + 6 visualiser modes
*
* Library: ESP32-A2DP by Phil Schatzmann (Library Manager)
* FastLED, arduinoFFT
*
* IMPORTANT: Bluetooth A2DP and Wi-Fi cannot run simultaneously on ESP32.
* This sketch uses Bluetooth for audio and a brief Wi-Fi window at boot
* for configuration. In production, serve config from BLE instead.
*/
#include <FastLED.h>
#include <BluetoothA2DPSink.h>
#include "arduinoFFT.h"
#define LED_PIN 5
#define NUM_LEDS 60
#define BRIGHTNESS 100
#define COLS 15 // FFT spectrum columns across strip
#define SAMPLES 512
CRGB leds[NUM_LEDS];
BluetoothA2DPSink a2dp_sink;
double vReal[SAMPLES], vImag[SAMPLES];
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, SAMPLES, 44100.0);
volatile int audioIdx = 0;
enum Mode { VU_METER, SPECTRUM, FIRE, RAINBOW_BEAT, SPARKLE, SOLID_BEAT };
Mode currentMode = SPECTRUM;
uint8_t sensitivity = 80; // 0–255
// Beat detection
float smoothedLevel = 0;
bool beatDetected = false;
// A2DP audio callback — called for each incoming audio frame
void audioDataCallback(const uint8_t* data, uint32_t len) {
int16_t* samples = (int16_t*)data;
int sampleCount = len / 4; // Stereo 16-bit = 4 bytes per frame
for (int i = 0; i < sampleCount && audioIdx < SAMPLES; i++) {
// Mix L+R channels to mono, normalise to -1.0..1.0
vReal[audioIdx] = (double)(samples[i*2] + samples[i*2+1]) / 65536.0;
vImag[audioIdx] = 0.0;
audioIdx++;
}
}
void processBeat() {
double rms = 0;
for (int i = 0; i < SAMPLES; i++) rms += vReal[i] * vReal[i];
rms = sqrt(rms / SAMPLES);
smoothedLevel = smoothedLevel * 0.9f + rms * 0.1f;
beatDetected = (rms > smoothedLevel * 1.5);
}
void drawVU() {
double peak = 0;
for (int i = 0; i < SAMPLES; i++) peak = max(peak, abs(vReal[i]));
int lit = (int)(peak * NUM_LEDS * (sensitivity / 255.0) * 20);
lit = constrain(lit, 0, NUM_LEDS);
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = i < lit ? CHSV(map(i, 0, NUM_LEDS-1, 96, 0), 255, 255) : CRGB::Black;
}
}
void drawFire() {
// Shift all LEDs up
for (int i = NUM_LEDS-1; i > 0; i--) leds[i] = leds[i-1];
double peak = 0;
for (int i = 0; i < SAMPLES; i++) peak = max(peak, abs(vReal[i]));
uint8_t intensity = (uint8_t)constrain(peak * 2000 * (sensitivity/255.0), 0, 255);
leds[0] = CHSV(random8(10, 25), 255, intensity); // Yellow-orange-red
}
void setup() {
Serial.begin(115200);
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.clear(); FastLED.show();
a2dp_sink.set_stream_reader(audioDataCallback);
a2dp_sink.start("ESP32 Visualizer"); // Bluetooth device name
Serial.println("Bluetooth started — pair from your phone as "ESP32 Visualizer"");
}
void loop() {
if (audioIdx >= SAMPLES) {
audioIdx = 0;
processBeat();
if (beatDetected) FastLED.setBrightness(255);
else FastLED.setBrightness(BRIGHTNESS);
switch (currentMode) {
case VU_METER: drawVU(); break;
case FIRE: drawFire(); break;
case SPECTRUM:
FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward);
FFT.compute(FFTDirection::Forward);
FFT.complexToMagnitude();
// Map to LED columns (simplified for clarity)
for (int c = 0; c < COLS; c++) {
int b = 1 + c * 3;
double mag = vReal[b] * 200 * (sensitivity/255.0);
int h = (int)constrain(mag, 0, NUM_LEDS/COLS);
int start = c * (NUM_LEDS/COLS);
for (int r = 0; r < NUM_LEDS/COLS; r++) {
leds[start+r] = r < h ? CHSV(map(r, 0, NUM_LEDS/COLS, 96, 0), 255, 255) : CRGB::Black;
}
}
break;
case RAINBOW_BEAT:
if (beatDetected) fill_rainbow(leds, NUM_LEDS, random8(), 5);
else fadeToBlackBy(leds, NUM_LEDS, 10);
break;
default:
fill_solid(leds, NUM_LEDS, beatDetected ? CRGB::White : CRGB::Black);
}
FastLED.show();
}
}How It Works
Bluetooth A2DP Sink: The ESP32-A2DP library configures the ESP32 as a Bluetooth A2DP sink — it appears as a Bluetooth speaker to your phone. When you connect and play music, the library calls audioDataCallback() with raw 16-bit stereo PCM audio data at 44100 Hz. You can process this data for FFT or peak detection just like microphone samples.
Beat Detection: The RMS (Root Mean Square) of each audio block is computed. A smoothed running average tracks the ambient level. A beat is detected when the current RMS exceeds 150% of the smoothed average — the classic energy-based beat detection algorithm used in commercial DJ lighting systems.
Multiple Visualiser Modes: An enum (Mode) tracks the current mode. A switch statement in loop() selects the appropriate drawing function. Switching modes at runtime can be done via a web API endpoint (brief Wi-Fi window at boot), MQTT command, or a push button cycling through the enum values.
Fire Effect: The fire effect shifts all LED values up the strip each frame and adds a new random orange/yellow pixel at the base with intensity proportional to audio level. This creates a flame that rises and flickers with the music — all from 5 lines of code using FastLED's HSV colour space.
Applications
- Remote monitoring from phone or laptop
- Automated alerts when limits are crossed
- Long-term trend logging for optimization
Troubleshooting
Phone cannot see "ESP32 Visualizer" Bluetooth device
Ensure no other device is already connected to this ESP32 A2DP instance. Bluetooth A2DP allows only one connection at a time. Restart the ESP32 and try pairing immediately. Some phones require "Forget" the old pairing before reconnecting.
Audio is choppy or dropping
A2DP streams at 44100×4 bytes/second ≈ 170 KB/s. If the callback processes audio too slowly, the buffer overflows. Ensure loop() processes audio data without blocking operations. The audioIdx check and FFT computation must complete within ~11 ms (one 512-sample frame at 44100 Hz).
Upgrades
- Add BLE (Bluetooth Low Energy) for a web-like configuration UI without disabling Wi-Fi
- Use ESP-NOW to synchronise beat signals across multiple LED strips in different rooms
- Implement an auto-mode selector that detects the music genre from frequency content and switches modes
FAQ
You need an ESP32 DevKit, TODO: sensor, MAX4466 OUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to LED Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.