midi-breadboard-instrument/README.md

184 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🎛️ 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.