midi-breadboard-instrument/README.md

10 KiB
Raw Blame History

🎛️ MIDI Breadboard Instrument — Desk Rave & 16-Track Symphony Controller

ESP-IDF MCU Language License: MIT

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).


Full Breadboard Overview Active OLED Visualizer & Controls Multi-Buzzer & Rotary Wiring
Breadboard Overview Active Visualizer Wiring Detail
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

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 .mid files

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_drum and hi_hat triggers.