├── Source └── pdxinfo ├── .gitignore ├── README.md ├── crankplayer_convert.sh ├── Makefile ├── CMakeLists.txt ├── crankplayer_convert.py └── src └── main.c /Source/pdxinfo: -------------------------------------------------------------------------------- 1 | name=Crank Player 2 | author=Saagar Jha 3 | description=A crank-based video player 4 | bundleID=com.saagarjha.crankplayer 5 | imagePath= 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CrankPlayer.pdx 2 | Source/pdex.bin 3 | Source/pdex.dylib 4 | Source/pdex.so 5 | Source/pdex.elf 6 | build/ 7 | .nova 8 | .cache 9 | compile_commands.json 10 | 11 | Source/video.pdv 12 | Source/audio.wav 13 | Source/audio_reversed.wav 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crank Player 2 | 3 | Crank Player is a Playdate "game" that lets you play a video with the crank. You can build it as any other Playdate game, but to supply your own content use the crankplayer_convert.sh script (requires FFmpeg) to generate the requisite pdv and wav files out of the video you give it. 4 | -------------------------------------------------------------------------------- /crankplayer_convert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | mkdir crankplayer_frames 6 | ffmpeg -i "$1" -s 400x240 -filter:v fps=30 crankplayer_frames/frame%06d.pbm # 640K ought to be enough for anybody 7 | ./crankplayer_convert.py crankplayer_frames/frame*.pbm > Source/video.pdv 8 | ffmpeg -i "$1" -acodec adpcm_ima_wav Source/audio.wav 9 | ffmpeg -i "$1" -af areverse -acodec adpcm_ima_wav Source/audio_reversed.wav 10 | rm -rf crankplayer_frames 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HEAP_SIZE = 8388208 2 | STACK_SIZE = 61800 3 | 4 | PRODUCT = CrankPlayer.pdx 5 | 6 | # Locate the SDK 7 | SDK = ${PLAYDATE_SDK_PATH} 8 | ifeq ($(SDK),) 9 | SDK = $(shell egrep '^\s*SDKRoot' ~/.Playdate/config | head -n 1 | cut -c9-) 10 | endif 11 | 12 | ifeq ($(SDK),) 13 | $(error SDK path not found; set ENV value PLAYDATE_SDK_PATH) 14 | endif 15 | 16 | ###### 17 | # IMPORTANT: You must add your source folders to VPATH for make to find them 18 | # ex: VPATH += src1:src2 19 | ###### 20 | 21 | VPATH += src 22 | 23 | # List C source files here 24 | SRC = src/main.c 25 | 26 | # List all user directories here 27 | UINCDIR = 28 | 29 | # List user asm files 30 | UASRC = 31 | 32 | # List all user C define here, like -D_DEBUG=1 33 | UDEFS = 34 | 35 | # Define ASM defines here 36 | UADEFS = 37 | 38 | # List the user directory to look for the libraries here 39 | ULIBDIR = 40 | 41 | # List all user libraries here 42 | ULIBS = 43 | 44 | include $(SDK)/C_API/buildsupport/common.mk 45 | 46 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | set(CMAKE_C_STANDARD 11) 3 | 4 | set(ENVSDK $ENV{PLAYDATE_SDK_PATH}) 5 | 6 | if (NOT ${ENVSDK} STREQUAL "") 7 | # Convert path from Windows 8 | file(TO_CMAKE_PATH ${ENVSDK} SDK) 9 | else() 10 | execute_process( 11 | COMMAND bash -c "egrep '^\\s*SDKRoot' $HOME/.Playdate/config" 12 | COMMAND head -n 1 13 | COMMAND cut -c9- 14 | OUTPUT_VARIABLE SDK 15 | OUTPUT_STRIP_TRAILING_WHITESPACE 16 | ) 17 | endif() 18 | 19 | if (NOT EXISTS ${SDK}) 20 | message(FATAL_ERROR "SDK Path not found; set ENV value PLAYDATE_SDK_PATH") 21 | return() 22 | endif() 23 | 24 | set(CMAKE_CONFIGURATION_TYPES "Debug;Release") 25 | set(CMAKE_XCODE_GENERATE_SCHEME TRUE) 26 | 27 | # Game Name Customization 28 | set(PLAYDATE_GAME_NAME crankplayer) 29 | set(PLAYDATE_GAME_DEVICE crankplayer_DEVICE) 30 | 31 | project(${PLAYDATE_GAME_NAME} C ASM) 32 | 33 | if (TOOLCHAIN STREQUAL "armgcc") 34 | add_executable(${PLAYDATE_GAME_DEVICE} ${SDK}/C_API/buildsupport/setup.c src/main.c) 35 | else() 36 | add_library(${PLAYDATE_GAME_NAME} SHARED src/main.c ) 37 | endif() 38 | 39 | include(${SDK}/C_API/buildsupport/playdate_game.cmake) 40 | 41 | -------------------------------------------------------------------------------- /crankplayer_convert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pathlib 4 | import struct 5 | import sys 6 | import zlib 7 | 8 | WIDTH = 400 9 | HEIGHT = 240 10 | 11 | def read_frame(frame): 12 | file = pathlib.Path(frame).read_bytes() 13 | magic = file.find(ord("\n")) + 1 14 | header = file.find(ord("\n"), magic) 15 | assert(file[:magic].decode().strip() == "P4") 16 | (width, height) = map(int, file[magic:header].decode().split()) 17 | assert(width == WIDTH and height == HEIGHT) 18 | # "On" for Playdate is black, so invert the pixels 19 | return zlib.compress(bytes(map(lambda x: x ^ 0b11111111, file[header + 1:]))) 20 | 21 | if __name__ == "__main__": 22 | frames = list(map(read_frame, sys.argv[2:])) 23 | sys.stdout.buffer.write("Playdate VID\x00\x00\x00\x00".encode()) 24 | sys.stdout.buffer.write(struct.pack(" 3 | #include 4 | #include 5 | #include 6 | 7 | PlaydateAPI *playdate; 8 | LCDVideoPlayer *video_player; 9 | FilePlayer *forward_player; 10 | FilePlayer *backward_player; 11 | FilePlayer *current_player; 12 | float framerate; 13 | float length; 14 | int frames; 15 | bool initialized; 16 | 17 | static float player_offset() { 18 | float offset = playdate->sound->fileplayer->getOffset(current_player); 19 | return current_player == forward_player ? offset : length - offset; 20 | } 21 | 22 | // Getting this to feel right is surprisingly difficult 23 | static float normalize_rate(float rate) { 24 | float base = fabsf(rate); 25 | if (base >= 15) { 26 | base = 1; 27 | } else if (base >= 1) { 28 | base = 0.5; 29 | } else { 30 | base = 0; 31 | } 32 | return ((rate > 0) - (rate < 0)) * base; 33 | } 34 | 35 | static int callback(void *userdata) { 36 | // this is a hack, since apparently you can't set an offset outside of here 37 | if (!initialized) { 38 | playdate->sound->fileplayer->setOffset(backward_player, length - playdate->sound->fileplayer->getOffset(forward_player)); 39 | initialized = true; 40 | } 41 | 42 | float rate = normalize_rate(playdate->system->getCrankChange()); 43 | FilePlayer *newPlayer = current_player; 44 | if (rate > 0) { 45 | newPlayer = forward_player; 46 | } else if (rate < 0) { 47 | newPlayer = backward_player; 48 | } 49 | 50 | if (newPlayer != current_player) { 51 | playdate->sound->fileplayer->pause(current_player); 52 | playdate->sound->fileplayer->setOffset(newPlayer, length - playdate->sound->fileplayer->getOffset(current_player)); 53 | playdate->sound->fileplayer->play(newPlayer, 1); 54 | } 55 | playdate->sound->fileplayer->setRate(current_player = newPlayer, fabsf(rate)); 56 | current_player = newPlayer; 57 | int frame = player_offset() * framerate; 58 | 59 | if (0 <= frame && frame <= frames) { 60 | playdate->graphics->video->renderFrame(video_player, frame); 61 | } 62 | return 1; 63 | } 64 | 65 | int eventHandler(PlaydateAPI *_playdate, PDSystemEvent event, uint32_t arg) { 66 | switch (event) { 67 | case kEventInit: 68 | playdate = _playdate; 69 | playdate->system->setUpdateCallback(callback, NULL); 70 | 71 | video_player = playdate->graphics->video->loadVideo("video"); 72 | playdate->graphics->video->useScreenContext(video_player); 73 | playdate->graphics->video->getInfo(video_player, NULL, NULL, &framerate, &frames, NULL); 74 | playdate->display->setRefreshRate(framerate); 75 | 76 | forward_player = playdate->sound->fileplayer->newPlayer(); 77 | playdate->sound->fileplayer->loadIntoPlayer(forward_player, "audio"); 78 | playdate->sound->fileplayer->setRate(forward_player, 0); 79 | playdate->sound->fileplayer->play(forward_player, 1); 80 | length = playdate->sound->fileplayer->getLength(forward_player); 81 | current_player = forward_player; 82 | 83 | backward_player = playdate->sound->fileplayer->newPlayer(); 84 | playdate->sound->fileplayer->loadIntoPlayer(backward_player, "audio_reversed"); 85 | playdate->sound->fileplayer->setRate(backward_player, 0); 86 | playdate->sound->fileplayer->play(backward_player, 1); 87 | playdate->sound->fileplayer->pause(backward_player); 88 | break; 89 | case kEventTerminate: 90 | playdate->graphics->video->freePlayer(video_player); 91 | playdate->sound->fileplayer->freePlayer(forward_player); 92 | playdate->sound->fileplayer->freePlayer(backward_player); 93 | break; 94 | default: 95 | break; 96 | } 97 | return 0; 98 | } 99 | --------------------------------------------------------------------------------