Intermediate3 hours12+7 parts needed

Parent info

Cost: ~$25
Time: 3 hours
Age: 12+
Difficulty: ●●●
Soldering: No soldering needed
What they'll learn: Microcontroller programming, LED circuits

Parts you need

Affiliate links — we may earn a small commission

ESP32-S3 Dev Board
4×4 Tactile Button Matrix (16 buttons)
MAX98357A I2S Amplifier
3W 4Ω Speaker
10kΩ Potentiometer
0.96" OLED Display (SSD1306 I2C)
WS2812B LED Strip (cut to 16 LEDs)
🎮

Try this circuit in your browser!

Run the code, press the buttons and watch what happens — before you buy any parts. No account needed.

Open in Simulator →

You press a button. A light turns on. Your beat plays forever.

Imagine this: a 4×4 grid of buttons sitting on your desk. You press button 1 — a red LED lights up. Press button 5 — another lights up. Press button 9 and 13. Hit Play. A kick drum pounds out exactly the beat you just programmed, the LEDs sweeping across as it plays.

That’s a drum machine. The Roland TR-808 invented this pattern in 1980 and every beat machine since uses the same idea. You’re building the core of it for $25.

Wiring diagram for Beat Box Drum Machine: esp32 s3 devkitc 1 connected to keypad, strip, Tempo, oled, amp


What you’ll need

Part What it does Price
ESP32-S3 Dev Board Brain. Runs the sequencer and drives audio. ~$12
4×4 button matrix (16 buttons) 16 sequencer steps — one button per step. ~$2
MAX98357A I2S Amplifier Digital audio signal → real speaker sound. ~$3
3W 4Ω speaker Makes actual noise. ~$2
10kΩ potentiometer Tempo knob — turn to change BPM. ~$1
0.96” OLED (SSD1306 I2C) Shows track name, BPM, and current step. ~$2
WS2812B LED strip (16 LEDs) One LED per step — lit when beat is on. ~$3

Total: ~$25 | Time: ~3 hours | Difficulty: ●●●○○

You’ll also need: 4 WAV audio files — kick.wav, snare.wav, hihat.wav, clap.wav. Keep them under 100ms, 16-bit, 44100 Hz mono. You can record them yourself or download free drum samples online. These get uploaded to the ESP32’s internal SPIFFS storage.


How it works (60 seconds)

Think of it like a grid of light switches arranged on a timeline.

Time is divided into 16 equal slices (called steps). For each step, each drum instrument is either ON or OFF. The sequencer sweeps through all 16 steps at a constant speed (set by BPM), triggering any instruments that are ON. When it reaches step 16, it loops back to step 1 — forever.

Your buttons toggle steps on/off. Your LEDs show the current pattern. The white LED sweeping across is your playhead — where you are in the loop right now.


Step 0: Prepare your drum samples

Time: ~10 minutes

Before wiring anything, get your WAV files ready.

Option A: Download free samples — search for “TR-808 samples free download” or “drum samples WAV free.” Look for 16-bit, 44100 Hz mono files.

Option B: Record your own — any recording app works. Clap your hands, hit a table, snap your fingers. Keep clips under 100ms each.

Upload to SPIFFS:

  1. Install the ESP32 SPIFFS Uploader plugin for Arduino IDE (search “Arduino ESP32 filesystem uploader”)
  2. Create a folder called data inside your Arduino sketch folder
  3. Copy your 4 WAV files into data/ — named exactly kick.wav, snare.wav, hihat.wav, clap.wav
  4. Use Tools → ESP32 Sketch Data Upload to write them to the board

Check: No error during upload = files are on the board. They survive power cycles.


Step 1: Wire it up

Time: ~20 minutes

MAX98357A I2S Amplifier (3 wires):

  1. MAX98357A BCLK → GPIO 39 (C6: GPIO 10)
  2. MAX98357A LRC → GPIO 40 (C6: GPIO 11)
  3. MAX98357A DIN → GPIO 41 (C6: GPIO 23)
  4. MAX98357A VIN → 5V — red wire
  5. MAX98357A GND → GND — black wire
  6. MAX98357A OUT+ → speaker +
  7. MAX98357A OUT- → speaker -

WS2812B LED Strip (3 wires): 8. LED DIN → GPIO 13 (C6: GPIO 5) — orange wire 9. LED VCC → 5V — red wire 10. LED GND → GND — black wire

