A music box plays a tune when you open it. This one plays any tune you like, and you choose it from your phone. The ESP32 runs a small web page with a list of songs: tap one and the passive buzzer plays it, while an LED blinks along with the notes. A slider changes the speed. There is also a box where you can write your own melody in a simple note language, like C4:1 E4:1 G4:2, and play it straight away.
What you'll learn
- How notes and octaves turn into frequencies
- How to play a melody without blocking the web server
- How to read a melody written as text
- How to send text from a web form to the ESP32
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Passive buzzer (ESP32 IoT Starter Kit)
- 1 LED and 1 resistor, 220 Ω (ESP32 IoT Starter Kit)
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (music box), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| Passive buzzer + | ESP32 GPIO 25 |
| Passive buzzer − | GND |
| ESP32 GPIO 26 | 220 Ω resistor, then the LED's long leg |
| LED short leg | GND |
The note language
A melody is a list of notes separated by spaces. Each note is a letter from A to G, an optional # for a sharp, the octave number, a colon and the length in beats. R is a rest. So C4:1 C4:1 G4:1 G4:1 A4:1 A4:1 G4:2 is the start of Twinkle, Twinkle, Little Star. Octave 4 is the middle of a piano; A4 is 440 Hz. Lengths can be halves: E5:0.5.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const int BUZZER_PIN = 25, LED_PIN = 26;
const int SONGS = 4;
const char* TITLES[SONGS] = {"Twinkle, Twinkle", "Ode to Joy", "Happy Birthday", "Jingle Bells"};
const char* MELODIES[SONGS] = {
"C4:1 C4:1 G4:1 G4:1 A4:1 A4:1 G4:2 F4:1 F4:1 E4:1 E4:1 D4:1 D4:1 C4:2",
"E4:1 E4:1 F4:1 G4:1 G4:1 F4:1 E4:1 D4:1 C4:1 C4:1 D4:1 E4:1 E4:1.5 D4:0.5 D4:2",
"C4:0.75 C4:0.25 D4:1 C4:1 F4:1 E4:2 C4:0.75 C4:0.25 D4:1 C4:1 G4:1 F4:2",
"E4:1 E4:1 E4:2 E4:1 E4:1 E4:2 E4:1 G4:1 C4:1.5 D4:0.5 E4:4"};
WebServer server(80);
String melody = ""; // what is playing now
int position = 0; // where we are in the text
int bpm = 120; // beats per minute
unsigned long nextNoteAt = 0;
bool playing = false;
// "A4" is 440 Hz; every semitone multiplies by the 12th root of 2
float frequency(char letter, bool sharp, int octave) {
const int STEPS[7] = {9, 11, 0, 2, 4, 5, 7}; // A B C D E F G, counted from C
int semitone = STEPS[letter - 'A'] + (sharp ? 1 : 0) + (octave - 4) * 12 - 9;
return 440.0 * pow(2, semitone / 12.0);
}
void play(String text) {
melody = text + " ";
melody.toUpperCase();
position = 0;
nextNoteAt = millis();
playing = true;
}
// Play the next note when the last one is over. Never waits, so the web page stays fast.
void music() {
if (!playing || millis() < nextNoteAt) return;
while (position < (int)melody.length() && melody[position] == ' ') position++;
if (position >= (int)melody.length()) { // the end
playing = false;
digitalWrite(LED_PIN, LOW);
return;
}
int end = melody.indexOf(' ', position);
String note = melody.substring(position, end); // for example "G#4:1.5"
position = end + 1;
int colon = note.indexOf(':');
float beats = colon > 0 ? note.substring(colon + 1).toFloat() : 1;
if (beats <= 0) beats = 1;
unsigned long ms = beats * 60000 / bpm;
char letter = note[0];
if (letter >= 'A' && letter <= 'G') {
bool sharp = note[1] == '#';
int octave = note.substring(sharp ? 2 : 1, colon > 0 ? colon : note.length()).toInt();
tone(BUZZER_PIN, frequency(letter, sharp, octave), ms * 9 / 10); // a tiny gap between notes
digitalWrite(LED_PIN, HIGH);
} else { // R, or anything else: a rest
digitalWrite(LED_PIN, LOW);
}
nextNoteAt = millis() + ms;
}
String safe(String s) {
s.replace("&", "&");
s.replace("<", "<");
return s;
}
void handleRoot() {
String html = "<!doctype html><meta name=viewport content='width=device-width'><title>Music box</title>"
"<style>body{font-family:sans-serif;max-width:440px;margin:24px auto;padding:0 16px}a.s{display:block;padding:14px;margin:8px 0;border-radius:12px;background:#1B3A7B;color:#fff;text-decoration:none}"
"textarea{width:100%;height:90px;box-sizing:border-box}button{padding:10px 16px}</style><h1>Music box</h1>";
for (int i = 0; i < SONGS; i++) html += "<a class=s href='/play?song=" + String(i) + "'>▶ " + TITLES[i] + "</a>";
html += "<p><a href='/stop'>■ Stop</a></p>"
"<form action='/play'><p><label>Speed: <input type=range name=bpm min=60 max=240 value='" + String(bpm) + "' onchange='this.form.submit()'></label> " + String(bpm) + " bpm</p></form>"
"<h2>Your own tune</h2><form action='/play' method=post><textarea name=notes>C4:1 E4:1 G4:1 C5:2</textarea><p><button>Play</button></p></form>";
server.send(200, "text/html", html);
}
void handlePlay() {
if (server.hasArg("bpm")) bpm = constrain(server.arg("bpm").toInt(), 60, 240);
if (server.hasArg("song")) {
int i = server.arg("song").toInt();
if (i >= 0 && i < SONGS) play(MELODIES[i]);
}
if (server.hasArg("notes")) play(server.arg("notes").substring(0, 600));
server.sendHeader("Location", "/");
server.send(303);
}
void handleStop() {
playing = false;
noTone(BUZZER_PIN);
digitalWrite(LED_PIN, LOW);
server.sendHeader("Location", "/");
server.send(303);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print(" connected. Open http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/play", handlePlay);
server.on("/stop", handleStop);
server.begin();
play("C5:0.5 E5:0.5 G5:1"); // a little hello
}
void loop() {
server.handleClient();
music();
}
Play
Open the Serial Monitor at 115200 baud: after connecting, the ESP32 prints its address, such as http://192.168.1.42. Open it in a browser on a phone or computer in the same Wi-Fi.
- Tap a song: the music box plays it and the LED blinks with every note.
- Move the speed slider and play it again: 60 is slow, 240 very fast.
- Write your own tune in the box and tap Play. Try
E5:0.5 D#5:0.5 E5:0.5 D#5:0.5 E5:0.5 B4:0.5 D5:0.5 C5:0.5 A4:2.
How it works
A note is a vibration: A4 vibrates 440 times a second. Going up one semitone (one key on a piano, white or black) multiplies the frequency by the 12th root of 2, about 1.06, so after 12 semitones, one octave, it has doubled. frequency() counts how many semitones the note is away from A4 and works out the frequency with pow().
The length of a beat comes from the speed: at 120 beats per minute, one beat is 60,000 / 120 = 500 ms.
The web server must keep answering while a song plays, so music() never waits. It starts one note with tone(), notes the time the next one is due, and returns. loop() calls it thousands of times a second; most times it has nothing to do.
Reading a melody is called parsing: the sketch cuts the text at the spaces with indexOf() and substring(), then splits each piece at the colon into the note and its length.
Try this next
- Save your own tune with Preferences, so it is still there after a restart, and give it a button.
- Play a song automatically at a set time with internet time, like a doorbell or a wake-up tune.
- Add a second LED and make it light only for high notes.
Troubleshooting
- No sound, but the LED blinks: it must be the passive buzzer (no sticker, a green board underneath); the active one only beeps at one pitch.
-
A note sounds wrong: check the spelling, like
F#4:1. Notes go from A to G only. -
The page is slow while a song plays: make sure there is no
delay()inmusic()orloop(). - It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.