Skip to content

rlaneth/badustudio

Repository files navigation

BadUStudio

BadUStudio is designed to play the iconic Bad Apple!! animation on the Ulanzi D200 U-Studio stream controller.

Note: The player is video-only. The D200 unfortunately has no speaker.

The Discovery

The Ulanzi D200 is a budget stream controller featuring 13 customizable keys, plus a stats window that normally serves either as a clock or for CPU/RAM/GPU usage monitoring.

D200 units have (accidentally?) been shipping with an open adb root shell and numerous other characteristics that make the device extremely tinker-friendly.

Lucas Teske (@racerxdl) mentioned this on October 11, 2025, which led to this project.

Features

  • 1-bit image format for 24x memory reduction vs RGB888
  • PNG support with fast 1-bit conversion
  • 2x upscaling with optimized nearest-neighbor interpolation
  • Smart compression with RLE and special handling for solid colors
  • Optimized framebuffer rendering using lookup tables
  • Rolling cache for smooth playback with minimal memory
  • Direct framebuffer rendering with no Qt, X11, or graphics library dependencies

Requirements

For Building the Player

  • CMake 3.16 or later
  • C++17 compatible compiler
  • Linux with framebuffer support
  • Standard C++ library with filesystem support

For Conversion Scripts

  • Python 3.7 or higher
  • PIL/Pillow library
  • OpenCV (cv2) for mp4_to_raw.py

Install Python dependencies:

pip3 install Pillow opencv-python

Building

Cross-Compiling for Ulanzi D200 (ARMv7)

The D200 uses ARMv7 architecture, so you need to cross-compile from your development machine.

Install ARM cross-compiler:

# Ubuntu/Debian
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf

# Arch Linux
sudo pacman -S arm-linux-gnueabihf-gcc

Build for D200: (A toolchain-armv7.cmake file is already provided in the repository)

mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-armv7.cmake ..
make

The compiled binary will be at build/BadUStudio.

Building Natively (x86_64 Linux)

For testing on your local machine:

mkdir build
cd build
cmake ..
make

Testing Locally

Option 1: Direct framebuffer (best for actual visual testing)

Switch to a TTY (text console) where the framebuffer is directly visible:

# Switch to TTY3 (Ctrl+Alt+F3)
# Login, then:
sudo ./build/BadUStudio --path frames --fps 30

# Switch back to X11 with Ctrl+Alt+F7 or Ctrl+Alt+F1

This shows the actual output on your screen. The video will render in the top-right corner.

Option 2: Capture framebuffer screenshots

Test without leaving X11 by capturing screenshots:

# Install fbcat or fbgrab
sudo apt-get install fbcat  # Ubuntu/Debian
# or
sudo apt-get install fbgrab

# Run player in background
sudo ./build/BadUStudio --path frames --fps 30 &

# Capture framebuffer to image
sudo fbcat > screenshot.ppm
# or
sudo fbgrab screenshot.png

# View the captured image
display screenshot.ppm  # ImageMagick

Usage

Quick Start

# Play PNG files directly
./build/BadUStudio --path frames_png --fps 30

# Play compressed .raw files
./build/BadUStudio --path frames --fps 30

# Play half-resolution with 2x upscaling
./build/BadUStudio --path frames_half --fps 30 --2x

# Loop continuously
./build/BadUStudio --path frames --fps 30 --loop

Command-Line Options

-f, --fb <device>      Framebuffer device (default: /dev/fb0)
-p, --path <path>      Path to frames directory (default: ./frames)
-r, --fps <fps>        Frame rate (default: 30)
-c, --cache <size>     Cache size in frames (default: 50)
--2x                   Enable 2x upscaling for raw frames
--loop                 Loop video playback (default: exit after one play)
-h, --help             Show help message

Conversion Tools

mp4_to_raw.py

Extracts frames from MP4 video and converts directly to compressed 1-bit raw format.

Usage:

python3 utils/mp4_to_raw.py video.mp4 -o frames_raw -f 30

