Index: apps/plugins/CATEGORIES =================================================================== --- apps/plugins/CATEGORIES (revision 21349) +++ apps/plugins/CATEGORIES (working copy) @@ -37,6 +37,7 @@ keybox,apps lamp,apps logo,demos +lrcplayer,apps lua,viewers mandelbrot,demos matrix,demos Index: apps/plugins/SOURCES =================================================================== --- apps/plugins/SOURCES (revision 21349) +++ 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 21349) +++ apps/plugins/viewers.config (working copy) @@ -16,6 +16,9 @@ m3u,viewers/iriverify,- mid,viewers/midi,7 rmi,viewers/midi,7 +lrc,apps/lrcplayer,1 +lrc8,apps/lrcplayer,1 +snc,apps/lrcplayer,1 pd,apps/pdbox,2 rsp,viewers/searchengine,8 sok,games/sokoban,1 Index: apps/plugins/lrcplayer.c =================================================================== --- apps/plugins/lrcplayer.c (revision 0) +++ apps/plugins/lrcplayer.c (revision 0) @@ -0,0 +1,2352 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2008 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 128 +#define MAX_CHARS 0x1000 /* 4 kiB */ +#define MAX_LRCS (MAX_CHARS/32) +#define MAX_LRCLINES (MAX_LRCS*4) + +struct lrc_display_line { + int count; + int width; +}; + +struct lrc { + long file_offset; /* offset of time tag in file */ + long time_start; + int width; + int nline[NB_SCREENS]; + struct lrc_display_line *lines[NB_SCREENS]; + unsigned char *pbuf; /* pointer to lrc_buffer */ + bool changed; +}; + +struct preferences { +#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_RIGHT, ALIGN_CENTER, ALIGN_LEFT, + } align; +#endif + char lrc_directry[64]; + enum codepages encoding; /* use NUM_ENCODING as `use default codepage' */ + /* hidden settings */ +#ifdef HAVE_LCD_BITMAP + bool display_title; + bool statusbar_on; +#endif + bool display_time; + bool backlight_on; + bool read_id3; +}; + +static struct preferences prefs; +static unsigned char lrc_buffer[MAX_CHARS]; +static int lrc_buffer_used; +enum extention_types {LRC, LRC8, SNC, TXT, NUM_TYPES}; +static const char *extentions[NUM_TYPES] = { + "lrc", "lrc8", "snc", "txt", +}; +static struct lrc_info { + struct mp3entry *id3; + long elapsed; + long length; + 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 */ + int nlrcline; + int nlrc; + struct lrc_display_line lrclines[MAX_LRCLINES]; + struct lrc lrcs[MAX_LRCS+1]; + bool loaded_lrc; + bool too_many_lines; /* true if nlrcline >= MAX_LRCLINES after calc pos */ + bool changed_lrc; +} current; +static off_t file_offset_offset; +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]; + +/******************************* + * set_int_screen + *******************************/ +#define SIS_SET_INT 0x00010000 +#define SIS_SET_FP 0x00020000 /* fixed point */ +#define SIS_SET_MSEC 0x00040000 +#define SIS_SET_SEC 0x00080000 +#define SIS_SET_MIN 0x00100000 +#define SIS_SET_HOUR 0x00200000 +#define SIS_WRAP 0x01000000 +#define SIS_BIG_STEP 0x02000000 +#define SIS_FIG_MASK 0x000000FF /* for fixed point */ +#define SIS_FIG_PACK(x) ((x)&SIS_FIG_MASK) +#define SIS_FIG_UNPACK(x) ((unsigned char)((x)&SIS_FIG_MASK)) + +#include "lib/pluginlib_actions.h" +const struct button_mapping *setint_contexts[] += {generic_directions, generic_actions}; +#define SIS_SET_TIME (SIS_SET_MSEC|SIS_SET_SEC|SIS_SET_MIN|SIS_SET_HOUR) +#ifdef HAVE_LCD_CHARCELLS +#define SIS_OFF_Y 0 +#else /* HAVE_LCD_BITMAP */ +#define SIS_OFF_Y 1 +#endif +int set_int_screen(const char *title, const char *unit, long *pval, + int step, int min, int max, int flags) +{ + char buffer[32]; + int value = *pval; + /* for fixed point */ + char format[8]; + int figure = 0, radix = 1; + /* for time */ + int pos = 0, x = 0, y = 0, p_start = 0, p_end = 0; + int time_steps[4] = { 0, 0, 0, 0 }; + + if(flags&SIS_SET_INT) + { + /* nothing to do */ + } + else if(flags&SIS_SET_FP) + { + int temp = SIS_FIG_UNPACK(flags); + while(figure < temp) + { + radix *= 10; + figure++; + } + rb->snprintf(format, 8, "%%d.%%0%dd", figure); /* "%d.%0nd" */ + } + else if(flags&SIS_SET_TIME) + { + pos = SIS_SET_MSEC; + while(!(pos&(flags&SIS_SET_TIME))) + pos <<= 1; + if(flags&SIS_SET_MSEC) + radix = 1; + else if(flags&SIS_SET_SEC) + radix = 1000; + else if(flags&SIS_SET_MIN) + radix = 60*1000; + else if(flags&SIS_SET_HOUR) + radix = 60*60*1000; + + time_steps[0] = step; + time_steps[1] = ((1000/radix+step-1)/step) * step; + time_steps[2] = ((60*1000/radix+step-1)/step) * step; + time_steps[3] = ((60*60*1000/radix+step-1)/step) * step; + } + else + { + return -1; + } + rb->button_clear_queue(); + rb->lcd_clear_display(); +#ifdef HAVE_LCD_BITMAP + rb->viewportmanager_set_statusbar(rb->viewportmanager_set_statusbar(0)); +#endif + if(title != NULL) + { + if(rb->lcd_getstringsize(title, NULL, NULL)>LCD_WIDTH) + rb->lcd_puts_scroll(0, SIS_OFF_Y, title); + else + rb->lcd_puts(0, SIS_OFF_Y, title); + } + while(1) + { + if(flags&SIS_SET_INT) + { + rb->snprintf(buffer, 32, "%d", value); + } + else if(flags&SIS_SET_FP) + { + int abs_val = value, len = 0; + if(value < 0) + { + rb->strcpy(buffer, "-"); + len++; + abs_val = -value; + } + rb->snprintf(buffer+len, 32-len, format, + abs_val/radix, abs_val%radix); + } + else if(flags&SIS_SET_TIME) + { + int abs_val = value, i, len = 0; + int val[4] = {-1, -1, -1, -1}; + const char sep[4] = {'.', '.', ':', ':'}; + const unsigned int time_scales[4] = {1000, 60, 60, 24}; + buffer[0] = 0; + if(value < 0) + { + rb->strcpy(buffer, "-"); + len++; + abs_val = -value; + } + for(i=0; i<4; i++) + { + if(flags&(SIS_SET_MSEC<0) val[i-1] %= time_scales[i-1]; + val[i] = abs_val; + abs_val /= time_scales[i]; + } + } + for(i=3; i>=0; i--) + { + if(flags&(SIS_SET_MSEC<lcd_getstringsize(buffer, &x, &y); + p_start = len; + } + rb->snprintf(&buffer[len], 32-len, i==0?"%03d%c":"%02d%c", + val[i], sep[i]); + len += rb->strlen(&buffer[len]); + if(pos == (SIS_SET_MSEC<strlen(buffer); + rb->snprintf(&buffer[len], 32-len, " (%s)", unit); + } + rb->lcd_puts(0, SIS_OFF_Y+1, buffer); + if(pos) + { + /* 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+SIS_OFF_Y), &buffer[p_start]); + rb->lcd_set_drawmode(DRMODE_SOLID); +#else + rb->lcd_remove_cursor(); + rb->lcd_put_cursor(x+rb->utf8length(&buffer[p_start])-1, y, 0x7F); +#endif + } + rb->lcd_update(); + int button = pluginlib_getaction(TIMEOUT_BLOCK, setint_contexts, 2); + int scale = 1; +#ifdef HAVE_LCD_CHARCELLS + if(pos) + rb->lcd_remove_cursor(); +#endif + switch(button) + { + /* scale * 10 if repeat and have big step, scale * -1 if down */ + case PLA_UP_REPEAT: + scale *= -1; /* -1 * -1 = 1 */ + case PLA_DOWN_REPEAT: + if(flags&SIS_BIG_STEP) + scale *= 10; + case PLA_DOWN: + scale *= -1; + case PLA_UP: + switch(pos) + { + case 0: + value += step * scale; + break; + case SIS_SET_MSEC: + value += time_steps[0] * scale; + break; + case SIS_SET_SEC: + value += time_steps[1] * scale; + break; + case SIS_SET_MIN: + value += time_steps[2] * scale; + break; + case SIS_SET_HOUR: + value += time_steps[3] * scale; + break; + } + if(value > max) + value = (flags&SIS_WRAP)? min: max; + if(value < min) + value = (flags&SIS_WRAP)? max: min; + break; + case PLA_LEFT: + case PLA_LEFT_REPEAT: + if(pos&SIS_SET_TIME) + { + pos <<= 1; + if(!(pos&(flags&SIS_SET_TIME))) + pos = SIS_SET_MSEC; + while(!(pos&(flags&SIS_SET_TIME))) + pos <<= 1; + } + break; + case PLA_RIGHT: + case PLA_RIGHT_REPEAT: + if(pos&SIS_SET_TIME) + { + pos >>= 1; + if(!(pos&(flags&SIS_SET_TIME))) + pos = SIS_SET_HOUR; + while(!(pos&(flags&SIS_SET_TIME))) + pos >>= 1; + } + 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.nlrcline = 0; + current.nlrc = 0; + current.loaded_lrc = false; + current.too_many_lines = false; + current.changed_lrc = false; + file_offset_offset = -1; + lrc_buffer_used = 0; +} + +/* 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 > MAX_CHARS) /* not enough buffer */ + return NULL; + rb->strcpy(pos, str); + lrc_buffer_used += size; + return pos; +} +static inline long get_time_start(int lrc_idx) +{ + long time = current.lrcs[lrc_idx].time_start + current.offset; + return time < 0? 0: time; +} +static inline void set_time_start(int idx, long time_start) +{ + time_start -= current.offset; + time_start -= time_start%10; + if(current.lrcs[idx].time_start != time_start) + { + current.lrcs[idx].time_start = time_start; + current.lrcs[idx].changed = true; + current.changed_lrc = true; + } +} +/* set time tag according to length of audio and total line count + * for not synched lyrics, so that scroll speed is almost constant. */ +static inline void init_time_tag(void) +{ + struct lrc *lrc = ¤t.lrcs[0]; + int i, idx, line; + for(idx = 0, line = 0; idxtime_start = line * current.length / current.nlrcline; + lrc->time_start -= lrc->time_start%10; + lrc->changed = true; + if(linenline[i]; + } + } +} + +/* sort lyrics by time */ +static void sort_lrcs(void) +{ + struct lrc temp; + int i, j; + + /* sort by time_start. should be stable. */ + for(i=1; i0 && current.lrcs[j-1].time_start > temp.time_start; j--) + { + current.lrcs[j] = current.lrcs[j-1]; + } + current.lrcs[j] = temp; + } +} + +/* 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 need to display each line and store it. */ +static void calc_pos(int start_idx, int end_idx) +{ + struct lrc *lrc; + struct lrc_display_line *lrc_line; + int i, idx, width, count, sp_width, sp_count; + const char *str, *sp; + char ch[4]; + + if(start_idx < 0) start_idx = 0; + if(end_idx > current.nlrc) end_idx = current.nlrc; + + current.nlrcline = 0; + lrc_line = ¤t.lrclines[0]; + lrc = ¤t.lrcs[start_idx]; + for(idx=start_idx; idxpbuf; + lrc->width = 0; + lrc->nline[i] = 0; + lrc->lines[i] = lrc_line; + + do + { + lrc_line->count = 0; + lrc_line->width = 0; + sp_width = 0; + sp_count = 0; + sp = NULL; + + while (*str) + { + if(*str == '\n') + { + str++; + break; + } + count = rb->utf8seek(str, 1); + rb->strncpy(ch, str, count); + ch[count] = 0; + rb->lcd_getstringsize(ch, &width, NULL); +#ifdef HAVE_LCD_BITMAP + if(isspace(*str)) /* remember position of last space */ + { + sp_width = lrc_line->width; + sp_count = lrc_line->count; + sp = str; + } + if(lrc_line->count && + lrc_line->width+width > vp_lyrics[i].width) + { + if(prefs.wrap && sp != NULL && sp_count) /* wrap */ + { + lrc_line->width = sp_width; + lrc_line->count = sp_count; + str = lrc_skip_space(sp); + } + break; + } +#endif + lrc_line->count += count; + lrc_line->width += width; + str += count; + } + lrc->width += lrc_line->width; + lrc->nline[i]++; + current.nlrcline++; + lrc_line++; + /* use one line even for empty line :) */ + } while (*str && current.nlrcline= MAX_LRCLINES) + current.too_many_lines = true; + + return; +} + +/******************************* + * 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 + */ +static bool find_lrc_file_helper(const char *base_dir) +{ + char temp_path[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->strncpy(temp_path, current.mp3_file, MAX_PATH); + /* get file name and remove extension */ + names[0] = rb->strrchr(temp_path, '/')+1; + if((p = rb->strrchr(names[0], '.')) != NULL) + *p = 0; + if(current.id3->title != NULL && rb->strcmp(names[0], current.id3->title)) + names[1] = current.id3->title; + + dir = temp_path; + 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(len == 0 /* root */ || 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(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. + *******************************/ +static off_t file_offset; +/* + * 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] + */ +static long get_time_value(char *tag, bool read_id_tags) +{ + long time; + int len = rb->strlen(tag); + + if(read_id_tags) + { + if(!rb->strncmp(tag, "ti:", 3)) + { + current.title = lrcbufadd(&tag[3]); + return -1; + } + if(!rb->strncmp(tag, "ar:", 3)) + { + current.artist = lrcbufadd(&tag[3]); + return -1; + } + if(!rb->strncmp(tag, "offset:", 7)) + { + current.offset = rb->atoi(&tag[7]); + file_offset_offset = file_offset; + return -1; + } + } + if(len != 5 && len != 8) return -1; + if(!isdigit(tag[0]) || !isdigit(tag[1]) || tag[2] != ':') + return -1; + if(!isdigit(tag[3]) || !isdigit(tag[4])) + return -1; + if(len == 8) + { + if((tag[5] != '.' && tag[5] != ':') + || !isdigit(tag[6]) || !isdigit(tag[7])) + return -1; + } + + /* 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 1 + * [time tag][time tag]line 2 + * [time tag]...[time tag]line 3 + * ... + */ +static bool parse_lrc_line(char *line) +{ + struct lrc *lrc; + int repeat; + long time; + char *str, *tagend; + + str = line; + lrc = ¤t.lrcs[current.nlrc]; + for(repeat = 0; current.nlrc+repeat < MAX_LRCS; repeat++) + { + if (*str != '[') + break; + tagend = rb->strchr(str, ']'); + if (tagend == NULL) break; + *tagend = 0; + time = get_time_value(str+1, !repeat); + if (time < 0) + { + *tagend = ']'; + break; + } + lrc->file_offset = file_offset + ((int)(str-line)); + lrc->time_start = (time/10)*10; + lrc->changed = false; + str = tagend+1; + lrc++; + } + if(!repeat) return true; + + if(current.nlrc+repeat >= MAX_LRCS) /* lrc is full */ + return false; + + /* initialize */ + lrc = ¤t.lrcs[current.nlrc++]; + lrc->changed = false; + if((lrc->pbuf = lrcbufadd(str)) == NULL) + return false; + + /* duplicate lrcs */ + while(--repeat) + { + lrc[1].pbuf = lrc[0].pbuf; + current.nlrc++; + lrc++; + } + + return true; +} + +/* format: + * \xa2\xe2hhmmssxx\xa2\xd0 + * line 1 + * line 2 + * \xa2\xe2hhmmssxx\xa2\xd0 + * line 3 + * ... + */ +static bool parse_snc_line(char *line) +{ +#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 *lrc = ¤t.lrcs[current.nlrc++]; + lrc->file_offset = file_offset+2; + lrc->time_start = hh*3600000+mm*60000+ss*1000+xx*10; + lrc->changed = false; + if((lrc->pbuf = lrcbufadd("")) == NULL) + 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.nlrc) + { + rb->strcat(line, "\n"); + lrc_buffer_used--; /* join line */ + if(lrcbufadd(line) == NULL) + return false; + } + return true; +} + +static bool parse_txt_line(char *line) +{ + /* initialize */ + struct lrc *lrc = ¤t.lrcs[current.nlrc++]; + lrc->file_offset = file_offset; + lrc->time_start = 0; + lrc->changed = false; + if((lrc->pbuf = lrcbufadd(line)) == NULL) + return false; + return true; +} + +static void load_lrc_file(void) +{ + char utf8line[MAX_LINE_LEN]; + int fd; + int encoding = prefs.encoding; + bool (*line_parser)(char *line) = NULL; + off_t readsize; + if(prefs.encoding == NUM_CODEPAGES) /* use default codepage */ + encoding = -1; + + switch(current.type) + { + case LRC8: + encoding = UTF_8; /* force utf8 temporarily */ + /* 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() uses temp_buf to read native data. */ + rb->iso_decode(temp_buf, utf8line, encoding, readsize+1); + if(!line_parser(utf8line)) + break; + file_offset += readsize; + } + rb->close(fd); + + sort_lrcs(); + calc_pos(0, current.nlrc); + if(current.type == TXT) + init_time_tag(); + + return; +} + +/******************************* + * 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;i < len;i++) { + /* Read the next byte and write it back, but don't increment the + write pointer */ + c = *rp++; + *wp = c; + if(*ff_found) { + /* Increment the write pointer if it isn't an unsynch pattern */ + if(c != 0) + wp++; + *ff_found = false; + } else { + if(c == 0xff) + *ff_found = true; + wp++; + } + } + return (long)wp - (long)tag; +} + +static int read_unsynched(int fd, void *buf, int len, bool *ff_found) +{ + int i; + int rc; + int remaining = len; + char *wp; + + wp = buf; + + while(remaining) { + rc = rb->read(fd, wp, remaining); + if(rc <= 0) + return rc; + + i = unsynchronize(wp, remaining, ff_found); + remaining -= i; + wp += i; + } + + return len; +} + +static bool read_id3(void) +{ + int fd, i; + int size; + long framelen; + char header[10]; + int bytesread = 0; + int flags; + bool global_unsynch = false; + bool global_ff_found = false; + bool unsynch = false; + enum {NOLT, SYLT, USLT} type = NOLT; + + fd = rb->open(current.mp3_file, O_RDONLY); + if(fd < 0) return false; + + if(current.id3->codectype != AFMT_MPA_L1 + && current.id3->codectype != AFMT_MPA_L2 + && current.id3->codectype != AFMT_MPA_L3) + goto err; + + /* Bail out if the tag is shorter than 10 bytes */ + if(current.id3->id3v2len < 10) + goto err; + + /* Read the ID3 tag version from the header */ + if(10 != rb->read(fd, header, 10)) + goto err; + + /* Get the total ID3 tag size */ + size = current.id3->id3v2len - 10; + + /* Is unsynchronization applied? */ + global_unsynch = (header[5] & 0x80); + + /* Skip the extended header if it is present */ + if(header[5] & 0x40) { + if(current.id3->id3version == ID3_VER_2_3) { + if(10 != rb->read(fd, header, 10)) + goto err; + /* 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); + } else if(current.id3->id3version >= ID3_VER_2_4) { + if(4 != rb->read(fd, header, 4)) + goto err; + + /* 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); + } + } + + while (size >= 0) { + flags = 0; + + /* Read frame header and check length */ + if(current.id3->id3version >= ID3_VER_2_3) { + if(global_unsynch && current.id3->id3version <= ID3_VER_2_3) + bytesread = read_unsynched(fd, header, 10, &global_ff_found); + else + bytesread = rb->read(fd, header, 10); + + if(bytesread != 10) + goto err; + /* Adjust for the 10 bytes we read */ + size -= 10; + + flags = bytes2int(0, 0, header[8], header[9]); + + if (current.id3->id3version >= 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)) + goto err; + /* 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) + goto err; + else + continue; + } + + unsynch = false; + + if(flags) + { + if (current.id3->id3version >= 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 (current.id3->id3version >= ID3_VER_2_4) { + if(flags & 0x0001) { /* Data length indicator */ + rb->lseek(fd, 4, SEEK_CUR); + /* We don't need the data length */ + framelen -= 4; + } + } + } + + if (framelen == 0) + continue; + + if (framelen < 0) + goto err; + + if(!memcmp( header, "SLT", 3 ) || !memcmp( header, "ULT", 3 ) + || !memcmp( header, "SYLT", 4 ) || !memcmp( header, "USLT", 4 )) { + /* found a supported tag */ + if(header[0] == 'S') { + type = SYLT; + } else if(header[0] == 'U') { + type = USLT; + } + break; + + } else { + /* supported tag was not found */ + + if(global_unsynch && current.id3->id3version <= 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 ) + goto err; + } + } + } + if(type == NOLT) + goto err; + + int encoding = 0; + char *p; + unsigned char* (*utf_decode)(const unsigned char *, + unsigned char *, int) = NULL; + if(framelen >= MAX_CHARS) + framelen = MAX_CHARS-1; + + p = lrc_buffer+MAX_CHARS-framelen-1; + if(global_unsynch && current.id3->id3version <= ID3_VER_2_3) + bytesread = read_unsynched(fd, p, framelen, &global_ff_found); + else + bytesread = rb->read(fd, p, framelen); + + if( bytesread != framelen ) + goto err; + + rb->close(fd); + + if(unsynch || (global_unsynch && current.id3->id3version >= ID3_VER_2_4)) + bytesread = unsynchronize(p, bytesread, NULL); + p[bytesread] = 0; + encoding = p[0]; + /* skip some data */ + if(type == SYLT) { + p += 6; + bytesread -= 6; + } else { + p += 4; + bytesread -= 4; + } + + /* check encoding and skip content descriptor */ + switch (encoding) { + case 0x01: /* Unicode with or without BOM */ + case 0x02: + + i = 0; + /* 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; + i += 2; + } else if(!rb->memcmp(p, "\xfe\xff", 2)) { /* Big endian? */ + utf_decode = rb->utf16BEdecode; + i += 2; + } + + while(p[i] || p[i+1]) + i+=2; + i+=2; + + p += i; + bytesread -= i; + encoding = 2; + break; + + case 0x03: /* UTF-8 encoded string */ + default: + encoding = encoding==0x03?1:0; + i = rb->strlen(p)+1; + p += i; + bytesread -= i; + break; + } + if(encoding == 2) + { + /* 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; + bytesread-=2; + } else if(!rb->memcmp(p, "\xfe\xff", 2)) { /* Big endian? */ + utf_decode = rb->utf16BEdecode; + p += 2; + bytesread-=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; + } + } + + current.nlrc = 0; + lrc_buffer_used = 0; + while ( bytesread > 0 && lrc_buffer_used+bytesread < MAX_CHARS) + { + bool is_crlf = false; + struct lrc *lrc = ¤t.lrcs[current.nlrc++]; + lrc->pbuf = &lrc_buffer[lrc_buffer_used]; + if(type == USLT) + { + /* replace 0x0a and 0x0d with 0x00 */ + i = 0; + if(encoding == 2) + { + char tmp[8]; + while(p[i] || p[i+1]) { + utf_decode(p+i, tmp, 2); + if(tmp[0] == 0x0d || tmp[0] == 0x0a) + { + if(tmp[0] == 0x0d && tmp[1] == 0x0a) + is_crlf = true; + p[i] = p[i+1] = 0; + break; + } + i+=2; + } + } else { + while(p[i]) { + if(p[i] == 0x0d || p[i] == 0x0a) + { + if(p[i] == 0x0d && p[i+1] == 0x0a) + is_crlf = true; + p[i] = 0; + break; + } + i+=1; + } + } + } + switch(encoding) { + case 0: + i = rb->strlen(p)+1; + rb->iso_decode(p, lrc->pbuf, -1, i); + p += i; + bytesread -= i; + lrc_buffer_used += rb->strlen(lrc->pbuf)+1; + break; + case 1: + i = rb->strlen(p)+1; + rb->strcpy(lrc->pbuf, p); + p += i; + bytesread -= i; + lrc_buffer_used += i; + break; + case 2: + { + unsigned char* utf8 = (unsigned char *)lrc->pbuf; + i = 0; + while(p[i] || p[i+1]) { + utf8 = utf_decode(p+i, utf8, 1); + i += 2; + } + *utf8++ = 0; /* Terminate the string */ + i += 2; + p += i; + bytesread -= i; + lrc_buffer_used += rb->strlen(lrc->pbuf)+1; + break; + } + } + i = 0; + if(type == SYLT) { /* timestamp */ + lrc->time_start = bytes2int(p[0], p[1], p[2], p[3]); + lrc->changed = true; + i += 4; + if(encoding == 2) { + if(!memcmp(p+4, "\x0a\x00", 2) || !memcmp(p+4, "\x00\x0a", 2)) + i += 2; + } else { + if(p[4] == 0x0a) + i++; + } + } else { /* USLT */ + lrc->time_start = 0; + lrc->changed = false; + if(is_crlf) { + if(encoding == 2) { + i+=2; + } else { + i++; + } + } + } + p += i; + bytesread -= i; + } + + sort_lrcs(); + calc_pos(0, current.nlrc); + if(type == USLT) + init_time_tag(); + + current.loaded_lrc = true; + + /* save to lrc8 file and treat as lrc8 */ + current.type = LRC8; + rb->strcpy(current.lrc_file, current.mp3_file); + rb->strcpy(rb->strrchr(current.lrc_file, '.')+1, extentions[LRC8]); + fd = rb->creat(current.lrc_file); + if(fd < 0) return true; + + for(i = 0; i < current.nlrc; i++) + { + struct lrc *lrc = ¤t.lrcs[i]; + lrc->changed = false; + lrc->file_offset = rb->lseek(fd, 0, SEEK_CUR); + long time_start = get_time_start(i); + rb->fdprintf(fd, "[%02ld:%02ld.%02ld]%s\n", + time_start/60000, time_start%60000/1000, + time_start%1000/10, lrc->pbuf); + } + rb->close(fd); + + return true; + +err: + rb->close(fd); + return false; +} + +/******************************* + * Display information + *******************************/ +static void display_title_artist(void) +{ +#ifdef HAVE_LCD_BITMAP + int i; + char *str = "Audio Stopped", *title = NULL, *artist = NULL; + + FOR_NB_SCREENS(i) + { + rb->screens[i]->clear_display(); + rb->screens[i]->update(); + } + if(current.audio_status & AUDIO_STATUS_PLAY) + { + if(!prefs.display_title) + return; + str = temp_buf; + if(current.id3 != NULL) + { + title = current.id3->title; + artist = current.id3->artist; + } + if(current.title != NULL) + title = current.title; + if(current.artist != NULL) + artist = current.artist; + + if(artist != NULL && title != NULL) + rb->snprintf(temp_buf, MAX_LINE_LEN, "%s/%s", title, artist); + else if(title != NULL) + str = title; + else if(current.mp3_file[0] == '/') + str = rb->strrchr(current.mp3_file, '/')+1; + else + str = "(no info)"; + } + + FOR_NB_SCREENS(i) + { + struct screen* display = rb->screens[i]; + display->set_viewport(&vp_info[i]); + display->clear_viewport(); + display->puts_scroll(0,0,str); + display->update_viewport(); + display->set_viewport(NULL); + } +#else + /* currently, there is no place to display title or artist. */ + rb->lcd_clear_display(); + if(!(current.audio_status & AUDIO_STATUS_PLAY)) + rb->lcd_puts_scroll(0, 0, "Audio Stopped"); + 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, width; + FOR_NB_SCREENS(i) + { + struct screen* display = rb->screens[i]; + width = display->getwidth(); + 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, width, SYSFONT_HEIGHT-2, + current.length, 0, current.elapsed, HORIZONTAL); + display->update_viewport_rect(0, y, 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_active(struct screen *display) +{ +#if (LCD_DEPTH > 1) || (defined(LCD_REMOTE_DEPTH) && (LCD_REMOTE_DEPTH > 1)) + if(display->depth > 1) + display->set_foreground(prefs.active_color); + else +#endif + display->set_drawmode(DRMODE_INVERSEVID); +} +static inline void set_to_inactive(struct screen *display) +{ +#if (LCD_DEPTH > 1) || (defined(LCD_REMOTE_DEPTH) && (LCD_REMOTE_DEPTH > 1)) + if(display->depth > 1) + display->set_foreground(prefs.inactive_color); + else +#endif + display->set_drawmode(DRMODE_SOLID); +} + +static int display_lrc_lines(enum screen_type i, int ypos, int lrc_idx) +{ + struct screen *display = rb->screens[i]; + struct lrc *lrc = ¤t.lrcs[lrc_idx]; + struct lrc_display_line *lrc_line = lrc->lines[i]; + long time_start, time_end, elapsed; + int xpos, height = ypos + lrc->nline[i] * font_ui_height; + const char *str = lrc->pbuf; + if(height > vp_lyrics[i].height) + height = vp_lyrics[i].height; + + time_start = get_time_start(lrc_idx); + time_end = get_time_start(lrc_idx+1); + + if(time_start > current.elapsed || + (prefs.active_one_line && time_end <= current.elapsed)) /* inactive */ + elapsed = 0; + else if(!prefs.wipe || time_end <= current.elapsed) /* active whole line */ + elapsed = lrc->width; + else + { + long rin = current.elapsed - time_start; + long len = time_end - time_start; + + if(len) + { + /* do not draw bar if interval is <3.5sec */ + if(lrc->width || len <= 3500) + elapsed = rin * lrc->width / len; + else + elapsed = rin * vp_lyrics[i].width / len; + } + else + elapsed = lrc->width; + } + + while(ypos <= -font_ui_height && ypos < height) + { + ypos += font_ui_height; + elapsed -= lrc_line->width; + str += lrc_line->count; + str = lrc_skip_space(str); + lrc_line++; + } + + set_to_active(display); + while(ypos < height) + { + switch(prefs.align) + { + case ALIGN_RIGHT: + xpos = vp_lyrics[i].width - lrc_line->width; + break; + case ALIGN_CENTER: + xpos = (vp_lyrics[i].width - lrc_line->width)/2; + break; + case ALIGN_LEFT: + default: + xpos = 0; + break; + } + + if(elapsed <= 0) + { +#if (LCD_DEPTH > 1) || (defined(LCD_REMOTE_DEPTH) && (LCD_REMOTE_DEPTH > 1)) + if(display->depth > 1) + display->set_foreground(prefs.inactive_color); +#endif + display->set_drawmode(DRMODE_SOLID); + } + else if(!lrc->width) /* draw bar during interval */ + { + 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); + elapsed = 0; + } + else if(elapsed < lrc_line->width)/* wipe text */ + { + display->fillrect(xpos, ypos, elapsed, font_ui_height); + set_to_inactive(display); + display->fillrect(xpos+elapsed, ypos, + lrc_line->width-elapsed, font_ui_height); +#if (LCD_DEPTH > 1) || (defined(LCD_REMOTE_DEPTH) && (LCD_REMOTE_DEPTH > 1)) + if(display->depth > 1) + display->set_drawmode(DRMODE_BG); + else +#endif + display->set_drawmode(DRMODE_INVERSEVID); + } + rb->strncpy(temp_buf, str, lrc_line->count); + temp_buf[lrc_line->count] = 0; + display->putsxy(xpos, ypos, temp_buf); + + ypos += font_ui_height; + elapsed -= lrc_line->width; + str += lrc_line->count; + str = lrc_skip_space(str); + lrc_line++; + } +#if (LCD_DEPTH > 1) || (defined(LCD_REMOTE_DEPTH) && (LCD_REMOTE_DEPTH > 1)) + if(display->depth > 1) + display->set_foreground(prefs.active_color); +#endif + display->set_drawmode(DRMODE_SOLID); + return ypos; +} +#endif /* HAVE_LCD_BITMAP */ + +static void display_lrcs(void) +{ + int lrc_center_idx; + long time_start, time_end, rin, len; + + if(!current.nlrc) return; + + current.lrcs[current.nlrc].time_start = current.length-current.offset+20; + lrc_center_idx = 0; + while(get_time_start(lrc_center_idx+1) <= current.elapsed) + lrc_center_idx++; + + time_start = get_time_start(lrc_center_idx); + time_end = get_time_start(lrc_center_idx+1); + rin = current.elapsed - time_start; + len = time_end - time_start; + + struct screen *display; + int i; + 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 *lrc = ¤t.lrcs[lrc_center_idx]; + int y, ypos = 0, lrc_idx = lrc_center_idx; + y = (display->getnblines()-1)/2; + if(current.too_many_lines) + calc_pos(lrc_idx-y, lrc_idx+y+1); + if(len && rin>=0) + { + if(!prefs.wipe) + ypos = - rin * lrc->nline[i] * font_ui_height / len; + else + { + long elapsed = rin * lrc->width / len; + struct lrc_display_line *lrc_line = lrc->lines[i]; + while(elapsed > lrc_line->width) + { + elapsed -= lrc_line->width; + y--; + lrc_line++; + } + } + } + + /* find top line to display */ + while(lrc_idx && y > 0) + { + lrc--; + lrc_idx--; + y -= lrc->nline[i]; + } + + ypos += y*font_ui_height; + while(lrc_idx < current.nlrc && ypos < vp_lyrics[i].height) + { + ypos = display_lrc_lines(i, ypos, lrc_idx); + lrc_idx++; + } + if(lrc_idx == current.nlrc && ypos < vp_lyrics[i].height) + display->putsxy(0, ypos, "[end]"); +#else /* HAVE_LCD_CHARCELLS */ + struct lrc *lrc = ¤t.lrcs[lrc_center_idx]; + int x = vp_lyrics[i].width/2, y = 0; + if(len) + x -= rin * lrc->width / len; + if (x < 0) + display->puts(0, y, + lrc->pbuf + rb->utf8seek(lrc->pbuf, -x)); + else + display->puts(x, y, lrc->pbuf); + x += rb->utf8length(lrc->pbuf)+1; + if(lrc_center_idx+1 == current.nlrc && x < vp_lyrics[i].width) + display->puts(x, y, "[end]"); +#endif /* HAVE_LCD_BITMAP */ + display->update_viewport(); + display->set_viewport(NULL); + } +} + +/******************************* + * Browse and edit file. + *******************************/ +/* point playing line in lyrics */ +static enum themable_icons get_icon(int selected, void * data) +{ + (void) data; + if(selected >= 0 && selected < current.nlrc) + { + long time_start = get_time_start(selected); + long time_end = get_time_start(selected+1); + long elapsed = current.id3->elapsed; + if(time_start <= elapsed && time_end > elapsed) + return Icon_Moving; + } + return Icon_NOICON; +} +static char *get_lrc_line(int selected, void *data, + char *buffer, size_t buffer_len) +{ + (void) data; + if(selected >= 0 && selected < current.nlrc) + { + long t = get_time_start(selected); + rb->snprintf(buffer, buffer_len, "[%02ld:%02ld.%02ld]%s", + t/60000, (t/1000)%60, (t/10)%100, + current.lrcs[selected].pbuf); + return buffer; + } + return NULL; +} + +static void save_changes(void) +{ + char backup_file[MAX_PATH], *p; + bool success = false; + int fd, fe, i, idx; + off_t curr, next, size, offset; + if(!current.changed_lrc) + return; + rb->splash(HZ/2, "Saving changes..."); + if(current.type == TXT) + { + /* save changes to new .lrc file */ + rb->strcpy(backup_file, current.lrc_file); + p = rb->strrchr(current.lrc_file, '.')+1; + rb->strcpy(p, extentions[LRC]); + } + else + { + /* create backup */ + rb->snprintf(backup_file, MAX_PATH, "%s~", current.lrc_file); + if(rb->file_exists(backup_file)) + rb->remove(backup_file); + rb->rename(current.lrc_file, backup_file); + } + fd = rb->creat(current.lrc_file); + fe = rb->open(backup_file, O_RDONLY); + if(fd >= 0 && fe >= 0) + { + if(current.offset != 0) + { + for(idx=0; idxfilesize(fe); + curr = 0; + while(curr= curr && offset < next) + idx = i, next = offset; + } + offset = file_offset_offset; + if(offset > curr && offset < next) + idx = -1, next = offset, file_offset_offset=-1; + /* 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(idx>=0) + { + struct lrc *lrc = ¤t.lrcs[idx]; + lrc->changed = false; + lrc->file_offset = rb->lseek(fd, 0, SEEK_CUR); + long t = get_time_start(idx); + if(current.type == LRC || current.type == LRC8 || + current.type == TXT) + { + rb->fdprintf(fd, "[%02ld:%02ld.%02ld]", + (t/60000)%100, (t/1000)%60, (t/10)%100); + } + else if(current.type == SNC) + { + rb->fdprintf(fd, "%02ld%02ld%02ld%02ld", (t/3600000)%100, + (t/60000)%60, (t/1000)%60, (t/10)%100); + curr += rb->read(fe, temp_buf, 8); + } + } + 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; + } + else + { + if(current.type == TXT) + { + rb->remove(current.lrc_file); + rb->strcpy(current.lrc_file, backup_file); + } + else /* restore backup */ + { + rb->remove(current.lrc_file); + rb->rename(backup_file, current.lrc_file); + } + rb->splash(HZ, "Could not save changes."); + } + current.changed_lrc = false; +} +static bool lrc_editor(void) +{ + struct gui_synclist gui_editor; + bool exit = false; + int button, idx; + + if(current.id3 == NULL || !current.nlrc) + { + rb->splash(HZ, "No lyrics"); + return false; + } + + rb->gui_synclist_init(&gui_editor, &get_lrc_line, NULL, false, 1, NULL); + rb->gui_synclist_set_nb_items(&gui_editor, current.nlrc); + rb->gui_synclist_set_icon_callback(&gui_editor, get_icon); + rb->gui_synclist_set_title(&gui_editor, "Lrc Editor", Icon_Menu_functioncall); + rb->gui_synclist_select_item(&gui_editor, 0); + 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); + if(idx >= 0 && idx < current.nlrc) + { + set_time_start(idx, current.id3->elapsed); + rb->gui_synclist_draw(&gui_editor); + } + break; + case ACTION_STD_CONTEXT: + idx = rb->gui_synclist_get_sel_pos(&gui_editor); + if(idx >= 0 && idx < current.nlrc) + { + long temp_time = get_time_start(idx); + if(set_int_screen(current.lrcs[idx].pbuf, NULL, + &temp_time, 10, 0, current.length, + SIS_SET_MSEC|SIS_SET_SEC|SIS_SET_MIN) == 1) + return true; + set_time_start(idx, 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; + } + } + sort_lrcs(); + if(current.changed_lrc) + { + MENUITEM_STRINGLIST(save_menu, "Save Changes?", NULL, + "Yes", "No (save later)", "Discard All Changes") + while(1) + { + switch(rb->do_menu(&save_menu, NULL, NULL, false)) + { + case 0: + save_changes(); + return false; + case 1: + return false; + case 2: + reset_current_data(); + 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 }, +#endif + { 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 }, + /* hidden settings (not shown in menu) */ +#ifdef HAVE_LCD_BITMAP + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.display_title }, "display title", NULL }, + { TYPE_BOOL, 0, 1, { .bool_p = &prefs.statusbar_on }, "statusbar on", 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_BOOL, 0, 1, { .bool_p = &prefs.read_id3 }, "read id3", NULL }, + }; + + 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; +#endif + rb->strcpy(prefs.lrc_directry, "/Lyrics"); + prefs.encoding = NUM_CODEPAGES; /* default is `default codepage' */ +#ifdef HAVE_LCD_BITMAP + prefs.display_title = true; + prefs.statusbar_on = false; +#endif + prefs.display_time = true; + prefs.backlight_on = false; + prefs.read_id3 = true; + + configfile_load(config_file, config, + sizeof(config) / sizeof(config[0]), 0); + } + else if(settings_changed) + { + configfile_save(config_file, config, + sizeof(config) / sizeof(config[0]), 0); + } + settings_changed = false; +} + +/* 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_LRC_DIR, + LRC_MENU_ENCODING, + LRC_MENU_PLAYBACK, + LRC_MENU_OFFSET, + LRC_MENU_LRC_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 + "Lrc Directry", "Encoding", "Audio Playback", + "offset", "Lrc Editor"); +#ifdef HAVE_LCD_BITMAP + struct opt_items align_names[] = { + {"Right", -1}, {"Center", -1}, {"Left", -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_pos(0, current.nlrc); + } + 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_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); + if(temp_buf[0] == '/') + rb->strcpy(prefs.lrc_directry, temp_buf); + else + rb->snprintf(prefs.lrc_directry, + sizeof(prefs.lrc_directry), "/%s", temp_buf); + } + break; + 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(); + /* let reload lrc file to apply encoding setting */ + reset_current_data(); + } + break; + case LRC_MENU_PLAYBACK: + return playback_control(NULL); + break; + case LRC_MENU_OFFSET: + usb = (set_int_screen("offset", "sec", ¤t.offset, + 10, -60*1000, 60*1000, + SIS_SET_FP|SIS_FIG_PACK(3)|SIS_BIG_STEP) + == 1); + break; + case LRC_MENU_LRC_EDITOR: + usb = lrc_editor(); + break; + case MENU_ATTACHED_USB: + usb = true; + break; + default: + exit = true; + break; + } + } + return usb; +} + +/******************************* + * Main. + *******************************/ +/* return true if song has changed to know when to load new lyrics. */ +static inline bool audio_has_changed_track(void) +{ + static int last_audio_status = 0; + struct mp3entry *temp_id3 = rb->audio_current_track(); + if((last_audio_status^current.audio_status)&AUDIO_STATUS_PLAY) + { + current.id3 = temp_id3; + last_audio_status = current.audio_status; + return true; + } + if(!(current.audio_status&AUDIO_STATUS_PLAY)) + return false; + if(current.id3 != temp_id3 || rb->strcmp(current.mp3_file, temp_id3->path)) + { + current.id3 = temp_id3; + return true; + } + return false; +} +static void ff_rewind(long time, bool resume) +{ + int audio_status = rb->audio_status(); + if(audio_status & AUDIO_STATUS_PLAY) + { + if(!(audio_status & AUDIO_STATUS_PAUSE)) + { + resume = true; + rb->audio_pause(); + } + rb->audio_ff_rewind(time); + rb->sleep(HZ/10); /* take affect seeking */ + if(resume) + rb->audio_resume(); + } +} + +/* this is the plugin entry point */ +enum plugin_status plugin_start(const void* parameter) +{ + int button=0; + bool found_lrc_file = false; + long ff_rewind_time = -1; + int temp, step = 0; + bool update_display_state = true; + long id3_timeout = 0; + bool exit = false, usb = false; +#ifdef HAVE_LCD_BITMAP + int oldbars = VP_SB_ALLSCREENS; +#endif + + /* initialize settings. */ + load_or_save_settings(false); + +#ifdef HAVE_LCD_BITMAP + if(prefs.statusbar_on) + oldbars = rb->viewportmanager_set_statusbar(VP_SB_ALLSCREENS); +#endif + if (prefs.backlight_on) + backlight_force_on(); + + /* initialize viewport */ +#ifdef HAVE_LCD_BITMAP + rb->lcd_getstringsize("O", NULL, &font_ui_height); + int info_height = (prefs.display_title?font_ui_height:0)+ + (prefs.display_time?SYSFONT_HEIGHT*2:0); +#endif + FOR_NB_SCREENS(temp) + { + rb->viewport_set_defaults(&vp_lyrics[temp], temp); +#ifdef HAVE_LCD_BITMAP + vp_info[temp] = vp_lyrics[temp]; + /* title + time + progress bar */ + vp_info[temp].height = info_height; + vp_lyrics[temp].y += info_height; + vp_lyrics[temp].height -= info_height; +#else + if(prefs.display_time) + { + vp_lyrics[temp].y += 1; /* time */ + vp_lyrics[temp].height -= 1; + } +#endif + } + + reset_current_data(); + current.id3 = NULL; + current.mp3_file[0] = 0; + current.audio_status = rb->audio_status(); + if (parameter && (current.audio_status&AUDIO_STATUS_PLAY)) + { + const char *ext; + current.id3 = rb->audio_current_track(); + rb->strncpy(current.mp3_file, current.id3->path, MAX_PATH); + /* use passed parameter as lrc file. */ + rb->strncpy(current.lrc_file, parameter, MAX_PATH); + current.lrc_file[MAX_PATH-1] = 0; + 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; + } + ext++; + 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; + } + found_lrc_file = true; + } + + while (!exit && !usb) + { + if(ff_rewind_time == -1) + current.audio_status = rb->audio_status(); + if(audio_has_changed_track()) + { + update_display_state = true; + if(!(current.audio_status&AUDIO_STATUS_PLAY)) + { + save_changes(); + reset_current_data(); + current.id3 = NULL; + id3_timeout = 0; + } + else if(rb->strcmp(current.mp3_file, current.id3->path)) + { + save_changes(); + reset_current_data(); + rb->strncpy(current.mp3_file, current.id3->path, MAX_PATH); + current.mp3_file[MAX_PATH-1] = 0; + id3_timeout = *rb->current_tick+HZ*2.5; + found_lrc_file = false; + } + } + if(current.id3 == NULL || !current.id3->length) + { + current.elapsed = 0; + current.length = 1; + } + else + { + if(ff_rewind_time == -1) + current.elapsed = current.id3->elapsed; + else + current.elapsed = ff_rewind_time; + current.length = current.id3->length; + if(current.elapsed > current.length) + current.elapsed = current.length; + } + + if(current.id3 != NULL && id3_timeout && + (TIME_AFTER(*rb->current_tick, id3_timeout) || + (current.id3->artist != NULL))) + { + update_display_state = true; + id3_timeout = 0; + + found_lrc_file = find_lrc_file(); + if(!found_lrc_file && prefs.read_id3) + { + /* no lyrics file found. try to read from id3 tag. */ + found_lrc_file = read_id3(); + } + } + if(found_lrc_file && !current.loaded_lrc) + { + /* current.loaded_lrc is false after changing encode setting */ + load_lrc_file(); + current.loaded_lrc = true; + } + if(update_display_state) + { + display_title_artist(); +#ifdef HAVE_LCD_BITMAP + if(prefs.statusbar_on) + rb->viewportmanager_set_statusbar(VP_SB_ALLSCREENS); +#endif + update_display_state = false; + } + if(current.audio_status & AUDIO_STATUS_PLAY) + { + if(prefs.display_time) + display_time(); + if(!id3_timeout) + display_lrcs(); + } + + button = rb->get_action(CONTEXT_WPS, HZ/16); + switch(button) + { + case ACTION_WPS_BROWSE: + case ACTION_WPS_STOP: + save_changes(); + exit = true; + break; + case ACTION_WPS_PLAY: + if (!current.audio_status && + 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 (current.audio_status & AUDIO_STATUS_PAUSE) + rb->audio_resume(); + else + rb->audio_pause(); + break; + case ACTION_WPS_SEEKFWD: + case ACTION_WPS_SEEKBACK: + if(current.id3 == NULL) + break; + if(ff_rewind_time > -1) + { + if (button == ACTION_WPS_SEEKFWD) + /* fast forwarding, calc max step relative to end */ + temp = (current.length - ff_rewind_time) * 3 / 100; + else + /* rewinding, calc max step relative to start */ + temp = (ff_rewind_time) * 3 / 100; + temp = MAX(temp, 500); + + if (step > temp) + step = temp; + + if (button == ACTION_WPS_SEEKFWD) + ff_rewind_time += step; + else + ff_rewind_time -= step; + + if (ff_rewind_time > current.length) + ff_rewind_time = current.length; + if (ff_rewind_time < 0) + ff_rewind_time = 0; + + /* smooth seeking by multiplying step by: 1 + (2 ^ -accel) */ + step += step >> (rb->global_settings->ff_rewind_accel + 3); + } + else + { + ff_rewind_time = current.elapsed; + if(!(current.audio_status & AUDIO_STATUS_PAUSE)) + rb->audio_pause(); + step = 1000 * rb->global_settings->ff_rewind_min_step; + } + break; + case ACTION_WPS_STOPSEEK: + if(ff_rewind_time == -1) + break; + ff_rewind(ff_rewind_time, !(current.audio_status & AUDIO_STATUS_PAUSE)); + ff_rewind_time = -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: + temp = rb->sound_min(SOUND_VOLUME); + if(--rb->global_settings->volume < temp) + rb->global_settings->volume = temp; + rb->sound_set(SOUND_VOLUME, rb->global_settings->volume); + break; + case ACTION_WPS_VOLUP: + temp = rb->sound_max(SOUND_VOLUME); + if(++rb->global_settings->volume > temp) + rb->global_settings->volume = temp; + rb->sound_set(SOUND_VOLUME, rb->global_settings->volume); + break; + case ACTION_WPS_CONTEXT: + usb = lrc_editor(); + update_display_state = true; + break; + case ACTION_WPS_MENU: + if (prefs.backlight_on) + backlight_use_settings(); + 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; + } + } + + rb->lcd_stop_scroll(); + load_or_save_settings(true); +#ifdef HAVE_LCD_BITMAP + if(prefs.statusbar_on) + rb->viewportmanager_set_statusbar(oldbars); +#endif + if (prefs.backlight_on) + backlight_use_settings(); + return usb? PLUGIN_USB_CONNECTED: PLUGIN_OK; +}