ESP32 Drum Step Sequencer: Here is my ESP32 project — a 16-step drum sequencer. It's easy to get the parts: 4x4 matrix buttons, a rotary encoder, SD card reader, and a 128x64 OLED display. You can download the full .ino zip file below.
- Kick, Snare, Hi-Hat samples
- Bluetooth A2DP output
- Real-time step sequencing via matrix buttons
- SD card support for loading custom samples
- Rotary encoder UI with OLED feedback
- Adjustable BPM
📂 Main ino code is below , view Full Code on GitHub
#include <Arduino.h>
#include <BluetoothA2DPSource.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "kick.h"
#include "snare.h"
#include "hihat.h"
#include <SPI.h>
#include "SDCard.h"
#include "Tus.h"
#include "RotaryEncoder.h"
// --- Keypad Setup
Tus tus;
// --- Rotary Encoder Pins
RotaryEncoder encoder; // uses default pins: 32, 33, 25
enum UIState { NAVIGATE, BPM_EDIT, SUB_SD_KICK ,SUB_SD_SNARE,SUB_SD_HIHAT};
UIState uiState = NAVIGATE;
enum Instrument { KICK, SNARE, HIHAT };
Instrument currentInstrument = KICK;
enum MenuItem { MENU_KICK, MENU_SNARE, MENU_HIHAT, MENU_BPM, MENU_COUNT };
MenuItem rotatedItem = MENU_KICK;
enum KickType { STANDARD_KICK, SD_DATA_SAMPLE_KICK };
KickType currentKick = STANDARD_KICK;
enum SnareType { STANDARD_SNARE, SD_DATA_SAMPLE_SNARE };
SnareType currentSnare = STANDARD_SNARE;
enum HihatType { STANDARD_HIHAT, SD_DATA_SAMPLE__HIHAT };
HihatType currentHihat = STANDARD_HIHAT;
SDCard sd;
#define SAMPLE_SIZE (13 * 1024 / 2)
int sampleLength1_sd_kick = 0;
int16_t buffer1_sd_kick[SAMPLE_SIZE];
int index_sd_kick = 0;
int sampleLength2_sd_snare = 0;
int16_t buffer2_sd_snare [SAMPLE_SIZE];
int index_sd_snare = 0;
int sampleLength3_sd_hihat= 0;
int16_t buffer3_sd_hihat [SAMPLE_SIZE];
int index_sd_hihat = 0;
#define OLED_SDA 4
#define OLED_SCL 15
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
BluetoothA2DPSource a2dp_source;
const int sampleRate = 44100;
float bpm = 60.0f;
int step_duration_samples = (int)((60.0f / bpm / 4.0f) * sampleRate);
const int NUM_STEPS = 16;
bool kick_steps[NUM_STEPS] = {false};
bool snare_steps[NUM_STEPS] = {false};
bool hihat_steps[NUM_STEPS] = {false};
int current_step = 0;
size_t sample_in_step = 0;
bool playing_kick = false, playing_snare = false, playing_hihat = false;
size_t kick_sample_index = 0, snare_sample_index = 0, hihat_sample_index = 0;
int32_t audio_data_callback(uint8_t *data, int32_t len) {
int16_t *buffer = (int16_t *)data;
int samples = len / 2;
for (int i = 0; i < samples; i++) {
if (sample_in_step == 0) {
if (kick_steps[current_step]) { playing_kick = true; kick_sample_index = 0; }
if (snare_steps[current_step]) { playing_snare = true; snare_sample_index = 0; }
if (hihat_steps[current_step]) { playing_hihat = true; hihat_sample_index = 0; }
}
int32_t sample = 0;
if (playing_kick) {
if (currentKick == STANDARD_KICK && kick_sample_index < kick_data_len) {
sample += kick_data[kick_sample_index++];
} else if (currentKick == SD_DATA_SAMPLE_KICK && kick_sample_index < sampleLength1_sd_kick) {
sample += buffer1_sd_kick[kick_sample_index++];
} else playing_kick = false;
}
if (playing_snare) {
if (currentSnare == STANDARD_SNARE && snare_sample_index < snare_data_len) {
sample += snare_data[snare_sample_index++];
} else if (currentSnare == SD_DATA_SAMPLE_SNARE && snare_sample_index < sampleLength2_sd_snare) {
sample += buffer2_sd_snare[snare_sample_index++];
} else playing_snare = false;
}
if (playing_hihat) {
if (currentHihat == STANDARD_HIHAT && hihat_sample_index < hihat_data_len) {
sample += hihat_data[hihat_sample_index++];
} else if (currentHihat == SD_DATA_SAMPLE__HIHAT && hihat_sample_index < sampleLength3_sd_hihat) {
sample += buffer3_sd_hihat[hihat_sample_index++];
} else playing_hihat = false;
}
sample = constrain(sample, -32768, 32767);
buffer[i] = (int16_t)sample;
sample_in_step++;
if (sample_in_step >= step_duration_samples) {
sample_in_step = 0;
current_step = (current_step + 1) % NUM_STEPS;
}
}
return len;
}
void drawSequencerState() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_5x7_tr);
char status[32] = "";
u8g2.drawStr(0, 7, "Kck");
u8g2.drawStr(0, 23, "Snr");
u8g2.drawStr(0, 39, "Hht");
for (int i = 0; i < NUM_STEPS; i++) {
int x = 20 + i * 6;
if (kick_steps[i]) u8g2.drawBox(x, 0, 5, 5); else u8g2.drawFrame(x, 0, 5, 5);
if (snare_steps[i]) u8g2.drawBox(x, 16, 5, 5); else u8g2.drawFrame(x, 16, 5, 5);
if (hihat_steps[i]) u8g2.drawBox(x, 32, 5, 5); else u8g2.drawFrame(x, 32, 5, 5);
}
int cursorX = 20 + current_step * 6;
u8g2.drawLine(cursorX, 0, cursorX, 38);
if (uiState == NAVIGATE) {
switch (rotatedItem) {
case MENU_KICK: strcpy(status, "SELECT: KICK"); break;
case MENU_SNARE: strcpy(status, "SELECT: SNARE"); break;
case MENU_HIHAT: strcpy(status, "SELECT: HIHAT"); break;
case MENU_BPM: sprintf(status, "SELECT: BPM (%.0f)", bpm); break;
}
} else if (uiState == BPM_EDIT) {
sprintf(status, "EDIT BPM: %.0f", bpm);
} else if (uiState == SUB_SD_KICK) {
strcpy(status, sd.sampleNames[index_sd_kick].c_str());
} else if (uiState == SUB_SD_SNARE) {
strcpy(status, sd.sampleNames[index_sd_snare].c_str());
} else if (uiState == SUB_SD_HIHAT) {
strcpy(status, sd.sampleNames[index_sd_hihat].c_str());
}
u8g2.drawStr(0, 60, status);
u8g2.sendBuffer();
}
void setup() {
Serial.begin(115200);
sd.begin();
sd.saveSampleNames("/seq");
sd.printStoredFileNames();
Wire.begin(OLED_SDA, OLED_SCL);
u8g2.begin();
encoder.begin();
a2dp_source.set_local_name("ESP32_DrumSeq");
a2dp_source.set_data_callback(audio_data_callback);
a2dp_source.start("NOVA MINI");
}
void loop() {
encoder.loop();
if (encoder.wasButtonPressed()) {
switch (uiState) {
case NAVIGATE:
if (rotatedItem == MENU_KICK) uiState = SUB_SD_KICK;
else if (rotatedItem == MENU_SNARE) uiState = SUB_SD_SNARE;
else if (rotatedItem == MENU_HIHAT) uiState = SUB_SD_HIHAT;
else if (rotatedItem == MENU_BPM) uiState = BPM_EDIT;
break;
case BPM_EDIT:
uiState = NAVIGATE;
break;
case SUB_SD_KICK:
if (sd.readSampleToBuffer16Bit(index_sd_kick, buffer1_sd_kick, SAMPLE_SIZE, sampleLength1_sd_kick)) {
currentKick = SD_DATA_SAMPLE_KICK;
} else currentKick = STANDARD_KICK;
uiState = NAVIGATE;
break;
case SUB_SD_SNARE:
if (sd.readSampleToBuffer16Bit(index_sd_snare, buffer2_sd_snare, SAMPLE_SIZE, sampleLength2_sd_snare)) {
currentSnare = SD_DATA_SAMPLE_SNARE;
} else currentSnare = STANDARD_SNARE;
uiState = NAVIGATE;
break;
case SUB_SD_HIHAT:
if (sd.readSampleToBuffer16Bit(index_sd_hihat, buffer3_sd_hihat, SAMPLE_SIZE, sampleLength3_sd_hihat)) {
currentHihat = SD_DATA_SAMPLE__HIHAT;
} else currentHihat = STANDARD_HIHAT;
uiState = NAVIGATE;
break;
}
}
int delta = encoder.getDelta();
if (delta != 0) {
switch (uiState) {
case NAVIGATE:
rotatedItem = (MenuItem)((rotatedItem + delta + MENU_COUNT) % MENU_COUNT);
if (rotatedItem == MENU_KICK) currentInstrument = KICK;
else if (rotatedItem == MENU_SNARE) currentInstrument = SNARE;
else if (rotatedItem == MENU_HIHAT) currentInstrument = HIHAT;
break;
case BPM_EDIT:
bpm += delta;
bpm = constrain(bpm, 40.0f, 300.0f);
step_duration_samples = (int)((60.0f / bpm / 4.0f) * sampleRate);
break;
case SUB_SD_KICK:
index_sd_kick = constrain(index_sd_kick + delta, 0, sd.fileCount - 1);
break;
case SUB_SD_SNARE:
index_sd_snare = constrain(index_sd_snare + delta, 0, sd.fileCount - 1);
break;
case SUB_SD_HIHAT:
index_sd_hihat = constrain(index_sd_hihat + delta, 0, sd.fileCount - 1);
break;
}
}
char key = tus.keypad.getKey();
if (key) {
int step = tus.mapKeyToStep(key);
if (step >= 0 && step < NUM_STEPS) {
if (currentInstrument == KICK) kick_steps[step] = !kick_steps[step];
else if (currentInstrument == SNARE) snare_steps[step] = !snare_steps[step];
else if (currentInstrument == HIHAT) hihat_steps[step] = !hihat_steps[step];
}
}
static unsigned long lastDraw = 0;
if (millis() - lastDraw > 100) {
drawSequencerState();
lastDraw = millis();
}
}
📂 View Full Code on GitHub