Bad Apple is the silhouette animation people already expect to see on a tiny screen. Hard black and white shapes survive a 128×64 mono panel better than a soft color clip. This build is that look as a 385-frame SSD1306 loop, exported from OLED Animation Maker and published as github.com/itzmeAshish/BadApple. The sketch draws pictures only. It does not play the track.
⬇ Free download — Bad Apple OLED code
Full main.ino with all PROGMEM frames · ZIP or git clone
Parts list
- Board: ESP32, or ESP8266 NodeMCU / Wemos D1 Mini with a 4MB flash setting
- Display: 0.96" SSD1306 I2C OLED, 128×64, address
0x3C - 4 Dupont wires (VCC, GND, SDA, SCL)
- USB cable + Arduino IDE with the matching ESP board package
Why this clip suits a 0.96" OLED
Bad Apple’s pictures are already close to 1-bit art: a bright figure against a dark field, then the reverse, with thin edges that still read after the frame is crushed to 128×64. A photograph of a face loses the eyes at this size. A high-contrast outline keeps the motion. That is why the same song shows up on so many SSD1306 demos, and why this export uses a short step (67 ms) instead of a slow slideshow.
One full pass at the number written in the sketch is 385 × 67 ms = 25,795 ms,
about 25.8 seconds. That matches a short chorus-length clip sampled near 15
frames per second. The OLED will look like the familiar animation only if the bus can
finish each display() inside that window. The speed section below shows when
the 67 ms line is real and when the wire is the real clock.
Connection / wiring
Power the module from 3.3V on an ESP. Do not tie VCC to 5V on a NodeMCU.
| OLED pin | Arduino Uno / Nano | ESP8266 (NodeMCU) | ESP32 |
|---|---|---|---|
| VCC | 5V only if the module has a regulator | 3.3V | 3.3V |
| GND | GND | GND | GND |
| SDA | A4 | D2 (GPIO4) | GPIO21 |
| SCL | A5 | D1 (GPIO5) | GPIO22 |
On ESP8266, call Wire.begin(4, 5) before display.begin() if the
core’s default pins are not D2 and D1. If the glass stays black after a successful upload,
try address 0x3D and use the
blank OLED checklist.
Leave A4 and A5 for a future short sketch. This file will not link on Uno.
Libraries
- Arduino IDE → Tools → Manage Libraries
- Install Adafruit SSD1306
- Install Adafruit GFX Library
- Install the ESP32 or ESP8266 board package if it is not already there
- Select the board you wired, a flash size that leaves the sketch at least 1MB (4MB is the safe ESP8266 choice), and the COM port
How the player works
Each picture is const uint8_t PROGMEM frameN[1024]. The index and the count
are uint16_t. A uint8_t wraps at 255, and this clip has 385
pictures, so an 8-bit counter would restart in the middle and never reach
frame384. loop() steps every 67 ms:
void loop() {
static uint16_t frameIdx = 0;
static unsigned long lastMs = 0;
const uint8_t* frames[] = { frame0, frame1, /* ... */ frame384 };
const uint16_t numFrames = 385;
if (millis() - lastMs >= 67) {
lastMs = millis();
display.clearDisplay();
display.drawBitmap(0, 0, frames[frameIdx], 128, 64, SSD1306_WHITE);
display.display();
frameIdx = (frameIdx + 1) % numFrames;
}
}
The pointer list in the published file names every frame from frame0 to
frame384. Keep that list the same length as numFrames.
Setting the count to 384 skips the last bitmap. Setting it to 386 reads past the table.
Speed table for the 67 ms player
Stock speed is 67 ms, about 14.9 fps. The arithmetic pass is 25.8 seconds. A 128×64 buffer is 1024 bytes. On a 100 kHz I2C bus that transfer is on the order of 90 ms before the next frame can start, so a watch may time one pass closer to 35 seconds until you raise the clock. At 400 kHz the same buffer is about 25 ms on the wire, and the 67 ms comparison can win.
| Delay you write | Asked fps | 385-frame pass if the bus keeps up | When to use it |
|---|---|---|---|
| 67 ms (stock) | ~15 | ~25.8 s | The published Bad Apple timing. Add Wire.setClock(400000) after the bus starts if the pass is much slower than 26 seconds. |
| 100 ms | 10 | 38.5 s | A calmer loop that still fits a default 100 kHz transfer |
| 150 ms | ~6.7 | ~58 s | Easier to film. Each silhouette stays readable across a desk |
| 40 ms | 25 | 15.4 s | Only after a 400 kHz clock, and only if a stopwatch agrees |
Change only the comparison millis() - lastMs >= 67. Do not also change
numFrames when you are only chasing speed. Measure one return to the opening
picture. If that return is near 26 seconds, the stock delay is really running. If it is
near 35 seconds, display() is the clock and the 67 is only a request.
385 frames and a 32 KB Uno
385 × 1024 = 394,240 bytes. That table alone is about twelve times a Uno’s flash. A classic Nano is the same size. Slowing the 67 ms step does not shrink the binary. The linker error is “sketch too big,” and it appears before the OLED ever lights. An ESP32 has room. A 4MB ESP8266 has room if Tools gives the sketch the space instead of a huge filesystem. A 1MB ESP8266 profile with a large SPIFFS or LittleFS slice can fail even though 385 KB looks smaller than 1MB on paper.
If you must stay on Uno, sample a much shorter clip in
OLED Animation Maker and export a
new sketch. Do not delete random frameN arrays from this file unless you
also shorten the pointer list and numFrames together. A mismatched count
is a crash, not a smaller show. The
PROGMEM limits guide
walks through how many 1024-byte frames a small board can actually hold.
Upload & run
- Open itzmeAshish/BadApple and download the ZIP, or run
git clone https://github.com/itzmeAshish/BadApple.git - The sketch file is
main.ino. GitHub’s ZIP folder is oftenBadApple-main. Rename that folder tomain, then open the.inofrom inside it - Install Adafruit GFX and Adafruit SSD1306
- Pick ESP32, or ESP8266 with a 4MB flash size, and the port that appears when you plug the board in
- Click Upload. The file is large, so the compile is slower than a blink sketch
- Open Serial at 115200.
SSD1306 allocation failedmeans the chip never answered: power, SDA/SCL, or0x3Cversus0x3D
Ready to flash?
Get the complete Bad Apple main.ino from GitHub.
What the BadApple sketch contains
setup() starts Serial at 115200, calls display.begin(SSD1306_SWITCHCAPVCC, 0x3C),
sets rotation 0, and clears the glass. There is no Wi-Fi, no button, and no brightness
control. loop() keeps frameIdx and lastMs as
static locals, builds the pointer table, and draws with
drawBitmap(0, 0, frames[frameIdx], 128, 64, SSD1306_WHITE).
The header also lists the project social links and the generator credit for
oledanimationmaker.com. None of that text is drawn on the panel.
The pictures are already 1-bit. You do not retune threshold inside the IDE. A muddy
silhouette means a new export, not a new delay. Opening the file and searching for
frame384 is a quick check that the download is complete. A truncated ZIP
often dies in the middle of a bitmap and fails to compile with a syntax error instead
of a size error.
Make your own clip
This main.ino came from the browser tool, the same path as the other song
posts on this site:
- Open oledanimationmaker.com in Chrome or Edge
- Use Import and drop a short video or GIF
- Tune threshold and dither until the live 128×64 preview keeps the edges
- Click Get the Code and choose Adafruit SSD1306
- Save the sketch. If the folder name and the
.inoname differ, rename the folder before you compile
Related guides: Video to OLED, GIF to OLED, ESP32 OLED guide.
A recording that proves all 385 frames ran
Film a little longer than one pass. At the stock request that is about 30 seconds of footage, so you have a margin if the bus stretches each frame toward 90 ms. Frame the 0.96" glass tightly and keep room lights out of the reflection. Start on a picture you can recognize and stop when that same picture comes back. One return is the proof the index wrapped, which a five-second clip of the opening cannot show.
Leave the take silent on the board. This project is the visual. A speaker is a
different circuit and is not in main.ino. Crisp white pixels on a black
field are the result you want. Shimmer that changes when you touch the dupont housing
is a loose contact. A stable image that looks filled-in or hollow is the threshold
from the export, already baked into the bytes.
Troubleshooting
- Sketch too big — you are on Uno, Nano, or a small ESP8266 flash profile. Move to ESP32 or a 4MB ESP8266 and give the sketch the flash, not a giant filesystem.
- Folder name error — rename
BadApple-maintomainbefore you openmain.ino. Fix this before you trust any later error. - Blank OLED after upload — 3.3V, shared GND, SDA/SCL for the chip you selected. Then try
0x3D. - Animation restarts early — the index must stay
uint16_t. An 8-bit counter dies at frame 255. - Slower than 26 seconds — the 100 kHz bus is pacing
display(). Use 400 kHz or accept the longer pass. Do not “fix” it by deleting frames. - No sound — expected. The file is pictures.
- Wrong board package — an ESP8266 binary on an ESP32, or the reverse, never starts the OLED. Match Tools to the module in your hand, and do not leave the port on a leftover Uno.
FAQ
Is the Bad Apple OLED code free?
Yes. Download it from github.com/itzmeAshish/BadApple.
Does it play the song?
No. The title is Bad Apple. The OLED shows the 385-frame picture loop. There is no audio sample and no lyric text in the sketch.
Why is the frame index 16-bit?
frame0 through frame384 is 385 pictures. A uint8_t
can count to 255. The published player uses static uint16_t frameIdx and
const uint16_t numFrames = 385 so the wrap happens after the last bitmap.
Can I change the clip?
Yes. Import another video in
OLED Animation Maker, export a new
Adafruit sketch, and keep the folder name matched to the .ino file.
Four checks before you call the upload bad
The IDE says the sketch is too big. Is the OLED broken?
No. That message is flash. 385 KB of bitmaps cannot link on Uno. Select ESP32 or a 4MB ESP8266 and compile again. The panel has not been tested yet.
Upload worked and Serial prints SSD1306 allocation failed. What failed?
The sketch fits and the display did not answer. Check 3.3V, GND, SDA, and SCL for the board you selected. Then change SCREEN_ADDR from 0x3C to 0x3D and upload once more.
The motion looks right but one pass takes about 35 seconds, not 26. Did I edit the delay?
Probably not. At 100 kHz, display() of a full 1024-byte frame takes longer than 67 ms, so the comparison never gets to wait. The pictures are still all 385. Raise the I2C clock to 400 kHz if you want the stock 25.8 second pass.
The ZIP folder is BadApple-main. Do I rename the file?
Rename the folder to main so it matches main.ino. Open the sketch from that folder. Renaming the file to BadApple.ino also works if the folder is then named BadApple. The two names have to match.
Build the next OLED animation in the browser
Video import, pixel editor, WebSerial preview, and Adafruit SSD1306 export — free, no install.
Open OLED Animation Maker →What people search, and what this page is
The searches that land on a page like this are specific. People type bad apple oled, bad apple arduino, bad apple esp32, bad apple esp8266, bad apple ssd1306, bad apple 128x64, and bad apple 0.96 oled. Some add the double mark, Bad Apple!!, or the fandom name Touhou. A few look for shadow art, which is the picture style: a white figure cut out of black, then black cut out of white.
Those words all point at the same famous fan video. They do not all point at the
same firmware. A full play of that video is a few minutes and several thousand
frames. Projects that store the whole thing usually compress it into SPIFFS or
read a .bin from an SD card, and some add a speaker. This page is the
other kind of project: one Arduino sketch, main.ino, with
385 raw bitmaps in flash. One pass is about 26 seconds
when the bus can keep the 67 ms step. If you searched for the complete song
with sound, you want a different build. If you searched for Bad Apple on a
0.96" OLED that you can upload from the Arduino IDE today, this is that file.
The repo is github.com/itzmeAshish/BadApple. It has no MP3, no lyric sheet, and no extra video file. Searching the sketch for a line of the song will not find one. The only “Bad Apple” in the project is the picture loop and the repository name.
Bad Apple on ESP32
An ESP32 is the board this loop fits without a fight. The bitmap table is 385 KB.
The ESP32 Arduino core and Adafruit SSD1306 sit on top of that, and a normal
DevKit still has flash left. Wiring is 3.3V, GND, SDA on GPIO21, SCL on GPIO22.
In Tools, choose the ESP32 Dev Module (or the exact module you bought), the port
that appears when the cable is plugged in, and Upload. You do not upload a
SPIFFS image for this repo. There is no video.hs and no
data folder. If a tutorial tells you to run “ESP32 Sketch Data
Upload,” that tutorial is about a compressed full-length player, not
itzmeAshish/BadApple.
After upload, Serial at 115200 should stay quiet unless
display.begin fails. A quiet monitor plus a moving silhouette means
the 385-frame player is running. Time one return to the opening picture. Near
26 seconds means the 67 ms request is winning. Near 35 seconds means the
100 kHz I2C bus is slower than the request, which is normal until you call
Wire.setClock(400000) after Wire.begin().
Bad Apple on ESP8266 NodeMCU
A NodeMCU or Wemos D1 Mini can play the same main.ino if the flash
menu gives the sketch enough room. Use 3.3V, GND, SDA on
D2 (GPIO4), SCL on D1 (GPIO5). If the glass
stays black and Serial says allocation failed, the core’s default I2C pins may
not be those two. Add Wire.begin(4, 5); before
display.begin().
The size trap on ESP8266 is the filesystem slice. A 4MB module with “1MB sketch / 3MB filesystem” can reject a 385 KB table plus the core. Pick a scheme that leaves the sketch at least 1MB, and treat 4MB flash as the module you buy. A 512 KB or 1MB ESP-01 style board is the wrong part for this file. The error looks like “sketch too big” or a section that does not fit. That is flash math, not a broken OLED.
Bad Apple on Arduino Uno
The comment at the top of main.ino says Board: Arduino Uno, SDA A4,
SCL A5. That line is the generator’s pin legend. It is not a promise that the
binary links. 385 × 1024 = 394,240 bytes of pictures. Uno flash is about
32,256 bytes for the whole sketch. The ratio is about twelve to one before
Wire and Adafruit GFX are counted. Compiling for Uno stops in the
linker. The OLED never gets a chance to show frame 0.
A Nano with the old ATmega328P is the same limit. Do not “fix” it by raising the
delay, and do not delete a few frame lines while leaving
numFrames at 385. Either move the cable to an ESP32, or export a
much shorter clip from
OLED Animation Maker
so the new sketch is actually small. The
Uno PROGMEM guide
shows how many 1024-byte frames a 32 KB board can hold. It is a handful, not 385.
0.96 inch SSD1306, SH1106, and 128×64
The code draws a 128×64 bitmap at column 0, row 0. That matches the common
0.96" I2C module sold as SSD1306. Address 0x3C is the usual one.
Some boards answer at 0x3D. Change SCREEN_ADDR only
after the sketch has uploaded and Serial reports
SSD1306 allocation failed.
A SH1106 1.3" panel is also 128×64 and often uses the same four pins, but
the controller is not the same chip. This sketch calls
Adafruit_SSD1306. On a SH1106 the image can look shifted by a few
columns or stay blank even when the wires are right. For this file, buy or use
a module that is marked SSD1306. The
SH1106 vs SSD1306 article
covers the difference if you already have the taller 1.3" glass.
A 128×32 display will not show this animation correctly. drawBitmap
is called with height 64. The bottom half has nowhere to go. Stay on 128×64.
Why the shadow art survives 128×64
Each stored frame is 1024 bytes because 128 × 64 pixels is 8192 bits, and 8192 / 8 = 1024. Every pixel is on or off. Bad Apple’s pictures are already close to that: a solid figure, a solid background, and a thin edge. Photos of faces lose the eyes at this size. A cut-out figure still reads when you are an arm’s length from a 0.96" panel.
The bytes in frame0 through frame384 were thresholded
in the browser before export. The IDE cannot “turn the contrast up.” If a
re-export looks filled-in, open the maker, move the threshold until the edge
is one or two pixels wide, and export again. Dithering is optional. On this
kind of art, dither can sprinkle gray noise into a region that should be a
flat black field. Try the clip once with dither off and once with dither on,
and keep the preview that still looks like a cut-out.
A worked timing example
Write these numbers down before you change the sketch. They are the check against a phone stopwatch.
- Frames in the file: 385 (
frame0…frame384) - Bytes per frame: 1024
- Picture flash: 385 × 1024 = 394,240 bytes
- Stock step: 67 ms
- Asked rate: 1000 / 67 ≈ 14.9 fps
- Asked length of one pass: 385 × 67 = 25,795 ms (25.8 s)
- If each
display()actually takes ~90 ms on a 100 kHz bus: 385 × 90 ≈ 34.7 s
Film or watch one wrap. A pass near 26 seconds means the stock comparison is
really in the binary and the bus is fast enough. A pass near 35 seconds means
you uploaded this file and the wire is the clock. A pass near 10 seconds means
a different delay is on the board. A loop that restarts while the figure is
still mid-motion, well before either of those times, often means an 8-bit
counter. This file uses uint16_t so it can pass frame 255.
Do not change that type when you only want a new speed.
First-upload checklist
- Download the ZIP from GitHub. The folder arrives as
BadApple-main. - Rename the folder to
main. The file inside is alreadymain.ino. Arduino IDE refuses a sketch whose folder name and file name disagree. - Open
main.inofrom inside that folder. Search forframe384. If the search fails, the download was cut off. Get the ZIP again. - Library Manager: Adafruit SSD1306 and Adafruit GFX. Install the ESP32 or ESP8266 board package, not only the libraries.
- Wire 3.3V, GND, SDA, and SCL for the chip you selected. Unplug 5V from an ESP module’s OLED pin.
- Tools → Board matches the module. Tools → Port is the new port, not a leftover Uno.
- Upload. A large
.inocompiles longer than Blink. Wait for “Done uploading.” - If the IDE stops with a size error, you are still on Uno or a small flash scheme. Change the board. Do not edit frames yet.
- Serial 115200. Allocation failed: fix power and pins, then try
0x3Donce. - When the picture moves, time one full return. Use the 26 second and 35 second marks above.
How this differs from a full-length Bad Apple player
Search results mix three designs. Knowing which one you opened saves an afternoon.
| This sketch | Compressed full video | SD card plus speaker | |
|---|---|---|---|
| Length | 385 frames, about 26 seconds at 67 ms | Often 6,000 or more frames, the long PV | The long PV, plus audio |
| Where pictures live | PROGMEM arrays inside main.ino | A compressed file in SPIFFS or LittleFS | A .bin on a FAT32 card |
| Extra upload | None. One Arduino upload. | A filesystem image besides the sketch | Copy files to the card, then flash |
| Sound | None | Usually none | Often an I2S amp and a speaker |
| Board | ESP32 or 4MB ESP8266 | ESP32 with room for the filesystem | ESP32, SD slot, and an amplifier |
| Uno | Will not link | Will not link | Will not link |
If your goal is “I want the silhouette on the OLED I already wired for other
projects on this site,” stay with this PROGMEM file. If your goal is the entire
music video with the track, you need storage and a speaker that this repository
does not contain. Do not paste an MP3 into main.ino. The player
never opens a file.
Compile errors, in the order they usually appear
- The folder is named BadApple-main. The IDE offers to move the sketch or says the name does not match. Rename the folder to
mainand open it again. Fix this before you read the next error. - Adafruit_SSD1306.h: No such file. Library Manager, not a random ZIP from a forum. Install Adafruit SSD1306. GFX is the dependency it asks for. Accept that install.
- Sketch too big / text section exceeds available space. Board is Uno, Nano, or a small ESP8266 scheme. Switch to ESP32 or a 4MB ESP8266 sketch-friendly scheme. The OLED is not involved yet.
- Upload failed, port busy, or wrong chip id. Close Serial Monitor, pick the port again, and hold the board’s BOOT button only if that module’s instructions say so. An ESP8266 package pointed at an ESP32 fails here even when the wires are perfect.
- Done uploading, then SSD1306 allocation failed. The binary fits. The panel did not answer on I2C. 3.3V, GND, SDA, SCL, then
0x3D. - It runs, then jumps backward in the middle. Something changed
frameIdxtouint8_t. Putuint16_tback. 385 does not fit in 8 bits.
Questions that match the way this is searched
Can I play Bad Apple on a 0.96 inch OLED?
Yes, if the module is a 128×64 SSD1306 and the board is an ESP32 or a 4MB
ESP8266. That is the hardware this main.ino was exported for.
The picture is the shadow-art loop, about 26 seconds, then it repeats.
Is there Bad Apple Arduino code without SPIFFS?
Yes. This file is that version. Every frame is a C array. You compile and
upload once. You do not format a filesystem or copy video.hs.
Does ESP8266 Bad Apple need the same pins as ESP32?
No. ESP32 uses GPIO21 and GPIO22. NodeMCU uses D2 and D1. Both use 3.3V and GND. The OLED sketch is the same file. Only the wires and the Tools board menu change.
Will a faster delay make Uno fit?
No. Flash size does not depend on 67. The bytes are in the
arrays whether you wait 67 ms or 200 ms. Uno needs a different, much shorter
export.