Community project

Wi-Fi Music-Reactive Robot

ESP32
Photo of Wi-Fi Music-Reactive Robot
Generated with AI

hannah louisa

Published October 11, 2026

HypeBeat is a Wi-Fi-connected robot that dances and lights up in response to music and sound. Built around an ESP32 microcontroller, it combines a microphone input stage, I2S audio amplifier, servo-controlled moving head, addressable LED strip, and local web server for wireless control via smartphone or computer.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to build HypeBeat from scratch. The included firmware enables standalone operation with a local Wi-Fi hotspot, music playback from microSD card, real-time audio reactivity, and web-based control of movement, lighting, and playback modes. Builders will learn audio signal conditioning, I2S audio interfacing, servo control, addressable LED programming, and embedded web server development on the ESP32.

Wiring diagram

Wiring diagram for Wi-Fi Music-Reactive Robot

Gather all the parts

QtyComponent
1

MAX4466 Microphone Amplifier

Adafruit MAX4466 electret microphone amplifier breakout with a manual gain trim pot (25x-125x). Outputs analog audio biased at ~VCC/2, swinging roughly 200mVpp at speaking volume up to ~1Vpp, readable by any 3.3V or 5V ADC. Unlike the AGC-based MAX9814 the gain is fixed by the trimmer, so it is best when you want a set gain and predictable dynamic range rather than auto-leveling.

1

MAX98357A I2S Class-D Mono Amplifier Breakout

I2S-input Class-D mono audio amplifier IC on a compact breakout board. Accepts I2S digital audio input (BCLK, LRC, DIN) and drives a small speaker or transducer directly. No I2C/SPI control bus is needed. The amplifier supply range is 2.5V-5.5V, and the I2S input pins are compatible with 3.3V logic. SD/MODE controls shutdown and channel selection; GAIN selects 3 dB, 6 dB, 9 dB, 12 dB, or 15 dB gain.

1

8Ω Speaker

8 Ω, 3 W

Generic small 8Ω 0.5-3W loudspeaker (~28mm typical). Pair with an I2S amp (MAX98357A) or class-D amp (TPA3116D2) for usable volume; do not drive directly from a GPIO pin. Audio output for music/voice playback.

1

MG90S Micro Servo

Tower Pro MG90S 9g metal-gear micro servo motor. Controlled via a standard 50 Hz PWM signal (1–2 ms pulse width). Operating voltage 4.8 V–6 V. Three-wire interface: VCC (red), GND (brown/black), and Signal (orange/yellow). Stall torque 1.8 kg·cm at 4.8 V / 2.2 kg·cm at 6 V. Rotation range 0°–180°.

1

MG90S Micro Servo

Tower Pro MG90S 9g metal-gear micro servo motor. Controlled via a standard 50 Hz PWM signal (1–2 ms pulse width). Operating voltage 4.8 V–6 V. Three-wire interface: VCC (red), GND (brown/black), and Signal (orange/yellow). Stall torque 1.8 kg·cm at 4.8 V / 2.2 kg·cm at 6 V. Rotation range 0°–180°.

8

WS2812B

8 LEDs, 5 V

Addressable RGB LED strip

1

MicroSD Card Module

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

1

74AHCT125 Quad Buffer / Level Shifter

Quad non-inverting buffer/line driver with 3-state outputs and active-low output-enable pins. In 5 V AHCT/HCT designs, 3.3 V MCU outputs are high enough for the TTL-level inputs, making it a common one-way 3.3 V to 5 V level shifter for WS2812/NeoPixel data and other fast digital lines.

1

LED

Green, 5 mm

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

330 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 Ω

Through-hole resistor (current-limiting in series with an LED)

1

24v Buck Converter

Set output to 5.0 V before connecting the robot

LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board.

1

2S protected 18650 Li-ion battery pack with holder

Two matched protected 18650 cells in series, providing 7.4 V nominal power for the robot.

1

5 A inline blade fuse and holder

A replaceable 5 A fuse that opens if a wiring fault draws unsafe current from the battery.

1

SPST 5 A slide switch

The main switch that turns battery power to the robot on and off.

1

SS54 Schottky diode

A high-current diode that protects the robot if the battery is connected backwards.

1

1000 µF 10 V electrolytic capacitor

A reservoir capacitor that supplies short current bursts on the shared 5 V rail.

1

470 µF 10 V electrolytic capacitor

A reservoir capacitor placed beside the amplifier to reduce audio crackle during bass notes.

1

100 nF ceramic capacitor

