A tiny device that plays the classic Bad Apple!! music video β screen and audio included β with a knob to control the volume.
Plays the full Bad Apple!! music video on a 128Γ64 OLED display with synchronized audio, powered by an ESP32 and an SD card.
How It Works β’ Hardware β’ Wiring β’ Setup β’ File Structure
|
π¬ Full Music Video π Synchronized Audio π₯οΈ OLED Display |
ποΈ Volume Control πΎ SD Card Storage β‘ Dual-Core FreeRTOS |
The music video and audio used in this project are from the iconic Bad Apple!! Touhou fan video:
Bad Apple!! feat.nomico β Alstroemeria Records
MV uploaded by kasidid2
All rights to the original music and video belong to their respective owners. This project is for personal/educational use only.
The SSD1306 is a 1-bit display β every pixel is either on or off. Each frame of the video is stored as raw binary: 128Γ64 pixels at 1 bit per pixel = exactly 1024 bytes per frame. All frames are packed back-to-back into a single bad_apple.bin file with no headers or delimiters.
To read frame N, the ESP32 seeks to byte N Γ 1024 in the file and reads 1024 bytes straight into a buffer, then pushes it to the display. No parsing needed.
The MP3 file is stored separately on the SD card and decoded in real time using the ESP8266Audio library, outputting to the MAX98357A I2S DAC.
Both audio and video start from the same millis() reference point. Instead of incrementing frames one by one, the ESP32 calculates the expected frame based on elapsed time β so if rendering falls behind, frames are skipped to catch up, keeping video locked to wall time and in sync with audio.
Audio decoding runs on Core 0 as a FreeRTOS task, while video rendering runs on Core 1 (the default Arduino loop core). This ensures the audio buffer stays fed even when the display is busy pushing a frame over I2C.
| Component | Details |
|---|---|
| ESP32 | DevKitC V4 (or any ESP32 with enough GPIOs) |
| OLED Display | SSD1306 128Γ64 I2C |
| DAC | MAX98357A I2S amplifier module |
| SD card module | SPI SD card reader |
| Rotary encoder | Any standard KY-040 type encoder |
| SD card | FAT32, 8GB or under recommended |
| Speaker | Any small 4Ξ© or 8Ξ© speaker |
| OLED | ESP32 |
|---|---|
| SDA | GPIO 21 |
| SCL | GPIO 22 |
| VCC | 3.3V |
| GND | GND |
| SD Module | ESP32 |
|---|---|
| MOSI | GPIO 23 |
| MISO | GPIO 19 |
| SCK | GPIO 18 |
| CS | GPIO 5 |
| VCC | 3.3V |
| GND | GND |
| MAX98357A | ESP32 |
|---|---|
| BCLK | GPIO 26 |
| LRC | GPIO 25 |
| DIN | GPIO 27 |
| VIN | 5V |
| GND | GND |
β οΈ The SD_MODE pin on the MAX98357A should be pulled to 3.3V through a 100kΞ© resistor for normal operation. Most breakout boards (e.g. Adafruit's) already do this onboard.
| Encoder | ESP32 |
|---|---|
| CLK | GPIO 32 |
| DT | GPIO 33 |
| SW | GPIO 34 |
| VCC | 3.3V |
| GND | GND |
β οΈ GPIO 34 is input-only and has no internal pullup. Connect a 10kΞ© resistor between SW and 3.3V externally.
In PlatformIO or Arduino IDE, install:
Adafruit SSD1306Adafruit GFX Libraryearlephilhower/ESP8266Audio
bad_apple.bin is already provided in this repo β no conversion needed. Just use it as-is.
If you want to regenerate it yourself for any reason, you'll need Python, Pillow, and ffmpeg. Extract frames with
ffmpeg -i bad_apple.mp4 -vf "fps=20,scale=128:64" frames/frame_%04d.pngthen runpython pack_frames.py.
Download the MP3 from the original video using a YouTube to MP3 converter like HC-YT Downloader:
γζ±ζΉγBad Apple!! οΌ°οΌΆγε½±η΅΅γ β https://youtu.be/FtutLA63Cp8
Rename the downloaded file to exactly bad_apple.mp3.
Format your SD card as FAT32, then copy both files to the root:
bad_apple.bin
bad_apple.mp3
Open bad-apple-esp32.ino in Arduino IDE or PlatformIO and flash to your ESP32. Open the serial monitor at 115200 baud β it will wait for the SD card to be inserted, then start playback automatically.
bad-apple-esp32/
βββ bad-apple-esp32.ino β Main firmware: display, audio, sync, volume control
βββ pack_frames.py β Python script to convert video frames into bad_apple.bin
βββ bad_apple.bin β Packed binary of all video frames (1024 bytes each)
βββ .gitignore
βββ LICENSE
βββ README.md
pack_frames.py β Takes the PNG frames extracted by ffmpeg, converts each one to 1-bit (black and white), and writes them back-to-back into a single binary file. Run this once on your PC before copying to the SD card.
bad-apple-esp32.ino β The ESP32 firmware. Handles SD card init, OLED display, I2S audio decoding, frame-skip sync logic, FreeRTOS task setup, and rotary encoder volume control.
bad_apple.bin β The output of pack_frames.py. Not stored in the repo (too large) β you generate it yourself. Lives on the SD card, not in the project folder.
Distributed under the MIT License. See LICENSE for more information.
Made with β€οΈ by HimC29