# πŸŽ›οΈ MIDI Breadboard Instrument β€” Desk Rave & 16-Track Symphony Controller [![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.2.2-E7352C?logo=espressif&logoColor=white)](https://docs.espressif.com/projects/esp-idf/en/v5.2.2/esp32/) [![MCU](https://img.shields.io/badge/MCU-ESP32%20Dual--Core%20Xtensa-blue)](https://www.espressif.com/en/products/socs/esp32) [![Language](https://img.shields.io/badge/Language-ISO%20C99%20%2B%20Python%203-00599C)](main/mmcontroller.c) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 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 | Full Breadboard Overview | Active OLED Visualizer & Controls | Multi-Buzzer & Rotary Wiring | | :---: | :---: | :---: | | ![Breadboard Overview](images/MIDI_FIRST.jpg) | ![Active Visualizer](images/MIDI_SECOND.jpg) | ![Wiring Detail](images/MIDI_THIRD.jpg) | | *Dual-breadboard layout featuring ESP32, 1.3" SPI OLED, rotary encoder, tactile buttons, passive melody piezo, and dual active drum buzzers.* | *Live 30 FPS OLED HUD showing track title (`WHISP`), playback state, `SPD`/`VOL` multiplier, kick/hi-hat drum boxes, and frequency bars.* | *Close-up of the KY-040 rotary encoder, tactile control buttons, kick/hi-hat LED indicators, and triple-buzzer acoustic stage.* | --- ## πŸ“– 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: 1. **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. 2. **Custom RLE Python Compiler (`scripts/midi_to_c.py`):** We engineered a custom `mido`-based Python parser with a **Run-Length Encoding (RLE)** state machine that coalesces contiguous identical frequency/drum states into solid `SequenceStep` blocks and routes MIDI Channel 10 (plus low bass notes `< MIDI 50`) to hardware percussion triggers. 3. **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. 4. **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) and `button_task` (50 ms debounced input polling). - **Core 1 (Real-Time Music Engine):** Runs `buzzer_sequence_task`, reading `SequenceStep` arrays 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. --- ## 🧠 2. System Architecture ```mermaid 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`](main/mmcontroller.c) using a compact 4-field C struct: ```c 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 `.mid` files ### Build & Flash Commands ```bash # 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`) ```bash python3 scripts/midi_to_c.py scripts/midillis/Alla_Turca/Alla_Turca.mid > my_track.h ``` --- ## πŸ“‚ Repository Structure ```text β”œβ”€β”€ 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_drum` and `hi_hat` triggers.