A small capacitor that filters high-frequency noise from the microphone signal.

Assemble it in 8 steps

1. Set the buck converter before connecting the robot

With the battery disconnected, turn the small adjustment screw on buck_1 while measuring its output with a multimeter. Set VOUT+ and VOUT- to exactly 5.0 V before connecting any robot electronics.

  • Use the exposed VOUT+ and VOUT- screw terminals for the measurement.
  • Do not connect the ESP32, servos, LEDs, or amplifier until the output is 5.0 V — a higher setting can damage them.

2. Build the protected battery lead

Connect battery_1 BAT+ to fuse_1 IN, fuse_1 OUT to switch_1 IN, switch_1 OUT to the unstriped ANODE end of diode_1, then connect the striped CATHODE end to buck_1 VIN+. Connect battery_1 BAT- and buck_1 VIN- to the shared GND return. This makes the fuse protect against shorts, the switch turn the robot off, and the diode protect against an accidentally reversed battery.

  • Use thick, short wires for this battery path: red for positive and black for ground.
  • Keep the battery disconnected while soldering. Never short the two battery wires together — the cells and wires can become dangerously hot.

3. Make the 5 V power rail

Run buck_1 VOUT+ to the 5 V rail and buck_1 VOUT- to the GND rail. Put rail_cap_1 across these rails: its long positive lead to 5 V and its striped negative lead to GND. Put amp_cap_1 the same way across 5 V and GND physically beside amp_1.

  • Keep the amplifier capacitor leads short because it supplies the quick bursts needed during loud music.
  • Make sure each capacitor's striped negative lead goes to GND — reversed electrolytic capacitors can fail or burst.

4. Connect the ESP32, SD card, and microphone

Power the ESP32 DevKit from the 5 V rail through its 5V/VIN pin and connect its GND to the shared GND rail. Connect sd_module_1 VCC to 3V3, GND to GND, MISO to GPIO19 (data), MOSI to GPIO23 (data), SCK to GPIO18 (clock), and CS to GPIO4 (select). Connect mic_1 VCC to 3V3 (power), GND to GND (ground), and OUT to GPIO34 (sound signal). Connect mic_filter_1 between that same OUT/GPIO34 signal and GND.

  • Use a 3.3 V-compatible microSD module. Format the card as FAT32 before inserting it.
  • Do not power a bare 3.3 V SD breakout from 5 V — that can damage the card or module.

5. Wire audio and the speaker

Connect amp_1 VIN to 5 V (power) and GND to GND (ground). Connect amp_1 BCLK to GPIO26 (audio clock), LRC to GPIO25 (audio timing), and DIN to GPIO27 (audio data). Connect amp_1 SPK+ directly to speaker_1 POS and amp_1 SPK- directly to speaker_1 NEG.

  • Use two separate wires from the amplifier to the speaker.
  • Do not connect either speaker wire to GND — the amplifier's SPK+ and SPK- terminals are both active outputs.

6. Wire the moving head

Connect each MG90S red VCC wire to 5 V and each brown or black GND wire to GND. Connect GPIO32 through servo_pan_resistor_1 to pan_servo_1 SIG, and GPIO33 through servo_tilt_resistor_1 to tilt_servo_1 SIG. Mount pan_servo_1 so it turns the head left and right, then mount tilt_servo_1 on that moving bracket to nod the head up and down.

  • Fit the servo horns only after the firmware has centered both servos at 90 degrees.
  • Servos draw large current spikes; never power either servo from the ESP32's 3.3 V pin.

7. Wire the light strip and Wi-Fi indicator

Connect led_strip_1 VCC to 5 V and GND to GND. Connect level_shifter_1 VCC to 5 V, GND to GND, and 1OE to GND so its output stays enabled. Connect GPIO21 to 1A, then connect 1Y through led_data_resistor_1 to the strip's DATA/DIN input. Connect GPIO13 through status_resistor_1 to status_led_1 ANODE; connect the LED's shorter leg to GND.

  • Follow the arrows printed on the LED strip: connect to DIN, not DOUT.
  • Make sure the LED's long leg is on the resistor side and its short leg is on GND — swapped LED polarity prevents it from lighting.

8. Inspect before first power-up

With the switch off, check that all modules share the same GND rail and that no bare positive wire can touch ground. Insert the FAT32 microSD card, place the microphone away from the speaker to reduce squealing feedback, then switch the robot on.

  • The status LED should light when the robot creates its Wi-Fi network.
  • If anything becomes hot, smells unusual, or the LEDs flicker badly, switch off immediately and recheck the 5 V rail and all GND connections.

