Sound-reactive LED strip on an ESP32-C3
A KY-038 microphone and sixteen WS2812Bs on an ESP32-C3: a VU meter with a falling peak dot, and colors that turn with every loud beat. The whole build, an ESP32-C3 and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 25 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $17. On Mokxi they're free.
What you'll learn
- Measuring loudness as a signal's swing, not its level
- A VU meter with a falling peak dot
- Fast rise and slow fall for smooth animation
- Detecting a beat from a comparator pin
- Choosing steps that follow how we hear: ratios
- Difficulty: Intermediate
- Time: About 25 minutes
- Board: ESP32-C3-DevKitM-1
- Parts: 4
How it works
A KY-038 microphone feeds pin 4 and a strip of 16 WS2812B LEDs hangs off pin 5. The strip is a VU meter: the louder the room, the more LEDs light, from green at the bottom through yellow to red at the top.
The microphone has no amplifier, so the sketch measures how far the signal swings over a quick burst of samples. That peak to peak swing is the loudness.
A white peak dot hangs for 400 ms and then drops. A loud bang trips the module's DO output on pin 3, which shifts the colors around the wheel and gives the strip a short flash.
Parts list
4 parts, plus the jumper wires. They retail for about $17; on Mokxi they're free.
- 1 × ESP32-C3-DevKitM-1In Handheld Console Kit, Desk Buddy Kit.
- 1 × Full-size breadboard
- 1 × Sound sensor, KY-038
- 1 × Addressable RGB LEDsIn Claw Machine Kit, Halloween Prop Kit, Christmas Lights Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Sound sensor, KY-038 pin VCC | ESP32-C3-DevKitM-1 pin 3V3 |
| Sound sensor, KY-038 pin DO | ESP32-C3-DevKitM-1 pin 3 |
| Sound sensor, KY-038 pin GND | ESP32-C3-DevKitM-1 pin GND |
| Addressable RGB LEDs pin VSS | ESP32-C3-DevKitM-1 pin GND |
| Sound sensor, KY-038 pin AO | ESP32-C3-DevKitM-1 pin 4 |
| Addressable RGB LEDs pin DIN | ESP32-C3-DevKitM-1 pin 5 |
| Addressable RGB LEDs pin VDD | ESP32-C3-DevKitM-1 pin 5V |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Sound-reactive LED strip: a KY-038 microphone and sixteen WS2812Bs.
//
// Wiring:
// KY-038 AO -> GPIO 4 DO -> GPIO 3 VCC -> 3V3 GND -> GND
// WS2812B DIN -> GPIO 5 VDD -> 5V VSS -> GND
//
// The strip is a VU meter: the louder the room, the further up the strip it
// lights, green at the bottom through yellow to red at the top, with a peak
// dot that hangs for a moment and then falls. A loud bang (a clap, a kick
// drum) trips the module's comparator on DO, and every one of those shifts
// the colors round the wheel, so the strip changes with the beat.
//
// # Reading a microphone with no amplifier
//
// The KY-038 has no amplifier: AO is the capsule's own few millivolts riding
// on half the supply. So the sketch does not read the level, it reads how far
// the signal swings. Every frame it takes a burst of samples and keeps the
// highest and the lowest; the difference, peak to peak, is the loudness. The
// ADC here is twelve bits on 3.3 V, about 0.8 mV a step, which is just fine
// enough to see a loud room move it. Quiet talk does not.
//
// In the simulator, set the room's sound level on the sensor, or press its
// clap button.
#include "mokxi_ws2812.h"
const uint8_t MIC_PIN = 4; // ADC1 channel 4
const uint8_t BEAT_PIN = 3; // the comparator output
const uint8_t DIN_PIN = 5;
const uint16_t LEDS = 16;
uint8_t pixels[LEDS * 3];
Ws2812 strip(DIN_PIN, pixels, LEDS);
// Peak-to-peak counts for each bar: roughly even steps of loudness, since
// your ear hears ratios, not differences.
const uint16_t BAR_AT[LEDS] = {4, 6, 9, 13, 18, 25, 34, 46, 62, 82, 108, 140, 180, 230, 290, 360};
const uint8_t BRIGHT = 60;
const uint32_t FRAME_MS = 25;
uint8_t level = 0; // bars lit, after smoothing
uint8_t peak = 0; // where the peak dot sits
uint32_t peakAt = 0; // when it got there
uint16_t hueShift = 0; // moves round the wheel on every beat
uint8_t flash = 0; // brightness boost after a beat, fading out
bool beatWas = false;
uint32_t beatGuardUntil = 0;
uint32_t reportAt = 0;
/** How far the microphone swings over a burst of samples. */
static uint16_t loudness()
{
int lo = 4095, hi = 0;
for (uint8_t i = 0; i < 120; i++) {
int v = analogRead(MIC_PIN);
if (v < lo) lo = v;
if (v > hi) hi = v;
delayMicroseconds(120);
}
return (uint16_t)(hi - lo);
}
static uint8_t barsFor(uint16_t p2p)
{
uint8_t n = 0;
while (n < LEDS && p2p >= BAR_AT[n]) {
n++;
}
return n;
}
void setup()
{
Serial.begin(115200);
pinMode(BEAT_PIN, INPUT);
strip.begin();
strip.clear();
strip.show();
Serial.println("Sound strip: make some noise");
}
void loop()
{
uint32_t frameStart = millis();
uint16_t p2p = loudness();
uint8_t bars = barsFor(p2p);
// Jump up at once, fall back a bar at a time: that is what makes it dance.
if (bars > level) {
level = bars;
} else if (level > 0) {
level--;
}
if (level >= peak) {
peak = level;
peakAt = frameStart;
} else if (frameStart - peakAt > 400 && peak > 0) {
peak--; // the dot hangs for 400 ms, then drops
peakAt = frameStart - 340;
}
// DO is a burst of pulses on a loud sound, not one clean level, so take
// the first edge and ignore the rest for a moment.
bool beat = digitalRead(BEAT_PIN) == HIGH;
if (beat && !beatWas && (int32_t)(frameStart - beatGuardUntil) >= 0) {
hueShift = (uint16_t)((hueShift + 256) % 1536);
flash = 120;
beatGuardUntil = frameStart + 150;
Serial.println("Beat!");
}
beatWas = beat;
uint8_t bright = (uint8_t)(BRIGHT + flash > 255 ? 255 : BRIGHT + flash);
for (uint16_t i = 0; i < LEDS; i++) {
uint8_t r = 0, g = 0, b = 0;
if (i < level || (i == peak && peak > 0)) {
// green at the bottom, through yellow, to red at the top, turned by beats
uint16_t hue = (uint16_t)((512 - i * 512 / (LEDS - 1) + hueShift) % 1536);
ws2812_wheel(hue, &r, &g, &b);
if (i == peak && i >= level) {
r = g = b = 255; // the peak dot is white
}
}
strip.setPixel(i, ws2812_scale(r, bright), ws2812_scale(g, bright), ws2812_scale(b, bright));
}
strip.show();
if (flash > 0) {
flash = (uint8_t)(flash > 15 ? flash - 15 : 0);
}
if (frameStart - reportAt >= 1000) {
reportAt = frameStart;
Serial.print("level ");
Serial.print(level);
Serial.print("/16, swing ");
Serial.print(p2p);
Serial.println(" counts");
}
uint32_t spent = millis() - frameStart;
if (spent < FRAME_MS) {
delay(FRAME_MS - spent);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
loudness sketch.ino
Takes 120 readings from the microphone, 120 microseconds apart, and tracks the lowest and highest. The difference is the swing, which grows with sound. A steady reading would show no swing at all.
/** How far the microphone swings over a burst of samples. */
static uint16_t loudness()
{
int lo = 4095, hi = 0;
for (uint8_t i = 0; i < 120; i++) {
int v = analogRead(MIC_PIN);
if (v < lo) lo = v;
if (v > hi) hi = v;
delayMicroseconds(120);
}
return (uint16_t)(hi - lo);
}barsFor sketch.ino
Counts how many LEDs the swing earns. It walks up the BAR_AT table, whose thresholds grow roughly by ratio, from 4 to 360. Each threshold passed adds one bar, up to 16.
static uint8_t barsFor(uint16_t p2p)
{
uint8_t n = 0;
while (n < LEDS && p2p >= BAR_AT[n]) {
n++;
}
return n;
}loop sketch.ino
Measures once, then lets the level jump up at once but fall one bar per frame. It moves the peak dot, checks DO for a beat, and paints all 16 LEDs. Each frame takes at least 25 ms.
void loop()
{
uint32_t frameStart = millis();
uint16_t p2p = loudness();
uint8_t bars = barsFor(p2p);
// Jump up at once, fall back a bar at a time: that is what makes it dance.
if (bars > level) {
level = bars;
} else if (level > 0) {
level--;
}
if (level >= peak) {
peak = level;
peakAt = frameStart;
} else if (frameStart - peakAt > 400 && peak > 0) {
peak--; // the dot hangs for 400 ms, then drops
peakAt = frameStart - 340;
}
// DO is a burst of pulses on a loud sound, not one clean level, so take
// the first edge and ignore the rest for a moment.
bool beat = digitalRead(BEAT_PIN) == HIGH;
if (beat && !beatWas && (int32_t)(frameStart - beatGuardUntil) >= 0) {
hueShift = (uint16_t)((hueShift + 256) % 1536);
flash = 120;
beatGuardUntil = frameStart + 150;
Serial.println("Beat!");
// ... 35 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The strip barely moves. This project
The thresholds start at 4 counts, and quiet talk may not reach them. Make louder noise, or lower the values inBAR_AT. - Colors change too often or not at all. This project
Beats come from the DO pin, set by the module's sensitivity knob. Turn it until a clap triggers but room noise does not. - The digital output is on all the time or never. The pot sets the threshold, and the sensor is only a microphone with a comparator. Sound sensor
Turn the pot until the DO LED just goes out in quiet, then a clap will trip it. Use the AO pin and analogRead for a level rather than a clean on or off. - Analog readings sit at a middle value and look flat, so it seems dead. A loudness change is small and fast. Sound sensor
Sample many readings quickly and take the peak minus the low. Power the module at 3.3 V or 5 V to match your ADC range, and keep it away from motor noise. - The first LED flickers, shows wrong colors, or the whole strip glitches. Addressable RGB LEDs
Join the grounds of the strip and the controller. Add a 300 to 500 ohm resistor in series with the data line. Put a 1000 uF capacitor across the strip's 5 V and GND. - The strip works at low brightness but dims, turns yellow, or resets when many LEDs go white. Addressable RGB LEDs
Each LED can draw about 60 mA at full white. Use a 5 V supply sized for the count, feed power in at several points, and lower the brightness in code. - Upload fails or the board is not found by the computer. ESP32-C3-DevKitM-1
Pick the ESP32-C3 board in your tools and the right port. If it still fails, hold BOOT, tap RESET, then release BOOT to enter download mode and try again. - GPIO pins act strangely or the board resets. ESP32-C3-DevKitM-1
GPIO is 3.3 V only, so never feed it 5 V. GPIO 18 and 19 are USB, and GPIO 2, 8 and 9 are boot strapping pins. Avoid loading them at reset.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.