Skip to content

Commit

Permalink
Prepare for publish
Browse files Browse the repository at this point in the history
  • Loading branch information
KasaneKona committed Sep 9, 2020
0 parents commit 1e12adc
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 0 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Bad Apple!! for Sansa Clip+ (Rockbox Lua)

Bad Apple!! video player implemented in Lua for Rockbox, designed to run on the Sansa Clip+ MP3 player.

Video data assumes a 128x64 display with a 2px gap between visible regions.

## Files for playing:
- badapple.lua: main script, copy to player storage or SD card
- vid.mp3 (download in Releases): audio data, MP3 format, copy to main script location
- vid.bin (download in Releases): video data, display native format, copy to main script location

## Additional files:
- vidsrc.mp4 (not provided): 30fps video source, 2:1 aspect ratio
- vidextract.bat: extracts vidsrc.mp4 to frame images and vid.mp3, requires ffmpeg
- writedata.py: converts frame images to display native format, vid.bin
- vidframes: frame image target for vidextract/writedata

## Theory of operation:
Audio is played natively by the Rockbox MP3 codec.

Video data is stored at pre-dithered 1-bit-per-pixel, in native display order. It is dithered at 100fps.
The data is read from the file and blitted directly to the display.
Each frame is delayed approximately 1/100th of a second, which achieves rough speed sync, then frames are skipped to maintain time sync.
As the source data is only 30fps before dithering, these skips are not noticeable.
I wanted to do some kind of compression, but both ends of the process use strings to store the binary data, and Lua's string manipulation is too slow for this.

The Lua script also performs some extra functions like setting the display to maximum brightness, and clearing some settings to ensure the audio plays at the right speed/pitch.
81 changes: 81 additions & 0 deletions badapple.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require("actions")
require("rbsettings")
require("settings")
--mathex=require("math_ex")
do_play=true

datafile = nil

function prepare_data(vidfn,audfn)
-- video
root = rb.current_path()
datafile = io.open(root..vidfn,"rb")
rb.sleep(rb.HZ/10)
-- audio
audfile=io.open(root..'_.m3u','w')
audfile:write(root..audfn..'\n')
audfile:close()
rb.playlist('create',root,'_.m3u')
rb.yield()
rb.playlist('start',0,0*1000,0)
rb.audio("pause") -- immediately pause - add silence at the start to avoid click
end

function play()
if datafile == nil then return end
do_play = true
-- get that data flowing! not sure why this helps. cache?
for i = 1,50 do
datafile:read(1024)
end
datafile:seek("set",0)
rb.lcd_clear_display() -- blank screen
rb.lcd_update()
rb.sleep(rb.HZ/2)
rb.yield()
collectgarbage("collect")
lastframe = -1
dat = nil
rb.audio("resume")
while do_play do
if rb.get_plugin_action(0)==rb.actions.PLA_CANCEL then break end -- kill on button
if rb.audio("status") == 0 then break end -- kill on stop
playtime = rb.audio("elapsed")
if playtime == nil then break end
timeframe = (playtime / 10) + 20 -- 100fps data
if timeframe >= 0 then
if lastframe > timeframe then break end -- kill on repeat
if lastframe < timeframe then
while lastframe < timeframe-20 do -- pos sync
datafile:seek("cur",1024)
lastframe = lastframe + 1
end
dat = datafile:read(1024)
lastframe = lastframe + 1
end
if dat == nil then break end -- kill on data end
rb.lcd_blit_mono(dat,nil,0,0,128,8,128)
rb.sleep(rb.HZ/100) -- rate sync
end
end
datafile:close()
end

--MAIN PROGRAM
rb.backlight_force_on() -- disable timeout
rb.cpu_boost(true) -- enable overclock
rb.audio('stop') -- clear currently playing
rb.sound_set_pitch(10000) -- reset pitch to +0
rb.lcd_clear_display() -- blank screen
rb.lcd_update()
rb.lcd_set_contrast(999) -- full brightness
prepare_data("vid.bin","vid.mp3") -- get files ready
play() -- play video
rb.audio('stop') -- stop music
os.remove(rb.current_path()..'_.m3u') -- remove temp playlist
rb.lcd_set_contrast(rb.settings.read("global_settings",rb.system.global_settings.contrast,"system")) -- restore brightness
rb.backlight_use_settings() -- restore timeout
rb.lcd_clear_display() -- blank screen
rb.lcd_update()
rb.sleep(20) -- wait a moment
rb.cpu_boost(false) -- disable overclock
6 changes: 6 additions & 0 deletions vidextract.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@echo off
del /Q vidframes\*
del vid.mp3
ffmpeg -i vidsrc.mp4 -s 132x66 -sws_flags neighbor vidframes/frame%%04d.png
ffmpeg -i vidsrc.mp4 -q:a 0 -map a vid.mp3
pause
49 changes: 49 additions & 0 deletions writedata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from PIL import Image
import os
import sys
import random

def itoxy(i):
ipage = i&1023
x = ipage/8
y = 7-ipage&7
y += (i//1024)*8
if y >= 16:
y += 2
x += 2
return (x,y)

f = open('vid.bin', 'w+b')
random.seed("ksk2020")
frame_multiplier = 4 # 30 -> 120
frame_skip_every = 6 # 120 -> 100
frame_limit = -1
fcnt = 0
scnt = 0
for filename in os.listdir("vidframes"):
if filename.endswith(".png"):
sys.stdout.write(filename+"\n")
fi = Image.open(os.path.join("vidframes", filename)).convert("RGB")
px = fi.load()
for fm in range(frame_multiplier):
scnt = (scnt + 1) % frame_skip_every
if scnt == 0:
continue
byte_arr = [0]*1024
for i in range(1024):
byte = 0
for j in range(8):
k = i*8+j
pv = px[itoxy(k)][1]
pv /= 255
pv = (pv * 1.04) - 0.02
#pv = 0.5
pv = 1 if pv >= random.random() else 0
byte += byte + pv
byte_arr[i] = byte
binary_format = bytearray(byte_arr)
f.write(binary_format)
fcnt += 1
if fcnt == frame_limit:
break
f.close()

0 comments on commit 1e12adc

Please sign in to comment.