From 47648b88c78f5ce75b1762360089d9acb07f873f Mon Sep 17 00:00:00 2001 From: Jörg Hohensohn Date: Thu, 5 Feb 2004 23:45:46 +0000 Subject: The new video player, with audio! git-svn-id: svn://svn.rockbox.org/rockbox/trunk@4298 a1c6a512-1295-4272-9138-f99709370657 --- apps/plugins/video.c | 1013 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 742 insertions(+), 271 deletions(-) (limited to 'apps') diff --git a/apps/plugins/video.c b/apps/plugins/video.c index 0800745b3c..3f21f7c9dd 100644 --- a/apps/plugins/video.c +++ b/apps/plugins/video.c @@ -9,6 +9,7 @@ * * Plugin for video playback * Reads raw image data + audio data from a file +* !!!!!!!!!! Code Police free zone !!!!!!!!!! * * Copyright (C) 2003-2004 Jörg Hohensohn aka [IDC]Dragon * @@ -19,132 +20,439 @@ * KIND, either express or implied. * ****************************************************************************/ + + +/****************** imports ******************/ + #include "plugin.h" #include "sh7034.h" #include "system.h" -#include "../apps/recorder/widgets.h" /* not in search path, booh */ +#include "../apps/recorder/widgets.h" // not in search path, booh + +#ifndef SIMULATOR // not for simulator by now +#ifdef HAVE_LCD_BITMAP // and definitely not for the Player, haha + +/****************** constants ******************/ + +#define IMIA4 (*((volatile unsigned long*)0x09000180)) // timer 4 + +#define INT_MAX ((int)(~(unsigned)0 >> 1)) +#define INT_MIN (-INT_MAX-1) + +#define SCREENSIZE (LCD_WIDTH*LCD_HEIGHT/8) // in bytes +#define FPS 68 // default fps for headerless (old video-only) file +#define MAX_ACC 20 // maximum FF/FR speedup +#define FF_TICKS 3000; // experimentally found nice -#ifndef SIMULATOR /* not for simulator by now */ -#ifdef HAVE_LCD_BITMAP /* and definitely not for the Player, haha */ +// trigger levels, we need about 80 kB/sec +#define PRECHARGE (1024 * 64) // the initial filling before starting to play +#define SPINUP 2500 // 2200 // from what level on to refill, in milliseconds +#define CHUNK (1024*32) // read size -#define SCREENSIZE (LCD_WIDTH*LCD_HEIGHT/8) /* in bytes */ -#define FILEBUFSIZE (SCREENSIZE*4) /* must result in a multiple of 512 */ -#define FPS 71 /* desired framerate */ -#define WIND_MAX 9 /* max FF/FR speed */ -#define FRAMETIME (FREQ/8/FPS) /* internal timer4 value */ +/****************** prototypes ******************/ +void timer_set(unsigned period); // setup ISR and timer registers +void timer4_isr(void) __attribute__((interrupt_handler)); // IMIA4 ISR +int check_button(void); // determine next relative frame + + +/****************** data types ******************/ + +// plugins don't introduce headers, so structs are repeated from rvf_format.h + +#define HEADER_MAGIC 0x52564668 // "RVFh" at file start +#define AUDIO_MAGIC 0x41756446 // "AudF" for each audio block +#define FILEVERSION 100 // 1.00 +#define CLOCK 11059200 // SH CPU clock + +// format type definitions +#define VIDEOFORMAT_NO_VIDEO 0 +#define VIDEOFORMAT_RAW 1 +#define AUDIOFORMAT_NO_AUDIO 0 +#define AUDIOFORMAT_MP3 1 +#define AUDIOFORMAT_MP3_BITSWAPPED 2 + +// bit flags +#define FLAG_LOOP 0x00000001 // loop the playback, e.g. for stills + +typedef struct // contains whatever might be useful to the player +{ + // general info (16 entries = 64 byte) + unsigned long magic; // HEADER_MAGIC + unsigned long version; // file version + unsigned long flags; // combination of FLAG_xx + unsigned long blocksize; // how many bytes per block (=video frame) + unsigned long bps_average; // bits per second of the whole stream + unsigned long bps_peak; // max. of above (audio may be VBR) + unsigned long reserved[10]; // reserved, should be zero + + // video info (16 entries = 64 byte) + unsigned long video_format; // one of VIDEOFORMAT_xxx + unsigned long video_1st_frame; // byte position of first video frame + unsigned long video_duration; // total length of video part, in ms + unsigned long video_payload_size; // total amount of video data, in bytes + unsigned long video_bitrate; // derived from resolution and frame time, in bps + unsigned long video_frametime; // frame interval in 11.0592 MHz clocks + long video_preroll; // video is how much ahead, in 11.0592 MHz clocks + unsigned long video_width; // in pixels + unsigned long video_height; // in pixels + unsigned long video_reserved[7]; // reserved, should be zero + + // audio info (16 entries = 64 byte) + unsigned long audio_format; // one of AUDIOFORMAT_xxx + unsigned long audio_1st_frame; // byte position of first video frame + unsigned long audio_duration; // total length of audio part, in ms + unsigned long audio_payload_size; // total amount of audio data, in bytes + unsigned long audio_avg_bitrate; // average audio bitrate, in bits per second + unsigned long audio_peak_bitrate; // maximum bitrate + unsigned long audio_headersize; // offset to payload in audio frames + long audio_min_associated; // minimum offset to video frame, in bytes + long audio_max_associated; // maximum offset to video frame, in bytes + unsigned long audio_reserved[7]; // reserved, should be zero + + // more to come... ? + + // Note: padding up to 'blocksize' with zero following this header +} tFileHeader; + +typedef struct // the little header for all audio blocks +{ + unsigned long magic; // AUDIO_MAGIC indicates an audio block + unsigned char previous_block; // previous how many blocks backwards + unsigned char next_block; // next how many blocks forward + short associated_video; // offset to block with corresponding video + unsigned short frame_start; // offset to first frame starting in this block + unsigned short frame_end; // offset to behind last frame ending in this block +} tAudioFrameHeader; + + + +/****************** globals ******************/ -/* globals */ static struct plugin_api* rb; /* here is a global api struct pointer */ +static char gPrint[32]; /* a global printf buffer, saves stack */ + -static enum +// playstate +static struct +{ + enum + { + playing, + paused, + } state; + bool bAudioUnderrun; + bool bVideoUnderrun; + bool bHasAudio; + bool bHasVideo; + int nTimeOSD; // OSD should stay for this many frames + bool bDirtyOSD; // OSD needs redraw + bool bRefilling; // set if refilling buffer + bool bSeeking; + int nSeekAcc; // accelleration value for seek + int nSeekPos; // current file position for seek +} gPlay; + +// buffer information +static struct +{ + int bufsize; + int granularity; // common multiple of block and sector size + unsigned char* pBufStart; // start of ring buffer + unsigned char* pBufEnd; // end of ring buffer + unsigned char* pOSD; // OSD memory (112 bytes for 112*8 pixels) + + int vidcount; // how many video blocks are known in a row + unsigned char* pBufFill; // write pointer for disk, owned by main task + unsigned char* pReadVideo; // video readout, maintained by timer ISR + unsigned char* pReadAudio; // audio readout, maintained by demand ISR + bool bEOF; // flag for end of file + int low_water; // reload threshold + int high_water; // end of reload threshold + int nReadChunk; // how much data for normal buffer fill + int nSeekChunk; // how much data while seeking +} gBuf; + +// statistics +static struct { - playing, - paused, - stop, - exit -} state = playing; + int minAudioAvail; + int minVideoAvail; + int nAudioUnderruns; + int nVideoUnderruns; +} gStats; -static int playstep = 1; /* for current speed and direction */ -static int acceleration = 0; -static long time; /* to calculate the playing time */ +tFileHeader gFileHdr; // file header +/****************** implementation ******************/ -/* test for button, returns relative frame and may change state */ -int check_button(void) +// tool function: return how much playable audio/video is left +int Available(unsigned char* pSnapshot) { - int button; - int frame; /* result: relative frame */ - bool loop; - - frame = playstep; /* default */ + if (pSnapshot <= gBuf.pBufFill) + return gBuf.pBufFill - pSnapshot; + else + return gBuf.bufsize - (pSnapshot - gBuf.pBufFill); +} - do +// debug function to draw buffer indicators +void DrawBuf(void) +{ + static int old_fill = -1; // indicate not initialized + static int old_video; + static int old_audio; + int fill, video, audio; + + // first call? + if (old_fill == -1) { - loop = false; - if (state == playing) - button = rb->button_get(false); - else + rb->memset(gBuf.pOSD, 0x10, LCD_WIDTH); // draw line + gBuf.pOSD[0] = gBuf.pOSD[LCD_WIDTH-1] = 0xFE; // ends + old_fill = 1; // do no harm below + } + + // calculate new tick positions + fill = 1 + ((gBuf.pBufFill - gBuf.pBufStart) * (LCD_WIDTH-2)) / gBuf.bufsize; + video = 1 + ((gBuf.pReadVideo - gBuf.pBufStart) * (LCD_WIDTH-2)) / gBuf.bufsize; + audio = 1 + ((gBuf.pReadAudio - gBuf.pBufStart) * (LCD_WIDTH-2)) / gBuf.bufsize; + + if (fill != old_fill || video != old_video || audio != old_audio) + { + // erase old ticks + gBuf.pOSD[old_fill] = 0x10; + gBuf.pOSD[old_video] = 0x10; + gBuf.pOSD[old_audio] = 0x10; + + gBuf.pOSD[fill] |= 0x20; // below the line, two pixels + gBuf.pOSD[video] |= 0x08; // one above + gBuf.pOSD[audio] |= 0x04; // two above + + old_fill = fill; + old_video = video; + old_audio = audio; + + gPlay.bDirtyOSD = true; // redraw it with next timer IRQ + } +} + + +// helper function to draw a position indicator +void DrawPosition(int pos, int total) +{ + int w,h; + int sec; // estimated seconds + int percent; + + + /* print the estimated position */ + sec = pos / (gFileHdr.bps_average/8); + if (sec < 100*60) /* fits into mm:ss format */ + rb->snprintf(gPrint, sizeof(gPrint), "%02d:%02dm", sec/60, sec%60); + else /* a very long clip, hh:mm format */ + rb->snprintf(gPrint, sizeof(gPrint), "%02d:%02dh", sec/3600, (sec/60)%60); + rb->lcd_puts(0, 7, gPrint); + + /* draw a slider over the rest of the line */ + rb->lcd_getstringsize(gPrint, &w, &h); + w++; + percent = pos/(total/100); + rb->slidebar(w, LCD_HEIGHT-7, LCD_WIDTH-w, 7, percent, Grow_Right); + + if (gPlay.state == paused) // we have to draw ourselves + rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8); + else // let the display time do it + { + gPlay.nTimeOSD = 70; + gPlay.bDirtyOSD = true; // redraw it with next timer IRQ + } +} + + +// helper function to change the volume by a certain amount, +/- +void ChangeVolume(int delta) +{ + int vol = rb->global_settings->volume + delta; + + if (vol > 100) vol = 100; + else if (vol < 0) vol = 0; + if (vol != rb->global_settings->volume) + { + rb->mpeg_sound_set(SOUND_VOLUME, vol); + rb->global_settings->volume = vol; + rb->snprintf(gPrint, sizeof(gPrint), "Vol: %d", vol); + rb->lcd_puts(0, 7, gPrint); + if (gPlay.state == paused) // we have to draw ourselves + rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8); + else // let the display time do it { - long current = *rb->current_tick; - button = rb->button_get(true); /* block */ - time += *rb->current_tick - current; /* don't time while waiting */ + gPlay.nTimeOSD = 50; // display it for 50 frames + gPlay.bDirtyOSD = true; // let the refresh copy it to LCD } + } +} - switch(button) - { - case BUTTON_NONE: - break; /* quick exit */ - case BUTTON_LEFT | BUTTON_REPEAT: - if (state == paused) - frame = -1; /* single step back */ - else if (state == playing) - { - acceleration--; - playstep = acceleration/4; /* FR */ - if (playstep > -2) - playstep = -2; - if (playstep < -WIND_MAX) - playstep = -WIND_MAX; - } - break; +// sync the video to the current audio +void SyncVideo(void) +{ + tAudioFrameHeader* pAudioBuf; - case BUTTON_RIGHT | BUTTON_REPEAT: - if (state == paused) - frame = 1; /* single step */ - else if (state == playing) + pAudioBuf = (tAudioFrameHeader*)(gBuf.pReadAudio); + if (pAudioBuf->magic == AUDIO_MAGIC) + { + gBuf.vidcount = 0; // nothing known + // sync the video position + gBuf.pReadVideo = gBuf.pReadAudio + + (long)pAudioBuf->associated_video * (long)gFileHdr.blocksize; + + // handle possible wrap + if (gBuf.pReadVideo >= gBuf.pBufEnd) + gBuf.pReadVideo -= gBuf.bufsize; + else if (gBuf.pReadVideo < gBuf.pBufStart) + gBuf.pReadVideo += gBuf.bufsize; + } +} + + +// setup ISR and timer registers +void timer_set(unsigned period) +{ + if (period) + { + and_b(~0x10, &TSTR); // Stop the timer 4 + and_b(~0x10, &TSNC); // No synchronization + and_b(~0x10, &TMDR); // Operate normally + + IMIA4 = (unsigned long)timer4_isr; // install ISR + + TSR4 &= ~0x01; + TIER4 = 0xF9; // Enable GRA match interrupt + + GRA4 = (unsigned short)(period/4 - 1); + TCR4 = 0x22; // clear at GRA match, sysclock/4 + IPRD = (IPRD & 0xFF0F) | 0x0010; // interrupt priority 1 (lowest) + or_b(0x10, &TSTR); // start timer 4 + } + else + { + and_b(~0x10, &TSTR); // stop the timer 4 + IPRD = (IPRD & 0xFF0F); // disable interrupt + } +} + + +// timer interrupt handler to display a frame +void timer4_isr(void) // IMIA4 +{ + int available; + tAudioFrameHeader* pAudioBuf; + int height; // height to display + + TSR4 &= ~0x01; // clear the interrupt + + // xor_b(0x40, &PBDRL); // test: toggle LED (PB6) + // debug code +/* + gPlay.nTimeOSD = 1; + DrawBuf(); + gPlay.bDirtyOSD = true; +*/ + + // reduce height if we have OSD on + height = gFileHdr.video_height/8; + if (gPlay.nTimeOSD > 0) + { + gPlay.nTimeOSD--; + height = MIN(LCD_HEIGHT/8-1, height); // reserve bottom line + if (gPlay.bDirtyOSD) + { // OSD to bottom line + rb->lcd_blit(gBuf.pOSD, 0, LCD_HEIGHT/8-1, + LCD_WIDTH, 1, LCD_WIDTH); + gPlay.bDirtyOSD = false; + } + } + + rb->lcd_blit(gBuf.pReadVideo, 0, 0, + gFileHdr.video_width, height, gFileHdr.video_width); + + available = Available(gBuf.pReadVideo); + + // loop to skip audio frame(s) + while(1) + { + // just for the statistics + if (!gBuf.bEOF && available < gStats.minVideoAvail) + gStats.minVideoAvail = available; + + if (available < (int)gFileHdr.blocksize) + { // no data for next frame + + if (gBuf.bEOF && (gFileHdr.flags & FLAG_LOOP)) + { // loop now, assuming the looped clip fits in memory + gBuf.pReadVideo = gBuf.pBufStart + gFileHdr.video_1st_frame; + } + else { - acceleration++; - playstep = acceleration/4; /* FF */ - if (playstep < 2) - playstep = 2; - if (playstep > WIND_MAX) - playstep = WIND_MAX; + gPlay.bVideoUnderrun = true; + timer_set(0); // disable ourselves + return; // no data available } - break; + } - case BUTTON_PLAY: - if (state == playing && (playstep == 1 || playstep == -1)) - state = paused; - else if (state == paused) - state = playing; - playstep = 1; - acceleration = 0; - break; + gBuf.pReadVideo += gFileHdr.blocksize; + if (gBuf.pReadVideo >= gBuf.pBufEnd) + gBuf.pReadVideo -= gBuf.bufsize; // wraparound + available -= gFileHdr.blocksize; - case BUTTON_LEFT: - if (state == paused) - frame = -1; /* single step back */ - else if (state == playing) - playstep = -1; /* rewind */ - acceleration = 0; - break; + if (!gPlay.bHasAudio) + break; // no need to skip any audio - case BUTTON_RIGHT: - if (state == paused) - frame = 1; /* single step */ - else if (state == playing) - playstep = 1; /* forward */ - acceleration = 0; - break; + if (gBuf.vidcount) + { + // we know the next is a video frame + gBuf.vidcount--; + break; // exit the loop + } + + pAudioBuf = (tAudioFrameHeader*)(gBuf.pReadVideo); + if (pAudioBuf->magic == AUDIO_MAGIC) + { // we ran into audio, can happen after seek + gBuf.vidcount = pAudioBuf->next_block; + if (gBuf.vidcount) + gBuf.vidcount--; // minus the audio block + } + } // while +} - case BUTTON_OFF: - state = stop; - break; - case SYS_USB_CONNECTED: - state = exit; - break; +// ISR function to get more mp3 data +void GetMoreMp3(unsigned char** start, int* size) +{ + int available; + int advance; - default: - if (state != playing) - loop = true; - } + tAudioFrameHeader* pAudioBuf = (tAudioFrameHeader*)(gBuf.pReadAudio); + + advance = pAudioBuf->next_block * gFileHdr.blocksize; + + available = Available(gBuf.pReadAudio); + + // just for the statistics + if (!gBuf.bEOF && available < gStats.minAudioAvail) + gStats.minAudioAvail = available; + + if (available < advance || advance == 0) + { + gPlay.bAudioUnderrun = true; + return; // no data available } - while (loop); - return frame; + gBuf.pReadAudio += advance; + if (gBuf.pReadAudio >= gBuf.pBufEnd) + gBuf.pReadAudio -= gBuf.bufsize; // wraparound + + *start = gBuf.pReadAudio + gFileHdr.audio_headersize; + *size = gFileHdr.blocksize - gFileHdr.audio_headersize; } @@ -161,225 +469,388 @@ int WaitForButton(void) } -/* play from memory, loop until OFF is pressed */ -int show_buffer(unsigned char* p_start, int frames) +int SeekTo(int fd, int nPos) { - unsigned char* p_current = p_start; - unsigned char* p_end = p_start + SCREENSIZE * frames; - int shown = 0; - int delta; /* next frame */ + int read_now, got_now; - do - { - /* wait for frame to be due */ - while (TCNT4 < FRAMETIME) /* use our timer 4 */ - rb->yield(); /* yield to the other treads */ - TCNT4 -= FRAMETIME; - - rb->lcd_blit(p_current, 0, 0, LCD_WIDTH, LCD_HEIGHT/8, LCD_WIDTH); + if (gPlay.bHasAudio) + rb->mp3_play_stop(); // stop audio ISR + if (gPlay.bHasVideo) + timer_set(0); // stop the timer 4 + + rb->lseek(fd, nPos, SEEK_SET); - shown++; + gBuf.pBufFill = gBuf.pBufStart; // all empty + gBuf.pReadVideo = gBuf.pReadAudio = gBuf.pBufStart; - delta = check_button(); - p_current += delta * SCREENSIZE; - if (p_current >= p_end || p_current < p_start) - p_current = p_start; /* wrap */ - } while(state != stop && state != exit); + read_now = (PRECHARGE + gBuf.granularity - 1); // round up + read_now -= read_now % gBuf.granularity; // to granularity + got_now = rb->read(fd, gBuf.pBufFill, read_now); + gBuf.bEOF = (read_now != got_now); + gBuf.pBufFill += got_now; - return (state != exit) ? shown : -1; + if (nPos == 0) + { // we seeked to the start + if (gPlay.bHasVideo) + gBuf.pReadVideo += gFileHdr.video_1st_frame; + + if (gPlay.bHasAudio) + gBuf.pReadAudio += gFileHdr.audio_1st_frame; + } + else + { // we have to search for the positions + if (gPlay.bHasAudio) // prepare audio playback, if contained + { + // search for audio frame + while (((tAudioFrameHeader*)(gBuf.pReadAudio))->magic != AUDIO_MAGIC) + gBuf.pReadAudio += gFileHdr.blocksize; + + rb->mp3_play_data(gBuf.pReadAudio + gFileHdr.audio_headersize, + gFileHdr.blocksize - gFileHdr.audio_headersize, GetMoreMp3); + + if (gPlay.bHasVideo) + SyncVideo(); // pick the right video for that + } + } + + // synchronous start + if (gPlay.bHasAudio) + { + gPlay.bAudioUnderrun = false; + rb->mp3_play_pause(true); // kickoff audio + } + if (gPlay.bHasVideo) + { + gPlay.bVideoUnderrun = false; + timer_set(gFileHdr.video_frametime); // start display interrupt + } + + return 0; } -/* play from file, exit if OFF is pressed */ -int show_file(unsigned char* p_buffer, int fd) +// returns >0 if continue, =0 to stop, <0 to abort (USB) +int PlayTick(int fd) { - long tag[FILEBUFSIZE/512]; /* I treat the buffer as direct-mapped cache */ - long framepos = 0; /* position of frame in file */ - long filesize = rb->filesize(fd); - long filepos = 0; /* my own counting */ - int shown = 0; - - long readfrom; - long readto; - int read; /* amount read from disk */ - long frame_offset; /* pos of frame in buffer */ - long writefrom; /* round down to sector */ - long sector; /* sector in frame buffer */ - long pos_aligned, orig_aligned; /* round down to sector */ - char buf[10]; - - rb->memset(&tag, 0xFF, sizeof(tag)); /* invalidate cache */ - - do - { - /* load the frame into memory */ - readfrom = readto = -1; /* invalidate */ - frame_offset = framepos % FILEBUFSIZE; /* pos of frame in buffer */ - writefrom = frame_offset & ~511; /* round down to sector */ - sector = frame_offset / 512; /* sector in frame buffer */ - orig_aligned = pos_aligned = framepos & ~511; /* down to sector */ - - do + int button; + int avail_audio = -1, avail_video = -1; + int retval = 1; + int filepos; + + if (gPlay.bHasAudio) + avail_audio = Available(gBuf.pReadAudio); + if (gPlay.bHasVideo) + avail_video = Available(gBuf.pReadVideo); + + if ((gPlay.bHasAudio && avail_audio < gBuf.low_water) + || (gPlay.bHasVideo && avail_video < gBuf.low_water)) + { + gPlay.bRefilling = true; /* go to refill mode */ + } + + if ((!gPlay.bHasAudio || gPlay.bAudioUnderrun) + && (!gPlay.bHasVideo || gPlay.bVideoUnderrun)) + return 0; // all expired + + if (!gPlay.bRefilling || gBuf.bEOF) + { // nothing to do + button = rb->button_get_w_tmo(HZ/10); + } + else + { // refill buffer + int read_now, got_now; + int buf_free; + + // how much can we reload, don't fill completely, would appear empty + buf_free = gBuf.bufsize - MAX(avail_audio, avail_video) - gBuf.high_water; + if (buf_free < 0) + buf_free = 0; // just for safety + buf_free -= buf_free % gBuf.granularity; // round down to granularity + + // in one piece max. up to buffer end (wrap after that) + read_now = MIN(buf_free, gBuf.pBufEnd - gBuf.pBufFill); + + // load piecewise, to stay responsive + read_now = MIN(read_now, gBuf.nReadChunk); + + if (read_now == buf_free) + gPlay.bRefilling = false; // last piece requested + + got_now = rb->read(fd, gBuf.pBufFill, read_now); + if (got_now != read_now || read_now == 0) { - if (tag[sector] != pos_aligned) /* in cache? */ - { /* not cached */ - tag[sector] = pos_aligned; - if (readfrom == -1) /* not used yet? */ - { - readfrom = pos_aligned; /* set start */ - writefrom += pos_aligned - orig_aligned; - } - readto = pos_aligned; /* set stop */ - } - pos_aligned += 512; - sector++; - } while (pos_aligned < framepos + SCREENSIZE); - - if (readfrom != -1) - { /* need to read from disk */ - if (filepos != readfrom) - { /* need to seek */ - filepos = rb->lseek(fd, readfrom, SEEK_SET); - } - read = readto - readfrom + 512; - /* read the sector(s) */ - filepos += rb->read(fd, p_buffer + writefrom, read); + gBuf.bEOF = true; + gPlay.bRefilling = false; } - else - read = 0; - /* wait for frame to be due */ - while (TCNT4 < FRAMETIME) /* use our timer 4 */ - rb->yield(); /* yield to the other treads */ - TCNT4 -= FRAMETIME; + if (!gPlay.bRefilling) + rb->ata_sleep(); // no point in leaving the disk run til timeout - /* display OSD if FF/FR */ - if (playstep != 1 && playstep != -1) - { - int w,h; + gBuf.pBufFill += got_now; + if (gBuf.pBufFill >= gBuf.pBufEnd) + gBuf.pBufFill = gBuf.pBufStart; // wrap - if (playstep > 0) - rb->snprintf(buf, sizeof(buf), "%d>>", playstep); - else - rb->snprintf(buf, sizeof(buf), "%d<<", -playstep); + rb->yield(); // have mercy with the other threads + button = rb->button_get(false); + } - rb->lcd_getstringsize(buf, &w, &h); - rb->lcd_putsxy(0, LCD_HEIGHT-h, buf); - - w++; - rb->slidebar(w, LCD_HEIGHT-7, LCD_WIDTH-w, 7, - (100 * filepos)/filesize, Grow_Right); - rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8); - rb->lcd_blit(p_buffer + frame_offset, 0, 0, - LCD_WIDTH, LCD_HEIGHT/8 - 1, LCD_WIDTH); - } + if (button != BUTTON_NONE) + { + filepos = rb->lseek(fd, 0, SEEK_CUR); + + if (gPlay.bHasVideo) // video position is more accurate + filepos -= Available(gBuf.pReadVideo); // take video position else - { /* display the frame normally */ - rb->lcd_blit(p_buffer + frame_offset, 0, 0, LCD_WIDTH, - LCD_HEIGHT/8, LCD_WIDTH); - } + filepos -= Available(gBuf.pReadAudio); // else audio - /* query keys to determine next frame */ - framepos += check_button() * SCREENSIZE; - if (framepos <= 0) - { - state = paused; - framepos = 0; - } - else if (framepos >= filesize) - { - if (state == playing) + switch (button) + { // set exit conditions + case BUTTON_OFF: + retval = 0; // signal "stop" to caller + break; + case SYS_USB_CONNECTED: + retval = -1; // signal "abort" to caller + break; + case BUTTON_PLAY: + if (gPlay.bSeeking) + { + gPlay.bSeeking = false; + gPlay.state = playing; + SeekTo(fd, gPlay.nSeekPos); + } + else if (gPlay.state == playing) { - if (playstep == 1) - state = stop; /* reached the end while playing normally */ - else + gPlay.state = paused; + if (gPlay.bHasAudio) + rb->mp3_play_pause(false); // pause audio + if (gPlay.bHasVideo) + and_b(~0x10, &TSTR); // stop the timer 4 + } + else if (gPlay.state == paused) + { + gPlay.state = playing; + if (gPlay.bHasAudio) { - state = paused; /* it may have been FF */ - playstep = 1; + if (gPlay.bHasVideo) + SyncVideo(); + rb->mp3_play_pause(true); // play audio } + if (gPlay.bHasVideo) + or_b(0x10, &TSTR); // start the video } - - framepos = filesize - SCREENSIZE; - /* in case the file size is no integer multiple */ - framepos -= framepos % SCREENSIZE; + break; + case BUTTON_UP: + case BUTTON_UP | BUTTON_REPEAT: + if (gPlay.bHasAudio) + ChangeVolume(1); + break; + case BUTTON_DOWN: + case BUTTON_DOWN | BUTTON_REPEAT: + if (gPlay.bHasAudio) + ChangeVolume(-1); + break; + case BUTTON_LEFT: + case BUTTON_LEFT | BUTTON_REPEAT: + if (!gPlay.bSeeking) // prepare seek + { + gPlay.nSeekPos = filepos; + gPlay.bSeeking = true; + gPlay.nSeekAcc = 0; + } + else if (gPlay.nSeekAcc > 0) // other direction, stop sliding + gPlay.nSeekAcc = 0; + else + gPlay.nSeekAcc--; + break; + case BUTTON_RIGHT: + case BUTTON_RIGHT | BUTTON_REPEAT: + if (!gPlay.bSeeking) // prepare seek + { + gPlay.nSeekPos = filepos; + gPlay.bSeeking = true; + gPlay.nSeekAcc = 0; + } + else if (gPlay.nSeekAcc < 0) // other direction, stop sliding + gPlay.nSeekAcc = 0; + else + gPlay.nSeekAcc++; + break; } + } /* if (button != BUTTON_NONE) */ - shown++; - - } while (state != stop && state != exit); + if (gPlay.bSeeking) // seeking? + { + if (gPlay.nSeekAcc < -MAX_ACC) + gPlay.nSeekAcc = -MAX_ACC; + else if (gPlay.nSeekAcc > MAX_ACC) + gPlay.nSeekAcc = MAX_ACC; + + gPlay.nSeekPos += gPlay.nSeekAcc * gBuf.nSeekChunk; + if (gPlay.nSeekPos < 0) + gPlay.nSeekPos = 0; + if (gPlay.nSeekPos > rb->filesize(fd) - gBuf.granularity) + { + gPlay.nSeekPos = rb->filesize(fd); + gPlay.nSeekPos -= gPlay.nSeekPos % gBuf.granularity; + } + DrawPosition(gPlay.nSeekPos, rb->filesize(fd)); + } - return (state != exit) ? shown : -1; + return retval; } int main(char* filename) { - char buf[32]; - int buffer_size, file_size; - unsigned char* p_buffer; + int file_size; int fd; /* file descriptor handle */ - int got_now; /* how many bytes read from file */ - int frames, shown; + int read_now, got_now; int button; - int fps; + int retval; - p_buffer = rb->plugin_get_buffer(&buffer_size); - if (buffer_size < FILEBUFSIZE) - return PLUGIN_ERROR; /* not enough memory */ - + // try to open the file fd = rb->open(filename, O_RDONLY); if (fd < 0) return PLUGIN_ERROR; - - /* init timer 4, crude code */ - IPRD = (IPRD & 0xFF0F); // disable interrupt - and_b(~0x10, &TSTR); // Stop the timer 4 - and_b(~0x10, &TSNC); // No synchronization - and_b(~0x10, &TMDR); // Operate normally - TCR4 = 0x03; // no clear at GRA match, sysclock/8 - TCNT4 = 0; // start counting at 0 - file_size = rb->filesize(fd); - if (file_size <= buffer_size) - { /* we can read the whole file in advance */ - got_now = rb->read(fd, p_buffer, file_size); + + // init statistics + rb->memset(&gStats, 0, sizeof(gStats)); + gStats.minAudioAvail = gStats.minVideoAvail = INT_MAX; + + // init playback state + rb->memset(&gPlay, 0, sizeof(gPlay)); + gPlay.state = playing; + + // init buffer + rb->memset(&gBuf, 0, sizeof(gBuf)); + gBuf.pOSD = rb->lcd_framebuffer + LCD_WIDTH*7; // last screen line + gBuf.pBufStart = rb->plugin_get_mp3_buffer(&gBuf.bufsize); + //gBuf.bufsize = 1700*1024; // test!!!! + gBuf.pBufFill = gBuf.pBufStart; // all empty + gBuf.pReadVideo = gBuf.pReadAudio = gBuf.pBufStart; + + // load file header + read_now = sizeof(gFileHdr); + got_now = rb->read(fd, &gFileHdr, read_now); + rb->lseek(fd, 0, SEEK_SET); // rewind to restart sector-aligned + if (got_now != read_now) + { rb->close(fd); - frames = got_now / (LCD_WIDTH*LCD_HEIGHT/8); - or_b(0x10, &TSTR); // start timer 4 - time = *rb->current_tick; - shown = show_buffer(p_buffer, frames); - time = *rb->current_tick - time; + return (PLUGIN_ERROR); + } + + // check header + if (gFileHdr.magic != HEADER_MAGIC) + { // old file, use default info + rb->memset(&gFileHdr, 0, sizeof(gFileHdr)); + gFileHdr.blocksize = SCREENSIZE; + if (file_size < SCREENSIZE * FPS) // less than a second + gFileHdr.flags |= FLAG_LOOP; + gFileHdr.video_format = VIDEOFORMAT_RAW; + gFileHdr.video_width = LCD_WIDTH; + gFileHdr.video_height = LCD_HEIGHT; + gFileHdr.video_frametime = CLOCK / FPS; + gFileHdr.bps_peak = gFileHdr.bps_average = LCD_WIDTH * LCD_HEIGHT * FPS; } + + // continue buffer init: align the end, calc low water, read sizes + gBuf.granularity = gFileHdr.blocksize; + while (gBuf.granularity % 512) // common multiple of sector size + gBuf.granularity *= 2; + gBuf.bufsize -= gBuf.bufsize % gBuf.granularity; // round down + gBuf.pBufEnd = gBuf.pBufStart + gBuf.bufsize; + gBuf.low_water = SPINUP * gFileHdr.bps_peak / 8000; + if (gFileHdr.audio_min_associated < 0) + gBuf.high_water = 0 - gFileHdr.audio_min_associated; else - { /* we need to stream */ - or_b(0x10, &TSTR); // start timer 4 - time = *rb->current_tick; - shown = show_file(p_buffer, fd); - time = *rb->current_tick - time; - rb->close(fd); + gBuf.high_water = 1; // never fill buffer completely, would appear empty + gBuf.nReadChunk = (CHUNK + gBuf.granularity - 1); // round up + gBuf.nReadChunk -= gBuf.nReadChunk % gBuf.granularity;// and align + gBuf.nSeekChunk = rb->filesize(fd) / FF_TICKS; + gBuf.nSeekChunk += gBuf.granularity - 1; // round up + gBuf.nSeekChunk -= gBuf.nSeekChunk % gBuf.granularity; // and align + + // precharge buffer with more data + read_now = MAX(gFileHdr.audio_1st_frame, gFileHdr.video_1st_frame); + read_now = (read_now + PRECHARGE + gBuf.granularity - 1); + read_now -= read_now % gBuf.granularity; // round up to granularity + got_now = rb->read(fd, gBuf.pBufFill, read_now); + gBuf.pBufFill += got_now; + + // prepare video playback, if contained + if (gFileHdr.video_format == VIDEOFORMAT_RAW) + { + gBuf.pReadVideo += gFileHdr.video_1st_frame; + gPlay.bHasVideo = true; + + if (rb->global_settings->backlight_timeout > 0) + rb->backlight_set_timeout(1); // keep the light on } - if (shown == -1) /* exception */ - return PLUGIN_USB_CONNECTED; + // prepare audio playback, if contained + if (gFileHdr.audio_format == AUDIOFORMAT_MP3_BITSWAPPED) + { + gBuf.pReadAudio += gFileHdr.audio_1st_frame; + gPlay.bHasAudio = true; - fps = (shown * HZ *100) / time; /* 100 times fps */ + rb->mp3_play_init(); + rb->mp3_play_data(gBuf.pReadAudio + gFileHdr.audio_headersize, + gFileHdr.blocksize - gFileHdr.audio_headersize, GetMoreMp3); + rb->mpeg_sound_set(SOUND_VOLUME, rb->global_settings->volume); + } + + // synchronous start + gPlay.state = playing; + if (gPlay.bHasAudio) + rb->mp3_play_pause(true); // kickoff audio + if (gPlay.bHasVideo) + timer_set(gFileHdr.video_frametime); // start display interrupt + + // all that's left to do is keep the buffer full + do // the main loop + { + retval = PlayTick(fd); + } while (retval > 0); + + rb->close(fd); // close the file + + if (gPlay.bHasVideo) + timer_set(0); // stop video ISR, now I can use the display again + + if (gPlay.bHasAudio) + rb->mp3_play_stop(); // stop audio ISR + // restore normal backlight setting + rb->backlight_set_timeout(rb->global_settings->backlight_timeout); + + if (retval < 0) // aborted? + { + return PLUGIN_USB_CONNECTED; + } + + // display statistics rb->lcd_clear_display(); - rb->snprintf(buf, sizeof(buf), "%d frames shown", shown); - rb->lcd_puts(0, 0, buf); - rb->snprintf(buf, sizeof(buf), "%d.%02d seconds", time/HZ, time%HZ); - rb->lcd_puts(0, 1, buf); - rb->snprintf(buf, sizeof(buf), "%d.%02d fps", fps/100, fps%100); - rb->lcd_puts(0, 2, buf); - rb->snprintf(buf, sizeof(buf), "file: %d bytes", file_size); - rb->lcd_puts(0, 6, buf); - rb->snprintf(buf, sizeof(buf), "buffer: %d bytes", buffer_size); - rb->lcd_puts(0, 7, buf); + rb->snprintf(gPrint, sizeof(gPrint), "AudioUnderrun: %d", gPlay.bAudioUnderrun); + rb->lcd_puts(0, 0, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "VideoUnderrun: %d", gPlay.bVideoUnderrun); + rb->lcd_puts(0, 1, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "MinAudio: %d bytes", gStats.minAudioAvail); + rb->lcd_puts(0, 2, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "MinVideo: %d bytes", gStats.minVideoAvail); + rb->lcd_puts(0, 3, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "ReadChunk: %d", gBuf.nReadChunk); + rb->lcd_puts(0, 4, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "SeekChunk: %d", gBuf.nSeekChunk); + rb->lcd_puts(0, 5, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "1st Video: %d", gFileHdr.video_1st_frame); + rb->lcd_puts(0, 6, gPrint); + rb->snprintf(gPrint, sizeof(gPrint), "pBufStart: %x", gBuf.pBufStart); + rb->lcd_puts(0, 7, gPrint); + rb->lcd_update(); button = WaitForButton(); return (button == SYS_USB_CONNECTED) ? PLUGIN_USB_CONNECTED : PLUGIN_OK; - - } @@ -393,7 +864,7 @@ enum plugin_status plugin_start(struct plugin_api* api, void* parameter) matches the machine it is running on */ TEST_PLUGIN_API(api); - rb = api; /* copy to global api pointer */ + rb = api; // copy to global api pointer if (parameter == NULL) { @@ -401,13 +872,13 @@ enum plugin_status plugin_start(struct plugin_api* api, void* parameter) return PLUGIN_ERROR; } - /* now go ahead and have fun! */ + // now go ahead and have fun! ret = main((char*) parameter); if (ret==PLUGIN_USB_CONNECTED) rb->usb_screen(); return ret; } -#endif /* #ifdef HAVE_LCD_BITMAP */ -#endif /* #ifndef SIMULATOR */ +#endif // #ifdef HAVE_LCD_BITMAP +#endif // #ifndef SIMULATOR -- cgit v1.2.3