Button Matrix (8 wires — 4 rows, 4 columns): 11. Row 0 → GPIO 4 (C6: GPIO 0) 12. Row 1 → GPIO 5 (C6: GPIO 19) 13. Row 2 → GPIO 6 (C6: GPIO 20) 14. Row 3 → GPIO 7 (C6: GPIO 21) 15. Col 0 → GPIO 15 (C6: GPIO 3) 16. Col 1 → GPIO 16 (C6: GPIO 4) 17. Col 2 → GPIO 17 (C6: GPIO 22) 18. Col 3 → GPIO 18 (C6: GPIO 18)

Tempo Potentiometer (3 wires): 19. Pot one end → 3.3V 20. Pot other end → GND 21. Pot wiper (middle) → GPIO 1 (C6: GPIO 2)

OLED (4 wires): 22. OLED SDA → GPIO 8 (C6: GPIO 6) 23. OLED SCL → GPIO 9 (C6: GPIO 7) 24. OLED VCC → 3.3V 25. OLED GND → GND

Check: Count 25 connections total. Rows go to OUTPUT pins (4–7; C6: 0, 19, 20, 21), columns go to INPUT_PULLUP pins (15–18; C6: 3, 4, 22, 18). This is the button matrix scanning trick — 16 buttons on only 8 pins.

Important: Add a 100Ω series resistor on the LED data line (between GPIO 13 (C6: GPIO 5) and the first LED’s DIN) to protect the first LED from voltage spikes.


Step 2: Upload the code

Time: ~5 minutes

Install libraries in Arduino IDE Library Manager:

  • FastLED — WS2812B control
  • Adafruit SSD1306 + Adafruit GFX Library — OLED
  • ESP8266Audio by Earle Philhower — WAV playback (works on ESP32 too)

The big picture first. A drum machine works like a grid of on/off switches arranged in time. Time is divided into 16 equal slices (steps). For each slice, each drum is either ON or OFF. A clock sweeps through all 16 steps at your BPM speed, triggers whatever is ON, then loops back to step 1 — forever. Your 16 buttons toggle steps on and off. The LEDs show which steps are lit. The white LED sweeping across is the playhead.

// ========== CHOOSE YOUR BOARD ==========
// Uncomment the line for YOUR board:
#define BOARD_S3    // ESP32-S3-DevKitC-1
//#define BOARD_C6  // ESP32-C6-DevKitC-1
// ========================================

#ifdef BOARD_S3
  #define PIN_ROW0             4
  #define PIN_ROW1             5
  #define PIN_ROW2             6
  #define PIN_ROW3             7
  #define PIN_COL0             15
  #define PIN_COL1             16
  #define PIN_COL2             17
  #define PIN_COL3             18
  #define PIN_TEMPO            1
  #define PIN_NEOPIXEL         13
  #define PIN_I2S_BCLK         39
  #define PIN_I2S_LRC          40
  #define PIN_I2S_DOUT         41
  #define PIN_SDA              8
  #define PIN_SCL              9
#endif
#ifdef BOARD_C6
  #define PIN_ROW0             0
  #define PIN_ROW1             19
  #define PIN_ROW2             20
  #define PIN_ROW3             21
  #define PIN_COL0             3
  #define PIN_COL1             4
  #define PIN_COL2             22
  #define PIN_COL3             18
  #define PIN_TEMPO            2
  #define PIN_NEOPIXEL         5
  #define PIN_I2S_BCLK         10
  #define PIN_I2S_LRC          11
  #define PIN_I2S_DOUT         23
  #define PIN_SDA              6
  #define PIN_SCL              7
#endif

#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <FastLED.h>
#include "AudioFileSourceSPIFFS.h"
#include "AudioGeneratorWAV.h"
#include "AudioOutputI2S.h"

const int ROW_PINS[4] = {PIN_ROW0, PIN_ROW1, PIN_ROW2, PIN_ROW3};
const int COL_PINS[4] = {PIN_COL0, PIN_COL1, PIN_COL2, PIN_COL3};
const int TEMPO_PIN   = PIN_TEMPO;
const int LED_PIN     = PIN_NEOPIXEL;
const int I2S_BCLK    = PIN_I2S_BCLK;
const int I2S_LRC     = PIN_I2S_LRC;
const int I2S_DOUT    = PIN_I2S_DOUT;

