/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * 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 * * All files in this archive are subject to the GNU General Public License. * See the file COPYING in the source tree root for full license agreement. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * 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 #ifndef SIMULATOR // not for simulator by now #ifdef HAVE_LCD_BITMAP // and definitely not for the Player, haha PLUGIN_HEADER /* variable button definitions */ #if CONFIG_KEYPAD == RECORDER_PAD #define VIDEO_STOP_SEEK BUTTON_PLAY #define VIDEO_RESUME BUTTON_PLAY #define VIDEO_DEBUG BUTTON_F1 #define VIDEO_CONTRAST_DOWN BUTTON_F2 #define VIDEO_CONTRAST_UP BUTTON_F3 #elif CONFIG_KEYPAD == ONDIO_PAD #define VIDEO_STOP_SEEK_PRE BUTTON_MENU #define VIDEO_STOP_SEEK (BUTTON_MENU | BUTTON_REL) #define VIDEO_RESUME BUTTON_RIGHT #define VIDEO_CONTRAST_DOWN (BUTTON_MENU | BUTTON_DOWN) #define VIDEO_CONTRAST_UP (BUTTON_MENU | BUTTON_UP) #endif /****************** constants ******************/ #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 // trigger levels, we need about 80 kB/sec #define SPINUP_INIT 5000 // from what level on to refill, in milliseconds #define SPINUP_SAFETY 700 // how much on top of the measured spinup time #define CHUNK (1024*32) // read size /****************** prototypes ******************/ void timer4_isr(void); // 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 // 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 resume_pos; // file position to resume to unsigned long reserved[9]; // 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 ******************/ static struct plugin_api* rb; /* here is a global api struct pointer */ static char gPrint[32]; /* a global printf buffer, saves stack */ // playstate static struct { enum { paused, playing, } 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 bool bDiskSleep; // disk is suspended #if FREQ == 12000000 /* Ondio speed kludge */ int nFrameTimeAdjusted; #endif } 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 spinup_safety; // safety margin when recalculating low_water int nReadChunk; // how much data for normal buffer fill int nSeekChunk; // how much data while seeking } gBuf; // statistics static struct { int minAudioAvail; int minVideoAvail; int nAudioUnderruns; int nVideoUnderruns; long minSpinup; long maxSpinup; } gStats; tFileHeader gFileHdr; // file header /****************** implementation ******************/ // tool function: return how much playable audio/video is left int Available(unsigned char* pSnapshot) { if (pSnapshot <= gBuf.pBufFill) return gBuf.pBufFill - pSnapshot; else return gBuf.bufsize - (pSnapshot - gBuf.pBufFill); } // debug function to draw buffer indicators void DrawBuf(void) { int fill, video, audio; rb->memset(gBuf.pOSD, 0x10, LCD_WIDTH); // draw line gBuf.pOSD[0] = gBuf.pOSD[LCD_WIDTH-1] = 0xFE; // ends // 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; gBuf.pOSD[fill] |= 0x20; // below the line, two pixels gBuf.pOSD[video] |= 0x08; // one above gBuf.pOSD[audio] |= 0x04; // two above if (gPlay.state == paused) // we have to draw ourselves rb->lcd_update_rect(0, LCD_HEIGHT-8, LCD_WIDTH, 8); else 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 /* 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++; rb->scrollbar(w, LCD_HEIGHT-7, LCD_WIDTH-w, 7, total, 0, pos, HORIZONTAL); 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 minvol = rb->sound_min(SOUND_VOLUME); int maxvol = rb->sound_max(SOUND_VOLUME); int vol = rb->global_settings->volume + delta; if (vol > maxvol) vol = maxvol; else if (vol < minvol) vol = minvol; if (vol != rb->global_settings->volume) { rb->sound_set(SOUND_VOLUME, vol); rb->global_settings->volume = vol; rb->snprintf(gPrint, sizeof(gPrint), "Vol: %d dB", 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 { gPlay.nTimeOSD = 50; // display it for 50 frames gPlay.bDirtyOSD = true; // let the refresh copy it to LCD } } } // helper function to change the LCD contrast by a certain amount, +/- void ChangeContrast(int delta) { static int mycontrast = -1; /* the "permanent" value while running */ int contrast; /* updated value */ if (mycontrast == -1) mycontrast = rb->global_settings->contrast; contrast = mycontrast + delta; if (contrast > 63) contrast = 63; else if (contrast < 5) contrast = 5; if (contrast != mycontrast) { rb->lcd_set_contrast(contrast); mycontrast = contrast; rb->snprintf(gPrint, sizeof(gPrint), "Contrast: %d", contrast); 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 { gPlay.nTimeOSD = 50; // display it for 50 frames gPlay.bDirtyOSD = true; // let the refresh copy it to LCD } } } // sync the video to the current audio void SyncVideo(void) { tAudioFrameHeader* pAudioBuf; 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; } } // timer interrupt handler to display a frame void timer4_isr(void) { int available; tAudioFrameHeader* pAudioBuf; int height; // height to display // 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; // FixMe: pReadVideo is incremented below } else { gPlay.bVideoUnderrun = true; rb->timer_unregister(); // disable ourselves return; // no data available } } else // normal advance for next time { gBuf.pReadVideo += gFileHdr.blocksize; if (gBuf.pReadVideo >= gBuf.pBufEnd) gBuf.pReadVideo -= gBuf.bufsize; // wraparound available -= gFileHdr.blocksize; } if (!gPlay.bHasAudio) break; // no need to skip any audio 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 } // ISR function to get more mp3 data void GetMoreMp3(unsigned char** start, int* size) { int available; int advance; 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 + (int)gFileHdr.blocksize || advance == 0) { gPlay.bAudioUnderrun = true; return; // no data available } 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; } int WaitForButton(void) { int button; do { button = rb->button_get(true); rb->default_event_handler(button); } while ((button & BUTTON_REL) && button != SYS_USB_CONNECTED); return button; } bool WantResume(int fd) { int button; rb->lcd_puts(0, 0, "Resume to this"); rb->lcd_puts(0, 1, "last position?"); rb->lcd_puts(0, 2, "PLAY = yes"); rb->lcd_puts(0, 3, "Any Other = no"); rb->lcd_puts(0, 4, " (plays from start)"); DrawPosition(gFileHdr.resume_pos, rb->filesize(fd)); rb->lcd_update(); button = WaitForButton(); return (button == VIDEO_RESUME); } int SeekTo(int fd, int nPos) { int read_now, got_now; if (gPlay.bHasAudio) rb->mp3_play_stop(); // stop audio ISR if (gPlay.bHasVideo) rb->timer_unregister(); // stop the timer rb->lseek(fd, nPos, SEEK_SET); gBuf.pBufFill = gBuf.pBufStart; // all empty gBuf.pReadVideo = gBuf.pReadAudio = gBuf.pBufStart; read_now = gBuf.low_water - 1; // less than low water, so loading will continue read_now -= read_now % gBuf.granularity; // round down to granularity got_now = rb->read(fd, gBuf.pBufFill, read_now); gBuf.bEOF = (read_now != got_now); gBuf.pBufFill += got_now; 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; if (gPlay.bHasVideo) SyncVideo(); // pick the right video for that } } // synchronous start gPlay.state = playing; if (gPlay.bHasAudio) { gPlay.bAudioUnderrun = false; rb->mp3_play_data(gBuf.pReadAudio + gFileHdr.audio_headersize, gFileHdr.blocksize - gFileHdr.audio_headersize, GetMoreMp3); rb->mp3_play_pause(true); // kickoff audio } if (gPlay.bHasVideo) { gPlay.bVideoUnderrun = false; // start display interrupt #if FREQ == 12000000 /* Ondio speed kludge */ rb->timer_register(1, NULL, gPlay.nFrameTimeAdjusted, 1, timer4_isr); #else rb->timer_register(1, NULL, gFileHdr.video_frametime, 1, timer4_isr); #endif } return 0; } // called from default_event_handler_ex() or at end of playback void Cleanup(void *fd) { rb->close(*(int*)fd); // close the file if (gPlay.bHasVideo) rb->timer_unregister(); // 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); // restore normal contrast rb->lcd_set_contrast(rb->global_settings->contrast); } // returns >0 if continue, =0 to stop, <0 to abort (USB) int PlayTick(int fd) { int button; static int lastbutton = 0; int avail_audio = -1, avail_video = -1; int retval = 1; int filepos; // check buffer level 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) && gBuf.bEOF) { if (gFileHdr.resume_pos) { // we played til the end, clear resume position gFileHdr.resume_pos = 0; rb->lseek(fd, 0, SEEK_SET); // save resume position rb->write(fd, &gFileHdr, sizeof(gFileHdr)); } Cleanup(&fd); 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; long spinup; // measure the spinup time // 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 spinup = *rb->current_tick; // in case this is interesting below got_now = rb->read(fd, gBuf.pBufFill, read_now); if (got_now != read_now || read_now == 0) { gBuf.bEOF = true; gPlay.bRefilling = false; } if (gPlay.bDiskSleep) // statistics about the spinup time { spinup = *rb->current_tick - spinup; gPlay.bDiskSleep = false; if (spinup > gStats.maxSpinup) gStats.maxSpinup = spinup; if (spinup < gStats.minSpinup) gStats.minSpinup = spinup; // recalculate the low water mark from real measurements gBuf.low_water = (gStats.maxSpinup + gBuf.spinup_safety) * gFileHdr.bps_peak / 8 / HZ; } if (!gPlay.bRefilling && rb->global_settings->disk_spindown < 20) // condition for test only { rb->ata_sleep(); // no point in leaving the disk run til timeout gPlay.bDiskSleep = true; } gBuf.pBufFill += got_now; if (gBuf.pBufFill >= gBuf.pBufEnd) gBuf.pBufFill = gBuf.pBufStart; // wrap rb->yield(); // have mercy with the other threads button = rb->button_get(false); } // check keypresses 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 filepos -= Available(gBuf.pReadAudio); // else audio switch (button) { // set exit conditions case BUTTON_OFF: if (gFileHdr.magic == HEADER_MAGIC // only if file has header && !(gFileHdr.flags & FLAG_LOOP)) // not for stills { gFileHdr.resume_pos = filepos; rb->lseek(fd, 0, SEEK_SET); // save resume position rb->write(fd, &gFileHdr, sizeof(gFileHdr)); } Cleanup(&fd); retval = 0; // signal "stopped" to caller break; case VIDEO_STOP_SEEK: #ifdef VIDEO_STOP_SEEK_PRE if (lastbutton != VIDEO_STOP_SEEK_PRE) break; #endif if (gPlay.bSeeking) { gPlay.bSeeking = false; gPlay.state = playing; SeekTo(fd, gPlay.nSeekPos); } else if (gPlay.state == playing) { gPlay.state = paused; if (gPlay.bHasAudio) rb->mp3_play_pause(false); // pause audio if (gPlay.bHasVideo) rb->timer_unregister(); // stop the timer } else if (gPlay.state == paused) { gPlay.state = playing; if (gPlay.bHasAudio) { if (gPlay.bHasVideo) SyncVideo(); rb->mp3_play_pause(true); // play audio } if (gPlay.bHasVideo) { // start the video #if FREQ == 12000000 /* Ondio speed kludge */ rb->timer_register(1, NULL, gPlay.nFrameTimeAdjusted, 1, timer4_isr); #else rb->timer_register(1, NULL, gFileHdr.video_frametime, 1, timer4_isr); #endif } } 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; #ifdef VIDEO_DEBUG case VIDEO_DEBUG: // debug key case VIDEO_DEBUG | BUTTON_REPEAT: DrawBuf(); // show buffer status gPlay.nTimeOSD = 30; gPlay.bDirtyOSD = true; break; #endif case VIDEO_CONTRAST_DOWN: // contrast down case VIDEO_CONTRAST_DOWN | BUTTON_REPEAT: if (gPlay.bHasVideo) ChangeContrast(-1); break; case VIDEO_CONTRAST_UP: // contrast up case VIDEO_CONTRAST_UP | BUTTON_REPEAT: if (gPlay.bHasVideo) ChangeContrast(1); break; default: if (rb->default_event_handler_ex(button, Cleanup, &fd) == SYS_USB_CONNECTED) retval = -1; // signal "aborted" to caller break; } lastbutton = button; } /* if (button != BUTTON_NONE) */ // handle seeking 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)); } // check + recover underruns if ((gPlay.bAudioUnderrun || gPlay.bVideoUnderrun) && !gBuf.bEOF) { gBuf.spinup_safety += HZ/2; // add extra spinup time for the future filepos = rb->lseek(fd, 0, SEEK_CUR); if (gPlay.bHasVideo && gPlay.bVideoUnderrun) { gStats.nVideoUnderruns++; filepos -= Available(gBuf.pReadVideo); // take video position SeekTo(fd, filepos); } else if (gPlay.bHasAudio && gPlay.bAudioUnderrun) { gStats.nAudioUnderruns++; filepos -= Available(gBuf.pReadAudio); // else audio SeekTo(fd, filepos); } } return retval; } int main(char* filename) { int file_size; int fd; /* file descriptor handle */ int read_now, got_now; int button = 0; int retval; // try to open the file fd = rb->open(filename, O_RDWR); if (fd < 0) return PLUGIN_ERROR; file_size = rb->filesize(fd); // reset pitch value to ensure synchronous playback rb->sound_set_pitch(1000); // init statistics rb->memset(&gStats, 0, sizeof(gStats)); gStats.minAudioAvail = gStats.minVideoAvail = INT_MAX; gStats.minSpinup = INT_MAX; // init playback state rb->memset(&gPlay, 0, sizeof(gPlay)); // init buffer rb->memset(&gBuf, 0, sizeof(gBuf)); gBuf.pOSD = rb->lcd_framebuffer + LCD_WIDTH*7; // last screen line gBuf.pBufStart = rb->plugin_get_audio_buffer(&gBuf.bufsize); //gBuf.bufsize = 1700*1024; // test, like 2MB version!!!! gBuf.pBufFill = gBuf.pBufStart; // all empty // 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); 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 = 11059200 / FPS; gFileHdr.bps_peak = gFileHdr.bps_average = LCD_WIDTH * LCD_HEIGHT * FPS; } #if FREQ == 12000000 /* Ondio speed kludge, 625 / 576 == 12000000 / 11059200 */ gPlay.nFrameTimeAdjusted = (gFileHdr.video_frametime * 625) / 576; #endif // 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_INIT * gFileHdr.bps_peak / 8000; gBuf.spinup_safety = SPINUP_SAFETY * HZ / 1000; // in time ticks if (gFileHdr.audio_min_associated < 0) gBuf.high_water = 0 - gFileHdr.audio_min_associated; else 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 // prepare video playback, if contained if (gFileHdr.video_format == VIDEOFORMAT_RAW) { gPlay.bHasVideo = true; if (rb->global_settings->backlight_timeout > 0) rb->backlight_set_timeout(1); // keep the light on } // prepare audio playback, if contained if (gFileHdr.audio_format == AUDIOFORMAT_MP3_BITSWAPPED) { gPlay.bHasAudio = true; } // start playback by seeking to zero or resume position if (gFileHdr.resume_pos && WantResume(fd)) // ask the user SeekTo(fd, gFileHdr.resume_pos); else SeekTo(fd, 0); // all that's left to do is keep the buffer full do // the main loop { retval = PlayTick(fd); } while (retval > 0); if (retval < 0) // aborted? { return PLUGIN_USB_CONNECTED; } #ifndef DEBUG // for release compilations, only display the stats in case of error if (gStats.nAudioUnderruns || gStats.nVideoUnderruns) #endif { // display statistics rb->lcd_clear_display(); rb->snprintf(gPrint, sizeof(gPrint), "%d Audio Underruns", gStats.nAudioUnderruns); rb->lcd_puts(0, 0, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "%d Video Underruns", gStats.nVideoUnderruns); rb->lcd_puts(0, 1, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "%d MinAudio bytes", gStats.minAudioAvail); rb->lcd_puts(0, 2, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "%d MinVideo bytes", gStats.minVideoAvail); rb->lcd_puts(0, 3, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "MinSpinup %d.%02d", gStats.minSpinup/HZ, gStats.minSpinup%HZ); rb->lcd_puts(0, 4, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "MaxSpinup %d.%02d", gStats.maxSpinup/HZ, gStats.maxSpinup%HZ); rb->lcd_puts(0, 5, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "LowWater: %d", gBuf.low_water); rb->lcd_puts(0, 6, gPrint); rb->snprintf(gPrint, sizeof(gPrint), "HighWater: %d", gBuf.high_water); rb->lcd_puts(0, 7, gPrint); rb->lcd_update(); button = WaitForButton(); } return (button == SYS_USB_CONNECTED) ? PLUGIN_USB_CONNECTED : PLUGIN_OK; } /***************** Plugin Entry Point *****************/ enum plugin_status plugin_start(struct plugin_api* api, void* parameter) { rb = api; // copy to global api pointer if (parameter == NULL) { rb->splash(HZ*2, true, "Play .rvf file!"); return PLUGIN_ERROR; } // now go ahead and have fun! return main((char*) parameter); } #endif // #ifdef HAVE_LCD_BITMAP #endif // #ifndef SIMULATOR