| images | ||
| main | ||
| scripts | ||
| .gitignore | ||
| CMakeLists.txt | ||
| LICENSE | ||
| README.md | ||
| sdkconfig | ||
🎛️ MIDI Breadboard Instrument — Desk Rave & 16-Track Symphony Controller
A standalone, dual-core ESP32 MIDI Synthesizer, Drum Sequencer, and 30 FPS OLED Visualizer built from scratch in ESP-IDF v5.2.2 (C99 / FreeRTOS), paired with a custom Run-Length Encoding (RLE) Python MIDI-to-C compiler (scripts/midi_to_c.py).
📸 Breadboard Hardware Gallery
📖 1. Engineering Origin & Technical Challenges Solved
This project began as a bare-metal hardware exploration to push an ESP32 and simple piezo/magnetic transducers to their physical limits—evolving from a single-tone passive buzzer test into a 16-track polyphonic-illusion "Desk Rave" synthesizer.
Key embedded engineering challenges solved during development:
- The "Strobe-Light" MIDI Fragmentation Bug: Raw MIDI files frequently split sustained notes into microscopic delta-time slices, causing the LEDs and OLED visualizer to strobe at 30+ Hz and exhausting flash memory.
- Custom RLE Python Compiler (
scripts/midi_to_c.py): We engineered a custommido-based Python parser with a Run-Length Encoding (RLE) state machine that coalesces contiguous identical frequency/drum states into solidSequenceStepblocks and routes MIDI Channel 10 (plus low bass notes< MIDI 50) to hardware percussion triggers. - Piezo Acoustic Resonance Limits: Passive piezo buzzers physically struggle to resonate at low fundamental bass frequencies. Tracks with deep basslines—such as Seven Nation Army—were transposed +2 full octaves so the piezo disc resonates clearly while retaining rhythmic punch via the dedicated Kick/Hi-Hat active buzzers.
- Dual-Core FreeRTOS Lockless IPC: To prevent SPI display transfers from introducing timing jitter into microsecond-sensitive audio generation, the firmware partitions tasks across both Tensilica Xtensa LX6 CPU cores:
- Core 0 (UI & Human Interface): Runs
oled_visualizer_task(30 FPS framebuffer renderer using a zero-dependency custom 5×7 font byte array) andbutton_task(50 ms debounced input polling). - Core 1 (Real-Time Music Engine): Runs
buzzer_sequence_task, readingSequenceSteparrays from flash, polling the hardware PCNT (Pulse Counter) quadrature rotary encoder for real-time tempo (0.2x–3.0x) and PWM duty-cycle volume (0%–100%) scaling, and driving the 13-bit hardware LEDC PWM timer.
- Core 0 (UI & Human Interface): Runs
🧠 2. System Architecture
flowchart LR
subgraph Host["Host PC Tooling (Python 3)"]
MIDI["Standard .mid Files"] --> PARSER["scripts/midi_to_c.py (RLE Compressor + Drum Router)"]
end
subgraph ESP32["ESP32 Dual-Core Firmware (ESP-IDF v5.2.2)"]
PARSER -->|Generates C Struct Arrays| FLASH["Flash Playlist (16 Tracks / SequenceStep[])"]
subgraph Core1["Core 1: Real-Time Audio Engine"]
FLASH --> ENGINE["buzzer_sequence_task"]
PCNT["Hardware PCNT Unit (Rotary CLK GPIO 32 / DT GPIO 33)"] --> ENGINE
end
subgraph Core0["Core 0: UI & Input Polling"]
BTN["button_task (Pause GPIO 13 / Next GPIO 14 / Mode SW GPIO 16)"]
OLED_TASK["oled_visualizer_task (30 FPS Framebuffer)"]
end
BTN <-->|Volatile IPC Flags| ENGINE
ENGINE -->|Volatile Pitch & Drum State| OLED_TASK
end
subgraph Hardware["Breadboard Peripherals"]
ENGINE -->|LEDC 13-Bit PWM GPIO 25| P_BUZ["Passive Buzzer (Main Melody)"]
ENGINE -->|GPIO 26 + GPIO 21| KICK["Active Buzzer 1 + Kick LED"]
ENGINE -->|GPIO 27 + GPIO 22| HAT["Active Buzzer 2 + Hi-Hat LED"]
OLED_TASK -->|10 MHz SPI2 DMA| OLED["1.3-inch SPI OLED (SH1106 / SSD1306)"]
end
🎵 3. The 16-Track Flash Symphony (SequenceStep)
All 16 tracks are compiled directly into ESP32 flash memory inside main/mmcontroller.c using a compact 4-field C struct:
typedef struct {
uint16_t frequency; // Melody pitch in Hz (0 = REST)
uint16_t duration; // Step duration in ms (scaled live by current_speed)
bool kick_drum; // Triggers Active Buzzer 1 (GPIO 26) + Kick LED (GPIO 21)
bool hi_hat; // Triggers Active Buzzer 2 (GPIO 27) + Hat LED (GPIO 22)
} SequenceStep;
| Index | OLED Tag | Track Title | Arrangement / Engineering Notes |
|---|---|---|---|
0 |
WHISP |
Careless Whisper | Extended Club Edit with synthesized four-on-the-floor kick/hat groove |
1 |
TAKE |
Take On Me | High-tempo EDM build-up arrangement |
2 |
RICK |
Never Gonna Give You Up (Rickroll) | Double chorus arrangement with syncopated percussion |
3 |
ELVIS |
Can't Help Falling In Love | Arpeggiated ballad / wedding mix |
4 |
GIMME |
Gimme! Gimme! Gimme! | ABBA midnight rave synth lead |
5 |
ELISE |
Für Elise | Beethoven trance remix |
6 |
MOZRT |
Symphony No. 40 | Mozart intense strings arrangement |
7 |
MARIO |
Super Mario Bros. Theme | RLE-compressed MIDI conversion with percussion routing |
8 |
PIRT1 |
Pirates of the Caribbean 1 | He's a Pirate — RLE MIDI conversion |
9 |
PIRT2 |
Pirates of the Caribbean 2 | The Black Pearl — RLE MIDI conversion |
10 |
ALLA |
Rondo Alla Turca | Mozart Turkish March (scripts/midillis/Alla_Turca/Alla_Turca.mid) |
11 |
TETRS |
Tetris (Korobeiniki) | Classic Type-A theme |
12 |
SONIC |
Sonic the Hedgehog | Green Hill Zone |
13 |
CAST |
Castlevania | Vampire Killer |
14 |
SEVEN |
Seven Nation Army | Transposed +2 octaves to overcome passive piezo bass roll-off |
15 |
MEGA |
Mega Man 2 | Dr. Wily Stage 1 |
🧱 4. Hardware Pinout & Peripherals
Target Microcontroller: ESP32 Development Board (Dual-Core Tensilica Xtensa LX6 @ 160 MHz)
🎛️ Inputs (Hardware PCNT & Active-Low Buttons)
| Component | ESP32 GPIO | Peripheral / Mode | Function |
|---|---|---|---|
| Rotary Encoder CLK | GPIO 32 |
Hardware PCNT Edge/Level |
Quadrature rotation tracking (1000 ns hardware glitch filter) |
| Rotary Encoder DT | GPIO 33 |
Hardware PCNT Edge/Level |
Quadrature rotation direction |
| Rotary Encoder SW | GPIO 16 |
Input (GPIO_PULLUP_ONLY) |
Toggles rotary knob mode between Speed (SPD) and Volume (VOL) |
| Pause Button | GPIO 13 |
Input (GPIO_PULLUP_ONLY) |
Play / Pause toggle (immediately silences outputs when paused) |
| Next Track Button | GPIO 14 |
Input (GPIO_PULLUP_ONLY) |
Cycles through the 16-track playlist (0 → 15) |
🔊 Audio & LED Outputs
| Component | ESP32 GPIO | Peripheral / Mode | Function |
|---|---|---|---|
| Passive Buzzer | GPIO 25 |
LEDC_CHANNEL_0 (13-bit PWM) |
Plays main melody pitch (step.frequency) & duty-cycle volume |
| Active Buzzer 1 | GPIO 26 |
Digital Output | Kick Drum acoustic transient (45 ms scaled pulse) |
| Active Buzzer 2 | GPIO 27 |
Digital Output | Hi-Hat acoustic transient (45 ms scaled pulse) |
| LED 1 (Kick) | GPIO 21 |
Digital Output | Flashes in sync with Kick Drum hits |
| LED 2 (Hi-Hat) | GPIO 22 |
Digital Output | Flashes in sync with Hi-Hat hits |
📺 1.3" 7-Pin SPI OLED Display (SH1106 / SSD1306, 128×64)
| OLED Pin | ESP32 GPIO | SPI2 Function |
|---|---|---|
| CS | GPIO 5 |
SPI Chip Select (10 MHz clock) |
| DC | GPIO 2 |
Data / Command control line |
| RES | GPIO 4 |
Hardware Reset line |
| SDA (MOSI) | GPIO 23 |
SPI2 Master Out Slave In |
| SCL (SCK) | GPIO 18 |
SPI2 Serial Clock |
🛠️ 5. Building & Flashing (ESP-IDF v5.2.2)
Prerequisites
- ESP-IDF v5.2.2 installed and exported (
source ~/esp/esp-idf/export.sh) - Python 3 with
mido(pip install mido) if converting custom.midfiles
Build & Flash Commands
# 1. Source the ESP-IDF environment
source ~/esp/esp-idf/export.sh
# 2. Build the firmware image
idf.py build
# 3. Flash to the ESP32 and open the serial monitor
idf.py -p /dev/ttyUSB0 flash monitor
Converting Custom MIDI Files (scripts/midi_to_c.py)
python3 scripts/midi_to_c.py scripts/midillis/Alla_Turca/Alla_Turca.mid > my_track.h
📂 Repository Structure
├── CMakeLists.txt # Top-level ESP-IDF project build configuration
├── sdkconfig # ESP32 target configuration (Dual-Core @ 160 MHz)
├── main/
│ ├── CMakeLists.txt # Component registration
│ └── mmcontroller.c # Complete 16-track synth, OLED driver, PCNT & FreeRTOS tasks
├── scripts/
│ ├── midi_to_c.py # Custom Python RLE MIDI-to-C struct converter
│ └── midillis/ # Sample MIDI source tracks (Alla Turca, etc.)
└── images/
├── MIDI_FIRST.jpg # Full breadboard hardware build photo
├── MIDI_SECOND.jpg # Active OLED visualizer & control interface photo
└── MIDI_THIRD.jpg # Buzzer & rotary encoder wiring close-up
🗺️ Roadmap & Future Iterations
- Perfboard / Custom PCB Migration: Consolidating the dual-breadboard layout onto a soldered perfboard or custom KiCad PCB.
- Optional WS2812B Addressable RGB Strip Expansion: Re-introducing RMT-driven RGB lighting effects synchronized to
kick_drumandhi_hattriggers.