#define NUM_LEDS 16
CRGB leds[NUM_LEDS];

const char* SAMPLES[4] = {"/kick.wav", "/snare.wav", "/hihat.wav", "/clap.wav"};
const CRGB  DRUM_COLORS[4] = {CRGB::Red, CRGB::Blue, CRGB::Yellow, CRGB::Green};
const char* DRUM_NAMES[4]  = {"KICK", "SNARE", "HAT", "CLAP"};

bool pattern[4][16] = {{false}};
int  currentTrack   = 0;
int  currentStep    = 0;
bool isPlaying      = true;
int  bpm            = 120;
unsigned long lastStep = 0;

AudioFileSourceSPIFFS* source   = nullptr;
AudioGeneratorWAV*     wav      = nullptr;
AudioOutputI2S*        i2sOut   = nullptr;

Adafruit_SSD1306 display(128, 64, &Wire, -1);

void playDrum(int track) {
  if (wav && wav->isRunning()) {
    wav->stop();
    source->close();
  }
  if (!i2sOut) {
    i2sOut = new AudioOutputI2S();
    i2sOut->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
    i2sOut->SetGain(0.8);
  }
  source = new AudioFileSourceSPIFFS(SAMPLES[track]);
  wav    = new AudioGeneratorWAV();
  wav->begin(source, i2sOut);
}

int scanMatrix() {
  for (int r = 0; r < 4; r++) {
    for (int i = 0; i < 4; i++) digitalWrite(ROW_PINS[i], HIGH);
    digitalWrite(ROW_PINS[r], LOW);
    delayMicroseconds(10);
    for (int c = 0; c < 4; c++) {
      if (digitalRead(COL_PINS[c]) == LOW) {
        for (int i = 0; i < 4; i++) digitalWrite(ROW_PINS[i], HIGH);
        return r * 4 + c;
      }
    }
  }
  for (int i = 0; i < 4; i++) digitalWrite(ROW_PINS[i], HIGH);
  return -1;
}

void updateLEDs() {
  for (int i = 0; i < 16; i++) {
    if (i == currentStep && isPlaying) {
      leds[i] = CRGB::White;
    } else if (pattern[currentTrack][i]) {
      leds[i] = DRUM_COLORS[currentTrack];
    } else {
      leds[i] = CRGB(8, 8, 8);
    }
  }
  FastLED.show();
}