Review all connections

1. Connections between "battery_1" and "ESP32"

Functionbattery_1ESP32
powerBAT+ → 5 A inline blade fuse and holder INEXT
groundBAT-GND

2. Connections between "fuse_1" and "ESP32"

Functionfuse_1ESP32
powerOUT → SPST 5 A slide switch INEXT

3. Connections between "switch_1" and "ESP32"

Functionswitch_1ESP32
powerOUT → SS54 Schottky diode ANODEEXT

4. Connections between "diode_1" and "ESP32"

Functiondiode_1ESP32
powerCATHODE → 24v Buck Converter VIN+EXT

5. Connections between "buck_1" and "ESP32"

Functionbuck_1ESP32
groundVIN-GND
powerVOUT+5V
groundVOUT-GND

6. Connections between "rail_cap_1" and "ESP32"

Functionrail_cap_1ESP32
powerPOS5V
groundNEGGND

7. Connections between "amp_cap_1" and "ESP32"

Functionamp_cap_1ESP32
powerPOS5V
groundNEGGND

8. Connections between "mic_1" and "ESP32"

Functionmic_1ESP32
powerVCC3V3
groundGNDGND
analogOUTGPIO 34

9. Connections between "mic_filter_1" and "ESP32"

Functionmic_filter_1ESP32
groundP2GND
analogP1 → MAX4466 Microphone Amplifier OUTEXT

10. Connections between "amp_1" and "ESP32"

Functionamp_1ESP32
powerVIN5V
groundGNDGND
dataBCLKGPIO 26
dataLRCGPIO 25
dataDINGPIO 27
dataSPK+ → 8Ω Speaker POSEXT
dataSPK- → 8Ω Speaker NEGEXT

11. Connections between "pan_servo_1" and "ESP32"

Functionpan_servo_1ESP32
powerVCC5V
groundGNDGND

12. Connections between "servo_pan_resistor_1" and "ESP32"

Functionservo_pan_resistor_1ESP32
digitalP1GPIO 32
digitalP2 → MG90S Micro Servo SIGEXT

13. Connections between "tilt_servo_1" and "ESP32"

Functiontilt_servo_1ESP32
powerVCC5V
groundGNDGND

14. Connections between "servo_tilt_resistor_1" and "ESP32"

Functionservo_tilt_resistor_1ESP32
digitalP1GPIO 33
digitalP2 → MG90S Micro Servo SIGEXT

15. Connections between "led_strip_1" and "ESP32"

Functionled_strip_1ESP32
powerVCC5V
groundGNDGND

16. Connections between "level_shifter_1" and "ESP32"

Functionlevel_shifter_1ESP32
powerVCC5V
groundGNDGND
ground1OEGND
digital1Y → Resistor P1EXT
digital1AGPIO 21

17. Connections between "led_data_resistor_1" and "ESP32"

Functionled_data_resistor_1ESP32
digitalP2 → WS2812B DATAEXT

18. Connections between "status_resistor_1" and "ESP32"

Functionstatus_resistor_1ESP32
digitalP1GPIO 13
digitalP2 → LED ANODEEXT

19. Connections between "status_led_1" and "ESP32"

Functionstatus_led_1ESP32
groundGNDGND

20. Connections between "sd_module_1" and "ESP32"

Functionsd_module_1ESP32
powerVCC3V3
groundGNDGND
spiMISOGPIO 19
spiMOSIGPIO 23
spiSCKGPIO 18
spiCSGPIO 4

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <SD.h>
#include <ESP32Servo.h>
#include <Adafruit_NeoPixel.h>
#include <driver/i2s.h>

// HypeBeat standalone local-control firmware.
// CLOUD_HOOK marks the non-blocking locations for a future cloud database client.
const char *AP_SSID = "HypeBeat-Control";
const char *AP_PASSWORD = "hypebeat123"; // Change before demonstrating publicly.
constexpr uint8_t MIC_PIN=34, I2S_BCLK_PIN=26, I2S_LRCK_PIN=25, I2S_DOUT_PIN=27;
constexpr uint8_t PAN_SERVO_PIN=32, TILT_SERVO_PIN=33, LED_DATA_PIN=21, STATUS_LED_PIN=13;
constexpr uint8_t SD_CS_PIN=4, SD_SCK_PIN=18, SD_MISO_PIN=19, SD_MOSI_PIN=23, PIXELS=8;

