Index: apps/plugins/CATEGORIES =================================================================== --- apps/plugins/CATEGORIES (revision 22629) +++ apps/plugins/CATEGORIES (working copy) @@ -38,6 +38,7 @@ keybox,apps lamp,apps logo,demos +lrcplayer,apps lua,viewers mandelbrot,demos matrix,demos Index: apps/plugins/SOURCES =================================================================== --- apps/plugins/SOURCES (revision 22629) +++ apps/plugins/SOURCES (working copy) @@ -6,6 +6,7 @@ jackpot.c keybox.c logo.c +lrcplayer.c mosaique.c properties.c random_folder_advance_config.c Index: apps/plugins/viewers.config =================================================================== --- apps/plugins/viewers.config (revision 22629) +++ apps/plugins/viewers.config (working copy) @@ -11,6 +11,9 @@ mp3,viewers/vbrfix,5 m3u,viewers/search,- txt,viewers/sort,- +lrc,apps/lrcplayer,1 +lrc8,apps/lrcplayer,1 +snc,apps/lrcplayer,1 gb,viewers/rockboy,6 gbc,viewers/rockboy,6 sgb,viewers/rockboy,6 Index: apps/plugins/lrcplayer.c =================================================================== --- apps/plugins/lrcplayer.c (revision 0) +++ apps/plugins/lrcplayer.c (revision 0) @@ -0,0 +1,2807 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id: lrcplayer.c 53 2009-09-05 11:04:25Z teru $ + * + * Copyright (C) 2008-2009 Teruaki Kawashima + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "plugin.h" +#include "lib/playback_control.h" +#include "lib/configfile.h" +#include "lib/helper.h" +#include + +PLUGIN_HEADER + +#define MAX_LINE_LEN 256 +#define LRC_BUFFER_SIZE 0x3000 /* 12 kiB */ +#if PLUGIN_BUFFER_SIZE >= 0x10000 /* no id3 support for low mem targets */ +/* define this to read lyrics in id3 tag */ +#define LRC_SUPPORT_ID3 +#endif +/* define this to show debug info in menu */ +/* #define LRC_DUBUG */ + +struct lrc_word { + long time_start; + short count; + short width; + unsigned char *word; +}; + +struct lrc_brpos { + short count; + short width; +}; + +struct lrc_line { + struct lrc_line *next; + off_t file_offset; /* offset of time tag in file */ + long time_start; + long old_time_start; + short nword; + short width; + short nline[NB_SCREENS]; + struct lrc_word *words; +}; + +struct preferences { + /* display settings */ +#if LCD_DEPTH > 1 + unsigned active_color; + unsigned inactive_color; +#endif +#ifdef HAVE_LCD_BITMAP + bool wrap; + bool wipe; + bool active_one_line; + enum { + ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, + } align; + bool statusbar_on; + bool display_title; +#endif + bool display_time; + bool backlight_on; + + /* file settings */ + char lrc_directry[64]; + enum codepages encoding; /* use NUM_ENCODING as `use default codepage' */ +#ifdef LRC_SUPPORT_ID3 + bool read_id3; +#endif +}; + +static struct preferences prefs; +static unsigned char lrc_buffer[LRC_BUFFER_SIZE]; +static size_t lrc_buffer_used, lrc_buffer_end; +enum extention_types {LRC, LRC8, SNC, TXT, NUM_TYPES, ID3_SYLT, ID3_USLT}; +static const char *extentions[NUM_TYPES] = { + ".lrc", ".lrc8", ".snc", ".txt", +}; +static struct lrc_info { + struct mp3entry *id3; + long elapsed; + long length; + long ff_rewind; + int audio_status; + char mp3_file[MAX_PATH]; + char lrc_file[MAX_PATH]; + char *title; /* use lrc_buffer */ + char *artist; /* use lrc_buffer */ + enum extention_types type; + long offset; /* msec */ + off_t offset_file_offset; /* offset of offset tag in file */ + int nlrcbrpos; + int nlrcline; + struct lrc_line *ll_head, **ll_tail; + bool found_lrc; + bool loaded_lrc; + bool changed_lrc; + bool too_many_lines; /* true if nlrcline >= max_lrclines after calc pos */ +#ifdef HAVE_LCD_BITMAP + bool wipe; /* false if lyrics is unsynched */ +#endif +} current; +static char temp_buf[MAX_LINE_LEN]; +#ifdef HAVE_LCD_BITMAP +static int font_ui_height = 1; +static struct viewport vp_info[NB_SCREENS]; +#endif +static struct viewport vp_lyrics[NB_SCREENS]; +static bool reset_viewport, update_display_state, exit_plugin; + +#define AUDIO_PAUSE (current.audio_status & AUDIO_STATUS_PAUSE) +#define AUDIO_PLAY (current.audio_status & AUDIO_STATUS_PLAY) +#define AUDIO_STOP (!(current.audio_status & AUDIO_STATUS_PLAY)) + +/******************************* + * lrc_set_time + *******************************/ +#define LST_SET_MSEC 0x00010000 +#define LST_SET_SEC 0x00020000 +#define LST_SET_MIN 0x00040000 +#define LST_SET_HOUR 0x00080000 + +#include "lib/pluginlib_actions.h" +#define LST_SET_TIME (LST_SET_MSEC|LST_SET_SEC|LST_SET_MIN|LST_SET_HOUR) +#ifdef HAVE_LCD_CHARCELLS +#define LST_OFF_Y 0 +#else /* HAVE_LCD_BITMAP */ +#define LST_OFF_Y 1 +#endif +int lrc_set_time(const char *title, const char *unit, long *pval, + int step, int min, int max, int flags) +{ + const struct button_mapping *lst_contexts[] = { + generic_directions, + generic_actions, + }; + /* how many */ + const unsigned char formats[4][8] = {"%03ld.", "%02ld.", "%02ld:", "%02ld:"}; + const unsigned int maxs[4] = {1000, 60, 60, 24}; + const unsigned int scls[4] = {1, 1000, 60*1000, 60*60*1000}; + char buffer[32]; + long value = *pval, scl_step = step, i = 0; + int pos = 0, last_pos = -1, pos_min = 3, pos_max = 0; + int x = 0, y = 0, p_start = 0, p_end = 0; + + if(!(flags&LST_SET_TIME)) + return -1; + + for(i = 0; i < 4; i++) + { + if(flags&(LST_SET_MSEC< i) pos_min = i; + if(pos_max < i) pos_max = i; + } + } + pos = pos_min; + + rb->button_clear_queue(); + rb->lcd_clear_display(); + rb->lcd_puts_scroll(0, LST_OFF_Y, title); + while(1) + { + int len = 0; + long abs_val = value; + long segvals[4] = {-1, -1, -1, -1}; + /* show negative value like -00:01 but 00:-1 */ + if(value < 0) + { + buffer[len++] = '-'; + abs_val = -value; + } + buffer[len] = 0; + /* calc value of each segments */ + for(i = pos_min; i <= pos_max; i++) + { + segvals[i] = abs_val % maxs[i]; + abs_val /= maxs[i]; + } + segvals[i-1] += abs_val * maxs[i-1]; + for(i = pos_max; i >= pos_min; i--) + { + if(pos == i) + { + rb->lcd_getstringsize(buffer, &x, &y); + p_start = len; + } + rb->snprintf(&buffer[len], 32-len, formats[i], segvals[i]); + len += rb->strlen(&buffer[len]); + if(pos == i) + p_end = len; + } + buffer[len-1] = 0; /* remove last separater */ + if(unit != NULL) + { + rb->snprintf(&buffer[len], 32-len, " (%s)", unit); + } + rb->lcd_puts(0, LST_OFF_Y+1, buffer); + if(pos_min != pos_max) + { + /* draw cursor */ + buffer[p_end-1] = 0; +#ifdef HAVE_LCD_BITMAP + rb->lcd_set_drawmode(DRMODE_SOLID|DRMODE_INVERSEVID); + rb->lcd_putsxy(x, y*(1+LST_OFF_Y), &buffer[p_start]); + rb->lcd_set_drawmode(DRMODE_SOLID); +#else + rb->lcd_put_cursor(x+rb->utf8length(&buffer[p_start])-1, y, 0x7F); +#endif + } + rb->lcd_update(); + int button = pluginlib_getaction(TIMEOUT_BLOCK, lst_contexts, 2); + int mult = 1; +#ifdef HAVE_LCD_CHARCELLS + if(pos_min != pos_max) + rb->lcd_remove_cursor(); +#endif + switch(button) + { + case PLA_UP_REPEAT: + case PLA_DOWN_REPEAT: + mult *= 10; + case PLA_DOWN: + case PLA_UP: + if(button == PLA_DOWN_REPEAT || button == PLA_DOWN) + mult *= -1; + if(pos != last_pos) + { + scl_step = ((scls[pos]/scls[pos_min]+step-1)/step) * step; + last_pos = pos; + } + value += scl_step * mult; + if(value > max) + value = max; + if(value < min) + value = min; + break; + case PLA_LEFT: + case PLA_LEFT_REPEAT: + if(++pos > pos_max) + pos = pos_min; + break; + case PLA_RIGHT: + case PLA_RIGHT_REPEAT: + if(--pos < pos_min) + pos = pos_max; + break; + case PLA_FIRE: + case PLA_START: + *pval = value; + return 0; + case PLA_MENU: + case PLA_QUIT: + rb->splash(HZ, "Cancelled"); + return -1; + break; + default: + if(rb->default_event_handler(button) == SYS_USB_CONNECTED) + return 1; + break; + } + } + return -1; +} + +/******************************* + * misc stuff + *******************************/ +static void reset_current_data(void) +{ + current.title = NULL; + current.artist = NULL; + current.offset = 0; + current.offset_file_offset = -1; + current.nlrcbrpos = 0; + current.nlrcline = 0; + current.ll_head = NULL; + current.ll_tail = ¤t.ll_head; + current.loaded_lrc = false; + current.changed_lrc = false; + current.too_many_lines = false; + lrc_buffer_used = 0; + lrc_buffer_end = LRC_BUFFER_SIZE; +} + +/* check space and add str to lrc_buffer. + * return NULL if there is not enough buffer. */ +static char *lrcbufadd(const char*str) +{ + int size = rb->strlen(str)+1; + char *pos = &lrc_buffer[lrc_buffer_used]; + if(lrc_buffer_used + size > lrc_buffer_end) + return NULL; + rb->strcpy(pos, str); + lrc_buffer_used += size; + return pos; +} +static char *lrcbufjoin(const char*str) +{ + lrc_buffer_used--; + return lrcbufadd(str); +} +static void *alloc_buf(size_t siz) +{ + siz = (siz+3) & ~3; + if (lrc_buffer_used + siz > lrc_buffer_end) + return NULL; + lrc_buffer_end -= siz; + return &lrc_buffer[lrc_buffer_end]; +} +static bool add_lrc_line(struct lrc_line *lrc_line, char *word) +{ + lrc_line->words = NULL; + lrc_line->next = NULL; + lrc_line->nword = 0; + if(word) + { + if((lrc_line->words = alloc_buf(sizeof(struct lrc_word))) == NULL) + return false; + if((lrc_line->words->word = lrcbufadd(word)) == NULL) + return false; + lrc_line->words->time_start = -1; + lrc_line->nword++; + } + *current.ll_tail = lrc_line; + current.ll_tail = &(lrc_line->next); + current.nlrcline++; + return true; +} +static struct lrc_line *get_lrc_line(int idx) +{ + static struct lrc_line *lrc_line = NULL; + static int n = 0; + if(idx < n) + { + lrc_line = current.ll_head; + n = 0; + } + while(n < idx && lrc_line) + { + lrc_line = lrc_line->next; + n++; + } + return lrc_line; +} +static char *get_lrc_str(struct lrc_line *lrc_line) +{ + return lrc_line->words[lrc_line->nword-1].word; +} +static inline long get_time_start(struct lrc_line *lrc_line) +{ + if(!lrc_line) return current.length+20; + long time = lrc_line->time_start + current.offset; + return time < 0? 0: time; +} +static inline void set_time_start(struct lrc_line *lrc_line, long time_start) +{ + time_start -= current.offset; + time_start -= time_start%10; + if(lrc_line->time_start != time_start) + { + lrc_line->time_start = time_start; + current.changed_lrc = true; + } +} +static int format_time_tag(char *buf, long t) +{ + return rb->snprintf(buf, 16, "%02ld:%02ld.%02ld", + t/60000, (t/1000)%60, (t/10)%100); +} +/* find start of next line */ +static inline const char *lrc_skip_space(const char *str) +{ +#ifdef HAVE_LCD_BITMAP + if(prefs.wrap) + { + while(*str && *str != '\n' && isspace(*str)) + str++; + } +#endif + if(*str == '\n') + str++; + return str; +} + +/* calculate how many lines is needed to display and store it. */ +static struct lrc_brpos *calc_brpos(struct lrc_line *lrc_line, int i) +{ + struct lrc_brpos *lrc_brpos; + struct lrc_word *lrc_word; + int nlrcbrpos = 0, max_lrcbrpos, nword; + int count, width; + const unsigned char *str; +#ifdef HAVE_LCD_BITMAP + struct font* pf = rb->font_get(FONT_UI); + unsigned short ch; +#endif + struct snap { + int count, width; + int nword; + int word_count, word_width; + const unsigned char *str; + } sp; + + lrc_buffer_used = (lrc_buffer_used+3)&~3; /* 4 bytes aligned */ + lrc_brpos = (struct lrc_brpos *) &lrc_buffer[lrc_buffer_used]; + max_lrcbrpos = (lrc_buffer_end-lrc_buffer_used) / sizeof(struct lrc_brpos); + + if(!lrc_line) + { + /* calc info for all lrcs and store them if possible */ + size_t buffer_used = lrc_buffer_used; + bool too_many_lines = false; + current.too_many_lines = true; + for(lrc_line = current.ll_head; lrc_line; + lrc_line = lrc_line->next) + { + FOR_NB_SCREENS(i) + { + lrc_brpos = calc_brpos(lrc_line, i); + if(!too_many_lines) + { + lrc_buffer_used += lrc_line->nline[i]*sizeof(struct lrc_brpos); + if(nlrcbrpos + lrc_line->nline[i] >= max_lrcbrpos) + { + too_many_lines = true; + lrc_buffer_used = buffer_used; + calc_brpos(lrc_line, i); + } + } + nlrcbrpos += lrc_line->nline[i]; + } + } + current.too_many_lines = too_many_lines; + lrc_buffer_used = buffer_used; + current.nlrcbrpos = nlrcbrpos; + return NULL; + } + + if(!current.too_many_lines) + { + /* use stored infos. */ + struct lrc_line *temp_lrc = current.ll_head; + for(; temp_lrc != lrc_line; temp_lrc = temp_lrc->next) + { + lrc_brpos += temp_lrc->nline[SCREEN_MAIN]; +#ifdef HAVE_REMOTE_LCD + lrc_brpos += temp_lrc->nline[SCREEN_REMOTE]; +#endif + } +#if NB_SCREENS >= 2 + while(i) + lrc_brpos += lrc_line->nline[--i]; +#endif + return lrc_brpos; + } + + /* calculate number of lines, line width and char count for each line. */ + lrc_line->width = 0; + nword = lrc_line->nword; + lrc_word = lrc_line->words+nword; + str = (lrc_word-1)->word; + sp.word_count = 0; + sp.word_width = 0; + sp.nword = 0; + sp.count = 0; + sp.width = 0; + do{ + lrc_brpos->count = 0; + lrc_brpos->width = 0; + sp.str = NULL; + + while (1) + { + while(nword > 0 && str >= (lrc_word-1)->word) + { + nword--; + lrc_word--; + lrc_word->count = 0; + lrc_word->width = 0; + } + if(*str == 0) + break; + if(*str == '\n') + { + str++; + break; + } +#ifdef HAVE_LCD_CHARCELLS + count = rb->utf8seek(str, 1); + width = 1; +#else + count = ((long)rb->utf8decode(str, &ch) - (long)str); + width = rb->font_get_width(pf, ch); + if(lrc_brpos->count && isspace(*str)) + { + /* remember position of last space */ + sp.count = lrc_brpos->count; + sp.width = lrc_brpos->width; + sp.nword = nword; + sp.word_count = lrc_word->count; + sp.word_width = lrc_word->width; + sp.str = str; + } + if(lrc_brpos->count && lrc_brpos->width+width > vp_lyrics[i].width) + { + if(prefs.wrap && sp.str != NULL) /* wrap */ + { + lrc_brpos->count = sp.count; + lrc_brpos->width = sp.width; + nword = sp.nword; + lrc_word = lrc_line->words+nword; + lrc_word->count = sp.word_count; + lrc_word->width = sp.word_width; + str = lrc_skip_space(sp.str); + } + break; + } +#endif + lrc_brpos->count += count; + lrc_brpos->width += width; + lrc_word->count += count; + lrc_word->width += width; + str += count; + } + lrc_line->width += lrc_brpos->width; + nlrcbrpos++; + lrc_brpos++; + } while (*str && nlrcbrpos < max_lrcbrpos); + lrc_line->nline[i] = nlrcbrpos; + + while(nword) + { + nword--; + lrc_word--; + lrc_word->count = 0; + lrc_word->width = 0; + } + return lrc_brpos-nlrcbrpos; +} + +/* sort lyrics by time using stable sort. */ +static void sort_lrcs(void) +{ + struct lrc_line *p = current.ll_head, **q = NULL, *t; + long time_max = 0; + + current.ll_head = NULL; + current.ll_tail = ¤t.ll_head; + while (p != NULL) + { + t = p->next; + q = p->time_start >= time_max? current.ll_tail: ¤t.ll_head; + while((*q) && (*q)->time_start <= p->time_start) + q = &((*q)->next); + p->next = *q; + *q = p; + if(!p->next) + { + time_max = p->time_start; + current.ll_tail = &p->next; + } + p = t; + } + get_lrc_line(-1); /* init static variables */ + calc_brpos(NULL, 0); /* stored data depends on order of lrcs */ +} +static void init_time_tag(void) +{ + struct lrc_line *lrc_line = current.ll_head; + int nline = 0; + if(current.type == TXT || current.type == ID3_USLT) + { + /* set time tag according to length of audio and total line count + * for not synched lyrics, so that scroll speed is almost constant. */ + for(; lrc_line; lrc_line = lrc_line->next) + { + lrc_line->time_start = nline * current.length / current.nlrcbrpos; + lrc_line->time_start -= lrc_line->time_start%10; + lrc_line->old_time_start = -1; + nline += lrc_line->nline[SCREEN_MAIN]; +#ifdef HAVE_REMOTE_LCD + nline += lrc_line->nline[SCREEN_REMOTE]; +#endif + } + get_lrc_line(-1); /* init static variables */ + } + else + { + /* reset timetags read from file */ + for(; lrc_line; lrc_line = lrc_line->next) + { + lrc_line->time_start = lrc_line->old_time_start; + } + sort_lrcs(); + } + current.changed_lrc = false; +} + +/******************************* + * Serch lrc file. + *******************************/ + +/* search in same or parent directries of playing file. + * assume playing file is /aaa/bbb/ccc/ddd.mp3, + * this function searchs lrc file following order. + * (base_dir)/aaa/bbb/ccc/ddd.lrc + * (base_dir)/aaa/bbb/ddd.lrc + * (base_dir)/aaa/ddd.lrc + * (base_dir)/bbb/ccc/ddd.lrc + * (base_dir)/bbb/ddd.lrc + * (base_dir)/ccc/ddd.lrc + * (base_dir)/ddd.lrc + */ + +/* taken from apps/recorder/albumart.c */ +static void fix_filename(char* name) +{ + static const char invalid_chars[] = "*/:<>?\\|"; + + while (1) + { + if (*name == 0) + return; + if (*name == '"') + *name = '\''; + else if (rb->strchr(invalid_chars, *name)) + *name = '_'; + name++; + } +} +static bool find_lrc_file_helper(const char *base_dir) +{ + char fname[MAX_PATH]; + char *names[3] = {NULL, NULL, NULL}; + char *p, *q, *dir; + int i, len; + /* /aaa/bbb/ccc/ddd.mp3 + * dir p q names[0] + */ + + /* assuming file name starts with '/' */ + rb->strcpy(temp_buf, current.mp3_file); + /* get file name and remove extension */ + names[0] = rb->strrchr(temp_buf, '/')+1; + if((p = rb->strrchr(names[0], '.')) != NULL) + *p = 0; + if(current.id3->title && rb->strcmp(names[0], current.id3->title)) + { + rb->strlcpy(fname, current.id3->title, sizeof(fname)); + fix_filename(fname); + names[1] = fname; + } + + dir = temp_buf; + do{ + q = p = names[0]-1; + do{ + *p = '/'; + *q = 0; + p = q; + DEBUGF("%s%s/%s.ext\n", base_dir, dir, names[0]); + len = rb->snprintf(current.lrc_file, MAX_PATH, "%s%s/", base_dir, dir); + if(rb->dir_exists(current.lrc_file)) + { + for(current.type=0; current.typesnprintf(¤t.lrc_file[len], MAX_PATH-len, + "%s%s", names[i], extentions[current.type]); + if(rb->file_exists(current.lrc_file)) + return true; + } + } + } + }while((q = rb->strrchr(dir, '/')) != NULL && q != dir); + *p = '/'; + }while((dir = rb->strchr(dir+1, '/')) != NULL); + return false; +} + +/* return true if a lrc file is found */ +static bool find_lrc_file(void) +{ + reset_current_data(); + + DEBUGF("find lrc file for `%s'\n", current.mp3_file); + /* find .lrc file */ + if(find_lrc_file_helper("")) return true; + if(prefs.lrc_directry[0] == '/' && rb->dir_exists(prefs.lrc_directry)) + { + if(find_lrc_file_helper(prefs.lrc_directry)) return true; + } + + current.lrc_file[0] = 0; + return false; +} + +/******************************* + * Load file. + *******************************/ + +/* check tag format and calculate value of the tag. + * supported tag: ti, ar, offset + * supported format of time tag: [mm:ss], [mm:ss.xx], [mm:ss:xx] + * returns value of timega if tag is time tag, -1 if tag is supported tag, + * -10 otherwise. + */ +static long get_time_value(char *tag, bool read_id_tags, off_t file_offset) +{ + long time; + int len = rb->strlen(tag); + + if(read_id_tags) + { + if(!rb->strncmp(tag, "ti:", 3)) + { + if(!current.id3->title || rb->strcmp(&tag[3], current.id3->title)) + current.title = lrcbufadd(&tag[3]); + return -1; + } + if(!rb->strncmp(tag, "ar:", 3)) + { + if(!current.id3->artist || rb->strcmp(&tag[3], current.id3->artist)) + current.artist = lrcbufadd(&tag[3]); + return -1; + } + if(!rb->strncmp(tag, "offset:", 7)) + { + current.offset = rb->atoi(&tag[7]); + current.offset_file_offset = file_offset; + return -1; + } + } + if(len != 5 && len != 8) return -10; + /* check if tag is [mm:ss...] */ + if(!isdigit(tag[0]) || !isdigit(tag[1]) || tag[2] != ':') + return -10; + if(!isdigit(tag[3]) || !isdigit(tag[4])) + return -10; + if(len == 8) + { + /* check if tag is [....xx] or [...:xx] */ + if((tag[5] != '.' && tag[5] != ':') + || !isdigit(tag[6]) || !isdigit(tag[7])) + return -10; + } + + /* tag is [mm:ss...] */ + time = rb->atoi(&tag[0])*60000 + rb->atoi(&tag[3])*1000; + if(len == 8) + { + /* tag is [mm:ss.xx] or [mm:ss:xx] */ + time += rb->atoi(&tag[6])*10; + } + + return time; +} + +/* format: + * [time tag]line + * [time tag]...[time tag]line + * [time tag]word... + */ +static bool parse_lrc_line(char *line, off_t file_offset) +{ + struct lrc_line *lrc_line = NULL, *first_lrc_line = NULL; + long time, time_start; + char *str, *tagstart, *tagend; + struct lrc_word *lrc_word; + + /* parse [time tag]...[time tag] type tags */ + str = line; + while(1) + { + if (*str != '[') break; + tagend = rb->strchr(str, ']'); + if (tagend == NULL) break; + *tagend = 0; + time = get_time_value(str+1, !lrc_line, file_offset); + *tagend = ']'; + if (time < 0) + break; + lrc_line = alloc_buf(sizeof(struct lrc_line)); + if(lrc_line == NULL) + return false; + if(!first_lrc_line) + first_lrc_line = lrc_line; + lrc_line->file_offset = file_offset; + lrc_line->time_start = (time/10)*10; + lrc_line->old_time_start = lrc_line->time_start; + add_lrc_line(lrc_line, NULL); + file_offset += (long)tagend - (long)str; + str = tagend+1; + } + if(!first_lrc_line) return true; /* no time tag in line */ + + lrc_line = first_lrc_line; + if(lrcbufadd("") == NULL) + return false; + + /* parse ... type tags */ + lrc_word = alloc_buf(0); + /* some lrcs can share the value. don't use lrc_line->time_start. */ + time_start = -1; + tagstart = str; + while(*tagstart) + { + tagstart = rb->strchr(tagstart, '<'); + if (!tagstart) break; + tagend = rb->strchr(tagstart, '>'); + if (!tagend) break; + *tagend = 0; + time = get_time_value(tagstart+1, false, + file_offset + ((long)tagend - (long)str)); + *tagend = '>'; + if (time < 0) + { + tagstart++; + continue; + } + *tagstart = 0; + /* found word time tag. */ + if(*str || time_start != -1) + { + if(alloc_buf(sizeof(struct lrc_word)) == NULL) + return false; + lrc_word--; + if((lrc_word->word = lrcbufjoin(str)) == NULL) + return false; + lrc_word->time_start = time_start; + lrc_line->nword++; + } + file_offset += (long)tagend - (long)str; + tagstart = str = tagend+1; + time_start = time; + } + if(alloc_buf(sizeof(struct lrc_word)) == NULL) + return false; + lrc_word--; + if((lrc_word->word = lrcbufjoin(str)) == NULL) + return false; + lrc_word->time_start = time_start; + lrc_line->nword++; + + lrc_line->words = lrc_word; + + /* duplicate lrc_lines */ + while(lrc_line->next) + { + lrc_line->next->nword = lrc_line->nword; + lrc_line->next->words = lrc_line->words; + lrc_line = lrc_line->next; + } + + return true; +} + +/* format: + * \xa2\xe2hhmmssxx\xa2\xd0 + * line 1 + * line 2 + * \xa2\xe2hhmmssxx\xa2\xd0 + * line 3 + * ... + */ +static bool parse_snc_line(char *line, off_t file_offset) +{ +#define SNC_TAG_START "\xa2\xe2" +#define SNC_TAG_END "\xa2\xd0" + + /* SNC_TAG can be dencoded, so use + * temp_buf which contains native data */ + if(!rb->memcmp(temp_buf, SNC_TAG_START, 2) + && !rb->memcmp(temp_buf+10, SNC_TAG_END, 2)) /* time tag */ + { + const char *pos = temp_buf+2; /* skip SNC_TAG_START */ + int hh, mm, ss, xx; + + hh = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2; + mm = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2; + ss = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2; + xx = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2; + pos += 2; /* skip SNC_TAG_END */ + + /* initialize */ + struct lrc_line *lrc_line = alloc_buf(sizeof(struct lrc_line)); + if(lrc_line == NULL) + return false; + lrc_line->file_offset = file_offset+2; + lrc_line->time_start = hh*3600000+mm*60000+ss*1000+xx*10; + lrc_line->old_time_start = lrc_line->time_start; + if(!add_lrc_line(lrc_line, "")) + return false; + if(pos[0]==0) + return true; + + /* encode rest of line and add to buffer */ + int encoding = prefs.encoding; + if(prefs.encoding == NUM_CODEPAGES) /* use default codepage */ + encoding = -1; + rb->iso_decode(pos, line, encoding, rb->strlen(pos)+1); + } + if(current.ll_head) + { + rb->strcat(line, "\n"); + if(lrcbufjoin(line) == NULL) + return false; + } + return true; +} + +static bool parse_txt_line(char *line, off_t file_offset) +{ + /* initialize */ + struct lrc_line *lrc_line = alloc_buf(sizeof(struct lrc_line)); + if(lrc_line == NULL) + return false; + lrc_line->file_offset = file_offset; + lrc_line->time_start = 0; + lrc_line->old_time_start = -1; + if(!add_lrc_line(lrc_line, line)) + return false; + return true; +} + +static void load_lrc_file(void) +{ + char utf8line[MAX_LINE_LEN*3]; + int fd; + int encoding = prefs.encoding; + bool (*line_parser)(char *line, off_t) = NULL; + off_t file_offset, readsize; + if(prefs.encoding == NUM_CODEPAGES) /* use default codepage */ + encoding = -1; + + switch(current.type) + { + case LRC8: + encoding = UTF_8; /* .lrc8 is utf8 */ + /* fall through */ + case LRC: + line_parser = parse_lrc_line; + break; + case SNC: + line_parser = parse_snc_line; + break; + case TXT: + line_parser = parse_txt_line; + break; + default: + return; + } + fd = rb->open(current.lrc_file, O_RDONLY); + if(fd < 0) return; + do{ + /* from apps/misc.c */ + #define BOM "\xef\xbb\xbf" + #define BOM_SIZE 3 + unsigned char bom[BOM_SIZE]; + rb->read(fd, bom, BOM_SIZE); + /* check for BOM */ + if(rb->memcmp(bom, BOM, BOM_SIZE)) + rb->lseek(fd, 0, SEEK_SET); + }while(0); + file_offset = rb->lseek(fd, 0, SEEK_CUR); /* used in line_parser */ + while((readsize = rb->read_line(fd, temp_buf, MAX_LINE_LEN)) > 0) + { + /* note: parse_snc_line() reads temp_buf for native data. */ + rb->iso_decode(temp_buf, utf8line, encoding, readsize+1); + if(!line_parser(utf8line, file_offset)) + break; + file_offset += readsize; + } + rb->close(fd); + + current.loaded_lrc = true; + calc_brpos(NULL, 0); + init_time_tag(); + + return; +} + +#ifdef LRC_SUPPORT_ID3 +/******************************* + * read lyrics from id3 + *******************************/ +/* taken from apps/metadata/mp3.c */ +static unsigned long unsync(unsigned long b0, unsigned long b1, + unsigned long b2, unsigned long b3) +{ + return (((long)(b0 & 0x7F) << (3*7)) | + ((long)(b1 & 0x7F) << (2*7)) | + ((long)(b2 & 0x7F) << (1*7)) | + ((long)(b3 & 0x7F) << (0*7))); +} + +static unsigned long bytes2int(unsigned long b0, unsigned long b1, + unsigned long b2, unsigned long b3) +{ + return (((long)(b0 & 0xFF) << (3*8)) | + ((long)(b1 & 0xFF) << (2*8)) | + ((long)(b2 & 0xFF) << (1*8)) | + ((long)(b3 & 0xFF) << (0*8))); +} + +static int unsynchronize(char* tag, int len, bool *ff_found) +{ + int i; + unsigned char c; + unsigned char *rp, *wp; + bool _ff_found = false; + if(ff_found) _ff_found = *ff_found; + + wp = rp = (unsigned char *)tag; + + rp = (unsigned char *)tag; + for(i = 0; iread(fd, wp, remaining); + if(rc <= 0) + return rc; + + i = unsynchronize(wp, remaining, ff_found); + remaining -= i; + wp += i; + } + + return len; +} + +static unsigned char* utf8cpy(const unsigned char *src, + unsigned char *dst, int count) +{ + rb->strlcpy(dst, src, count+1); + return dst+rb->strlen(dst); +} + +static void parse_id3v2(int fd) +{ + int minframesize; + int size; + long framelen; + char header[10]; + char tmp[8]; + unsigned char version; + int bytesread = 0; + unsigned char global_flags; + int flags; + int skip; + bool global_unsynch = false; + bool global_ff_found = false; + bool unsynch = false; + int rc; + enum {NOLT, SYLT, USLT} type = NOLT; + + /* Bail out if the tag is shorter than 10 bytes */ + if(current.id3->id3v2len < 10) + return; + + /* Read the ID3 tag version from the header */ + if(10 != rb->read(fd, header, 10)) + return; + + /* Get the total ID3 tag size */ + size = current.id3->id3v2len - 10; + + version = current.id3->id3version; + switch ( version ) + { + case ID3_VER_2_2: + minframesize = 8; + break; + + case ID3_VER_2_3: + minframesize = 12; + break; + + case ID3_VER_2_4: + minframesize = 12; + break; + + default: + /* unsupported id3 version */ + return; + } + + global_flags = header[5]; + + /* Skip the extended header if it is present */ + if(global_flags & 0x40) { + skip = 0; + + if(version == ID3_VER_2_3) { + if(10 != rb->read(fd, header, 10)) + return; + /* The 2.3 extended header size doesn't include the header size + field itself. Also, it is not unsynched. */ + framelen = + bytes2int(header[0], header[1], header[2], header[3]) + 4; + + /* Skip the rest of the header */ + rb->lseek(fd, framelen - 10, SEEK_CUR); + } + + if(version >= ID3_VER_2_4) { + if(4 != rb->read(fd, header, 4)) + return; + + /* The 2.4 extended header size does include the entire header, + so here we can just skip it. This header is unsynched. */ + framelen = unsync(header[0], header[1], + header[2], header[3]); + + rb->lseek(fd, framelen - 4, SEEK_CUR); + } + } + + /* Is unsynchronization applied? */ + if(global_flags & 0x80) { + global_unsynch = true; + } + + /* We must have at least minframesize bytes left for the + * remaining frames to be interesting */ + while (size >= minframesize) { + flags = 0; + + /* Read frame header and check length */ + if(version >= ID3_VER_2_3) { + if(global_unsynch && version <= ID3_VER_2_3) + rc = read_unsynched(fd, header, 10, &global_ff_found); + else + rc = rb->read(fd, header, 10); + if(rc != 10) + return; + /* Adjust for the 10 bytes we read */ + size -= 10; + + flags = bytes2int(0, 0, header[8], header[9]); + + if (version >= ID3_VER_2_4) { + framelen = unsync(header[4], header[5], + header[6], header[7]); + } else { + /* version .3 files don't use synchsafe ints for + * size */ + framelen = bytes2int(header[4], header[5], + header[6], header[7]); + } + } else { + if(6 != rb->read(fd, header, 6)) + return; + /* Adjust for the 6 bytes we read */ + size -= 6; + + framelen = bytes2int(0, header[3], header[4], header[5]); + } + + if(framelen == 0){ + if (header[0] == 0 && header[1] == 0 && header[2] == 0) + return; + else + continue; + } + + unsynch = false; + + if(flags) + { + if (version >= ID3_VER_2_4) { + if(flags & 0x0040) { /* Grouping identity */ + rb->lseek(fd, 1, SEEK_CUR); /* Skip 1 byte */ + framelen--; + } + } else { + if(flags & 0x0020) { /* Grouping identity */ + rb->lseek(fd, 1, SEEK_CUR); /* Skip 1 byte */ + framelen--; + } + } + + if(flags & 0x000c) /* Compression or encryption */ + { + /* Skip it */ + size -= framelen; + rb->lseek(fd, framelen, SEEK_CUR); + continue; + } + + if(flags & 0x0002) /* Unsynchronization */ + unsynch = true; + + if (version >= ID3_VER_2_4) { + if(flags & 0x0001) { /* Data length indicator */ + if(4 != rb->read(fd, tmp, 4)) + return; + + /* We don't need the data length */ + framelen -= 4; + } + } + } + + if (framelen == 0) + continue; + + if (framelen < 0) + return; + + if(!rb->memcmp( header, "SLT", 3 ) || + !rb->memcmp( header, "SYLT", 4 )) + { + /* found a supported tag */ + type = SYLT; + break; + } + else if(!rb->memcmp( header, "ULT", 3 ) || + !rb->memcmp( header, "USLT", 4 )) + { + /* found a supported tag */ + type = USLT; + break; + } + else + { + /* not a supported tag*/ + if(global_unsynch && version <= ID3_VER_2_3) { + size -= read_unsynched(fd, lrc_buffer, framelen, &global_ff_found); + } else { + size -= framelen; + if( rb->lseek(fd, framelen, SEEK_CUR) == -1 ) + return; + } + } + } + if(type == NOLT) + return; + + int encoding = 0, chsiz; + char *tag, *p, utf8line[MAX_LINE_LEN*3]; + unsigned char* (*utf_decode)(const unsigned char *, + unsigned char *, int) = NULL; + /* use middle of lrc_buffer to store tag data. */ + if(framelen >= LRC_BUFFER_SIZE/3) + framelen = LRC_BUFFER_SIZE/3-1; + tag = lrc_buffer+LRC_BUFFER_SIZE*2/3-framelen-1; + if(global_unsynch && version <= ID3_VER_2_3) + bytesread = read_unsynched(fd, tag, framelen, &global_ff_found); + else + bytesread = rb->read(fd, tag, framelen); + + if( bytesread != framelen ) + return; + + if(unsynch || (global_unsynch && version >= ID3_VER_2_4)) + bytesread = unsynchronize(tag, bytesread, NULL); + + tag[bytesread] = 0; + encoding = tag[0]; + p = tag; + /* skip some data */ + if(type == SYLT) { + p += 6; + } else { + p += 4; + } + + /* check encoding and skip content descriptor */ + switch (encoding) { + case 0x01: /* Unicode with or without BOM */ + case 0x02: + + /* Now check if there is a BOM + (zero-width non-breaking space, 0xfeff) + and if it is in little or big endian format */ + if(!rb->memcmp(p, "\xff\xfe", 2)) { /* Little endian? */ + utf_decode = rb->utf16LEdecode; + } else if(!rb->memcmp(p, "\xfe\xff", 2)) { /* Big endian? */ + utf_decode = rb->utf16BEdecode; + } else + utf_decode = NULL; + + encoding = NUM_CODEPAGES; + while(p[0] || p[1]) + p += 2; + p += 2; + chsiz = 2; + break; + + default: + utf_decode = utf8cpy; + if(encoding == 0x03) /* UTF-8 encoded string */ + encoding = UTF_8; + else if(prefs.encoding == NUM_CODEPAGES) /* use default codepage */ + encoding = -1; + else + encoding = prefs.encoding; + p += rb->strlen(p)+1; + chsiz = 1; + break; + } + if(encoding == NUM_CODEPAGES) + { + /* recheck if there is a BOM + (zero-width non-breaking space, 0xfeff) + and if it is in little or big endian format */ + if(!rb->memcmp(p, "\xff\xfe", 2)) { /* Little endian? */ + utf_decode = rb->utf16LEdecode; + p += 2; + } else if(!rb->memcmp(p, "\xfe\xff", 2)) { /* Big endian? */ + utf_decode = rb->utf16BEdecode; + p += 2; + } else if(!utf_decode) { + /* If there is no BOM (which is a specification violation), + let's try to guess it. If one of the bytes is 0x00, it is + probably the most significant one. */ + if(p[1] == 0) + utf_decode = rb->utf16LEdecode; + else + utf_decode = rb->utf16BEdecode; + } + } + bytesread -= (long)p - (long)tag; + tag = p; + + while ( bytesread > 0 + && lrc_buffer_used+bytesread < LRC_BUFFER_SIZE*2/3 + && LRC_BUFFER_SIZE*2/3 < lrc_buffer_end) + { + bool is_crlf = false; + struct lrc_line *lrc_line = alloc_buf(sizeof(struct lrc_line)); + if(!lrc_line) + break; + lrc_line->file_offset = -1; + if(type == USLT) + { + /* replace 0x0a and 0x0d with 0x00 */ + p = tag; + while(p[0] || p[chsiz-1]) { + utf_decode(p, tmp, 2); + if(tmp[0] == 0x0d || tmp[0] == 0x0a) + { + if(tmp[0] == 0x0d && tmp[1] == 0x0a) + is_crlf = true; + p[0] = 0; + p[chsiz-1] = 0; + break; + } + p += chsiz; + } + } + if(encoding == NUM_CODEPAGES) + { + unsigned char* utf8 = utf8line; + p = tag; + while(p[0] || p[1]) { + utf8 = utf_decode(p, utf8, 1); + p += 2; + } + *utf8++ = 0; /* Terminate the string */ + p += 2; + } + else + { + size = rb->strlen(tag)+1; + rb->iso_decode(tag, utf8line, encoding, size); + p = tag+size; + } + + if(type == SYLT) { /* timestamp */ + lrc_line->time_start = bytes2int(p[0], p[1], p[2], p[3]); + lrc_line->old_time_start = lrc_line->time_start; + p += 4; + utf_decode(p, tmp, 1); + if(tmp[0] == 0x0a) + p += chsiz; + } else { /* USLT */ + lrc_line->time_start = 0; + lrc_line->old_time_start = -1; + if(is_crlf) p += chsiz; + } + bytesread -= (long)p - (long)tag; + tag = p; + if(!add_lrc_line(lrc_line, utf8line)) + break; + } + + current.type = ID3_SYLT-SYLT+type; + rb->strcpy(current.lrc_file, current.mp3_file); + + current.loaded_lrc = true; + calc_brpos(NULL, 0); + init_time_tag(); + + return; +} + +static bool read_id3(void) +{ + int fd; + + if(current.id3->codectype != AFMT_MPA_L1 + && current.id3->codectype != AFMT_MPA_L2 + && current.id3->codectype != AFMT_MPA_L3) + return false; + + fd = rb->open(current.mp3_file, O_RDONLY); + if(fd < 0) return false; + current.loaded_lrc = false; + parse_id3v2(fd); + rb->close(fd); + return current.loaded_lrc; +} +#endif /* LRC_SUPPORT_ID3 */ + +/******************************* + * Display information + *******************************/ +static void display_state(void) +{ + char *str = NULL; + + if(AUDIO_STOP) + str = "Audio Stopped"; + else if(current.found_lrc) + { + if(!current.loaded_lrc) + str = "Loading lrc"; + else if(!current.ll_head) + str = "No lyrics"; + } + +#ifdef HAVE_LCD_BITMAP + int i, w, h; + struct screen* display; + FOR_NB_SCREENS(i) + { + display = rb->screens[i]; + display->clear_display(); + if(str) + { + display->set_viewport(&vp_lyrics[i]); + display->getstringsize(str, &w, &h); + if(vp_lyrics[i].width - w < 0) + display->puts_scroll(0, vp_lyrics[i].height/font_ui_height/2, + str); + else + display->putsxy((vp_lyrics[i].width - w)*prefs.align/2, + (vp_lyrics[i].height-font_ui_height)/2, str); + display->set_viewport(NULL); + } + display->update(); + } + + if(AUDIO_STOP || !prefs.display_title) + return; + + str = temp_buf; + + if(!current.title) current.title = current.id3->title; + if(!current.artist) current.artist = current.id3->artist; + + if(current.artist != NULL && current.title != NULL) + rb->snprintf(str, MAX_LINE_LEN, "%s/%s", + current.title, current.artist); + else if(current.title != NULL) + str = current.title; + else if(current.mp3_file[0] == '/') + str = rb->strrchr(current.mp3_file, '/')+1; + else + str = "(no info)"; + + FOR_NB_SCREENS(i) + { + display = rb->screens[i]; + display->set_viewport(&vp_info[i]); + display->puts_scroll(0, 0, str); + display->set_viewport(NULL); + } +#else + /* there is no place to display title or artist. */ + rb->lcd_clear_display(); + if(str) + rb->lcd_puts_scroll(0, 0, str); + rb->lcd_update(); +#endif /* HAVE_LCD_BITMAP */ +} + +static void display_time(void) +{ + rb->snprintf(temp_buf, MAX_LINE_LEN, "%ld:%02ld/%ld:%02ld", + current.elapsed/60000, (current.elapsed/1000)%60, + current.length/60000, (current.length)/1000%60); +#ifdef HAVE_LCD_BITMAP + int y = (prefs.display_title? font_ui_height:0), i; + FOR_NB_SCREENS(i) + { + struct screen* display = rb->screens[i]; + display->set_viewport(&vp_info[i]); + display->setfont(FONT_SYSFIXED); + display->putsxy(0, y, temp_buf); + rb->gui_scrollbar_draw(display, 0, y+SYSFONT_HEIGHT+1, + vp_info[i].width, SYSFONT_HEIGHT-2, + current.length, 0, current.elapsed, HORIZONTAL); + display->update_viewport_rect(0, y, vp_info[i].width, SYSFONT_HEIGHT*2); + display->setfont(FONT_UI); + display->set_viewport(NULL); + } +#else + rb->lcd_puts(0, 0, temp_buf); + rb->lcd_update(); +#endif /* HAVE_LCD_BITMAP */ +} + +/******************************* + * Display lyrics + *******************************/ +#ifdef HAVE_LCD_BITMAP +static inline void set_to_default(struct screen *display) +{ +#if (LCD_DEPTH > 1) +#ifdef HAVE_LCD_REMOTE + if(display->screen_type != SCREEN_REMOTE) +#endif + display->set_foreground(prefs.active_color); +#endif + display->set_drawmode(DRMODE_SOLID); +} +static inline void set_to_active(struct screen *display) +{ +#if (LCD_DEPTH > 1) +#ifdef HAVE_LCD_REMOTE + if(display->screen_type == SCREEN_REMOTE) + display->set_drawmode(DRMODE_INVERSEVID); + else +#endif + { + display->set_foreground(prefs.active_color); + display->set_drawmode(DRMODE_SOLID); + } +#else /* LCD_DEPTH == 1 */ + display->set_drawmode(DRMODE_INVERSEVID); +#endif +} +static inline void set_to_inactive(struct screen *display) +{ +#if (LCD_DEPTH > 1) +#ifdef HAVE_LCD_REMOTE + if(display->screen_type != SCREEN_REMOTE) +#endif + display->set_foreground(prefs.inactive_color); +#endif + display->set_drawmode(DRMODE_SOLID); +} + +static int display_lrc_line(struct lrc_line *lrc_line, int ypos, int i) +{ + struct screen *display = rb->screens[i]; + struct lrc_word *lrc_word; + struct lrc_brpos *lrc_brpos; + long time_start, time_end, elapsed; + int count, width, nword; + int xpos; + const char *str; + bool active_line; + + time_start = get_time_start(lrc_line); + time_end = get_time_start(lrc_line->next); + active_line = (time_start <= current.elapsed + && time_end > current.elapsed); + + if(!lrc_line->width) + { + /* empty line. draw bar during interval. */ + long rin = current.elapsed - time_start; + long len = time_end - time_start; + if(active_line && len >= 3500) + { + elapsed = rin * vp_lyrics[i].width / len; + set_to_active(display); + display->drawrect(0, ypos+font_ui_height/4, + vp_lyrics[i].width, font_ui_height/2); + display->fillrect(1, ypos+font_ui_height/4+1, + elapsed-1, font_ui_height/2-2); + set_to_inactive(display); + display->fillrect(elapsed, ypos+font_ui_height/4+1, + vp_lyrics[i].width-elapsed-1, font_ui_height/2-2); + set_to_default(display); + } + return ypos + font_ui_height; + } + + lrc_brpos = calc_brpos(lrc_line, i); + /* initialize line */ + xpos = (vp_lyrics[i].width - lrc_brpos->width)*prefs.align/2; + count = 0; + width = 0; + + active_line = active_line || !prefs.active_one_line; + nword = lrc_line->nword-1; + lrc_word = lrc_line->words + nword; + str = lrc_word->word; + while(nword >= 0) + { + /* only time_start of first word could be -1 */ + if(lrc_word->time_start != -1) + { + time_start = lrc_word->time_start + current.offset; + if(time_start < 0) time_start = 0; + } + if(nword > 0) + time_end = (lrc_word-1)->time_start; + else + time_end = get_time_start(lrc_line->next); + + if(time_start > current.elapsed || !active_line) + { + /* inactive */ + elapsed = 0; + } + else if(!current.wipe || time_end <= current.elapsed) + { + /* active whole word */ + elapsed = lrc_word->width; + } + else + { + /* wipe word */ + long rin = current.elapsed - time_start; + long len = time_end - time_start; + elapsed = rin * lrc_word->width / len; + } + + int word_count = lrc_word->count; + int word_width = lrc_word->width; + set_to_active(display); + while(word_count > 0 && word_width > 0) + { + int c = lrc_brpos->count - count; + int w = lrc_brpos->width - width; + if(c > word_count || w > word_width) + { + c = word_count; + w = word_width; + } + if(elapsed <= 0) + { + set_to_inactive(display); + } + else if(elapsed < w) + { + /* wipe text */ + display->fillrect(xpos, ypos, elapsed, font_ui_height); + set_to_inactive(display); + display->fillrect(xpos+elapsed, ypos, + w-elapsed, font_ui_height); +#if (LCD_DEPTH > 1) +#ifdef HAVE_LCD_REMOTE + if(display->screen_type == SCREEN_REMOTE) + display->set_drawmode(DRMODE_INVERSEVID); + else +#endif + display->set_drawmode(DRMODE_BG); +#else + display->set_drawmode(DRMODE_INVERSEVID); +#endif + } + rb->strlcpy(temp_buf, str, c+1); + display->putsxy(xpos, ypos, temp_buf); + str += c; + xpos += w; + count += c; + width += w; + word_count -= c; + word_width -= w; + elapsed -= w; + if(count >= lrc_brpos->count || width >= lrc_brpos->width) + { + /* prepare for next line */ + lrc_brpos++; + str = lrc_skip_space(str); + xpos = (vp_lyrics[i].width - lrc_brpos->width)*prefs.align/2; + ypos += font_ui_height; + count = 0; + width = 0; + } + } + nword--; + lrc_word--; + } + set_to_default(display); + return ypos; +} +#endif /* HAVE_LCD_BITMAP */ + +static void display_lrcs(void) +{ + long time_start, time_end, rin, len; + int i, nline[NB_SCREENS] = {0}; + struct lrc_line *lrc_center = current.ll_head; + + if(!lrc_center) return; + + while(get_time_start(lrc_center->next) <= current.elapsed) + { + nline[SCREEN_MAIN] += lrc_center->nline[SCREEN_MAIN]; +#ifdef HAVE_REMOTE_LCD + nline[SCREEN_REMOTE] += lrc_center->nline[SCREEN_REMOTE]; +#endif + lrc_center = lrc_center->next; + } + + time_start = get_time_start(lrc_center); + time_end = get_time_start(lrc_center->next); + rin = current.elapsed - time_start; + len = time_end - time_start; + + struct screen *display; + FOR_NB_SCREENS(i) + { + display = rb->screens[i]; + /* display current line at the center of the viewport */ + display->set_viewport(&vp_lyrics[i]); + display->clear_viewport(); +#ifdef HAVE_LCD_BITMAP + struct lrc_line *lrc_line; + int y, ypos = 0, nblines = vp_lyrics[i].height/font_ui_height; + y = (nblines-1)/2; + if(rin < 0) + y--; + else if(len > 0) + { + if(!current.wipe) + ypos = - rin * lrc_center->nline[i] * font_ui_height / len; + else + { + long elapsed = rin * lrc_center->width / len; + struct lrc_brpos *lrc_brpos = calc_brpos(lrc_center, i); + while(elapsed > lrc_brpos->width) + { + elapsed -= lrc_brpos->width; + y--; + lrc_brpos++; + } + } + } + + /* find first line to display */ + y -= nline[i]; + lrc_line = current.ll_head; + while(y < -lrc_line->nline[i]) + { + y += lrc_line->nline[i]; + lrc_line = lrc_line->next; + } + + ypos += y*font_ui_height; + while(lrc_line && ypos < vp_lyrics[i].height) + { + ypos = display_lrc_line(lrc_line, ypos, i); + lrc_line = lrc_line->next; + } + if(!lrc_line && ypos < vp_lyrics[i].height) + display->putsxy(0, ypos, "[end]"); +#else /* HAVE_LCD_CHARCELLS */ + struct lrc_line *lrc_line = lrc_center; + struct lrc_brpos *lrc_brpos = calc_brpos(lrc_line, i); + long elapsed = 0; + char *str = get_lrc_str(lrc_line); + int x = vp_lyrics[i].width/2, y = 0; + + if(len > 0) + { + elapsed = rin * lrc_center->width / len; + while(elapsed > lrc_brpos->width) + { + elapsed -= lrc_brpos->width; + str = lrc_skip_space(str+lrc_brpos->count); + lrc_brpos++; + } + } + rb->strlcpy(temp_buf, str, lrc_brpos->count+1); + display->putsxy(xpos, ypos, temp_buf); + + x -= elapsed; + if (x < 0) + display->puts(0, y, temp_buf + rb->utf8seek(temp_buf, -x)); + else + display->puts(x, y, temp_buf); + x += rb->utf8length(temp_buf)+1; + lrc_line = lrc_line->next; + if(!lrc_line && x < vp_lyrics[i].width) + { + if(x < vp_lyrics[i].width/2) + x = vp_lyrics[i].width/2; + display->puts(x, y, "[end]"); + } +#endif /* HAVE_LCD_BITMAP */ + display->update_viewport(); + display->set_viewport(NULL); + } +} + +/******************************* + * Browse lyrics and edit time. + *******************************/ +/* point playing line in lyrics */ +static enum themable_icons get_icon(int selected, void * data) +{ + (void) data; + struct lrc_line *lrc_line = get_lrc_line(selected); + if(lrc_line) + { + long time_start = get_time_start(lrc_line); + long time_end = get_time_start(lrc_line->next); + long elapsed = current.id3->elapsed; + if(time_start <= elapsed && time_end > elapsed) + return Icon_Moving; + } + return Icon_NOICON; +} +static const char *get_lrc_timeline(int selected, void *data, + char *buffer, size_t buffer_len) +{ + (void) data; + struct lrc_line *lrc_line = get_lrc_line(selected); + if(lrc_line) + { + format_time_tag(temp_buf, get_time_start(lrc_line)); + rb->snprintf(buffer, buffer_len, "[%s]%s", + temp_buf, get_lrc_str(lrc_line)); + return buffer; + } + return NULL; +} + +static void save_changes(void) +{ + char new_file[MAX_PATH], *p; + bool success = false; + int fd, fe; + if(!current.changed_lrc) + return; + rb->splash(HZ/2, "Saving changes..."); + if(current.type > NUM_TYPES) + { + /* save changes to new .lrc8 file */ + rb->strcpy(new_file, current.lrc_file); + p = rb->strrchr(new_file, '.'); + rb->strcpy(p, extentions[LRC8]); + } + else if(current.type == TXT) + { + /* save changes to new .lrc file */ + rb->strcpy(new_file, current.lrc_file); + p = rb->strrchr(new_file, '.'); + rb->strcpy(p, extentions[LRC]); + } + else + { + /* file already exists. use temp file. */ + rb->snprintf(new_file, MAX_PATH, "%s~", current.lrc_file); + } + fd = rb->creat(new_file); + fe = rb->open(current.lrc_file, O_RDONLY); + if(fd >= 0 && fe >= 0) + { + struct lrc_line *lrc_line, *temp_lrc; + off_t curr = 0, next = 0, size = 0, offset = 0; + if(current.offset != 0) + { + for(lrc_line = current.ll_head; + lrc_line; lrc_line = lrc_line->next) + { + lrc_line->time_start = get_time_start(lrc_line); + } + current.offset = 0; + } + if(current.type > NUM_TYPES) + curr = -1; + else + size = rb->filesize(fe); + while(currnext) + { + offset = temp_lrc->file_offset; + if(offset == -1 || (offset < next && + temp_lrc->old_time_start != temp_lrc->time_start)) + { + lrc_line = temp_lrc; + next = offset; + if(offset <= curr) break; + } + } + offset = current.offset_file_offset; + if(offset >= curr && offset < next) + { + lrc_line = NULL; + next = offset; + current.offset_file_offset = -1; + } + if(next > curr) + { + if(curr == -1) curr = 0; + /* copy before the next tag */ + while(curr MAX_LINE_LEN) + count = MAX_LINE_LEN; + if(rb->read(fe, temp_buf, count)!=count) + break; + rb->write(fd, temp_buf, count); + curr += count; + } + if(curr=size) break; + } + /* write tag to new file and skip tag in backup */ + if(lrc_line != NULL) + { + lrc_line->file_offset = rb->lseek(fd, 0, SEEK_CUR); + lrc_line->old_time_start = lrc_line->time_start; + long t = lrc_line->time_start; + if(current.type == SNC) + { + rb->fdprintf(fd, "%02ld%02ld%02ld%02ld", (t/3600000)%100, + (t/60000)%60, (t/1000)%60, (t/10)%100); + /* skip time tag */ + curr += rb->read(fe, temp_buf, 8); + } + else /* LRC || LRC8 */ + { + format_time_tag(temp_buf, t); + rb->fdprintf(fd, "[%s]", temp_buf); + } + if(next == -1) + { + rb->fdprintf(fd, "%s\n", get_lrc_str(lrc_line)); + } + } + if(current.type == LRC || current.type == LRC8) + { + /* skip both time tag and offset tag */ + while(curr++read(fe, temp_buf, 1)==1) + if(temp_buf[0]==']') break; + } + } + success = (curr>=size); + } + if(fe >= 0) rb->close(fe); + if(fd >= 0) rb->close(fd); + if(success) + { + if(current.type == TXT) + { + current.type = LRC; + rb->strcpy(current.lrc_file, new_file); + } + else if(current.type > NUM_TYPES) + { + current.type = LRC8; + rb->strcpy(current.lrc_file, new_file); + } + else + { + rb->remove(current.lrc_file); + rb->rename(new_file, current.lrc_file); + } + } + else + { + rb->remove(new_file); + rb->splash(HZ, "Could not save changes."); + } + rb->reload_directory(); + current.changed_lrc = false; +} +static bool timetag_editor(void) +{ + struct gui_synclist gui_editor; + struct lrc_line *lrc_line; + bool exit = false; + int button, idx; + + if(current.id3 == NULL || !current.ll_head) + { + rb->splash(HZ, "No lyrics"); + return false; + } + + for(lrc_line = current.ll_head, idx = 0; + lrc_line; lrc_line = lrc_line->next, idx++) + { + long time_start = get_time_start(lrc_line); + long time_end = get_time_start(lrc_line->next); + long elapsed = current.id3->elapsed; + if(time_start <= elapsed && time_end > elapsed) + break; + } + + rb->gui_synclist_init(&gui_editor, &get_lrc_timeline, NULL, false, 1, NULL); + rb->gui_synclist_set_nb_items(&gui_editor, current.nlrcline); + rb->gui_synclist_set_icon_callback(&gui_editor, get_icon); + rb->gui_synclist_set_title(&gui_editor, "Timetag Editor", + Icon_Menu_functioncall); + rb->gui_synclist_select_item(&gui_editor, idx); + rb->gui_synclist_draw(&gui_editor); + + while (!exit) + { + button = rb->get_action(CONTEXT_TREE, TIMEOUT_BLOCK); + if(rb->gui_synclist_do_button(&gui_editor, &button, + LIST_WRAP_UNLESS_HELD)) + continue; + + switch (button) + { + case ACTION_STD_OK: + idx = rb->gui_synclist_get_sel_pos(&gui_editor); + lrc_line = get_lrc_line(idx); + if(lrc_line) + { + set_time_start(lrc_line, current.id3->elapsed); + rb->gui_synclist_draw(&gui_editor); + } + break; + case ACTION_STD_CONTEXT: + idx = rb->gui_synclist_get_sel_pos(&gui_editor); + lrc_line = get_lrc_line(idx); + if(lrc_line) + { + long temp_time = get_time_start(lrc_line); + if(lrc_set_time(get_lrc_str(lrc_line), NULL, + &temp_time, 10, 0, current.length, + LST_SET_MSEC|LST_SET_SEC|LST_SET_MIN) == 1) + return true; + set_time_start(lrc_line, temp_time); + rb->gui_synclist_draw(&gui_editor); + } + break; + case ACTION_TREE_STOP: + case ACTION_STD_CANCEL: + exit = true; + break; + default: + if(rb->default_event_handler(button) == SYS_USB_CONNECTED) + return true; + break; + } + } + if(current.changed_lrc) + { + MENUITEM_STRINGLIST(save_menu, "Save Changes?", NULL, + "Yes", "No (save later)", "Discard All Changes") + button = 0; + while(1) + { + switch(rb->do_menu(&save_menu, &button, NULL, false)) + { + case 0: + sort_lrcs(); + save_changes(); + return false; + case 1: + sort_lrcs(); + return false; + case 2: + init_time_tag(); + return false; + case MENU_ATTACHED_USB: + return true; + } + } + } + return false; +} + +/******************************* + * Settings. + *******************************/ +static bool settings_changed = false; +static void load_or_save_settings(bool save) +{ + static const char config_file[] = "lrcplayer.cfg"; + static struct configdata config[] = { +#ifdef HAVE_LCD_COLOR + { TYPE_INT, 0, 0xffffff, { .int_p = &prefs.inactive_color }, + "inactive color", NULL }, +#endif +#ifdef HAVE_LCD_BITMAP + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.wrap }, "wrap", NULL }, + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.wipe }, "wipe", NULL }, + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.active_one_line }, + "active one line", NULL }, + { TYPE_INT, 0, 2, { .int_p = (int *) &prefs.align }, "align", NULL }, + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.statusbar_on }, + "statusbar on", NULL }, + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.display_title }, + "display title", NULL }, +#endif + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.display_time }, + "display time", NULL }, + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.backlight_on }, + "backlight on", NULL }, + + { TYPE_STRING, 0, sizeof(prefs.lrc_directry), + { .string = prefs.lrc_directry }, "lrc directry", NULL }, + { TYPE_INT, 0, NUM_CODEPAGES, { .int_p = (int *) &prefs.encoding }, + "encoding", NULL }, +#ifdef LRC_SUPPORT_ID3 + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.read_id3 }, "read id3", NULL }, +#endif + }; + + if(!save) + { +#if LCD_DEPTH > 1 + prefs.active_color = rb->lcd_get_foreground(); + prefs.inactive_color = LCD_LIGHTGRAY; +#endif +#ifdef HAVE_LCD_BITMAP + prefs.wrap = true; + prefs.wipe = true; + prefs.active_one_line = false; + prefs.align = ALIGN_CENTER; + prefs.statusbar_on = false; + prefs.display_title = true; +#endif + prefs.display_time = true; + prefs.backlight_on = false; +#ifdef LRC_SUPPORT_ID3 + prefs.read_id3 = true; +#endif + rb->strcpy(prefs.lrc_directry, "/Lyrics"); + prefs.encoding = NUM_CODEPAGES; /* default is `default codepage' */ + + configfile_load(config_file, config, ARRAYLEN(config), 0); + } + else if(settings_changed) + { + configfile_save(config_file, config, ARRAYLEN(config), 0); + } + settings_changed = false; +} + +/* returns true if USB was inserted, false otherwise */ +static bool lrc_settings_menu(void) +{ + enum { +#ifdef HAVE_LCD_BITMAP + LRC_MENU_STATUSBAR, + LRC_MENU_DISP_TITLE, +#endif + LRC_MENU_DISP_TIME, + LRC_MENU_BACKLIGHT, +#ifdef LRC_SUPPORT_ID3 + LRC_MENU_READ_ID3, +#endif + LRC_MENU_LRC_DIR, + }; + + unsigned old_val; + int selected = 0; + bool exit = false, usb = false; + + MENUITEM_STRINGLIST(menu, "Lrcplayer Menu", NULL, +#ifdef HAVE_LCD_BITMAP + "Show Statusbar", "Display Title", +#endif + "Display Time", "Backlight Force On", + "Read ID3 tag", "Lrc Directry"); + + while(!exit && !usb) + { + switch(rb->do_menu(&menu, &selected, NULL, false)) + { +#ifdef HAVE_LCD_BITMAP + case LRC_MENU_STATUSBAR: + old_val = prefs.statusbar_on; + usb = rb->set_bool("Show Statusbar", &prefs.statusbar_on); + if(prefs.statusbar_on != old_val) + { + settings_changed = true; + reset_viewport = true; + } + break; + case LRC_MENU_DISP_TITLE: + old_val = prefs.display_title; + usb = rb->set_bool("Display Title", &prefs.display_title); + if(prefs.display_title != old_val) + { + settings_changed = true; + reset_viewport = true; + } + break; +#endif + case LRC_MENU_DISP_TIME: + old_val = prefs.display_time; + usb = rb->set_bool("Display Time", &prefs.display_time); + if(prefs.display_time != old_val) + { + settings_changed = true; + reset_viewport = true; + } + break; + case LRC_MENU_BACKLIGHT: + old_val = prefs.backlight_on; + usb = rb->set_bool("Backlight Force On", &prefs.backlight_on); + settings_changed |= prefs.backlight_on != old_val; + break; +#ifdef LRC_SUPPORT_ID3 + case LRC_MENU_READ_ID3: + old_val = prefs.read_id3; + usb = rb->set_bool("Read ID3 tag", &prefs.read_id3); + settings_changed |= (prefs.read_id3 != old_val); + break; +#endif + case LRC_MENU_LRC_DIR: + rb->strcpy(temp_buf, prefs.lrc_directry); + if (!rb->kbd_input(temp_buf, sizeof(prefs.lrc_directry))) + { + settings_changed |= rb->strcmp(prefs.lrc_directry, temp_buf); + rb->strcpy(prefs.lrc_directry, temp_buf); + } + break; + case MENU_ATTACHED_USB: + usb = true; + break; + default: + exit = true; + break; + } + } + + return usb; +} + +#ifdef LRC_DUBUG +static const char* lrc_debug_data(int selected, void * data, + char * buffer, size_t buffer_len) +{ + (void)data; + switch(selected) { + case 0: + rb->strlcpy(buffer, current.mp3_file, buffer_len); + break; + case 1: + rb->strlcpy(buffer, current.lrc_file, buffer_len); + break; + case 2: + rb->snprintf(buffer, buffer_len, "buf usage: %d,%d/%d", + (int)lrc_buffer_used, (int)lrc_buffer_end, + LRC_BUFFER_SIZE); + break; + case 3: + rb->snprintf(buffer, buffer_len, "line count: %d,%d", + current.nlrcline, current.nlrcbrpos); + break; + case 4: + rb->snprintf(buffer, buffer_len, "loaded lrc? %s", + current.loaded_lrc?"yes":"no"); + break; + case 5: + rb->snprintf(buffer, buffer_len, "too many lines? %s", + current.too_many_lines?"yes":"no"); + break; + default: + return NULL; + } + return buffer; +} + +static bool lrc_debug_menu(void) +{ + struct simplelist_info info; + rb->simplelist_info_init(&info, "Debug Menu", 6, NULL); + info.hide_selection = true; + info.scroll_all = true; + info.get_name = lrc_debug_data; + return rb->simplelist_show_list(&info); +} +#endif + +/* returns true if USB was inserted, false otherwise */ +static bool lrcplayer_menu(void) +{ + enum { +#ifdef HAVE_LCD_COLOR + LRC_MENU_INACTIVE_COLOR, +#endif +#ifdef HAVE_LCD_BITMAP + LRC_MENU_WRAP, + LRC_MENU_WIPE, + LRC_MENU_LINE_MODE, + LRC_MENU_ALIGN, +#endif + LRC_MENU_ENCODING, + LRC_MENU_SETTINGS, + LRC_MENU_PLAYBACK, +#ifdef LRC_DUBUG + LRC_MENU_DEBUG, +#endif + LRC_MENU_OFFSET, + LRC_MENU_TIMETAG_EDITOR, + }; + + MENUITEM_STRINGLIST(menu, "Lrcplayer Menu", NULL, +#ifdef HAVE_LCD_COLOR + "Inactive Color", +#endif +#ifdef HAVE_LCD_BITMAP + "Wrap", "Wipe", "Activate Only Current Line", "Align", +#endif + "Encoding", "Other Settings", "Playback Control", +#ifdef LRC_DUBUG + "Debug Menu", +#endif + "Time Offset", "Timetag Editor"); +#ifdef HAVE_LCD_BITMAP + struct opt_items align_names[] = { + {"Left", -1}, {"Center", -1}, {"Right", -1}, + }; +#endif + struct opt_items cp_names[NUM_CODEPAGES+1]; + unsigned old_val; + int selected = 0, idx; + bool exit = false, usb = false; + + for (idx=0; idx < NUM_CODEPAGES; idx++) + { + cp_names[idx].string = rb->get_codepage_name(idx); + cp_names[idx].voice_id = -1; + } + cp_names[idx].string = "Use default codepage"; + cp_names[idx].voice_id = -1; + + while(!exit && !usb) + { + switch(rb->do_menu(&menu, &selected, NULL, false)) + { +#ifdef HAVE_LCD_COLOR + case LRC_MENU_INACTIVE_COLOR: + old_val = prefs.inactive_color; + usb = rb->set_color(NULL, "Inactive Color", + &prefs.inactive_color, -1); + settings_changed |= (prefs.inactive_color != old_val); + break; +#endif +#ifdef HAVE_LCD_BITMAP + case LRC_MENU_WRAP: + old_val = prefs.wrap; + usb = rb->set_bool("Wrap", &prefs.wrap); + if(prefs.wrap != old_val) + { + settings_changed = true; + calc_brpos(NULL, 0); + } + break; + case LRC_MENU_WIPE: + old_val = prefs.wipe; + usb = rb->set_bool("Wipe", &prefs.wipe); + settings_changed |= (prefs.wipe != old_val); + break; + case LRC_MENU_LINE_MODE: + old_val = prefs.active_one_line; + usb = rb->set_bool("Activate Only Current Line", + &prefs.active_one_line); + settings_changed |= (prefs.active_one_line != old_val); + break; + case LRC_MENU_ALIGN: + old_val = prefs.align; + usb = rb->set_option("Align", &prefs.align, INT, + align_names, 3, NULL); + settings_changed |= (prefs.align != old_val); + break; +#endif + case LRC_MENU_ENCODING: + old_val = prefs.encoding; + usb = rb->set_option("Encoding", &prefs.encoding, INT, + cp_names, NUM_CODEPAGES+1, NULL); + if(prefs.encoding != old_val) + { + settings_changed = true; + save_changes(); + if(current.type < NUM_TYPES) + { + /* let reload lrc file to apply encoding setting */ + reset_current_data(); + } + } + break; + case LRC_MENU_SETTINGS: + usb = lrc_settings_menu(); + break; + case LRC_MENU_PLAYBACK: + return playback_control(NULL); + break; +#ifdef LRC_DUBUG + case LRC_MENU_DEBUG: + return lrc_debug_menu(); + break; +#endif + case LRC_MENU_OFFSET: + usb = (lrc_set_time("Time Offset", "sec", ¤t.offset, + 10, -60*1000, 60*1000, + LST_SET_MSEC|LST_SET_SEC) == 1); + break; + case LRC_MENU_TIMETAG_EDITOR: + usb = timetag_editor(); + break; + case MENU_ATTACHED_USB: + usb = true; + break; + default: + exit = true; + break; + } + } + return usb; +} + +/******************************* + * Main. + *******************************/ +/* returns true if song has changed to know when to load new lyrics. */ +static inline bool check_audio_status(void) +{ + static int last_audio_status = 0; + if(current.ff_rewind == -1) + current.audio_status = rb->audio_status(); + current.id3 = rb->audio_current_track(); + if((last_audio_status^current.audio_status)&AUDIO_STATUS_PLAY) + { + last_audio_status = current.audio_status; + return true; + } + if(AUDIO_STOP || current.id3 == NULL) + return false; + if(rb->strcmp(current.mp3_file, current.id3->path)) + { + return true; + } + return false; +} +static void ff_rewind(long time, bool resume) +{ + if(AUDIO_PLAY) + { + if(!AUDIO_PAUSE) + { + resume = true; + rb->audio_pause(); + } + rb->audio_ff_rewind(time); + rb->sleep(HZ/10); /* take affect seeking */ + if(resume) + rb->audio_resume(); + } +} + +static bool handle_button(void) +{ + static int step = 0; + bool usb = false; + int limit, button = rb->get_action(CONTEXT_WPS, HZ/16); + switch(button) + { + case ACTION_WPS_BROWSE: + case ACTION_WPS_STOP: + save_changes(); + exit_plugin = true; + break; + case ACTION_WPS_PLAY: + if (AUDIO_STOP && rb->global_status->resume_index != -1) + { + if (rb->playlist_resume() != -1) + { + rb->playlist_start(rb->global_status->resume_index, + rb->global_status->resume_offset); + } + } + else if (AUDIO_PAUSE) + rb->audio_resume(); + else + rb->audio_pause(); + break; + case ACTION_WPS_SEEKFWD: + case ACTION_WPS_SEEKBACK: + if(AUDIO_STOP) + break; + if(current.ff_rewind > -1) + { + if (button == ACTION_WPS_SEEKFWD) + /* fast forwarding, calc max step relative to end */ + limit = (current.length - current.ff_rewind) * 3 / 100; + else + /* rewinding, calc max step relative to start */ + limit = (current.ff_rewind) * 3 / 100; + limit = MAX(limit, 500); + + if (step > limit) + step = limit; + + if (button == ACTION_WPS_SEEKFWD) + current.ff_rewind += step; + else + current.ff_rewind -= step; + + if (current.ff_rewind > current.length-100) + current.ff_rewind = current.length-100; + if (current.ff_rewind < 0) + current.ff_rewind = 0; + + /* smooth seeking by multiplying step by: 1 + (2 ^ -accel) */ + step += step >> (rb->global_settings->ff_rewind_accel + 3); + } + else + { + current.ff_rewind = current.elapsed; + if(!AUDIO_PAUSE) + rb->audio_pause(); + step = 1000 * rb->global_settings->ff_rewind_min_step; + } + break; + case ACTION_WPS_STOPSEEK: + if(current.ff_rewind == -1) + break; + ff_rewind(current.ff_rewind, !AUDIO_PAUSE); + current.elapsed = current.ff_rewind; + current.ff_rewind = -1; + break; + case ACTION_WPS_SKIPNEXT: + rb->audio_next(); + break; + case ACTION_WPS_SKIPPREV: + if(current.elapsed < 3000) + rb->audio_prev(); + else + ff_rewind(0, false); + break; + case ACTION_WPS_VOLDOWN: + limit = rb->sound_min(SOUND_VOLUME); + if(--rb->global_settings->volume < limit) + rb->global_settings->volume = limit; + rb->sound_set(SOUND_VOLUME, rb->global_settings->volume); + break; + case ACTION_WPS_VOLUP: + limit = rb->sound_max(SOUND_VOLUME); + if(++rb->global_settings->volume > limit) + rb->global_settings->volume = limit; + rb->sound_set(SOUND_VOLUME, rb->global_settings->volume); + break; + case ACTION_WPS_CONTEXT: +#ifdef HAVE_LCD_BITMAP + rb->viewportmanager_set_statusbar(VP_SB_HIDE_ALL); +#endif + usb = timetag_editor(); + update_display_state = true; + break; + case ACTION_WPS_MENU: + if (prefs.backlight_on) + backlight_use_settings(); +#ifdef HAVE_LCD_BITMAP + rb->viewportmanager_set_statusbar(VP_SB_HIDE_ALL); +#endif + usb = lrcplayer_menu(); + if (prefs.backlight_on) + backlight_force_on(); + update_display_state = true; + break; + default: + usb = (rb->default_event_handler(button) == SYS_USB_CONNECTED); + break; + } + return usb; +} + +/* this is the plugin entry point */ +enum plugin_status plugin_start(const void* parameter) +{ + int i; + long id3_timeout = 0; + bool usb = false; + + /* initialize settings. */ + load_or_save_settings(false); + + if (prefs.backlight_on) + backlight_force_on(); + +#ifdef HAVE_LCD_BITMAP + rb->lcd_getstringsize("O", NULL, &font_ui_height); +#endif + reset_viewport = true; + update_display_state = true; + exit_plugin = false; + + reset_current_data(); + current.id3 = NULL; + current.mp3_file[0] = 0; + current.lrc_file[0] = 0; + current.ff_rewind = -1; + current.found_lrc = false; + if (parameter && check_audio_status()) + { + const char *ext; + rb->strcpy(current.mp3_file, current.id3->path); + /* use passed parameter as lrc file. */ + rb->strcpy(current.lrc_file, parameter); + if(!rb->file_exists(current.lrc_file)) + { + rb->splash(HZ, "Specified file dose not exist."); + return PLUGIN_ERROR; + } + ext = rb->strrchr(current.lrc_file, '.'); + if(ext == NULL) + { + rb->splashf(HZ, "%s is not supported", current.lrc_file); + return PLUGIN_ERROR; + } + for(current.type=0; current.typestrcmp(ext, extentions[current.type])) + break; + } + if(current.type==NUM_TYPES) + { + rb->splashf(HZ, "%s is not supported", ext); + return PLUGIN_ERROR; + } + current.found_lrc = true; + } + + while (!exit_plugin) + { + if(reset_viewport) + { +#ifdef HAVE_LCD_BITMAP + /* y offset of vp_lyrics */ + int h = (prefs.display_title?font_ui_height:0)+ + (prefs.display_time?SYSFONT_HEIGHT*2:0); + + rb->viewportmanager_set_statusbar( + prefs.statusbar_on? VP_SB_ALLSCREENS: VP_SB_HIDE_ALL); +#endif + FOR_NB_SCREENS(i) + { + rb->viewport_set_defaults(&vp_lyrics[i], i); +#ifdef HAVE_LCD_BITMAP + vp_info[i] = vp_lyrics[i]; + vp_lyrics[i].y += h; + vp_lyrics[i].height -= h; +#else + if(prefs.display_time) + { + vp_lyrics[i].y += 1; /* time */ + vp_lyrics[i].height -= 1; + } +#endif + } + reset_viewport = false; + update_display_state = true; + } + if(check_audio_status()) + { + update_display_state = true; + if(AUDIO_STOP) + { + current.id3 = NULL; + id3_timeout = 0; + } + else if(rb->strcmp(current.mp3_file, current.id3->path)) + { + save_changes(); + reset_current_data(); + rb->strcpy(current.mp3_file, current.id3->path); + id3_timeout = *rb->current_tick+HZ*3; + current.found_lrc = false; + } + } + if(current.id3 && current.id3->length) + { + if(current.ff_rewind == -1) + { + long di = current.id3->elapsed - current.elapsed; + if(di < -250 || di > 0) + current.elapsed = current.id3->elapsed; + } + else + current.elapsed = current.ff_rewind; + current.length = current.id3->length; + if(current.elapsed > current.length) + current.elapsed = current.length; + } + else + { + current.elapsed = 0; + current.length = 1; + } + + if(current.id3 && id3_timeout && + (TIME_AFTER(*rb->current_tick, id3_timeout) || + current.id3->artist)) + { + update_display_state = true; + id3_timeout = 0; + + current.found_lrc = find_lrc_file(); +#ifdef LRC_SUPPORT_ID3 + if(!current.found_lrc && prefs.read_id3) + { + /* no lyrics file found. try to read from id3 tag. */ + current.found_lrc = read_id3(); + } +#endif + } + else if(current.found_lrc && !current.loaded_lrc) + { + /* current.loaded_lrc is false after changing encode setting */ + update_display_state = true; + load_lrc_file(); + } + if(update_display_state) + { +#ifdef HAVE_LCD_BITMAP + rb->viewportmanager_set_statusbar( + prefs.statusbar_on? VP_SB_ALLSCREENS: VP_SB_HIDE_ALL); + + if(current.type == TXT || current.type == ID3_USLT) + current.wipe = false; + else + current.wipe = prefs.wipe; +#endif + display_state(); + update_display_state = false; + } + if(AUDIO_PLAY) + { + if(prefs.display_time) + display_time(); + if(!id3_timeout) + display_lrcs(); + } + + if((usb = handle_button())) + exit_plugin = true; + } + + rb->lcd_stop_scroll(); +#ifdef HAVE_REMOTE_LCD + rb->lcd_remote_stop_scroll(); +#endif + load_or_save_settings(true); + if (prefs.backlight_on) + backlight_use_settings(); + return usb? PLUGIN_USB_CONNECTED: PLUGIN_OK; +}