void setup() {
  for (int i = 0; i < 4; i++) {
    pinMode(ROW_PINS[i], OUTPUT);
    digitalWrite(ROW_PINS[i], HIGH);
    pinMode(COL_PINS[i], INPUT_PULLUP);
  }

  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(60);

  Wire.begin(PIN_SDA, PIN_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(WHITE);

  SPIFFS.begin(true);
}

void loop() {
  int potVal = analogRead(TEMPO_PIN);
  bpm = map(potVal, 0, 4095, 60, 200);

  unsigned long stepMs = 60000UL / (bpm * 4);
  if (isPlaying && (millis() - lastStep >= stepMs)) {
    lastStep = millis();
    for (int t = 0; t < 4; t++) {
      if (pattern[t][currentStep]) {
        playDrum(t);
        break;
      }
    }
    currentStep = (currentStep + 1) % 16;
  }

  if (wav && wav->isRunning()) {
    wav->loop();
  }

  static int lastPressed = -1;
  static unsigned long lastPressTime = 0;
  int pressed = scanMatrix();
  if (pressed != lastPressed && pressed != -1) {
    if (millis() - lastPressTime > 50) {
      pattern[currentTrack][pressed] = !pattern[currentTrack][pressed];
      lastPressTime = millis();
    }
  }
  lastPressed = pressed;

  updateLEDs();

  static unsigned long lastDisplay = 0;
  if (millis() - lastDisplay > 50) {
    lastDisplay = millis();
    display.clearDisplay();
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.print(DRUM_NAMES[currentTrack]);
    display.print("  BPM:");
    display.print(bpm);
    display.setCursor(0, 10);
    display.print("STEP:");
    display.print(currentStep + 1);
    display.print("/16");

    for (int i = 0; i < 16; i++) {
      int px = (i % 8) * 8;
      int py = 30 + (i / 8) * 10;
      if (pattern[currentTrack][i]) {
        display.fillRect(px, py, 6, 8, WHITE);
      } else {
        display.drawRect(px, py, 6, 8, WHITE);
      }
    }
    display.display();
  }
}

Line-by-line: what every line does and why

Lines 1–6: Instruction books

Three audio books: Adafruit_SSD1306 for the screen, FastLED for the LEDs, and three audio books (AudioFileSourceSPIFFS, AudioGeneratorWAV, AudioOutputI2S) that work together like a chain — read file from flash → decode WAV → send to speaker.


Lines 8–16: Pin assignments

const int ROW_PINS[4] = {PIN_ROW0, PIN_ROW1, PIN_ROW2, PIN_ROW3};
const int COL_PINS[4] = {PIN_COL0, PIN_COL1, PIN_COL2, PIN_COL3};

const int means “a number that never changes.” Instead of 16 individual button pins, you use a 4×4 matrix: 4 row output pins + 4 column input pins = 8 pins for 16 buttons. This is how every keyboard in the world works.


Lines 22–26: Drum track data

const char* SAMPLES[4] = {"/kick.wav", "/snare.wav", "/hihat.wav", "/clap.wav"};
const CRGB  DRUM_COLORS[4] = {CRGB::Red, CRGB::Blue, CRGB::Yellow, CRGB::Green};

These are arrays — lists with four slots each. Slot 0 = kick, slot 1 = snare, etc. When currentTrack = 0, the LEDs show red and the audio plays kick.wav. Using arrays like this means you can change all four tracks by editing one list.


Lines 28–34: Sequencer state

bool pattern[4][16] = {{false}};
int  currentStep    = 0;
int  bpm            = 120;

pattern[4][16] is a grid — 4 rows (drum tracks) × 16 columns (time steps). Each cell is true (beat on) or false (beat off). currentStep is the playhead position — which column is playing right now. bpm = 120 means 120 beats per minute — standard dance tempo.


scanMatrix(): Reading 16 buttons with 8 pins

for (int r = 0; r < 4; r++) {
  for (int i = 0; i < 4; i++) digitalWrite(ROW_PINS[i], HIGH);
  digitalWrite(ROW_PINS[r], LOW);
  delayMicroseconds(10);
  for (int c = 0; c < 4; c++) {
    if (digitalRead(COL_PINS[c]) == LOW) {
      return r * 4 + c;
    }
  }
}
return -1;

The matrix trick: set all rows HIGH, then lower one row at a time. If a button in that row is pressed, its column pin reads LOW (because the pressed button connects the LOW row to the column). r * 4 + c converts (row, column) to a number 0–15. Returns -1 if no button is pressed.


loop() — BPM from potentiometer

int potVal = analogRead(TEMPO_PIN);
bpm = map(potVal, 0, 4095, 60, 200);

analogRead returns 0–4095 (the ESP32 has a 12-bit ADC — 4096 possible values). map() converts that range to 60–200 BPM. Turn the pot clockwise: BPM goes up. Counter-clockwise: it slows down.


loop() — Sequencer clock

unsigned long stepMs = 60000UL / (bpm * 4);
if (isPlaying && (millis() - lastStep >= stepMs)) {
  currentStep = (currentStep + 1) % 16;
}

60000 / (bpm × 4) calculates how many milliseconds between steps. At 120 BPM: 60000 / 480 = 125ms per step. millis() is the stopwatch — when enough time has passed, advance to the next step. % 16 wraps around after step 15 back to step 0 — the loop repeats forever.


loop() — Button toggle

pattern[currentTrack][pressed] = !pattern[currentTrack][pressed];

! means NOT. If the cell was false (beat off), it becomes true (beat on). If it was true, it becomes false. One line — a perfect toggle. The 50ms debounce check before it prevents a single button press from toggling multiple times.


The whole thing in one sentence

The loop reads the tempo pot, checks if enough time has passed to advance the step, plays whichever drums are ON at this step, reads button presses to toggle the pattern, and updates LEDs and the OLED screen 20 times per second.

First thing to try: Upload with all 4 WAV files on SPIFFS. The white LED should sweep across immediately. Press button 1 (step 1) — a kick drum should play when the white light reaches it.


Step 3: Config — switch between drum tracks

Time: ~2 minutes

Right now currentTrack is always 0 (kick). To program all 4 drums, you need to switch tracks.

Quick fix: Add a dedicated “track select” button. Wire a button between any free GPIO (say GPIO 0; C6: GPIO 9) and GND. In the loop, detect a button press and cycle currentTrack through 0, 1, 2, 3. The LED color shows which track you’re editing — red for kick, blue for snare, yellow for hi-hat, green for clap.

The OLED already shows the track name, so you always know which drum you’re programming.


Step 4: Program your first beat!

Time: ~2 minutes

The sequencer starts running immediately at 120 BPM (white LED sweeping). The 4×4 grid maps to 16 steps left-to-right, top-to-bottom.

Classic hip-hop beat: Kick on steps 1 and 9, snare on steps 5 and 13, hi-hat on all 16 steps. That’s the foundation of most hip-hop.

Basic rock beat: Kick on steps 1 and 9, snare on steps 5 and 13, hi-hat on every other step (1, 3, 5, 7…).

Techno: Kick on every 4th step (1, 5, 9, 13). Just that. Nothing else. Feel the difference?

Turn the potentiometer to change the tempo. Watch the LEDs react — the playhead sweeps faster.


What just happened (what you learned)

  • I2S (Inter-IC Sound) — a digital audio protocol: three wires (BCLK, LRC, DATA) that carry audio as a stream of numbers. The MAX98357A receives these numbers and drives a speaker directly. All digital, no analog noise.

  • Button Matrix — how keyboards and drum pads squeeze many buttons onto few pins. 16 buttons normally need 16 GPIO pins. The matrix uses 8: 4 row outputs + 4 column inputs. You drive one row LOW at a time and read which column is also LOW. The button at that intersection is pressed. This is how every keyboard in the world works.

  • SPIFFS (Serial Peripheral Interface Flash File System) — the ESP32’s onboard storage, a 4MB flash chip that acts like a tiny hard drive. You upload files once and they survive forever, including reboots. That’s why this machine sounds like real drums instead of synthesized bleeps.

  • Sequencer timing — the sequencer fires a new step every 60000 / (BPM × 4) milliseconds. At 120 BPM: 60000 / 480 = 125ms per step. Sixteen steps × 125ms = 2 seconds = one bar. The human brain starts feeling “groove” around 80–160 BPM.


Level Up

Add swing. Real drum machines don’t play on a perfectly even grid — that sounds robotic. Swing delays every odd step slightly. Add a swingAmount variable (0–30ms) and add it to odd-numbered steps: if (currentStep % 2 == 1) delay(swingAmount);. Does it feel different? That shuffle is the soul of jazz and hip-hop.

Add track switching button. Wire a button to cycle currentTrack through 0, 1, 2, 3 and program all four drum voices independently.

Upgrade to NeoTrellis. The Adafruit NeoTrellis is a 4×4 grid of NeoPixel-backlit silicone buttons that communicates over I2C — purpose-built for exactly this application. It replaces the button matrix + LED strip combination with one clean board.

★★ You completed: Beat Box Drum Machine!


Troubleshooting

Problem Fix
No sound from speaker Check MAX98357A wiring: BCLK→39, LRC→40, DIN→41 (C6: BCLK→10, LRC→11, DIN→23). Check speaker polarity (OUT+ / OUT-). Check 5V power to VIN.
WAV files not found Did SPIFFS upload succeed? Check that files are named exactly kick.wav etc. (lowercase). Check SPIFFS.begin() in setup().
LEDs all stay off Check DIN wire on GPIO 13 (C6: GPIO 5) with 100Ω resistor. Check 5V power to LED VCC.
Button doesn’t toggle step Check matrix scan: rows on OUTPUT pins (4–7; C6: 0, 19, 20, 21), columns on INPUT_PULLUP pins (15–18; C6: 3, 4, 22, 18).
Tempo doesn’t change Check pot wiper on GPIO 1 (C6: GPIO 2) (ADC). Check 3.3V/GND on pot ends (not 5V — ADC max is 3.3V).
OLED shows nothing Check SDA/SCL on GPIO 8/9 (C6: GPIO 6/7). I2C address 0x3C or try 0x3D. Check 3.3V power.

Affiliate disclosure: Some links on this page are affiliate links. If you buy through them, we may earn a small commission at no extra cost to you.