- 02 // Engineering Challenges Solved
- From a Squeaking Buzzer to a Dual-Core Synthesizer
-
- Pushing a bare-metal breadboard setup to play recognizable, energetic multi-track music required solving several real embedded systems challenges:
+
+
+ 02 // Terminal Commands, Python RLE Compiler & C99 Firmware
+ Build, Flash & MIDI Conversion Commands
+
+ Clone the repository, convert any `.mid` file into an RLE-compressed C struct array using `scripts/midi_to_c.py`, and flash the 16-track firmware to an ESP32:
-
- - The Strobe-Light Bug & RLE Compression (
midi_to_c.py): Raw MIDI files often slice sustained notes into microscopic fragments, causing the LEDs and OLED UI to strobe at 30+ Hz while wasting flash memory. We built a custom Python Run-Length Encoding (RLE) compressor using mido that merges contiguous identical pitch/drum states into solid SequenceStep blocks and routes bass/percussion channels to dedicated drum triggers.
- - Acoustic Hardware Limits (+2 Octave Transposition): Passive piezo buzzers physically struggle to resonate at low bass frequencies. Tracks with deep basslines like Seven Nation Army were transposed up two full octaves so the piezo disc resonates clearly while the active buzzers carry the rhythmic punch.
- - Dual-Core FreeRTOS Lockless IPC: Core 0 runs
oled_visualizer_task (30 FPS SPI DMA framebuffer engine) and button_task (debounced input polling), while Core 1 exclusively runs buzzer_sequence_task for microsecond-accurate LEDC PWM pitch generation and hardware PCNT rotary encoder scaling.
-
+
+
+
+
# 1. Clone the repository
+git clone https://github.com/ertunc-ozbay/midi-breadboard-instrument.git
+cd midi-breadboard-instrument
+
+# 2. (Optional) Convert a custom .mid file into an RLE-compressed C struct array
+pip install mido
+python3 scripts/midi_to_c.py scripts/midillis/Alla_Turca/Alla_Turca.mid > my_track.h
+
+# 3. Build and flash the 16-track firmware to ESP32
+source ~/esp/esp-idf/export.sh
+idf.py build
+idf.py -p /dev/ttyUSB0 flash monitor
+
+
+ Python RLE MIDI-to-C Compiler (scripts/midi_to_c.py)
+
+ To prevent fragmented MIDI delta-times from strobing the LEDs/OLED at 30+ Hz and exhausting flash memory, midi_to_c.py merges contiguous identical states via Run-Length Encoding and routes Channel 10 + bass notes (`< MIDI 50`) to hardware percussion buzzers:
+
+
+
+
+
#!/usr/bin/env python3
+import sys, mido
+
+def convert_midi_to_c(midi_filename):
+ mid = mido.MidiFile(midi_filename)
+ safe_name = midi_filename.split('/')[-1].replace('.mid', '').replace('-', '_')
+ print(f"const SequenceStep midi_{safe_name}[] = {{")
+
+ active_melody_notes = set()
+ time_accum = 0.0
+ trigger_kick = trigger_hat = False
+ rle_freq, rle_dur, rle_kick, rle_hat = -1, 0, False, False
+
+ for msg in mid:
+ time_accum += msg.time
+ if msg.type in ['note_on', 'note_off'] and time_accum > 0.01:
+ duration_ms = int(time_accum * 1000)
+ freq = int(440.0 * (2.0 ** ((max(active_melody_notes) - 69) / 12.0))) if active_melody_notes else 0
+
+ # RLE Engine: coalesce contiguous identical frequency & drum states
+ if freq == rle_freq and trigger_kick == rle_kick and trigger_hat == rle_hat:
+ rle_dur += duration_ms
+ else:
+ if rle_dur > 0:
+ print(f" {{{rle_freq}, {rle_dur}, {str(rle_kick).lower()}, {str(rle_hat).lower()}}},")
+ rle_freq, rle_dur, rle_kick, rle_hat = freq, duration_ms, trigger_kick, trigger_hat
+
+ time_accum, trigger_kick, trigger_hat = 0.0, False, False
+
+
+ Dual-Core FreeRTOS Music & Visualizer Architecture (main/mmcontroller.c)
+
+
+
typedef struct {
+ uint16_t frequency; // Melody pitch in Hz (driven by 13-bit LEDC PWM on GPIO 25)
+ uint16_t duration; // Step duration in ms (scaled live by PCNT rotary 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;
+
+void app_main(void) {
+ printf("Desk Rave: 16-Track Symphony Edition Initializing...\n");
+ init_hardware();
+
+ // Core 0: 30 FPS SPI OLED Framebuffer Visualizer & 50ms Button Polling
+ xTaskCreatePinnedToCore(oled_visualizer_task, "oled_task", 4096, NULL, 4, NULL, 0);
+ xTaskCreatePinnedToCore(button_task, "btn_task", 4096, NULL, 5, NULL, 0);
+
+ // Core 1: Dedicated Real-Time Audio Synthesizer & PCNT Rotary Scaling
+ xTaskCreatePinnedToCore(buzzer_sequence_task, "music_task", 4096, NULL, 5, NULL, 1);
+}
+
@@ -188,9 +286,6 @@
04 // Flash Memory Playlist
The 16-Track Symphony
-
- All 16 tracks are stored in ESP32 flash memory as SequenceStep arrays featuring frequency, duration, kick_drum, and hi_hat triggers:
-
- Careless Whisper (Extended Club Edit)
- Take On Me (EDM Build-up Edition)
@@ -210,20 +305,6 @@
- Mega Man 2 (Dr. Wily Stage)
-
-
- 05 // Workshop Notes & Next Steps
- Current State & Roadmap
-
-
Current State & Next Steps
-
- The firmware is rock-solid on the breadboard: the rotary encoder dynamically scales volume and tempo via lockless IPC variables, all 16 tracks play from flash memory, and the 1.3" SPI OLED renders real-time frequency bars and drum hits at 30 FPS.
-
-
- Next Step: Migrating the dual-breadboard circuit onto a soldered perfboard or custom KiCad PCB.
-
-
-