WebServer server(80); SPIClass sdSPI(VSPI); Servo panServo, tiltServo;
Adafruit_NeoPixel strip(PIXELS, LED_DATA_PIN, NEO_GRB+NEO_KHZ800);
File wav; File uploadFile; bool sdReady=false, playing=false, session=true, lights=true;
uint8_t bright=128, rr=120, gg=0, bb=255; String mode="groove", current=""; unsigned long lastMove=0;

uint16_t le16(const uint8_t *p){return p[0]|(p[1]<<8);} uint32_t le32(const uint8_t *p){return p[0]|(p[1]<<8)|(p[2]<<16)|(p[3]<<24);}
String safeName(String n){int p=n.lastIndexOf('/');if(p>=0)n=n.substring(p+1);String s;for(size_t i=0;i<n.length();i++){char c=n[i];if(isAlphaNumeric(c)||c=='.'||c=='_'||c=='-')s+=c;}return s.length()?s:"music.wav";}
void showLights(){strip.setBrightness(bright);uint32_t c=lights?strip.Color(rr,gg,bb):0;for(int i=0;i<PIXELS;i++)strip.setPixelColor(i,c);strip.show();}
void stopAudio(){if(wav)wav.close();playing=false;}
bool beginWav(String requested){stopAudio();String p=requested.startsWith("/")?requested:"/"+requested;if(!sdReady||!SD.exists(p))return false;wav=SD.open(p);uint8_t h[44];if(wav.read(h,44)!=44||memcmp(h,"RIFF",4)||memcmp(h+8,"WAVE",4)||memcmp(h+12,"fmt ",4)||memcmp(h+36,"data",4)){wav.close();return false;}uint16_t format=le16(h+20),channels=le16(h+22),bits=le16(h+34);uint32_t rate=le32(h+24);if(format!=1||channels>2||bits!=16||rate<8000||rate>48000){wav.close();return false;}i2s_set_clk(I2S_NUM_0,rate,I2S_BITS_PER_SAMPLE_16BIT,channels==2?I2S_CHANNEL_STEREO:I2S_CHANNEL_MONO);current=p;playing=true;return true;}
void feedAudio(){if(!playing)return;static uint8_t input[512];int n=wav.read(input,sizeof(input));if(n<=0){stopAudio();return;}size_t done=0;i2s_write(I2S_NUM_0,input,n,&done,0);}
String listFiles(){String out="[";bool first=true;if(sdReady){File root=SD.open("/");File f=root.openNextFile();while(f){String n=f.name(),low=n;low.toLowerCase();if(!f.isDirectory()&&low.endsWith(".wav")){if(!first)out+=',';out+='\"'+n+'\"';first=false;}f.close();f=root.openNextFile();}root.close();}return out+"]";}
void sendState(){String out="{\"sdReady\":"+String(sdReady?"true":"false")+",\"playing\":"+String(playing?"true":"false")+",\"ip\":\""+WiFi.softAPIP().toString()+"\",\"file\":\""+current+"\"}";server.send(200,"application/json",out);}
void changeControl(){if(server.hasArg("lights"))lights=server.arg("lights")=="1";if(server.hasArg("session"))session=server.arg("session")=="1";if(server.hasArg("brightness"))bright=constrain(server.arg("brightness").toInt(),0,255);if(server.hasArg("r"))rr=constrain(server.arg("r").toInt(),0,255);if(server.hasArg("g"))gg=constrain(server.arg("g").toInt(),0,255);if(server.hasArg("b"))bb=constrain(server.arg("b").toInt(),0,255);if(server.hasArg("mode"))mode=server.arg("mode");showLights();/* CLOUD_HOOK: publish current controls */sendState();}
void receiveUpload(){HTTPUpload &u=server.upload();if(!sdReady)return;if(u.status==UPLOAD_FILE_START){String p="/"+safeName(u.filename);if(SD.exists(p))SD.remove(p);uploadFile=SD.open(p,FILE_WRITE);}else if(u.status==UPLOAD_FILE_WRITE&&uploadFile)uploadFile.write(u.buf,u.currentSize);else if((u.status==UPLOAD_FILE_END||u.status==UPLOAD_FILE_ABORTED)&&uploadFile)uploadFile.close();}
void moveHead(){if(!session||mode=="idle"){panServo.write(90);tiltServo.write(90);return;}if(millis()-lastMove<50)return;lastMove=millis();int lo=4095,hi=0;for(int i=0;i<24;i++){int x=analogRead(MIC_PIN);lo=min(lo,x);hi=max(hi,x);}int maxAngle=mode=="party"?48:26;int a=constrain(map(hi-lo,10,900,2,maxAngle),2,maxAngle);panServo.write(constrain(90+random(-a,a+1),35,145));tiltServo.write(constrain(90+a/2,55,125));}
void root(){const char page[] PROGMEM=R"HTML(<!doctype html><html><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{font-family:Arial;background:#181020;color:#fff;margin:20px}section{border:1px solid #8040a0;margin:10px 0;padding:10px;border-radius:8px}button,input,select{font-size:16px;margin:3px;padding:7px}button{background:#8040df;color:#fff;border:0;border-radius:5px}</style><h1>HypeBeat local control</h1><p id="s">Loading</p><section><b>Music — 16-bit PCM WAV only in this build</b><form action="/upload" method="post" enctype="multipart/form-data"><input type="file" name="music" accept="audio/wav"><button>Upload WAV</button></form><select id="f"></select><button onclick="post('/play?file='+encodeURIComponent(f.value))">Play</button><button onclick="post('/stop')">Stop</button></section><section><b>Lights</b><input id="c" type="color" value="#7800ff"><input id="br" type="range" min="0" max="255" value="128"><label><input id="on" type="checkbox" checked>On</label><button onclick="light()">Apply</button></section><section><b>Movement</b><select id="m"><option value="idle">Idle</option><option value="groove" selected>Groove</option><option value="party">Party</option></select><label><input id="se" type="checkbox" checked>React to sound</label><button onclick="post('/control?session='+(se.checked?1:0)+'&mode='+m.value)">Apply</button></section><script>async function post(x){await fetch(x,{method:'POST'});refresh()}async function refresh(){let x=await(await fetch('/state')).json();s.textContent='Open http://'+x.ip+' | SD '+(x.sdReady?'ready':'missing')+(x.playing?' | playing '+x.file:'');let a=await(await fetch('/files')).json();f.innerHTML=a.map(x=>'<option>'+x+'</option>').join('')}function light(){let x=c.value;post('/control?lights='+(on.checked?1:0)+'&brightness='+br.value+'&r='+parseInt(x.slice(1,3),16)+'&g='+parseInt(x.slice(3,5),16)+'&b='+parseInt(x.slice(5,7),16))}refresh();setInterval(refresh,3000)</script></html>)HTML";server.send(200,"text/html",page);}
void setup(){Serial.begin(115200);pinMode(STATUS_LED_PIN,OUTPUT);strip.begin();showLights();panServo.setPeriodHertz(50);tiltServo.setPeriodHertz(50);panServo.attach(PAN_SERVO_PIN,500,2400);tiltServo.attach(TILT_SERVO_PIN,500,2400);sdSPI.begin(SD_SCK_PIN,SD_MISO_PIN,SD_MOSI_PIN,SD_CS_PIN);sdReady=SD.begin(SD_CS_PIN,sdSPI,10000000);i2s_config_t c={.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_TX),.sample_rate=44100,.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT,.channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT,.communication_format=I2S_COMM_FORMAT_STAND_MSB,.intr_alloc_flags=0,.dma_buf_count=8,.dma_buf_len=256,.use_apll=false,.tx_desc_auto_clear=true,.fixed_mclk=0};i2s_pin_config_t p={.bck_io_num=I2S_BCLK_PIN,.ws_io_num=I2S_LRCK_PIN,.data_out_num=I2S_DOUT_PIN,.data_in_num=I2S_PIN_NO_CHANGE};i2s_driver_install(I2S_NUM_0,&c,0,nullptr);i2s_set_pin(I2S_NUM_0,&p);WiFi.mode(WIFI_AP);WiFi.softAP(AP_SSID,AP_PASSWORD);digitalWrite(STATUS_LED_PIN,HIGH);server.on("/",HTTP_GET,root);server.on("/state",HTTP_GET,sendState);server.on("/files",HTTP_GET,[](){server.send(200,"application/json",listFiles());});server.on("/control",HTTP_POST,changeControl);server.on("/play",HTTP_POST,[](){if(!server.hasArg("file")||!beginWav(safeName(server.arg("file"))))server.send(400,"text/plain","Use a 16-bit PCM WAV file");else sendState();});server.on("/stop",HTTP_POST,[](){stopAudio();sendState();});server.on("/upload",HTTP_POST,[](){if(uploadFile)uploadFile.close();server.send(200,"text/plain","Upload complete");},receiveUpload);server.begin();}
void loop(){server.handleClient();feedAudio();moveHead();/* CLOUD_HOOK: database polling or subscription */}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik