OLED Animation Maker OLED Maker
🎵 Free Arduino project · SSD1306

Bad Apple OLED Animation for Arduino

Play Bad Apple!! on a 0.96" 128×64 SSD1306 with ESP32 or ESP8266 — 385 PROGMEM frames, a 67 ms Adafruit player, free Arduino code on GitHub. Pictures only. The sketch does not play the song.

By Ashish Oct 7, 2026 ~18 min read ESP32 · ESP8266
🎞️

385 picture frames

High-contrast Bad Apple visuals as 1024-byte PROGMEM bitmaps, frame0 through frame384.

⚡

67 ms player

Adafruit SSD1306 + GFX. The index is uint16_t so it can pass frame 255.

🔌

ESP wiring

ESP32 and ESP8266 pin tables below. Classic Uno cannot hold this file.

🆓

Free on GitHub

Clone or ZIP download from itzmeAshish/BadApple.

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

Open GitHub repo →

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 pinArduino Uno / NanoESP8266 (NodeMCU)ESP32
VCC5V only if the module has a regulator3.3V3.3V
GNDGNDGNDGND
SDAA4D2 (GPIO4)GPIO21
SCLA5D1 (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

  1. Arduino IDE → Tools → Manage Libraries
  2. Install Adafruit SSD1306
  3. Install Adafruit GFX Library
  4. Install the ESP32 or ESP8266 board package if it is not already there
  5. 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 writeAsked fps385-frame pass if the bus keeps upWhen to use it
67 ms (stock)~15~25.8 sThe published Bad Apple timing. Add Wire.setClock(400000) after the bus starts if the pass is much slower than 26 seconds.
100 ms1038.5 sA calmer loop that still fits a default 100 kHz transfer
150 ms~6.7~58 sEasier to film. Each silhouette stays readable across a desk
40 ms2515.4 sOnly 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

  1. Open itzmeAshish/BadApple and download the ZIP, or run git clone https://github.com/itzmeAshish/BadApple.git
  2. The sketch file is main.ino. GitHub’s ZIP folder is often BadApple-main. Rename that folder to main, then open the .ino from inside it
  3. Install Adafruit GFX and Adafruit SSD1306
  4. Pick ESP32, or ESP8266 with a 4MB flash size, and the port that appears when you plug the board in
  5. Click Upload. The file is large, so the compile is slower than a blink sketch
  6. Open Serial at 115200. SSD1306 allocation failed means the chip never answered: power, SDA/SCL, or 0x3C versus 0x3D

Ready to flash?

Get the complete Bad Apple main.ino from GitHub.

⬇ Download code

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:

  1. Open oledanimationmaker.com in Chrome or Edge
  2. Use Import and drop a short video or GIF
  3. Tune threshold and dither until the live 128×64 preview keeps the edges
  4. Click Get the Code and choose Adafruit SSD1306
  5. Save the sketch. If the folder name and the .ino name 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-main to main before you open main.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

  1. Download the ZIP from GitHub. The folder arrives as BadApple-main.
  2. Rename the folder to main. The file inside is already main.ino. Arduino IDE refuses a sketch whose folder name and file name disagree.
  3. Open main.ino from inside that folder. Search for frame384. If the search fails, the download was cut off. Get the ZIP again.
  4. Library Manager: Adafruit SSD1306 and Adafruit GFX. Install the ESP32 or ESP8266 board package, not only the libraries.
  5. Wire 3.3V, GND, SDA, and SCL for the chip you selected. Unplug 5V from an ESP module’s OLED pin.
  6. Tools → Board matches the module. Tools → Port is the new port, not a leftover Uno.
  7. Upload. A large .ino compiles longer than Blink. Wait for “Done uploading.”
  8. 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.
  9. Serial 115200. Allocation failed: fix power and pins, then try 0x3D once.
  10. 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 sketchCompressed full videoSD card plus speaker
Length385 frames, about 26 seconds at 67 msOften 6,000 or more frames, the long PVThe long PV, plus audio
Where pictures livePROGMEM arrays inside main.inoA compressed file in SPIFFS or LittleFSA .bin on a FAT32 card
Extra uploadNone. One Arduino upload.A filesystem image besides the sketchCopy files to the card, then flash
SoundNoneUsually noneOften an I2S amp and a speaker
BoardESP32 or 4MB ESP8266ESP32 with room for the filesystemESP32, SD slot, and an amplifier
UnoWill not linkWill not linkWill 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 main and 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 frameIdx to uint8_t. Put uint16_t back. 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.

Related posts