Options:

  • -f, --fps: Target frame rate (default: source FPS)
  • -t, --threshold: Binarization threshold 0-255 (default: 128)
  • --start-frame: Skip to specific frame number
  • --max-frames: Limit number of frames extracted
  • --scale: Scale factor for output resolution (e.g., 0.5 for half resolution)
  • --png-output: Output PNG files instead of .raw

convert_to_1bit_raw.py

Converts PNG images to compressed 1-bit raw format.

Single file:

python3 utils/convert_to_1bit_raw.py single input.png output.raw

Batch conversion:

python3 utils/convert_to_1bit_raw.py batch input_dir output_dir

Adjust binarization threshold (default: 128):

python3 utils/convert_to_1bit_raw.py batch input_dir output_dir --threshold 200

Higher threshold = more white pixels (0-255).

File Formats

PNG Files (.png)

Standard 1-bit (or grayscale) PNG files. Loaded using stb_image, converted to 1-bit on-the-fly with threshold of 128.

Note: using PNG images may be more effective storage-wise but leads to poor performance due to the decoding process. Expect no more than 14 FPS on the Ulanzi D200 while using PNG.

Compressed 1-bit Raw Format (.raw)

Custom format optimized for speed and compression.

Header (9 bytes):

  • Width: 4 bytes (uint32, little-endian)
  • Height: 4 bytes (uint32, little-endian)
  • Compression type: 1 byte
    • 0 = Full black (no data follows)
    • 1 = Full white (no data follows)
    • 2 = RLE compressed (count+value pairs)
    • 3 = Uncompressed packed bits

Data (variable length):

  • Type 0/1: No data (image is solid color)
  • Type 2: Pairs of [count byte][value byte]
  • Type 3: Packed bits (1 bit per pixel, MSB first, row-major)

Deployment to Ulanzi D200

Check ADB Access

# Connect device via USB
adb devices

# You should see the D200 listed

Push to Device

# Copy player and frames to device
adb push build/BadUStudio /userdata/
adb push data/frames /userdata/

# Make executable
adb shell chmod +x /userdata/BadUStudio

Run on Device

# Run via ADB shell
adb shell
cd /userdata
chmod +x BadUStudio
./BadUStudio --path frames --fps 30 --loop

Troubleshooting

"No image files found"

Error: No image files found in ./frames

Solution:

  • Ensure frames are in .raw or .png format
  • Check the path is correct: --path frames
  • Verify files exist: ls frames/

Low FPS / Choppy Playback

Check CPU usage on device:

adb shell top

On quad-core: ~22-25% = one core maxed out (normal)

Solutions:

  • Use half-resolution with --2x for 60+ FPS
  • Use compressed .raw format instead of PNG

Out of Memory

Solution:

  • Reduce cache: --cache 20
  • Use half-resolution frames
  • Check storage: du -sh frames

Images Too Dark/Bright

Solution:

  • Adjust threshold during conversion: --threshold 150
  • Try values between 0-255
  • Lower = more black, higher = more white

Project Structure

.
├── src/
│   ├── main.cpp           # Main player loop, argument parsing
│   ├── framebuffer.h/cpp  # Optimized framebuffer with lookup tables
│   ├── image_scaler.h/cpp # PNG/raw loading, 2x upscaling, caching
│   └── stb_image.h        # PNG decoder (header-only library)
├── utils/
│   ├── mp4_to_raw.py          # Video to compressed 1-bit converter
│   └── convert_to_1bit_raw.py # PNG to compressed 1-bit converter
├── CMakeLists.txt         # Build configuration
├── README.md              # This file
└── CLAUDE.md              # AI assistant guidance

Credits

License

The code in this project is released into the public domain under the Unlicense.

Exceptions:

  • external/stb_image.h: Public domain (see file header for details)
  • Video data (Bad Apple!! frames): Subject to original copyright holders

Use this code however you want. No attribution required (but appreciated!).

About

High-performance Bad Apple!! player for Ulanzi D200 stream controller

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published