Index: apps/plugins/SOURCES =================================================================== --- apps/plugins/SOURCES (revision 21996) +++ apps/plugins/SOURCES (working copy) @@ -18,6 +18,7 @@ stopwatch.c vbrfix.c viewer.c +snclrc.c #ifdef HAVE_BACKLIGHT lamp.c #endif /* HAVE_BACKLIGHT */ Index: apps/plugins/CATEGORIES =================================================================== --- apps/plugins/CATEGORIES (revision 21996) +++ apps/plugins/CATEGORIES (working copy) @@ -81,6 +81,7 @@ snake,games snake2,games snow,demos +snclrc,apps sokoban,games solitaire,games sort,viewers Index: apps/plugins/snclrc.c =================================================================== --- apps/plugins/snclrc.c (revision 0) +++ apps/plugins/snclrc.c (revision 0) @@ -0,0 +1,3362 @@ +#include "plugin.h" +#include "lib/read_image.h" + +PLUGIN_HEADER +/******************************************/ +/* Keymapping */ +#define CONTEXT_SNC CONTEXT_WPS +#define SNC_SELECT_OR_MODE ACTION_WPS_BROWSE +#define SNC_PLAY_PAUSE ACTION_WPS_PLAY +#define SNC_VISUAL_REW ACTION_WPS_SEEKBACK +#define SNC_VISUAL_FF ACTION_WPS_SEEKFWD +#define SNC_STOP_SEEK ACTION_WPS_STOPSEEK +#define SNC_NEXT ACTION_WPS_SKIPNEXT +#define SNC_PREV ACTION_WPS_SKIPPREV +#define SNC_QUIT ACTION_WPS_STOP +#define SNC_DEC ACTION_WPS_VOLDOWN +#define SNC_INC ACTION_WPS_VOLUP +#define SNC_OPEN_MENU ACTION_WPS_CONTEXT +#define SNC_AB_MENU ACTION_WPS_QUICKSCREEN + +#ifdef CUSTOM_PATCH + #define SNC_SAVE ACTION_WPS_FN + #define SNC_TIMESTAMP_BL ACTION_WPS_CUSTOM /* button rec */ + #define SNC_AB ACTION_WPS_MENU + #define SNC_PLIST ACTION_WPS_PLIST +#else + #define SNC_SAVE ACTION_WPS_REC + #define SNC_TIMESTAMP_BL ACTION_WPS_MENU + #define SNC_AB ACTION_WPS_ABRESET +#endif + +#ifdef HAVE_SCROLLWHEEL /* iPods and Sansa e200 */ + #define SNC_LIST_UP ACTION_WPS_VOLDOWN + #define SNC_LIST_DOWN ACTION_WPS_VOLUP +#else + #define SNC_LIST_UP ACTION_WPS_VOLUP + #define SNC_LIST_DOWN ACTION_WPS_VOLDOWN +#endif +/******************************************/ +#define APP_NAME "snclrc" + +/* Color definitions */ + #define SNC_INACTIVE_COLOR LCD_DARKGRAY +#ifdef HAVE_LCD_COLOR + #define SNC_FG_COLOR LCD_WHITE + #define SNC_BG_COLOR LCD_BLACK +#else + #define SNC_FG_COLOR LCD_BLACK + #define SNC_BG_COLOR LCD_WHITE +#endif +/******************************************/ +/* icons */ +static unsigned char volume_icons[][7]={ + {0x00,0x1c,0x1c,0x3e,0x7f,0x00,0x00}, /* Speaker */ + {0x01,0x1e,0x1c,0x3e,0x7f,0x20,0x40}, /* Speaker mute */ +}; +#define ICON_WIDTH 9 +#define ICON_HEIGHT 8 +#define UTF8_ICON bitmap_icons_8x8[0] +#define VBR_ICON bitmap_icons_8x8[1] +#define NOTE_ICON bitmap_icons_7x8[0] +#define PAUSE_ICON bitmap_icons_7x8[5] +#define AA_ICON_OFFSET 5 +#define TR_ICON bitmap_icons_7x8[9] +#define ARTIST_ICON bitmap_icons_7x8[10] +#define NEXT_ICON bitmap_icons_7x8[11] +#define RIGHT_ICON bitmap_icons_3x8[0] +#define LEFT_ICON bitmap_icons_3x8[1] +#define BUSY_ICON bitmap_icons_7x8[12] + +static const unsigned char bitmap_icons_3x8[][3]={ + {0x7c,0x38,0x10}, /* right */ + {0x10,0x38,0x7c}, /* left */ +}; + +static const unsigned char bitmap_icons_8x8[][8]={ + {0x07,0x08,0x0e,0x21,0xd7,0xb1,0x4e,0x05}, /* utf8 */ + {0x0e,0xe4,0x53,0xa0,0x0f,0x15,0x0a,0x00}, /* vbr */ +}; + +static const unsigned char bitmap_icons_7x8[][7] = +{ + {0x00,0x60,0x7f,0x03,0x33,0x3f,0x00}, /* Musical note */ + {0x44,0x4e,0x5f,0x44,0x44,0x44,0x38}, /* Repeat playmode */ + {0x44,0x4e,0x5f,0x44,0x38,0x02,0x7f}, /* Repeat-one playmode */ + {0x3e,0x41,0x51,0x41,0x45,0x41,0x3e}, /* Shuffle playmode (dice) */ + {0x7f,0x04,0x4e,0x5f,0x44,0x38,0x7f}, /* Repeat-AB playmode */ + {0x00,0x7f,0x7f,0x00,0x7f,0x7f,0x00}, /* Pause */ + {0x1c,0x14,0x18,0x00,0x2a,0x2a,0x20}, /* Album Art west */ + {0x00,0x08,0xea,0xaa,0xca,0x08,0x00}, /* Album Art south */ + {0x00,0x3c,0x24,0x2c,0x38,0x00,0x00}, /* Album Art center */ + {0x01,0x05,0x29,0x20,0xb8,0x80,0x80}, /* Translation */ + //{0x00,0x88,0xde,0xfe,0xfe,0xcc,0x00}, /* Artist */ + //{0x00,0x3c,0x76,0x7e,0x76,0x3c,0x00}, /* Artist */ + {0x38,0x7c,0xd6,0x9e,0xd6,0x7c,0x38}, /* Artist */ + {0x44,0x28,0x54,0x28,0x10,0x7c,0x00}, /* Next */ + {0xc3,0xa5,0xd8,0xfd,0xe5,0xc3,0x00}, /* Hourglass */ +}; +static const unsigned char volume_values[]={0x00,0x08,0x14,0x2a,0x55,0x5f}; +/******************************************/ +#define BUFFERSIZE 512 +#define MAX_ROWS 8 /* max 7 lines [0]line1[1]line2[2]...line7[7] */ + +#define MAX_SECTIONS 300 /* section: time lyrics */ +#define NOT_INIT -1 +#define MAX_ULONG (unsigned long) -1 +#define STATUS_ROW 0 +#define SET_TIME_LATENCY 100 +#define UPDATE_LATENCY 500 +#define FF_REW_TOLERANCE 1000 +#define MODIFIED_TIMETAG 2 +#define MODIFIED_OFFSET_TIMETAG 4 +#define MODIFIED_LYRICS 8 +#define END_MARK '~' +#define COUNT_DOWN_MARK '*' +#define MARK '*' +#define TIME_FORMAT "%d:%02d" +#define RET_OK 1 +#define RET_NOK -1 +#define TIMEBAR_LYRICS_SPACE 3 +#define NL "\r\n" +#define BREAK_LENGTH 5000 +#define BMP_WEST 1 +#define BMP_SOUTH 2 +#define BMP_CENTER 3 +#define MIN_WIDTH_PEAKMETER 10 + +/* status x-positions */ +#define STATUS_VOL_POS 0 +#define STATUS_BUSY_POS 11 +#define STATUS_AA_POS 22 +#define STATUS_UTF8_POS 33 +#define STATUS_VBR_POS 44 +#define STATUS_TR_POS 55 +#define STATUS_MODIFIED_POS 66 +#define STATUS_PAUSE_POS 77 +#define STATUS_DELBOX_POS 88 +#define STATUS_REPEAT_MODE_POS LCD_WIDTH-8 + +#ifdef HAVE_REMOTE_LCD + #define MAX_TYPE 3 + #define RC 2 +#else + #define MAX_TYPE 2 +#endif + +#define SCROLL 0 +#define EDIT 1 + +#define SNC_CONFIG_VERSION 5 /* increment this every time the struct Prefs is restructered */ +/* only for the binary settings */ +#define PREF_BACKLIGHT 1 +#define PREF_TRANSLATION 2 +#define PREF_PEAKMETER 4 +#define PREF_INVERT_DISPLAY 8 +#define PREF_SAVE 0xFFFF + +#define SCROLL_MARKER_WIDTH 3 +#define SCROLL_MARKER_X LCD_WIDTH-SCROLL_MARKER_WIDTH +#define AUTOCUE_ID 1 +#define MAX_FILES2DELETE 20 +/* makros */ +#define SET_ROWS(snc,val) {int ii=0; for(;iix[ii].rows=val;} +#define SET_LYRICS(snc,addr,row) {int ii=0; for(;iix[ii].lyrics[row]=addr;} +#define COPY_SNC(dest,src) {int ii=0; for(;iix[ii]), &(src->x[ii]));} + +#define CALC_MS(time) (int)(time%1000)/10 +#define CALC_SS(time) (int)(time/1000)%60 +#define CALC_MM(time) (int)time/60000 +#define LCD_CLEAR_AREA(screen,x,y,w,h) \ + rb->screen_clear_area(rb->screens[screen],x,y,w,h); +#define AUDIO_ELAPSED() \ + (rb->audio_current_track()!=NULL ? rb->audio_current_track()->elapsed : 0) +#define IS_ID3_VALID(id3entry) (id3entry!=NULL && id3entry->path[0]=='/') +/* spaces at the beginning are skipped, so it's not necessary to check for a space */ +#define IS_BLANK(id) (sncs[id].x[SCROLL].lyrics[0][0] == 0) +#define NO_LYRICS() (num_snc < 2) +#define IS_AUTOCUE() (sncs[0].time_in_ms==AUTOCUE_ID) +#define SET_SCROLL_ELEMENT(e, icon_, text_, y_, yoffset_)\ + e->text = &text_; e->y = y_; e->icon = icon_; e->icon_y_offset = yoffset_; +#define SNCLRC_DATA_DIR PLUGIN_APPS_DIR +#define SETTINGS_FILE SNCLRC_DATA_DIR"/"APP_NAME".dat" +#define TIME_BAR_ROW 2 +/* structs */ +static struct Preferences { + int version; + int load_bmp; + int sxs_bmp; + bool backlight; + int save; + int load_translation; + bool peakmeter; + int scroll_limit_y; + bool invert_display; + int fontheight; + long cue_interval; + int vscroll_delay; + int vscroll_speed; +}prefs; + +static struct SNCText{ + int rows; + unsigned char* lyrics[MAX_ROWS]; +} g_art, g_tit, g_next_at; + +static struct SNCSection{ + unsigned long time_in_ms; + struct SNCText x[MAX_TYPE]; +}sncs[MAX_SECTIONS]; + +struct ScrollElement{ + int y; + const unsigned char* icon; + int icon_y_offset; + struct SNCText* text; +}; + +static struct{ + int num; + bool auto_insert; + char files[MAX_FILES2DELETE][MAX_PATH]; +} delete_list; + +static unsigned char artist_title_buffer[BUFFERSIZE]; +static unsigned char lyrics_buffer[MAX_SECTIONS*BUFFERSIZE]; +static int lyrics_buffer_used; +static struct mp3entry* id3; +static unsigned char buf[BUFFERSIZE]; /* temp. buffer */ +static unsigned char lyrics_filename[MAX_PATH]; +static bool auto_scroll; +static int num_snc; +static int current_snc; /* 0..num_snc-1 */ +static int current_snc_edit; /* 0..num_snc-1 */ +static int end_snc_edit; + +static int start_snc; +static int stop_snc; +static long time_offset; +static int indentwidth, sysfont_height; +static int lcd_max_rows; +static bool utf8, utf8_tmp; +static int modified; +static const unsigned char BOM[]={'\xef','\xbb','\xbf','\0'}; +static const unsigned char* FORMATS[]={"lrc","lrc8","snc","txt","ab", + "tr","mp3","cue"}; +enum e_supported_formats{EXT_LRC,EXT_LRC8,EXT_SNC,EXT_TXT, + EXT_AB,EXT_TR,EXT_MP3,EXT_CUE}; +static enum e_supported_formats g_ext; + +enum e_repeat_modes{RMODE_OFF, RMODE_ALL, RMODE_ONE, RMODE_SHUFFLE, RMODE_AB}; +enum e_load_translation{TR_FAILED=-1, TR_OFF=0, TR_OK=2}; +static int scroll_y0; +static int bmp_width; +static int bmp_height; +static const int DOUBLE_CLICK_TICKS = HZ>>2; +static char language[3]; + +enum e_abfile_state{AB_NO,AB_YES,AB_UNKNOWN}; + +static enum e_abfile_state abfile_state; +static void update_display(void); +static bool load_file(enum e_supported_formats); +static int delete_list_menu(void); +static bool store_line_(struct SNCText*,const unsigned char*, int); +static void v_scroll(struct SNCText**, int*); +static void update(void); +static void add2delete_list(bool remove); + +static void default_backlight(void){ + rb->backlight_set_timeout(rb->global_settings->backlight_timeout); +#if CONFIG_CHARGING + rb->backlight_set_timeout_plugged( + rb->global_settings->backlight_timeout_plugged); +#endif /* CONFIG_CHARGING */ +} + +static void force_backlight_on(void){ + rb->backlight_set_timeout(0); +#if CONFIG_CHARGING + rb->backlight_set_timeout_plugged(0); +#endif /* CONFIG_CHARGING */ +} + +static int rb_menu(const struct menu_item_ex *menu, int *start_selected){ + /* don't invert the colors */ +#ifdef HAVE_LCD_INVERT + if(prefs.invert_display) + rb->lcd_set_invert_display(false); +#endif + int ret = rb->do_menu(menu, start_selected, NULL, false); +#ifdef HAVE_LCD_INVERT + if(prefs.invert_display) + rb->lcd_set_invert_display(true); +#endif + return ret; +} + +#ifndef NO_SNC_ID3 +static unsigned long bytes2int(unsigned long b0, unsigned long b1, + unsigned long b2, unsigned long b3) +{ + return (((long)(b0 & 0xFF) << 24) | /* 3*8 */ + ((long)(b1 & 0xFF) << 16) | /* 2*8 */ + ((long)(b2 & 0xFF) << 8) | /* 1*8 */ + ((long)(b3 & 0xFF))); /* 0*8 */ +} +#endif +inline int lrccmp(const char * str1, const char *str2){ + return (str1==str2)?0:rb->strcmp(str1,str2); +} + +inline void iso2utf8(const unsigned char *iso, unsigned char *utf8_buf, + unsigned int len){ + if(!utf8) rb->iso_decode(iso, utf8_buf, -1, len); + else rb->strlcpy(utf8_buf,iso,len+1); +} +/* no time */ +static void snc_copy_(struct SNCText* dest,const struct SNCText* src){ + dest->rows=src->rows; + int r=0; + for(;rrows;r++) dest->lyrics[r]=src->lyrics[r]; +} +/* copy with time [offset] */ +static void snc_copy(struct SNCSection* dest,const struct SNCSection* src,int offset){ + dest->time_in_ms=src->time_in_ms+offset; + COPY_SNC(dest,src); +} + +static char* cat_time(char* str,unsigned long time) +{ + return (time!=MAX_ULONG) ? + &str[rb->snprintf(str,6,TIME_FORMAT,(int)(CALC_MM(time)),(int)(CALC_SS(time)))]: + rb->strcpy(str,"?:??")+4; +} + +static int get_center_pos(const unsigned char* const str, int min){ + int w=0; + if(str!=NULL){ + rb->lcd_getstringsize(str, &w, NULL); + if(str[rb->strlen(str)-1]==' '){ + int sp_w; + rb->lcd_getstringsize(" ", &sp_w, NULL); + w -= sp_w; + } + } + w=(LCD_WIDTH-w)>>1; + if(w0){ + for(;i 0 */ + break; + } + } + if(i==MAX_FUNCS){ /* is a new func -> assign to a free place */ + i = free>=0 ? free : 0; /* overwrite first if queue is too small */ + } + } + funcs[i].t=delay; + funcs[i].exec=func; + size++; + DEBUGF("func %p added, num func in queue: %d\n",func,size); + } + if(size>0){ /* there are functions in the queue waiting */ + for(i=0;irows = 0; + if(txt == NULL){ + at->lyrics[0] = 0; + return; + } + + int len = rb->strlen(txt)+1; + if(at == &g_art || len+used >= BUFFERSIZE) used = 0; + char* buffer = artist_title_buffer + used; + rb->memcpy(buffer, txt, len); + at->lyrics[0] = buffer; + store_line_(at,buffer,LCD_WIDTH-ICON_WIDTH); + used += len; +} + +static int print_text_line(struct SNCText* snc, int r, + void (*lcd_puts)(int, int, const unsigned char*),int x, int y, + int min_x){ + char c=0; + if(r+1rows){ + c=*(snc->lyrics[r+1]); /* backup */ + *(snc->lyrics[r+1])=0; + } + if(x<0) + x = get_center_pos(snc->lyrics[r],min_x); + lcd_puts(x,y,snc->lyrics[r]); + /* restore */ + if(r+1rows) *(snc->lyrics[r+1])=c; + return x; +} + +static void print_artist_title_line(struct ScrollElement* e, int r){ + int x = print_text_line(e->text, r, rb->lcd_putsxy, -1, e->y, ICON_WIDTH); + if(r == 0) + rb->lcd_mono_bitmap(e->icon, x-ICON_WIDTH, e->y + e->icon_y_offset, 7, ICON_HEIGHT); + else + rb->lcd_mono_bitmap(LEFT_ICON, 0, + e->y + e->icon_y_offset, SCROLL_MARKER_WIDTH, ICON_HEIGHT); + + if(r+1 < e->text->rows){ + rb->lcd_mono_bitmap(RIGHT_ICON, SCROLL_MARKER_X, + e->y + e->icon_y_offset, SCROLL_MARKER_WIDTH, ICON_HEIGHT); + } +} + +static int print_lyrics_line(struct SNCText* snc, int r, bool set_sysfont, + void (*lcd_puts)(int, int, const unsigned char*),int x, int y){ + if(set_sysfont) rb->lcd_setfont(FONT_SYSFIXED); + print_text_line(snc, r, lcd_puts, x, y, 0); + if(set_sysfont) rb->lcd_setfont(FONT_UI); + return set_sysfont ? sysfont_height:prefs.fontheight; +} + +static void print_next_playing(void){ + if(!auto_scroll) return; + if(g_next_at.rows==0){ /* not assigned yet */ + const struct mp3entry* nextid3 = rb->audio_next_track(); + if(IS_ID3_VALID(nextid3)){ + unsigned char* buffer=buf; + const unsigned char* art=nextid3->artist; + const unsigned char* tit=nextid3->title; + if(art!=NULL && tit!=NULL){ + if(rb->strcmp(art, g_art.lyrics[0])!=0) + rb->snprintf(buf,BUFFERSIZE,"%s - %s", tit, art); + else + rb->strcpy(buf, tit); /* only title */ + } + else /* no artist and no title -> display filename */ + buffer=rb->strrchr(nextid3->path,'/')+1; + + store_artist_title(&g_next_at,buffer); + } + } + if(g_next_at.rows>0){ + struct SNCText* text = &g_next_at; + v_scroll(&text,NULL); + } +} +static void draw_volume_icon(void){ + int vol = rb->global_settings->volume; + unsigned char* volume_icon=volume_icons[1]; /* mute */ + if(vol>-60){ + volume_icon=volume_icons[0]; + if(vol>-15) volume_icons[0][6]=volume_values[5]; + else if(vol>-25) volume_icons[0][6]=volume_values[4]; + else if(vol>-30) volume_icons[0][6]=volume_values[3]; + else if(vol>-40) volume_icons[0][6]=volume_values[2]; + else if(vol>-50) volume_icons[0][6]=volume_values[1]; + else volume_icons[0][6]=volume_values[0]; /* >20 */ + } + rb->lcd_mono_bitmap(volume_icon,STATUS_VOL_POS,0,7,ICON_HEIGHT); +} + +static void update_status_display(void) +{ + rb->lcd_setfont(FONT_SYSFIXED); + LCD_CLEAR_AREA(SCREEN_MAIN,0,0,LCD_WIDTH,sysfont_height); + if(prefs.load_translation) + rb->lcd_mono_bitmap(TR_ICON,STATUS_TR_POS,0,7,ICON_HEIGHT); + if(modified){ + const unsigned char modified_icon[]={0x4f,0x6f}; /* modified*/ + rb->lcd_mono_bitmap(modified_icon,STATUS_MODIFIED_POS,0,2,ICON_HEIGHT); + if(time_offset!=0){ + unsigned char sign_icon[]={0x08,0x08,0x3e,0x08,0x08}; /* + */ + if(time_offset<0) sign_icon[2]= 0x08; /* - */ + rb->lcd_mono_bitmap(sign_icon,STATUS_MODIFIED_POS+3,0,5,ICON_HEIGHT); + } + } + if(id3->vbr && id3->codectype==AFMT_MPA_L3){ /* sync problems with vbr mp3 */ + rb->lcd_mono_bitmap(VBR_ICON,STATUS_VBR_POS,0,8,ICON_HEIGHT); + } + + if(rb->audio_status() & AUDIO_STATUS_PAUSE) + rb->lcd_mono_bitmap(PAUSE_ICON,STATUS_PAUSE_POS,0,7,ICON_HEIGHT); + if(prefs.load_bmp) + rb->lcd_mono_bitmap(bitmap_icons_7x8[prefs.load_bmp+AA_ICON_OFFSET] + ,STATUS_AA_POS,0,7,ICON_HEIGHT); + + if(utf8) rb->lcd_mono_bitmap(UTF8_ICON,STATUS_UTF8_POS,0,8,ICON_HEIGHT); + + if(delete_list.num>0){ + rb->snprintf(buf,BUFFERSIZE,"%d", delete_list.num); + rb->lcd_putsxy(STATUS_DELBOX_POS,0,buf); + } + + draw_volume_icon(); + +#ifdef CUSTOM_PLUGIN_PATCH + rb->snprintf(buf,BUFFERSIZE,"%d/%d",rb->playlist_get_display_index(), + rb->playlist_amount()); + rb->lcd_putsxy(LCD_WIDTH-rb->strlen(buf)*6-12,0,buf); +#endif + + rb->lcd_mono_bitmap(bitmap_icons_7x8[rb->global_settings->repeat_mode], + STATUS_REPEAT_MODE_POS,0,7,ICON_HEIGHT); + + rb->lcd_setfont(FONT_UI); + rb->lcd_update_rect(0,0,LCD_WIDTH,sysfont_height); + + if(IS_ID3_VALID(rb->audio_next_track())) + print_next_playing(); /* show next playing immediately if available */ + exec_func(10,print_next_playing); /* update next playing */ +} + +static void update_status_busy(int action){ + static int last_action = ACTION_NONE; + if(last_action == action || ACTION_UNKNOWN == action + || SNC_INC == action || SNC_DEC == action) + return; + last_action = action; + if(action != ACTION_NONE){ + //LCD_CLEAR_AREA(SCREEN_MAIN, STATUS_BUSY_POS, 0, ICON_WIDTH, ICON_HEIGHT); + rb->lcd_mono_bitmap(BUSY_ICON,STATUS_BUSY_POS,0,7,ICON_HEIGHT); + rb->lcd_update_rect(STATUS_BUSY_POS, 0, 7, ICON_HEIGHT); + } + else + exec_func(1, update_status_display); +} + +static void browse_snc(void) +{ + const int max_height = LCD_HEIGHT - sysfont_height; + LCD_CLEAR_AREA(SCREEN_MAIN,0,sysfont_height,LCD_WIDTH,max_height-sysfont_height); + int id, y; + if(end_snc_edit == MAX_SECTIONS){ + /* set end_snc_edit */ + id = num_snc-1; + y = sysfont_height; + do{ + y += sncs[id].x[EDIT].rows * + (prefs.load_translation==TR_OK && + sncs[id].time_in_ms==sncs[id-1].time_in_ms ? + sysfont_height:prefs.fontheight); + }while(y < max_height && --id > 0); + end_snc_edit = id>0 ? id+1 : 0; + } + int r, mark_y, mark_height = prefs.fontheight; + + id = current_snc_edit <= end_snc_edit ? + current_snc_edit-1:end_snc_edit; + y = mark_y = sysfont_height; + if(id<1){ + rb->lcd_putsxy(0, sysfont_height,g_tit.lyrics[0]); + id=1; + y+=prefs.fontheight; + } + bool add_mark; + int mark_width; + rb->lcd_getstringsize(">", &mark_width, NULL); + bool set_sysfont=prefs.load_translation==TR_OK && + sncs[id].time_in_ms==sncs[id-1].time_in_ms; + while(idrows) + { + add_mark=true; + if(r==0 && !set_sysfont){ + char* end=cat_time(buf,sncs[id].time_in_ms); + if(id==start_snc){ + *end='>'; + add_mark=false; + } + else if(id==stop_snc) *end='<'; + else if(id==current_snc){ + *end=MARK; + add_mark=false; + } + *(end+1)=0; + rb->lcd_putsxy(0,y,buf); + } + if(id=start_snc && add_mark) + rb->lcd_putsxy(indentwidth - mark_width ,y,"¦"); + y+=print_lyrics_line(snc,r,set_sysfont,rb->lcd_putsxy,indentwidth,y); + if(id==current_snc_edit) mark_height=y-mark_y; + if(y + prefs.fontheight > max_height) + goto END_BROWSE_SNC; /* quit the nested loop */ + r++; + } + if(prefs.load_translation==TR_OK) + set_sysfont=sncs[id].time_in_ms==sncs[id+1].time_in_ms; + id++; + } + END_BROWSE_SNC: + /* invert current_snc_edit */ + rb->lcd_set_drawmode(DRMODE_COMPLEMENT); + rb->lcd_fillrect(0, mark_y, LCD_WIDTH, mark_height); + rb->lcd_set_drawmode(DRMODE_SOLID); + rb->lcd_update(); +} + +static void draw_ab(int x, int y, int snc){ + if(sncs[snc].time_in_ms==(unsigned) NOT_INIT) return; + const int sign=snc==start_snc?1:-1; + rb->lcd_hline(x+sign*2,x-sign,y); + rb->lcd_set_drawmode(DRMODE_COMPLEMENT); + rb->lcd_vline(x,y-2,y+2); /* | */ + rb->lcd_drawpixel(x+sign,y); /* . */ + rb->lcd_set_drawmode(DRMODE_SOLID); +} + +static void draw_bar(int x1, int x2, int y, int x_current, bool draw_snc_pos){ + rb->lcd_hline(x1,x2,y-1); /* top line */ + rb->lcd_drawpixel(x2+1,y); + rb->lcd_hline(x1-1,x_current,y); + rb->lcd_hline(x1,x2,y+1); /* bottom line */ + if(draw_snc_pos){ + int i=1; + int len = x2-x1; + y+=3; + for(;ilcd_drawpixel((len * sncs[i].time_in_ms / id3->length) + x1,y); + } + } +} + +static long update_time_display(long time_in_ms, bool force){ + if(time_in_ms < 0) return 0; + else if(time_in_ms > (long) id3->length) return id3->length; + + static unsigned int old; + unsigned int time=(time_in_ms/1000)&1; + if(old==time && !force) return time_in_ms; /* update every second */ + old=time; + int y=auto_scroll ? TIME_BAR_ROW*prefs.fontheight+sysfont_height: + LCD_HEIGHT - sysfont_height; + + LCD_CLEAR_AREA(SCREEN_MAIN,0,y,LCD_WIDTH,sysfont_height); + cat_time(buf,id3->length); /* getstringsize from length: can be >9 min */ + rb->lcd_setfont(FONT_SYSFIXED); + int x1,x2; + x1=rb->lcd_getstringsize(buf,&x1,&x2)+3; + x2=LCD_WIDTH-x1; + rb->lcd_putsxy(x2+3,y,buf); + cat_time(buf,time_in_ms); /* update elapsed time */ + rb->lcd_putsxy(0,y,buf); + rb->lcd_setfont(FONT_UI); + + /* timebar */ + int width=x2-x1; + y+=(sysfont_height>>1)-1; + + draw_bar(x1,x2,y,x1+width*time_in_ms/id3->length, true); + if(!auto_scroll){ + rb->lcd_vline(x1+width*sncs[current_snc_edit].time_in_ms/id3->length, + y-3,y+3); /* | */ + } + if(start_snc!=NOT_INIT) + draw_ab(x1+width*sncs[start_snc].time_in_ms/id3->length,y,start_snc); + if(stop_snc!=NOT_INIT) + draw_ab(x1+width*sncs[stop_snc].time_in_ms/id3->length-1,y,stop_snc); + rb->lcd_update_rect(0,y-(sysfont_height>>1),LCD_WIDTH,sysfont_height); + return time_in_ms; +} + +static void v_scroll(struct SNCText** c, int* r){ + #define MAX_SCROLL_ELEMENTS 3 + if(r!=NULL && g_art.rows + g_tit.rows + g_next_at.rows < 4) + return; + + struct SNCText* current = *c; + int row = r!=NULL ? *r : -1; + const int min_r = r!=NULL ? 1 : 0; + + struct ScrollElement elements[MAX_SCROLL_ELEMENTS]; + struct ScrollElement* e = NULL; + int num_elements = 0; + /* fill array with elements */ + if(g_art.rows > min_r){ + e = &elements[num_elements]; + SET_SCROLL_ELEMENT(e, ARTIST_ICON, g_art, sysfont_height,0); + num_elements++; + } + if(g_tit.rows > min_r){ + e = &elements[num_elements]; + SET_SCROLL_ELEMENT(e, NOTE_ICON, g_tit, sysfont_height+prefs.fontheight,0); + num_elements++; + } + if(g_next_at.rows > min_r){ + e = &elements[num_elements]; + SET_SCROLL_ELEMENT(e, NEXT_ICON, g_next_at, + LCD_HEIGHT-prefs.fontheight, (prefs.fontheight-ICON_HEIGHT)>>1 + ); + num_elements++;; + } + if(num_elements == 0) return; + /* get current pos from array */ + int i = 0; + while(i change to the first with more than 1 line */ + i = 0; + current = elements[0].text; + } + /* next */ + if(row+1 >= current->rows && num_elements>1){ + LCD_CLEAR_AREA(SCREEN_MAIN,0,e->y,LCD_WIDTH, prefs.fontheight); + print_artist_title_line(e, 0); + rb->lcd_update_rect(0,e->y,LCD_WIDTH, prefs.fontheight); + + e = &elements[(i + 1)%num_elements]; + current = e->text; + row = 0; + } + else{ + row = (row + 1)%current->rows; + } + LCD_CLEAR_AREA(SCREEN_MAIN,0,e->y,LCD_WIDTH, prefs.fontheight); + print_artist_title_line(e, row); + rb->lcd_update_rect(0,e->y,LCD_WIDTH, prefs.fontheight); + + if(r!=NULL){ + *c = current; + *r = row; + } +} + +static void v_scroll_artist_title(bool reset){ + if(!auto_scroll || prefs.vscroll_speed == 0) return; + static int current_row=0; + static struct SNCText* current = &g_art; + static int wait = -1; + if(reset){ + current_row = 0; + wait = prefs.vscroll_delay; + current = &g_art; + return; + } + if(wait < 0) wait = prefs.vscroll_delay; + if(wait == 0){ + v_scroll(¤t, ¤t_row); + wait = current_row==0 ? prefs.vscroll_delay : prefs.vscroll_speed; + } + wait--; +} + +static void base_display(void) +{ + rb->lcd_clear_display(); + update_status_display(); + struct SNCText* text = &g_art; + v_scroll(&text, NULL); + text = &g_tit; + v_scroll(&text, NULL); + +#ifdef HAVE_REMOTE_LCD + rb->lcd_remote_clear_display(); + rb->snprintf(buf,BUFFERSIZE,"%s/%s", g_art.lyrics[0], g_tit.lyrics[0]); + rb->lcd_remote_puts_scroll(0,0,buf); + rb->lcd_remote_update(); +#endif + update_time_display(AUDIO_ELAPSED(),true); +} +/* + Modes: + 0: draw bitmap from buffer + 1: draw bitmap from file + 2: no loading/drawing; only check and set size +*/ +static void load_bmp(int mode){ + #define BITMAP_BUFFERSIZE 97200 /* max 180 x 180 colour image */ + static unsigned char bitmap_buffer[BITMAP_BUFFERSIZE]; + static struct bitmap bm; + static int bmp_size = 0; + bool load_bmp_mode=prefs.load_bmp && !NO_LYRICS(); + const int height = LCD_HEIGHT - (scroll_y0 + prefs.fontheight + 2); + bool invert_image = true; + if(mode > 0){ + if(!rb->find_albumart(id3,buf,BUFFERSIZE) && !load_bmp_mode){ + /* only load default bmp if there are no lyrics + (not in prefs.load_bmp mode) */ + rb->strcpy(buf, SNCLRC_DATA_DIR"/"APP_NAME".bmp"); + invert_image = false; + } + bm.data = bitmap_buffer; + bm.width = bm.height = (prefs.load_bmp == BMP_CENTER || NO_LYRICS()) ? + MIN(height,LCD_WIDTH) : prefs.sxs_bmp; + if(prefs.peakmeter && LCD_WIDTH - bm.width < (MIN_WIDTH_PEAKMETER << 1)){ + bm.width = bm.height = LCD_WIDTH - (MIN_WIDTH_PEAKMETER << 2); + } + if(mode == 1){ + bmp_size=read_image_file(buf, &bm, BITMAP_BUFFERSIZE, + FORMAT_NATIVE|FORMAT_RESIZE|FORMAT_KEEP_ASPECT, NULL); + + if(prefs.invert_display && invert_image){ + long i=0; + for(;i not inverted bitmap */ + } + } + } + if(bmp_size>0 || mode == 2){ + bmp_height = prefs.load_bmp==BMP_SOUTH ? bm.height + 2 : NOT_INIT; /* hack */ + bmp_width = bm.width+3; + if(mode == 2) return; + int x,y; + if(prefs.load_bmp==BMP_WEST && !NO_LYRICS()){ + x = 0; + y = scroll_y0 + ((height - bm.height)/3); + } + else{ + x=(LCD_WIDTH-bm.width)>>1; + y = prefs.load_bmp==BMP_SOUTH && !NO_LYRICS()? LCD_HEIGHT - + (prefs.fontheight + bm.height + 1) : scroll_y0; + } +#if LCD_DEPTH > 1 + rb->lcd_bitmap((fb_data*) bm.data, x , y, bm.width, bm.height); +#else + rb->lcd_mono_bitmap((fb_data*) bm.data, 0, y, bm.width, bm.height); +#endif + } + else if(load_bmp_mode){ + bmp_width=NOT_INIT; /* cannot load bitmap */ + bmp_height=NOT_INIT; + } +} + +static void peak_meter(void){ + int y=scroll_y0; + int width = bmp_width != NOT_INIT ? + ((LCD_WIDTH-bmp_width)/5)<<1 : LCD_WIDTH >> 3; + if(width < MIN_WIDTH_PEAKMETER) + width = MIN_WIDTH_PEAKMETER; + const int max_height=LCD_HEIGHT-y-prefs.fontheight-3; + int left_peak,right_peak; +#if (CONFIG_CODEC == MAS3587F) || (CONFIG_CODEC == MAS3539F) + left_peak = rb->mas_codec_readreg(0xC); + right_peak = rb->mas_codec_readreg(0xD); +#elif (CONFIG_CODEC == SWCODEC) + rb->pcm_calculate_peaks(&left_peak, &right_peak); +#endif + /* clear */ + LCD_CLEAR_AREA(SCREEN_MAIN,0,y,width,max_height); + int xr=LCD_WIDTH-width; + LCD_CLEAR_AREA(SCREEN_MAIN,xr,y,width,max_height); + /* normalize */ + left_peak=(max_height*left_peak)>>14; /* scale: 4x */ + right_peak=(max_height*right_peak)>>14; + + /* draw bars */ + const int bar_height=max_height>>3; /* max: 8 bars */ + int num_bars_l=left_peak/bar_height; + int num_bars_r=right_peak/bar_height; + const int max_num_bars=num_bars_llcd_set_foreground(SNC_INACTIVE_COLOR); + if(ilcd_fillrect(i*step,y2,w2,bar_frame_height); + } + if(ilcd_fillrect(xr,y2,w2,bar_frame_height); + } + rb->lcd_set_foreground(SNC_FG_COLOR); + rb->lcd_drawrect(0,y2,width,bar_frame_height); + rb->lcd_drawrect(xr,y2,width,bar_frame_height); + + y2+=bar_height; + w2-=step; + i++; + } + rb->lcd_update(); +} + +static void scroll_snc(void) +{ + bool set_sysfont=false; + int r; + const int x = bmp_width != NOT_INIT && prefs.load_bmp == BMP_WEST ? + bmp_width : NOT_INIT; /* NOT_INIT -> center */ + const int max_y = LCD_HEIGHT - (prefs.fontheight + prefs.fontheight + + (bmp_height == NOT_INIT ? 0 : bmp_height)); + LCD_CLEAR_AREA(SCREEN_MAIN, x >= 0 ? x:0, scroll_y0, + LCD_WIDTH, max_y + prefs.fontheight - scroll_y0); + + /* show the vertical scroll limit */ + rb->lcd_set_foreground(SNC_INACTIVE_COLOR); + rb->lcd_mono_bitmap(LEFT_ICON, SCROLL_MARKER_X, + prefs.scroll_limit_y + ((prefs.fontheight-ICON_HEIGHT)>>1), + SCROLL_MARKER_WIDTH, ICON_HEIGHT); + rb->lcd_set_foreground(SNC_FG_COLOR); + + static int current_y0 = LCD_HEIGHT; + int id=current_snc; + struct SNCText* snc; + snc=&(sncs[id].x[SCROLL]); + int tr_height = prefs.load_translation==TR_OK && + sncs[id].time_in_ms==sncs[id+1].time_in_ms ? + sncs[id+1].x[SCROLL].rows * sysfont_height : 0; + /* position the current lyrics line */ + if(current_y0 > prefs.scroll_limit_y) + current_y0 = prefs.scroll_limit_y; + if((max_y < current_y0 + (snc->rows * prefs.fontheight) + tr_height) /* height exceeded */ + || id < 2 /* id=0: blank; id=1: 1st */ + || (IS_BLANK(id) && + sncs[id+1].time_in_ms >= sncs[id].time_in_ms + BREAK_LENGTH) + || (id>0 && IS_BLANK(id-1) && (scroll_y0 + prefs.fontheight == current_y0))) + current_y0 = scroll_y0; + + /* printing the lines on screen */ + int y=current_y0; + int new_current_y0 = current_y0; + while(idrows;r++){ + if(y>max_y) goto END_PRINT_LINES; /* quit the nested loop */ + y+=print_lyrics_line(snc,r,set_sysfont,rb->lcd_putsxy,x,y); + } + set_sysfont=prefs.load_translation==TR_OK && + sncs[id].time_in_ms==sncs[id+1].time_in_ms; + if(!set_sysfont){ + rb->lcd_set_foreground(SNC_INACTIVE_COLOR); + if(sncs[id].time_in_ms==sncs[current_snc].time_in_ms){ + new_current_y0 = y; + y+=((max_y-scroll_y0)%prefs.fontheight)>>1; /* additional space */ + } + } + snc=&(sncs[++id].x[SCROLL]); + } + END_PRINT_LINES: + /* show previous lines */ + y = current_y0; + id = current_snc; + while(id>1){ + snc=&(sncs[--id].x[SCROLL]); + set_sysfont=prefs.load_translation==TR_OK && + sncs[id].time_in_ms==sncs[id-1].time_in_ms; + for(r=snc->rows-1; r>=0; r--){ + y-=set_sysfont ? sysfont_height:prefs.fontheight; + if(ylcd_putsxy,x,y); + } + } + END_PRINT_PREVIOUS_LINES: + current_y0 = new_current_y0; + rb->lcd_set_foreground(SNC_FG_COLOR); /* restore foreground color */ + +#ifdef HAVE_REMOTE_LCD + snc=&(sncs[current_snc].x[RC]); + LCD_CLEAR_AREA(SCREEN_REMOTE,0,prefs.fontheight, + LCD_REMOTE_WIDTH,LCD_REMOTE_HEIGHT-prefs.fontheight); + const int rc_max_line=LCD_REMOTE_HEIGHT/prefs.fontheight; + for(r=0;rrows && rlcd_remote_putsxy,0, + r*prefs.fontheight + prefs.fontheight); + } +#endif + FOR_NB_SCREENS(r) rb->screens[r]->update(); +} + +static void update_display(void) +{ + if(!auto_scroll && num_snc>0){ + update_status_display(); + browse_snc(); + } + else{ + base_display(); + if(!NO_LYRICS()){ + if(prefs.load_bmp && bmp_width>0) + load_bmp(0); + scroll_snc(); + } + else{ +#ifdef HAVE_REMOTE_LCD + LCD_CLEAR_AREA(SCREEN_REMOTE,0,prefs.fontheight, + LCD_REMOTE_WIDTH,LCD_REMOTE_HEIGHT-sysfont_height); + rb->lcd_remote_putsxy(0,prefs.fontheight,"Lyrics?"); +#endif + if(bmp_width>0) + load_bmp(0); + int i; + FOR_NB_SCREENS(i) rb->screens[i]->update(); + } + } +} + +static void set_repeat_mode(int mode){ /* 0=off 1=repeat all 2=repeat one 3=shuffle 4=ab */ + if(rb->global_settings->repeat_mode==mode) return; + static int repeat_mode[]={NOT_INIT,0,0,0,0}; /* orig,repeat one, ab */ + int i,m=NOT_INIT; + if(mode==0){ /* clear */ + for(i=1;i<5;i++) repeat_mode[i]=0; + if(repeat_mode[0]!=-1){ /* restore */ + m=repeat_mode[0]; + repeat_mode[0]=NOT_INIT; + } + } + else if(mode<0){ /* restore */ + if(repeat_mode[0]>NOT_INIT){ + repeat_mode[-mode]=0; + for(i=4;i>0;i--){ /* i==mode */ +/* DEBUGF("repeat mode: %i val: %i\n",i,repeat_mode[i]); */ + if(repeat_mode[i]==1) break; + } + m=i==0?repeat_mode[i]:i; + if(i==0) repeat_mode[0]=NOT_INIT; /* no custom mode is set */ + } /* else call restore before backup */ + } + else { + if(repeat_mode[0]==NOT_INIT) + repeat_mode[0]=rb->global_settings->repeat_mode; /* backup */ + repeat_mode[mode]=1; + m=mode; /* set mode */ + } + if(m!=NOT_INIT){ + rb->global_settings->repeat_mode=m; + rb->audio_flush_and_reload_tracks(); + } + exec_func(5,update_status_display); +} +static int open_file(enum e_supported_formats type,int flags) +{ + char* filename = lyrics_filename; + if(type==EXT_AB || type==EXT_TR) filename=buf; + rb->strcpy(filename,id3->path); + char* ext=rb->strrchr(filename,'.')+1; + rb->strcpy(ext,FORMATS[type]); + int fd=rb->open(filename, flags); + if(fd>=0){ + switch(type){ + case EXT_LRC8: utf8=true; break; + case EXT_LRC:{ + unsigned short ucs=0; + const int idlen=sizeof(ucs); + rb->read(fd,&ucs,idlen); + if(ucs != 0xFEFF && ucs != 0xFFFE) rb->lseek(fd,-idlen,SEEK_CUR); + else{ + rb->splash(HZ,"Converting to UTF8..."); + int len=rb->read(fd,lyrics_buffer,rb->filesize(fd)-idlen); + rb->close(fd); + size_t size; + unsigned char* buffer=(unsigned char*) rb->plugin_get_buffer(&size); + unsigned char* end=rb->utf16LEdecode(lyrics_buffer,buffer,len>>1); + rb->remove(filename); + rb->strcat(filename,"8"); + fd=rb->open(filename, O_WRONLY|O_CREAT|O_TRUNC); + rb->write(fd, buffer,end-buffer); + rb->close(fd); + fd=-1; /* loading order: lrc, lrc8 -> lrc fails, + try to open the newly converted lrc8 */ + } + break; + } + default: /* txt || snc */ + rb->read(fd,buf,3); + utf8=rb->strncmp(buf,BOM,3)==0; /* bom */ + if(!utf8) rb->lseek(fd,0,SEEK_SET); + } + } + return fd; +} + +static int clear_ab(void){ + start_snc=NOT_INIT; + stop_snc=NOT_INIT; + set_repeat_mode(-RMODE_AB);/* reset repeat ab */ + return RET_OK; +} + +static int get_snc_index(unsigned long time){ + if(time == MAX_ULONG) + return NOT_INIT; + if(time < (unsigned) num_snc) /* make it compatible to the old stored a-b */ + return time; + int i=num_snc-2; + while((sncs[i].time_in_ms < time || sncs[--i].time_in_ms >= time) && i>1); + return i+1; +} + +static bool ab_file(bool load){ /* check: load=false */ + int fd = open_file(EXT_AB,O_RDONLY); + if(fd<0){ + abfile_state=AB_NO; + return false; + } + abfile_state=AB_YES; + if(load){ + int len = rb->read_line(fd, buf, BUFFERSIZE); + if(len > 0){ + start_snc=get_snc_index(rb->atoi(buf)); + rb->read_line(fd, buf, BUFFERSIZE); /* no test needed */ + stop_snc=get_snc_index(rb->atoi(buf)); + } + } + rb->close(fd); + return true; +} + +static void set_ab_marker(void) +{ + if(NO_LYRICS()) return; + int snc=auto_scroll?current_snc:current_snc_edit; + if(snc==start_snc) clear_ab(); + else if(snc==stop_snc) stop_snc=NOT_INIT; + else{ + if(start_snc==NOT_INIT) start_snc=snc; + else if(stop_snc==NOT_INIT) stop_snc=snc; + else if(sncfdprintf(fd,"%lu\n%lu\n", + sncs[start_snc].time_in_ms, + stop_snc == NOT_INIT ? MAX_ULONG : sncs[stop_snc].time_in_ms); + rb->close(fd); + abfile_state=AB_YES; + break; + } + case 2: /* load */ + ab_file(true); + break; + case 3: /* remove */ + rb->strcpy(buf,lyrics_filename); + char* ext=rb->strrchr(buf,'.')+1; + rb->strcpy(ext,FORMATS[EXT_AB]); + rb->remove(buf); + clear_ab(); + abfile_state=AB_NO; + break; + } +} + +static void set_volume(int vol) +{ + if(vol < rb->sound_min(SOUND_VOLUME) || vol > rb->sound_max(SOUND_VOLUME)) + return; + rb->sound_set(SOUND_VOLUME, vol); + rb->global_settings->volume = vol; + rb->snprintf(buf, BUFFERSIZE, "%d", vol); + LCD_CLEAR_AREA(SCREEN_MAIN,0,0,LCD_WIDTH,ICON_HEIGHT); + /* draw icon */ + draw_volume_icon(); + /* draw bar */ + #define VOL_BAR_POS 30 /* > 3x6 */ + draw_bar(VOL_BAR_POS,LCD_WIDTH-VOL_BAR_POS,3, + VOL_BAR_POS + ((LCD_WIDTH-2*VOL_BAR_POS))* + (vol - rb->sound_min(SOUND_VOLUME)) / + (rb->sound_max(SOUND_VOLUME) - rb->sound_min(SOUND_VOLUME)), + false); + /* show value*/ + int width; + rb->lcd_setfont(FONT_SYSFIXED); + rb->lcd_getstringsize(buf,&width,&sysfont_height); + rb->lcd_putsxy(LCD_WIDTH-width-2,0,buf); + rb->lcd_setfont(FONT_UI); + + rb->lcd_update_rect(0,0,LCD_WIDTH,ICON_HEIGHT); + exec_func(12,update_status_display); +} + +static bool tt2int(unsigned char** buffer, int* number, char end_char){ + unsigned char* start=*buffer; + bool ok; + while((ok=**buffer!=0)==true && *(*buffer)++!=end_char); + size_t size=*buffer-start-1; + if(ok &= size<=3){ + char xx[size+1]; + xx[size]=0; + rb->memcpy(xx,start,size); + *number=rb->atoi(xx); + } + else *buffer=start; + return ok; +} + +static bool lrc_timetag(unsigned char** buffer,unsigned long* time) +{ + /* format: [m:ss.xxx],[mm:ss.xxx],[mm:ss.xx],[mm:ss] */ + bool is_timetag=**buffer=='[' && + (*buffer)[1] >= '0' && (*buffer)[1] <= '9' && /* digit */ + ((*buffer)[3]==':' || (*buffer)[2]==':'); + if(is_timetag){ + int mm,ss,ms=0; + (*buffer)++; /* skip [ */ + tt2int(buffer,&mm,':'); + tt2int(buffer,&ss,'.') ? tt2int(buffer,&ms,']'): tt2int(buffer,&ss,']'); + /* +1 ensure that time is > 0 */ + *time=mm*60000+ss*1000+(ms<100 ? ms*10 : ms) +1; /* wrong if ms=0xx !!! */ + } + return is_timetag; +} + +inline bool is_wrap_char(const unsigned char* const buffer){ + return *buffer<'A' && *buffer!=0; +} + +static unsigned char* get_utf_char_size_width( + const unsigned char* pos, int* size, int* width){ + if (*pos < 0x80) *size = 1; /* U-00000000 - U-0000007F, 1 *size */ + else if (*pos < 0xe0) *size = 2; /* U-00000080 - U-000007FF, 2 *sizes */ + else if (*pos < 0xf0) *size = 3; /* U-00000800 - U-0000FFFF, 3 *sizes */ + else if (*pos < 0xf5) *size = 4; /* U-00010000 - U-001FFFFF, 4 *sizes */ + /* else: Invalid size. */ + + unsigned char* end = (unsigned char*) pos + *size; + if(width != NULL){ + /* get char width */ + unsigned char c=*end; + *end=0; /* terminate string */ + rb->lcd_getstringsize(pos, width, NULL); /* get width */ + *end=c; /* restore string */ + } + return end; +} + +static bool store_line_(struct SNCText* snc,const unsigned char* buffer, int max_width){ + bool ok; + int w; /* temp vars to hold for result passing */ + int byte=1; + const unsigned char* pos = buffer; /* current position */ + const unsigned char* sp = buffer; /* position of last space */ + int width = 0; /* total width */ + int sp_width = 0; /* width from start to space */ + while((ok=(snc->rows+1)1) + utf8_tmp=true; + width += w; /* add to total */ + } + if(width<=max_width){ + /* don't store empty lines (exception: 1st line) */ + if(pos>buffer+1 || snc->rows==0){ + /* set next row point to the end */ + snc->lyrics[++(snc->rows)]=(unsigned char*) pos; + } + break; + } + else {/* width exceeds max_width */ + const unsigned char* npos = sp+1; /* there was a space, we want to wrap after it */ + width -= sp_width; /* update width */ + if(sp==buffer){ /* no space */ + pos -= byte; /* restore the last position, which was in range */ + npos = pos; + width = 0; + } + while (is_wrap_char(npos)) npos++; /* new line shouldn't begin with one of these chars */ + snc->lyrics[++(snc->rows)]=(unsigned char*) npos; + buffer=npos; /* set beginning to the new position */ + sp = npos; + } + } + if(!ok) rb->splash(HZ,"Not Enough Rows!"); + /* look for end of string and set end mark AFTER \0 to prevent truncation of the line */ + while(*(snc->lyrics[snc->rows])++!=0); + return ok; +} +static bool store_line( + struct SNCSection *snc, const unsigned char* buffer, bool reset_rows) +{ + if(reset_rows){ + SET_ROWS(snc,0); + } + /* remove trailing spaces */ + unsigned char* end = (unsigned char*) buffer + rb->strlen(buffer) - 1; + while(*end == ' ') *end-- = 0; + bool ok=store_line_(&(snc->x[SCROLL]),buffer, + bmp_width>0 && prefs.load_bmp == BMP_WEST? + LCD_WIDTH-bmp_width : LCD_WIDTH); + ok &= store_line_(&(snc->x[EDIT]),buffer,LCD_WIDTH-indentwidth); +#ifdef HAVE_REMOTE_LCD + ok &= store_line_(&(snc->x[RC]),buffer,LCD_REMOTE_WIDTH); +#endif + return ok; +} + +static void init_snc(struct SNCSection* snc, bool reset_time){ + if(snc==NULL) return; + if(reset_time) snc->time_in_ms=0; + SET_ROWS(snc,1); + unsigned char* lyrics=snc->x[SCROLL].lyrics[0]; + *lyrics=0; + /* [0]0[1] */ + SET_LYRICS(snc,lyrics,0); + SET_LYRICS(snc,lyrics+1,1); +} + +#ifndef NO_SNC_ID3 +/* snc: extract time from the buffer, if the buffer contains a timetag */ +static bool snc_timetag(const char* buffer,unsigned long* time) +{ + /* format: \xA2\xE2hhmmss..\xA2\xD0 */ + bool is_timetag = rb->strncmp(buffer,"\xA2\xE2",2)==0; + if(is_timetag) + { + char xx[3]; + xx[2]='\0'; + int mm,ss,ms; + const char* pos=&buffer[4]; /* skip hh */ + + xx[0]=*pos++; + xx[1]=*pos++; + mm=rb->atoi(xx); + + xx[0]=*pos++; + xx[1]=*pos++; + ss=rb->atoi(xx); + + xx[0]=*pos++; + xx[1]=*pos++; + ms=rb->atoi(xx); + + *time=mm*60000+ss*1000+ms*10+1; /* ms*10, +1 ensure that time is > 0 */ + } + return is_timetag; +} + +static int store_snc_section(struct SNCSection *snc, int fd) +{ + int len; + unsigned char* lyrics; + SET_ROWS(snc,0); + bool is_time_tag; + bool ok=true; + while((len = rb->read_line(fd, buf, BUFFERSIZE))>0 && + (is_time_tag = snc_timetag(buf,&(snc->time_in_ms)))==false){ + lyrics=snc->x[SCROLL].lyrics[snc->x[SCROLL].rows]; + snc->x[EDIT].lyrics[snc->x[EDIT].rows]=lyrics; +#ifdef HAVE_REMOTE_LCD + snc->x[RC].lyrics[snc->x[RC].rows]=lyrics; +#endif + iso2utf8(buf, lyrics, len); + if(ok) ok=store_line(snc,lyrics,false); + } + return len; +} + +static bool read_snc(int fd) +{ + num_snc=0; + bool ok; + do{ + ok=store_snc_section(&sncs[num_snc],fd)>0; + struct SNCText* snc=&(sncs[num_snc].x[SCROLL]); + if(snc->rows==0) init_snc(&sncs[num_snc],false); + if(++num_snc < MAX_SECTIONS) + sncs[num_snc].x[SCROLL].lyrics[0]=snc->lyrics[snc->rows]; + else ok=false; + } while(ok); + /* update time: time[i]=time[i-1] */ + int i=num_snc-1; + for(;i>0;i--) sncs[i].time_in_ms=sncs[i-1].time_in_ms; + sncs[0].time_in_ms=0; + return true; +} + +static bool read_id3(int fd){ + if(id3->codectype>AFMT_MPA_L3 + || id3->codectype==AFMT_UNKNOWN + || id3->length>600000){ + /* not mp1/2/3 or > 10 min */ + return false; + } + const int HEADER_SIZE=10; + char header[HEADER_SIZE]; + rb->read(fd, header, 10); + /* DEBUGF("**ID3: %i,%i, %i\n",header[3], id3->id3version, id3->id3v2len); */ + unsigned int pos=HEADER_SIZE; + unsigned long framelen=0; + unsigned char* buffer=0; + int type=0; + int header_size=6; + enum {SYLT=1,USLT}; + while(posid3v2len){ + rb->read(fd, header, HEADER_SIZE); + framelen = bytes2int(header[4], header[5], header[6], header[7]); + if(rb->memcmp(header,"SYLT",4)==0) /* SYLT found */ + type=SYLT; + else if(rb->memcmp(header,"USLT",4)==0){ + type=USLT; + header_size=4; + /* check if there's a txt file */ + if(load_file(EXT_TXT)) return true; + /* handle like a txt file */ + rb->strcpy(rb->strrchr(lyrics_filename,'.')+1,FORMATS[EXT_TXT]); + g_ext=EXT_TXT; + auto_scroll=false; + current_snc_edit=1; + } + if(type){ + size_t size; + buffer=(unsigned char*) rb->plugin_get_buffer(&size); + rb->read(fd, buffer, framelen); + break; + } + rb->lseek(fd, framelen, SEEK_CUR); /* skip content */ + pos+=HEADER_SIZE; + } + + if(type){ /* found */ + unsigned char* bpos=buffer+header_size; /* skip header */ + bool unicode=buffer[0]==1; + unsigned char* (*utf_decode)(const unsigned char *, unsigned char *, int); + utf_decode=rb->utf16LEdecode; + if(!unicode) + while(*bpos++!=0); /* skip content descriptor */ + else{ + if(rb->strncmp(bpos,"\xFE\xFF",2)==0) utf_decode=rb->utf16BEdecode; + while(*bpos!=0 || *(bpos+1)!=0) bpos+=2; + bpos+=2; + while(*bpos==0xFF || *bpos==0xFE) bpos++; /* skip them (tagrename) */ + } + + init_snc(&sncs[0],true); + num_snc=1; /* start with snc = 1 */ + SET_LYRICS((&sncs[num_snc]),sncs[0].x[SCROLL].lyrics[0]+1,0); + unsigned char* endpos=buffer+framelen; + /* ensure termination (+1: unicode) */ + *(endpos)=0; *(endpos+1)=0; + while(endpos>bpos){ + struct SNCText* snc=&(sncs[num_snc].x[SCROLL]); + unsigned char* lyrics=snc->lyrics[0]; + if(type==USLT){ + /* replace USLT endchar 0x0A with 0x00 and 0x0D with a space */ + lyrics=bpos; + while((*lyrics)!=0x0A){ + if(*lyrics==0x0D) *lyrics=' '; + lyrics++; + } + *lyrics=0; /* now it's like SYLT and independent of endianess */ + lyrics=snc->lyrics[0]; + } + if(!unicode) + while((*lyrics++=*bpos++)!=0); + else{ + while(*bpos==0xFF || *bpos==0xFE) bpos++; /* skip FF and FE */ + unsigned char* start=bpos; + while(*bpos!=0 || *(bpos+1)!=0) bpos+=2; /* not 00 */ + bpos+=2; /* include 00 */ + utf_decode(start,lyrics,bpos-start); + } + + SET_LYRICS((&sncs[num_snc]),snc->lyrics[0],0); + store_line(&sncs[num_snc],snc->lyrics[0],true); + if(type==SYLT){ + /* timestamp */ + sncs[num_snc].time_in_ms=bytes2int(bpos[0],bpos[1],bpos[2],bpos[3]); + bpos+=4; + } + else sncs[num_snc].time_in_ms=NOT_INIT; + sncs[++num_snc].x[SCROLL].lyrics[0]=snc->lyrics[snc->rows]; + } + } + return type; +} +#endif /* NO_SNC_ID3 */ + +static bool read_lrc(int fd, bool store_notag, bool append){ + int append_start=0; + if(append==false){ + init_snc(&sncs[0],true); + num_snc=1; /* start with snc = 1 */ + SET_LYRICS((&sncs[num_snc]),sncs[0].x[SCROLL].lyrics[0]+1,0); + } + else{ /* append */ + if(NO_LYRICS()) return false; /* cannot append if there weren't any before */ + if(sncs[num_snc-1].x[SCROLL].lyrics[0][0]==END_MARK) + num_snc--; /* last: ~..~ -> append before */ + SET_LYRICS((&sncs[num_snc]),&lyrics_buffer[lyrics_buffer_used],0); + append_start = num_snc; + } + + int snc_id,src_id,len,old_len=100; + unsigned char* buffer; + while ((len = rb->read_line(fd, buf, BUFFERSIZE))>0 && num_sncstrncmp(buffer,"[la:",4)==0){ /* language tag */ + if(buffer[4] != ']'){ + language[0]=buffer[4]; + language[1]=buffer[5]; + language[2]=0; + } + continue; + } + else continue; /* not handled lrc tag */ + + struct SNCText* snc=&(sncs[num_snc].x[SCROLL]); + unsigned char* lyrics=snc->lyrics[0]; + iso2utf8(buffer, lyrics, len-(buffer-buf)+1); + SET_LYRICS((&sncs[num_snc]),lyrics,0); + store_line(&sncs[num_snc],lyrics,true); + while(++num_snc!=snc_id){ + COPY_SNC((&sncs[num_snc]),(&sncs[src_id])); + } + /* next */ + sncs[num_snc].x[SCROLL].lyrics[0]=snc->lyrics[snc->rows]; + old_len=len; + } + lyrics_buffer_used=sncs[num_snc].x[SCROLL].lyrics[0]-lyrics_buffer; + if(store_notag==false){ /* don't sort if it's txt */ + int i; + if(append){ + /* now: */ + /* lrc1 lrc2 .. [tr1 tr2 ..] */ + /* to ensure order after sort: */ + /* lrc1 tr1 lrc2 tr2 ... */ + for(i=append_start;itime_in_ms - + ((struct SNCSection*) p2)->time_in_ms; + } + rb->qsort(sncs,num_snc,sizeof(struct SNCSection),compare); + if(append){ + /* restore time */ + for(i=0;i+1read_line(fd, buf, BUFFERSIZE)>0 && num_snclyrics[0]; + + s = buf; + while(*s==' ' || *s=='\t') s++; /* skip space(s) */ + if (!rb->strncmp(s, "TRACK", 5)) + { + sncs[++num_snc].x[SCROLL].lyrics[0]=snc->lyrics[snc->rows]; + } + else if (!rb->strncmp(s, "INDEX 01", 8)) + { + s+=8; + while(*s==' ' || *s=='\t') s++; /* skip space(s) */ + sncs[num_snc].time_in_ms = 60000 * rb->atoi(s); + s = rb->strchr(s,':') + 1; + sncs[num_snc].time_in_ms += 1000 * rb->atoi(s); + s = rb->strchr(s,':') + 1; + sncs[num_snc].time_in_ms += 13 * rb->atoi(s); + if(sncs[num_snc].time_in_ms==0) sncs[num_snc].time_in_ms=1; + } + else if (!rb->strncmp(s, "TITLE", 5)) + { + char *string, *end; + string = rb->strchr(s, '"'); + if (!string) + { + string = rb->strchr(s, ' '); + if (!string) break; + } + end = rb->strchr(++string, '"'); + if (end) *end = '\0'; + + /* iso2utf8(string, lyrics, rb->strlen(string)+1); */ + rb->strcpy(lyrics,string); + SET_LYRICS((&sncs[num_snc]),lyrics,0); + store_line(&sncs[num_snc],lyrics,true); + } + } + sncs[0].x[SCROLL].lyrics[0][0]=0; + SET_ROWS((&sncs[0]),1); + if(snc!=NULL) sncs[++num_snc].x[SCROLL].lyrics[0]=snc->lyrics[snc->rows]; + return true; +} + +static void append_end_section(void){ + /* add_last_section */ + if(num_snc+1time_in_ms=id3->length; + + static char lastBuf[LCD_WIDTH>>2]; + if(lastBuf[0]!=END_MARK){ /* initialized? */ + int w,tmp; + rb->lcd_getstringsize("~", &w, &tmp); + tmp=LCD_WIDTH/w; + rb->memset(lastBuf,END_MARK,tmp); /* fill with ~ */ + lastBuf[tmp]=0; + } + SET_LYRICS(last,lastBuf,0); + num_snc++; + } +} + +static bool load_file(enum e_supported_formats type){ + bool ret=true; + int fd = open_file(type, O_RDONLY); + if (fd < 0){ + if(type==EXT_TR) prefs.load_translation=TR_FAILED; + return type==EXT_LRC ? load_file(EXT_LRC8):false; /* try to open lrc8 */ + } + if(type!=EXT_TR){ + g_ext=type; + lyrics_buffer_used=0; + SET_LYRICS((&sncs[0]),lyrics_buffer,0); + } + switch(type){ + case EXT_LRC: + case EXT_LRC8: + read_lrc(fd, false, false); + break; + case EXT_TR: + prefs.load_translation=TR_OK; + rb->lcd_setfont(FONT_SYSFIXED); + read_lrc(fd, false, true); + rb->lcd_setfont(FONT_UI); + break; +#ifndef NO_SNC_ID3 + case EXT_SNC: read_snc(fd); break; + case EXT_MP3: ret=read_id3(fd); break; +#endif + case EXT_CUE: read_cue(fd); break; + default: /* read txt */ + bmp_width=NOT_INIT; + bmp_height=NOT_INIT; + read_lrc(fd,true,false); + if(sncs[1].time_in_ms==MAX_ULONG) sncs[1].time_in_ms=1; + current_snc_edit=1; + } + rb->close(fd); + + /* if prefs.load_translation is set, try to load translation file */ + if(prefs.load_translation && + type!=EXT_TR && /* prevent loop */ + load_file(EXT_TR)){ /* try to load translation file */ + return true; /* continue if loading translation fails, + return otherwise (finishing is already done by EXT_TR)*/ + } + + /* lrc: after sorting, the last snc could be somewhere in the lyrics buffer */ + if(lyrics_buffer_used==0) + lyrics_buffer_used=sncs[num_snc].x[SCROLL].lyrics[0]-lyrics_buffer; + + append_end_section(); + + if(utf8_tmp && utf8==false) utf8=true; + return ret; +} + +static void ff_rew(unsigned long time_in_ms){ + bool playing=!(rb->audio_status()&AUDIO_STATUS_PAUSE); + if(playing) rb->audio_pause(); + rb->audio_ff_rewind(time_in_ms); + update_time_display(time_in_ms, true); + int diff; + long counter = 0; /* counter to prevent endless loop */ + while(((diff=AUDIO_ELAPSED()-time_in_ms)>FF_REW_TOLERANCE || + diff < -FF_REW_TOLERANCE) && ++counter < 100000){ + rb->yield(); + } + if(playing){ + rb->audio_resume(); + /* wait until status is updated */ + counter = 0; + while((rb->audio_status()&AUDIO_STATUS_PAUSE) && ++counter < 100000) + rb->yield(); + } +} +static void ff_rew_snc(int snc){ + snc%=(num_snc-1); /* 0 .. num_snc-2 */ + if(sncs[snc].time_in_ms==MAX_ULONG) return; /* txt */ + ff_rew(sncs[snc].time_in_ms); +} + +static void switch_mode(bool discard) +{ + if(num_snclength/prefs.cue_interval) >= MAX_SECTIONS || num_cues < 2) + { + if(IS_AUTOCUE()){ + auto_scroll = true; + end_snc_edit=MAX_SECTIONS; + num_snc=0; + } + return RET_NOK; + } + if(NO_LYRICS()) g_ext=EXT_CUE; /* no sncs => set extension to cue */ + else if(!NO_LYRICS() && !IS_AUTOCUE()) + return RET_NOK; + + lyrics_buffer_used=0; + SET_LYRICS((&sncs[0]),lyrics_buffer,0); + init_snc(&sncs[0],true); + sncs[0].time_in_ms=AUTOCUE_ID; /* hack: to identify auto_cue */ + SET_LYRICS((&sncs[1]),sncs[0].x[SCROLL].lyrics[0]+1,0); + struct SNCText* snc=NULL; + int i=1; + while(i<=num_cues){ + snc=&(sncs[i].x[SCROLL]); + sncs[i].time_in_ms=(i-1)*prefs.cue_interval+2; /* +2: ensure it's > sncs[0].time_in_ms */ + int bytes=rb->snprintf(snc->lyrics[0],11,"%02d Track",i)+1; /* XX Track */ + SET_ROWS((&sncs[i]),1); + SET_LYRICS((&sncs[i]),snc->lyrics[0],0); + SET_LYRICS((&sncs[i]),snc->lyrics[0]+bytes,1); + sncs[++i].x[SCROLL].lyrics[0]=snc->lyrics[snc->rows]; + } + num_snc=num_cues + 1; + lyrics_buffer_used=sncs[num_snc].x[SCROLL].lyrics[0]-lyrics_buffer; + append_end_section(); + current_snc = 0; + end_snc_edit=MAX_SECTIONS; + return RET_OK; +} + +static void reset(bool all) +{ + if(all){ + do{ + id3 = rb->audio_current_track(); + rb->yield(); + }while(!(IS_ID3_VALID(id3) && id3->length > 0)); /* wait 'til the id3 is valid */ + + store_artist_title(&g_art, id3->artist); + store_artist_title(&g_tit, + id3->title != NULL ? id3->title : rb->strrchr(id3->path,'/')+1); + store_artist_title(&g_next_at, 0); + + num_snc=0; + clear_ab(); + time_offset=0; + utf8=false; + modified=0; + utf8_tmp = false; + abfile_state=AB_UNKNOWN; + + char* end=cat_time(buf,id3->length); + *end='>'; + *++end=0; + rb->lcd_getstringsize(buf, &indentwidth, NULL); /* update indentwidth */ + } + v_scroll_artist_title(true); + language[0] = 0; + current_snc=0; + current_snc_edit=0; + end_snc_edit=MAX_SECTIONS; + bmp_width=NOT_INIT; + + if(delete_list.auto_insert) add2delete_list(false); + if(auto_scroll) + base_display(); + update_status_busy(ACTION_REDRAW); + + if(prefs.load_bmp){ + num_snc=MAX_SECTIONS; /* hack: to ensure that we are in prefs.load_bmp mode */ + load_bmp(2); + num_snc=0; + /* if BMP_CENTER && AA available then show bitmap without lyrics */ + if(prefs.load_bmp == BMP_CENTER){ + load_bmp(1); + update_display(); + return; + } + } +#ifdef HAVE_ADJUSTABLE_CPU_FREQ + rb->cpu_boost(true); +#endif + if(!(load_file(EXT_LRC) || +#ifndef NO_SNC_ID3 + load_file(EXT_MP3) || + load_file(EXT_SNC) || +#endif + load_file(EXT_CUE))){ /* all supported formats failed */ + num_snc=0; + sncs[0].time_in_ms=0; /* to enable ff_rew */ + SET_ROWS((&sncs[0]),0); + if(prefs.load_translation) prefs.load_translation=TR_FAILED; + auto_cue(); + } + + if(NO_LYRICS() && !auto_scroll){ + auto_scroll = true; + base_display(); + } + update_time_display(AUDIO_ELAPSED(),false); + if(auto_scroll){ + current_snc = num_snc - 1; /* force scrolling */ + update(); + } + if(prefs.load_bmp || NO_LYRICS()){ + load_bmp(1); + if(auto_scroll) + NO_LYRICS() ? rb->lcd_update() : scroll_snc(); + } + if(!auto_scroll) + browse_snc(); +#ifdef HAVE_ADJUSTABLE_CPU_FREQ + rb->cpu_boost(false); +#endif +} + +static void save2cue(void){ + int fd = rb->open(lyrics_filename, O_WRONLY|O_CREAT|O_TRUNC); + if(fd<0) return; + + rb->fdprintf(fd,"PERFORMER \"%s\"" NL "TITLE \"%s\"" NL "FILE \"%s\" MP3" NL, + id3->artist,id3->title,id3->path); + int i = 1,last = num_snc-1; + + for(;ifdprintf(fd, + "TRACK %02d AUDIO" NL " TITLE \"%s\"" NL " INDEX 01 %02d:%02d:%02d" NL, + i, sncs[i].x[SCROLL].lyrics[0], + (int) CALC_MM(sncs[i].time_in_ms), + (int) CALC_SS(sncs[i].time_in_ms), + (int) CALC_MS(sncs[i].time_in_ms)); + } + rb->close(fd); +} + +static void save2file(void) +{ + if(!modified) return; + rb->splash(HZ,"Saving..."); + + int r,i; + int last=num_snc-1; + int save_tr=prefs.load_translation==TR_OK? 2:0; + char filename_buf[MAX_PATH]; + char* filename=lyrics_filename; + if(save_tr==2){ + rb->strcpy(filename_buf,lyrics_filename); + filename=filename_buf; + } + char* ext=rb->strrchr(filename,'.')+1; + if(g_ext==EXT_CUE){ + rb->strcpy(ext,FORMATS[EXT_CUE]); /* ensure the extension is cue */ + save2cue(); + modified=0; + time_offset=0; + return; + } + bool is_txt=g_ext==EXT_TXT; + if( is_txt ||(g_ext==EXT_LRC && utf8)) /* lrc: -> rename to lrc8 */ + { + bool complete=false; + i=1; + while(iremove(lyrics_filename); /* remove old file */ + rb->strcpy(ext,FORMATS[utf8 ? EXT_LRC8 : EXT_LRC]); /* rename file to lrc/8 */ + g_ext=EXT_LRC; + } + } + else if(g_ext==EXT_MP3 || g_ext==EXT_SNC){ /* SYLT or SNC */ + g_ext=utf8 ? EXT_LRC8 : EXT_LRC; + rb->strcpy(ext,FORMATS[g_ext]); + } + int fd; + SAVE_TRANSLATION: + fd = rb->open(filename, O_WRONLY|O_CREAT|O_TRUNC); + if(fd<0) return; + + bool is_lrc=g_ext==EXT_LRC || g_ext==EXT_LRC8 || g_ext==EXT_TR; + if(is_lrc){ + if(language[0]!=0){ + rb->fdprintf(fd,"[la:%s]" NL,language); + } + rb->fdprintf(fd,"[ti:%s]" NL "[ar:%s]" NL, g_tit.lyrics[0],g_art.lyrics[0]); + } + else if(is_txt && utf8){ + rb->fdprintf(fd,BOM); + } + bool save[last]; + rb->memset(save,true,last); + int pos,j=last; + for(i=1;ifdprintf(fd,"[%02d:%02d.%02d]", + CALC_MM(sncs[pos].time_in_ms), + CALC_SS(sncs[pos].time_in_ms), + CALC_MS(sncs[pos].time_in_ms)); + } + if(is_lrc){ /* [timetag][timetag]..[timetag]lyrics */ + j=pos+1; + for(;jfdprintf(fd,"%s",snc->lyrics[0]); + + for(r=1; rrows;r++){ + unsigned char* pos=snc->lyrics[r]; + if(*(--pos)==0){ + if(*(--pos)!=' ') rb->fdprintf(fd," "); + rb->fdprintf(fd,"%s",snc->lyrics[r]); + } + } + rb->fdprintf(fd,NL); + } + rb->close(fd); + + if(save_tr==2){ + rb->strcpy(ext,FORMATS[EXT_TR]); + save_tr=1; + goto SAVE_TRANSLATION; + } + modified=0; + time_offset=0; + /* set_repeat_mode(-RMODE_ONE); */ +} + +static bool yes_no(char* const what){ + const char* items[2] = {"Yes", "No"}; + struct menu_callback_with_desc desc = {NULL,what,Icon_Questionmark}; + struct menu_item_ex menu = { + MT_RETURN_ID|MENU_HAS_DESC|MENU_ITEM_COUNT(2), + {.strings = items }, + {.callback_and_desc = &desc} + }; + return rb_menu(&menu,NULL)==0; +} + +static void confirm_saving(void) +{ + if(modified==0) return; + if(yes_no("Save Changes?")) save2file(); + else modified=0; +} + +static void do_list(const char* title, int count, void* data, + int (*action_callback)(int, struct gui_synclist *), int selected){ + + char* get_list_name(int selected_item, void * data, + char * buffer, size_t buffer_len){ + (void) buffer; + (void) buffer_len; + return ((char**) data )[selected_item]; + } + enum themable_icons get_list_icon(int item, void * data){ + return ((char**) data )[item][0]== '<' ? Icon_Recording : Icon_Questionmark; + } + + struct simplelist_info list; + rb->simplelist_info_init(&list, (char*) title, count, data); + list.get_icon = get_list_icon; + list.get_name = get_list_name; + list.action_callback = action_callback; + list.selection = selected; + rb->simplelist_show_list(&list); +} + +static void add2delete_list(bool remove){ + if(delete_list.num >= MAX_FILES2DELETE){ + rb->splash(HZ,"Delete Buffer is Full!"); + delete_list_menu(); + if(delete_list.num >= MAX_FILES2DELETE) + return; + } + int i=delete_list.num-1; + while(i>=0){ + if(rb->strcmp(delete_list.files[i],id3->path) == 0){ /* is the file already in the list? */ + if(remove){ + rb->strcpy(delete_list.files[i], + delete_list.files[--delete_list.num]); + } + break; /* return */ + } + i--; + } + if(i<0){ /* it's a new entry */ + rb->strcpy(delete_list.files[delete_list.num++],id3->path); /* add */ + } +} + +static int delete_list_menu(void){ + #define NUM_F2D_MENUITEMS 4 + char* items[delete_list.num + NUM_F2D_MENUITEMS]; + enum{DLIST_DEL=0,DLIST_AUTOINS,DLIST_ADD,DLIST_CLEAR}; + + items[DLIST_DEL]=""; + items[DLIST_ADD]=""; + items[DLIST_AUTOINS]=""; + items[DLIST_CLEAR]=""; + int i = 0; + while(istrrchr(delete_list.files[i],'/') + 1; + i++; + } + + int list_callback(int action, struct gui_synclist *lists){ + if(action != ACTION_STD_OK) return action; + bool quit = true; + int i = rb->gui_synclist_get_sel_pos(lists); + switch(i){ + case DLIST_DEL: /* delete all the files in the list */ + if(delete_list.num == 0) break; + rb->splash(HZ,"Deleting Files..."); + while(delete_list.num>0) + rb->remove(delete_list.files[--delete_list.num]); + break; + case DLIST_AUTOINS: + delete_list.auto_insert = !delete_list.auto_insert; + case DLIST_ADD: /* add current playing file to the list */ + add2delete_list(false); + break; + case DLIST_CLEAR: /* clear the list */ + delete_list.num = 0; + break; + default: /* remove file from list */ + rb->strcpy(delete_list.files[i-NUM_F2D_MENUITEMS], + delete_list.files[--delete_list.num]); + quit = delete_list.num == 0; + rb->gui_synclist_set_nb_items(lists,delete_list.num+NUM_F2D_MENUITEMS); + } + return quit ? ACTION_STD_CANCEL : ACTION_REDRAW; + } + do_list("Delete-List", delete_list.num+NUM_F2D_MENUITEMS, items, + list_callback,0); + return RET_OK; +} + +static bool audio_changed_track(void){ + return id3!=rb->audio_current_track(); +} + +static void update(void) +{ + if(audio_changed_track() && /* new track is playing */ + rb->global_settings->repeat_mode!=RMODE_ONE && + rb->global_settings->repeat_mode!=RMODE_AB){ //no repeat + confirm_saving(); + reset(true); + } + exec_func(0,NULL); /* update func queue */ + v_scroll_artist_title(false); + if(NO_LYRICS()) return; + + unsigned long elapsed = AUDIO_ELAPSED()+UPDATE_LATENCY; + int show_snc=current_snc; + int next_snc=current_snc; + while(sncs[current_snc].time_in_ms==sncs[++next_snc].time_in_ms); + if(auto_scroll){ + /* display countdown */ + struct SNCSection* snc=&sncs[current_snc]; + if(IS_BLANK(current_snc) && + (sncs[next_snc].time_in_ms >= snc->time_in_ms + BREAK_LENGTH)){ + int remain=1+(sncs[next_snc].time_in_ms-elapsed)/1000; /* +1: 0.9 -> 0 */ + if(remain<6){ + rb->memset(buf,COUNT_DOWN_MARK,remain); + buf[remain]=0; + if(bmp_width == NOT_INIT || prefs.load_bmp == BMP_SOUTH){ + LCD_CLEAR_AREA(SCREEN_MAIN,0,scroll_y0,LCD_WIDTH,prefs.fontheight); + rb->lcd_putsxy(get_center_pos(buf,0),scroll_y0,buf); + } + else{ + LCD_CLEAR_AREA(SCREEN_MAIN,bmp_width,scroll_y0,LCD_WIDTH,prefs.fontheight); + rb->lcd_putsxy(bmp_width,scroll_y0,buf); + } + rb->lcd_update_rect(0,scroll_y0,LCD_WIDTH,prefs.fontheight); + } + } + } + /* update snc */ + while(sncs[next_snc].time_in_ms <= elapsed || + sncs[current_snc].time_in_ms > elapsed){ /* -> ; <- */ + current_snc=next_snc%num_snc; + next_snc=current_snc; + while(sncs[current_snc].time_in_ms==sncs[++next_snc].time_in_ms); /* update next snc */ + if(show_snc==current_snc) + sncs[num_snc].time_in_ms=NOT_INIT; /* ensure snc[0] < elapsed time < snc[max] */ + } + if(show_snc!=current_snc){ + if(auto_scroll) scroll_snc(); + else if((current_snc+1)>=current_snc_edit && + current_snc=stop_snc))) + ff_rew_snc(start_snc); +} + +static int insert_sections(void){ + if(start_snc+1==num_snc || current_snc_edit+1==num_snc || + (start_snc current_snc_edit)) + return false; + if(start_snc==NOT_INIT) start_snc=0; /* insert blank */ + int offset=sncs[current_snc_edit].time_in_ms-sncs[start_snc].time_in_ms + 1; + int nr2Copy=(stop_snc==NOT_INIT)?1:stop_snc-start_snc; + num_snc+=nr2Copy; + if(num_snc>=MAX_SECTIONS){ + rb->splash(HZ,"Not Enough Sections!"); + num_snc=MAX_SECTIONS-1; + } + int i=num_snc; + for(;i>current_snc_edit+nr2Copy;i--)/* move */ + snc_copy(&sncs[i],&sncs[i-nr2Copy],0); + if(start_snc>current_snc_edit) start_snc+=nr2Copy; + + for(i=0;inum_snc-1) current_snc_edit=num_snc-1; + end_snc_edit=MAX_SECTIONS; + current_snc = 0; /* to ensure current_snc is always in range */ + return RET_OK; +} + +static int join_sections(void){ + if(current_snc_edit>=num_snc-2 || current_snc_edit==0 || prefs.load_translation==TR_OK) + return false; + /* need to save reference pointer because current_snc_edit is not fixed */ + unsigned char* join_ref1=sncs[current_snc_edit].x[SCROLL].lyrics[0]; + unsigned char* join_ref2=sncs[current_snc_edit+1].x[SCROLL].lyrics[0]; + unsigned char* lyrics; + unsigned long time=sncs[current_snc_edit].time_in_ms; /* save time for restoring current_snc_edit */ + int i; + for(i=num_snc-3;i>0;i--){ /* look for other lines with the same constellation */ + if(lrccmp(join_ref1,sncs[i].x[SCROLL].lyrics[0])==0 + && lrccmp(join_ref2,sncs[i+1].x[SCROLL].lyrics[0])==0){ + + lyrics=sncs[i+1].x[SCROLL].lyrics[0]; + if(*(lyrics-1)==0){ + rb->strcpy(buf,sncs[i+1].x[SCROLL].lyrics[0]); + *(lyrics-1)=' '; /* replace \0 with ' ' */ + while(*(lyrics-2)==' ') --lyrics; /* remove spaces */ + rb->strcpy(lyrics,buf); + } + store_line(&sncs[i],sncs[i].x[SCROLL].lyrics[0],true); /* reformat */ + clear_ab(); /* make sure there ab is not set */ + current_snc_edit=i; /* make sure current_snc_edit fulfills the deletion criteria */ + start_snc=i+1; /* point to merged sync */ + delete_sections(); /* and get rid of it (does other housekeeping as well) */ + } + } + for(i=1;iglobal_settings->repeat_mode!=RMODE_ONE; + set_repeat_mode(RMODE_ONE); + char* lyrics=sncs[current_snc_edit].x[SCROLL].lyrics[0]; + rb->strcpy(buf,lyrics); + bool changed; + if((changed=!rb->kbd_input(buf,BUFFERSIZE))) + { + unsigned length=rb->strlen(buf); + /* append the edited lyrics at the end of the lyrics buffer */ + /* lyrics buffer: line1_line2_..lineN_new */ + struct SNCSection* snc=&sncs[current_snc_edit]; + SET_LYRICS(snc,&lyrics_buffer[lyrics_buffer_used],0); + lyrics=snc->x[SCROLL].lyrics[0]; + rb->strcpy(lyrics,buf); + store_line(snc,lyrics,true); /* format */ + /* check if there's enough space left? */ + lyrics_buffer_used+=length+1; /* +1: \0 */ + modified|=MODIFIED_LYRICS; + } + if(restore) set_repeat_mode(-RMODE_ONE); + update(); + return changed; +} + +static int edit_and_dup_lines(void){ + size_t size; + /* backup old line */ + unsigned char* old_lyrics=rb->plugin_get_buffer(&size); + rb->strcpy(old_lyrics,sncs[current_snc_edit].x[SCROLL].lyrics[0]); + if(edit_line()){ + int i=1; + for(;ilcd_clear_display(); + rb->lcd_puts(0,1,title); + do{ + if(old_val!=val){ + if(callback == NULL){/* output val as time */ + old_val=val<0?-val:val; + rb->snprintf(buf,BUFFERSIZE,"%c" TIME_FORMAT ".%.1d", + val<0?'-':'+', CALC_MM(old_val),CALC_SS(old_val),CALC_MS(old_val)); + rb->lcd_puts(0,2,buf); + } + else callback(&val); + rb->lcd_update(); + old_val=val; + } + button = rb->get_action(CONTEXT_SNC,HZ); + switch(button){ + case SNC_NEXT: val+=step/5; break; + case SNC_PREV: val-=step/5; break; + case SNC_SELECT_OR_MODE: + *init_val=val; + case SNC_QUIT: + loop=false; + break; + case SNC_INC: val += step; break; + case SNC_DEC: val -= step; break; + case SNC_AB: val = 0; break; + default:; + } + if(val exit */ + return 0; + } + rb->yield(); + } while(loop); + return button; +} +static int set_time_offset(void) +{ + if(NO_LYRICS()) return false; + int offset=time_offset; /* save old value */ + if(set_val_screen("Time Offset:",500, + &time_offset,-id3->length, NULL)==SNC_QUIT){ /* cancelled */ + return RET_OK; + } + offset=time_offset-offset; /* calculate change */ + if(offset!=0){ + int start=1, stop=num_snc-1; + if(start_snc!=NOT_INIT){ + start=start_snc; + if(stop_snc!=NOT_INIT) stop=stop_snc; + } + int i=start; + for(;i id3->length){ + sncs[i].time_in_ms = NOT_INIT; + } + } + } + modified=(time_offset!=0)? + modified|MODIFIED_OFFSET_TIMETAG:modified&~MODIFIED_OFFSET_TIMETAG; + return RET_OK; +} + +static int set_auto_cue(void){ + long old = prefs.cue_interval; + set_val_screen("Cue Interval:",10000, &prefs.cue_interval, 0, NULL); /* 10 s */ + if(old == prefs.cue_interval) + return RET_NOK; + auto_cue(); + if(old == 0 || prefs.cue_interval == 0) { + load_bmp(1); /* reload AA */ + } + prefs.save=PREF_SAVE; + return RET_OK; +} +/****************************************************************/ +/* dictionary definitions only */ +#define DICT_EXT "dic" +#define MAX_INDEX 200 +#define MAX_WORDS 30 +#define MAX_DICTS 10 +#define KEY_LENGTH 3 +#define KB_BUF_LEN 20 +#define LF 10 +struct dict_index_struct{ + unsigned long pos; + char key[KEY_LENGTH]; +}; + +static unsigned long get_dict_pos(const struct dict_index_struct* dict_index, + const char* word){ + int i=1; + int sign=NOT_INIT,res=0; + int len=rb->strlen(word); + if(len>KEY_LENGTH) len=KEY_LENGTH; + for(;istrncasecmp(word,dict_index[i].key,len)) == 0) + break; + + if(sign != NOT_INIT){ + if(sign != (res >= 0)) /* sign has changed! */ + break; + } + else + sign = res >= 0; /* sign = 1 if res positiv */ + + } + if(--i < 0) /* return previous index */ + i = 0; + return dict_index[i].pos; +} + +static int create_dict_index(struct dict_index_struct* dict_index, + const char* dict, const char* idx){ + int fd=rb->open(dict, O_RDONLY); + if(fd<0) return RET_NOK; + if(rb->lcd_get_backdrop!=NULL){ + rb->lcd_set_backdrop(NULL); + rb->lcd_update(); + } + rb->splash(HZ,"Creating Index..."); + ssize_t section_size=rb->filesize(fd)/MAX_INDEX; + size_t size; + char* buffer=rb->plugin_get_buffer(&size); + char* old_key = "???"; + ssize_t len; + ssize_t plen; + int i=0; + unsigned long offset=0; + char* pos; + char* end; + while((len=rb->read(fd,buffer,size))>0){ + pos=buffer; + pos[len-1]=LF; + end = buffer + len; + if(i!=0){ + while(pos < end && *pos++!=LF); + old_key = pos; /* to prevent the key not being the 1st entry */ + } + while((plen=pos-buffer)strncasecmp(old_key,pos,KEY_LENGTH)!=0){ + rb->strlcpy(dict_index[i].key,pos,KEY_LENGTH); + dict_index[i++].pos=offset+plen; + if(pos + section_size >= end) + break; + pos+=section_size; /* move to next section */ + while(pos < end && *pos++!=LF); /* ensure the next read pos will start at ^*/ + } + old_key=pos; + while(pos < end && *pos++!=LF); /* next line */ + } + offset+=size; + } + rb->close(fd); + dict_index[i].pos=0; /* terminate */ + fd = rb->creat(idx); + rb->write(fd, dict_index, sizeof(struct dict_index_struct)*i); + rb->close(fd); + return RET_OK; +} + +static int open_dict(struct dict_index_struct* dict_index){ + /* open default dictionary if the language is not set */ + const char* lang = language[0]==0 ? APP_NAME : language; + char idx[MAX_PATH]; + char dict[MAX_PATH]; + rb->snprintf(idx,MAX_PATH,SNCLRC_DATA_DIR"/%s.idx",lang); + rb->snprintf(dict,MAX_PATH,SNCLRC_DATA_DIR"/%s."DICT_EXT,lang); + + int fd=rb->open(idx, O_RDONLY); + if(fd >= 0){ + rb->read(fd,dict_index,sizeof(struct dict_index_struct)*MAX_INDEX); + rb->close(fd); + } + else if(create_dict_index(dict_index,dict,idx)!=RET_OK){ + rb->splashf(HZ, "Cannot Open %s!", dict); + return RET_NOK; + } + /* open dictionary */ + return rb->open(dict, O_RDONLY); +} + +static int change_dict(int* fd, struct dict_index_struct* dict_index){ + /* list dictionaries */ + DIR* dir = rb->opendir(SNCLRC_DATA_DIR); + if (!dir) { + rb->splashf(HZ,"Cannot open directory: '%s'!",SNCLRC_DATA_DIR); + return RET_NOK; + } + + char* dict_items[MAX_DICTS]; + size_t size; + dict_items[0]=rb->plugin_get_buffer(&size); + struct dirent *entry; + unsigned char* ext; + int i=0; + while((entry=rb->readdir(dir))!=0 && istrrchr(entry->d_name,'.'); + if(ext == NULL) continue; /* directory */ + ext++; + if(rb->strncasecmp(ext,DICT_EXT,3)==0){ /* check if it's a dictionary */ + rb->strcpy(dict_items[i], entry->d_name); + dict_items[i+1]=dict_items[i]+rb->strlen(entry->d_name)+1; + i++; + } + } + rb->closedir(dir); + + if(i==0) return RET_NOK; + struct menu_callback_with_desc entry_desc = {NULL,"Dictionaries", Icon_Language}; + struct menu_item_ex entry_list = { + MT_RETURN_ID|MENU_HAS_DESC|MENU_ITEM_COUNT(i), + {.strings = (const char **) dict_items}, + {.callback_and_desc = &entry_desc} + }; + i=rb->do_menu(&entry_list, NULL, NULL, false); + if(i>=0){ + language[0] = 0; + if(rb->strlen(dict_items[i])-rb->strlen(DICT_EXT)-1 == 2){ + language[0] = dict_items[i][0]; + language[1] = dict_items[i][1]; + language[2] = 0; + } + if(*fd>0) rb->close(*fd); + *fd = open_dict(dict_index); + modified = 1; + } + return *fd; +} + +static int lookup_word(void){ + struct dict_index_struct dict_index[MAX_INDEX]; + + /* parse line (build menu) */ + struct Dict_Entry{ + const char* word; + int length; + } word_list[MAX_WORDS]; /* wordlist: items[0] items[1] ... */ + char* items[MAX_WORDS]; + + const char* new_pos; + const char* pos= current_snc_edit > 0 ? + sncs[current_snc_edit].x[SCROLL].lyrics[0] : g_tit.lyrics[0]; + #define I1ST 2 + int byte,i=I1ST; + + items[0]=""; + items[1]=""; + items[I1ST]=buf; + while(*pos != 0 && istrlcpy(items[i],pos,word_list[i].length+1); /* copy word into item list */ + items[i+1]=items[i]+word_list[i].length+1; /* next item */ + pos=new_pos; + i++; + } + if(i==I1ST) return RET_NOK; + + int cmp_len; + const char* search_word; + char* entries[MAX_WORDS]; + char* str; + size_t size; + ssize_t len, len_bak; + char* const plugin_buffer = rb->plugin_get_buffer(&size); + char kb_buffer[KB_BUF_LEN]; + kb_buffer[0]=0; + int fd=-1; + ssize_t read_next_block(void){ + len=rb->read(fd,plugin_buffer, + dict_index[2].pos < size ? dict_index[2].pos : size); /* read two sections */ + len_bak = len; + plugin_buffer[len-1]=0; /* ensure termination */ + return len; + } + + int list_callback(int action, struct gui_synclist *lists){ + if(action != ACTION_STD_OK + && action != ACTION_STD_CONTEXT + && action != ACTION_STD_MENU) + return action; + int selected = rb->gui_synclist_get_sel_pos(lists); + if(action == ACTION_STD_CONTEXT){ + rb->strcpy(kb_buffer,items[selected]);/* put selected word into keybord buffer */ + selected = 1; /* call keyboard input */ + } + + switch(selected){ + case 0: + change_dict(&fd, dict_index); + return ACTION_REDRAW; + case 1: + { + if(rb->kbd_input(kb_buffer,MAX_PATH) || kb_buffer[0]==0) + return ACTION_REDRAW; /* cancel input: return to menu */ + search_word = kb_buffer; + cmp_len = rb->strlen(search_word); + break; + } + default: + { + search_word = word_list[selected].word; + cmp_len = word_list[selected].length; + if(action == ACTION_STD_MENU && selected+1 < lists->nb_items){ + cmp_len += word_list[selected+1].length; + if(*(word_list[selected+1].word-1)==' ') + cmp_len++; /* there's a space between the words */ + } + } + } + if(fd<0) fd = open_dict(dict_index); + if(fd<0) return RET_NOK; + rb->splash(HZ, "Searching..."); + rb->lseek(fd,get_dict_pos(dict_index,search_word),SEEK_SET); + read_next_block(); /* first block */ + + SEEKING_WORD: + entries[0] = str = plugin_buffer; + + NEXT_PAGE: + i=0; + do{ + while(*str!=LF && *str!=0) str++; /* set str to end of line */ + *str++=0; + len-=(str-entries[i]); + if(rb->strncasecmp(search_word, entries[i], cmp_len)==0){ + /* found */ + i++; + if(len<=0) read_next_block(); + } + else if(i!=0) break; /* stop if it doesn't match */ + }while(len>0 && + i+1 */ + *(entries[i]=str)!=0); + if(i>0){ /* show found entries */ + if(i+1==MAX_WORDS) entries[i++] = ""; + struct menu_callback_with_desc entry_desc = + {NULL,(char*) search_word, Icon_Questionmark}; + struct menu_item_ex entry_list = { + MT_RETURN_ID|MENU_HAS_DESC|MENU_ITEM_COUNT(i), + {.strings = (const char **) entries}, + {.callback_and_desc = &entry_desc} + }; + int sel = rb->do_menu(&entry_list, NULL, NULL, false); + if(sel >= 0){ + if(sel + 1 == MAX_WORDS){ + entries[0]=str; /* next entries */ + goto NEXT_PAGE; + } + else + rb->strcpy(kb_buffer,entries[sel]); + } + } + else { + if(cmp_len>KEY_LENGTH){ + /* no exact match found, try with parts of the word */ + cmp_len >>= 1; + if(cmp_len0){ + /* maybe the search word is in the next block? */ + goto SEEKING_WORD; + } + else + rb->splashf(HZ, "Word '%s' not found!", + selected>=I1ST ? items[selected] : kb_buffer); + } + return ACTION_REDRAW; + } + do_list(language, i, items, list_callback, I1ST); + rb->close(fd); + return RET_OK; +} + +#ifdef CUSTOM_PLUGIN_PATCH +/* execute a function with possible track change */ +static void exec_func_tc(bool (*func)(void)){ + unsigned long elapsed = id3->elapsed; + func(); + if(elapsed > rb->audio_current_track()->elapsed) + reset(true); + else{ + g_next_at.rows=0; /* force reread next title */ + update(); + } +} + +static int show_playlist(void){ + exec_func_tc(rb->playlist_viewer); + return RET_OK; +} + +static int browse_dir(void){ + bool browse_dir_(void){ + return rb->rockbox_browse(id3->path,SHOW_MUSIC); + } + exec_func_tc(browse_dir_); + return RET_OK; +} + +static int show_id3(void){ + rb->browse_id3(); + return RET_OK; +} +#endif + +static int delete_lyrics_file(void){ + if(NO_LYRICS() || !rb->file_exists(lyrics_filename)) return RET_NOK; + char* ext=rb->strrchr(lyrics_filename,'/')+1; + rb->snprintf(buf,BUFFERSIZE,"Delete '%s'?",ext); + if(yes_no(buf)){ + rb->remove(lyrics_filename); /* remove lyrics file */ + if(prefs.load_translation==TR_OK){ + ext=rb->strrchr(lyrics_filename,'.')+1; + rb->strcpy(ext,FORMATS[EXT_TR]); + rb->remove(lyrics_filename); /* also remove translation file */ + } + reset(true); /* reset to ensure all is done */ + } + return RET_OK; +} + +/* + goto next gap: + a gap is >= 8s between 2 sncs or a blank snc +*/ +static int goto_next_gap(void){ + if(NO_LYRICS()) return RET_NOK; + int current = auto_scroll ? current_snc:current_snc_edit; + int id = (current+1)%num_snc; + while( current != id && /* one cycle */ + !(IS_BLANK(id)) && /* not blank */ + sncs[id].time_in_ms + 8000 > sncs[(id=(id+1)%num_snc)].time_in_ms); /* not a gap */ + + if(current != id && /* only goto next gap if the song has one */ + id + 3 < num_snc){ /* and the gap is not at the end (problems with ff) */ + if(auto_scroll) + ff_rew_snc(id); + else{ + current_snc_edit = id; + browse_snc(); + } + } + return RET_OK; +} + +/* + clean_blanks: + delete the blank snc, if the blank length is shorter than the set time +*/ +static int clean_blanks(void){ + if(NO_LYRICS()) return RET_NOK; + int id = 1; + int num_blanks=0; + long delta = 1000; + if(set_val_screen("Min Blank Length:",500,&delta,500, NULL)==SNC_QUIT) + return RET_NOK; /* cancelled */ + while(id < num_snc){ + if(IS_BLANK(id) + && (sncs[id].time_in_ms == MAX_ULONG + || (sncs[id].time_in_ms + delta > sncs[id+1].time_in_ms))){ + if(sncs[id].time_in_ms != MAX_ULONG){ + /* copy time from blank snc and delete the blank */ + sncs[id+1].time_in_ms = sncs[id].time_in_ms + 1; + } + current_snc_edit = id; + start_snc = id; + stop_snc = id + 1; + delete_sections(); + num_blanks++; + } + id++; + } + /* report */ + if(num_blanks) + rb->splashf(HZ,"Number of Blanks Deleted: %d",num_blanks); + return RET_OK; +} + +/* + set_scroll_limit: + configure the vertical scroll limit +*/ +static int set_scroll_limit(void){ + if(NO_LYRICS()) return RET_NOK; /* only possible with lyrics */ + int button; + bool save = false; + int old_val = prefs.scroll_limit_y; + auto_scroll = true; + update_display(); + do{ + button=rb->get_action(CONTEXT_SNC,HZ>>1); + switch (button) { + case SNC_SELECT_OR_MODE: + save = true; + button = SNC_QUIT; + break; + case SNC_LIST_DOWN: + if(prefs.scroll_limit_y + prefs.fontheight < LCD_HEIGHT - prefs.fontheight) + prefs.scroll_limit_y += prefs.fontheight; + break; + case SNC_LIST_UP: + prefs.scroll_limit_y -= prefs.fontheight; + if(prefs.scroll_limit_y < scroll_y0) + prefs.scroll_limit_y = scroll_y0; + break; + } + update(); + (button==ACTION_NONE) ? + update_time_display(AUDIO_ELAPSED(),false): + update_display(); + /* show that we are in configuration mode */ + rb->lcd_set_drawmode(DRMODE_COMPLEMENT); + rb->lcd_fillrect(SCROLL_MARKER_X,prefs.scroll_limit_y, + SCROLL_MARKER_WIDTH,prefs.fontheight); + rb->lcd_set_drawmode(DRMODE_SOLID); + rb->lcd_update(); + } while (button!=SNC_QUIT); + + if(save) + prefs.save=PREF_SAVE; + else + prefs.scroll_limit_y = old_val; + return RET_OK; +} + +static int switch_peakmeter(void){ + prefs.save^=PREF_PEAKMETER; + prefs.peakmeter=!prefs.peakmeter; + load_bmp(1); + return RET_OK; +} + +#ifdef HAVE_LCD_INVERT +static int invert_colors(void){ + prefs.save^=PREF_INVERT_DISPLAY; + prefs.invert_display=!prefs.invert_display; + rb->lcd_set_invert_display(prefs.invert_display); + load_bmp(1); + return RET_OK; +} +#endif + +static int set_album_art_size(void){ + const int max_aa_size = MIN(LCD_HEIGHT,LCD_WIDTH)>>1; + void preview_aa_size(long* size){ + if(*size > max_aa_size) + *size = max_aa_size; /* limit max aa size */ + + LCD_CLEAR_AREA(SCREEN_MAIN,0,scroll_y0, max_aa_size, max_aa_size); + rb->snprintf(buf,BUFFERSIZE,"%d",(int) *size); + rb->lcd_puts(0,2,buf); + rb->lcd_drawrect(0,scroll_y0,*size,*size); + } + if(set_val_screen("Album Art Size:", 1, (long*) &prefs.sxs_bmp, 30, + preview_aa_size) != SNC_QUIT){ + prefs.save=PREF_SAVE; + reset(false); + } + return RET_OK; +} + +static int switch_album_art(void){ + /* NO_AA, BMP_WEST, BMP_SOUTH, BMP_CENTER */ + MENUITEM_STRINGLIST(aa_menu,"Album Art",NULL, + "- Lyrics || AA","< AA next to Lyrics","v AA below Lyrics","+ AA || Lyrics", + "Size..." + ); + int selected = prefs.load_bmp; + if(rb->do_menu(&aa_menu,&selected,NULL,false)<0) + return RET_OK; /* cancelled */ + if(selected > BMP_CENTER) + return set_album_art_size(); + prefs.save=PREF_SAVE; + prefs.load_bmp=selected; + if(!prefs.load_bmp) + bmp_width=bmp_height=NOT_INIT; + reset(false); + return RET_OK; +} + +static int set_scrolling(void){ + #define LINE_SCROLL_DELAY "Line-Scroll-Start-Delay" + #define LINE_SCROLL_SPEED "Line-Scroll-Delay" + MENUITEM_STRINGLIST(scrolling_menu,"Scrolling",NULL, + "Scroll-limit", LINE_SCROLL_DELAY, LINE_SCROLL_SPEED); + int selected = 0; + if(rb->do_menu(&scrolling_menu,&selected,NULL,false)<0) + return RET_OK; /* cancelled */ + if(selected == 0) + return set_scroll_limit(); + + void set_val(long* val){ + rb->snprintf(buf,BUFFERSIZE,"%d",(int) *val); + rb->lcd_puts(0,2,buf); + } + char* title = LINE_SCROLL_DELAY; + int* item = &prefs.vscroll_delay; + if(selected == 2){ + title = LINE_SCROLL_SPEED; + item = &prefs.vscroll_speed; + } + if(set_val_screen(title, 1,(long*)item, 0, set_val) != SNC_QUIT){ + prefs.save=PREF_SAVE; + } + return RET_OK; +} + +static int switch_repeat1(void){ + set_repeat_mode(rb->global_settings->repeat_mode!=RMODE_ONE ? + RMODE_ONE:-RMODE_ONE); + return RET_OK; +} + +static int load_translation(void){ + confirm_saving(); + prefs.save^=PREF_TRANSLATION; + prefs.load_translation=!prefs.load_translation; + unsigned long a=MAX_ULONG, b=MAX_ULONG, c=MAX_ULONG; + if(start_snc!=NOT_INIT) + a = sncs[start_snc].time_in_ms; + if(stop_snc!=NOT_INIT) + b = sncs[stop_snc].time_in_ms; + c = sncs[current_snc_edit].time_in_ms; + if(prefs.load_translation){ + load_file(EXT_TR); + end_snc_edit=MAX_SECTIONS; + update_display(); + } + else reset(false); + + start_snc = get_snc_index(a); + stop_snc = get_snc_index(b); + current_snc_edit = get_snc_index(c); + return RET_OK; +} + +static int set_preview_song_mode(void){ + auto_scroll = false; + prefs.cue_interval = 20000; /* 20 s */ + auto_cue(); + delete_list.auto_insert = true; + add2delete_list(false); + return RET_OK; +} +/* Make menu items */ +#define SNC_MENU_ITEM(item,name,func,icon)\ + MENUITEM_FUNCTION(item,MENU_FUNC_CHECK_RETVAL,name, \ + func,NULL,NULL,icon) + +/* menus in edit mode */ +SNC_MENU_ITEM(delete_item,"Delete (A-B/current)",delete_sections, Icon_NOICON); +SNC_MENU_ITEM(insert_item,"Insert (A-B/blank)", insert_sections, Icon_NOICON); +SNC_MENU_ITEM(join_item,"Join Sections", join_sections, Icon_NOICON); +SNC_MENU_ITEM(edit_all_item,"Edit+", edit_and_dup_lines, Icon_NOICON); +SNC_MENU_ITEM(edit_item,"Edit", edit_line, Icon_NOICON); +SNC_MENU_ITEM(dict_item,"Dictionary", lookup_word, Icon_Language); + +/* general menu items */ +SNC_MENU_ITEM(time_offset_item,"Time Offset...", set_time_offset,Icon_NOICON); +SNC_MENU_ITEM(repeat1_item,"Repeat 1", switch_repeat1, Icon_Audio); +SNC_MENU_ITEM(translation_item,"Translation", load_translation, Icon_Language); +SNC_MENU_ITEM(delete_file_item,"Delete File...", delete_lyrics_file, Icon_NOICON); +SNC_MENU_ITEM(clean_blanks_item,"Clean Blanks...", clean_blanks, Icon_NOICON); +SNC_MENU_ITEM(next_gap_item,"Next Gap", goto_next_gap, Icon_Audio); +SNC_MENU_ITEM(auto_cue_item,"Auto Cue...", set_auto_cue, Icon_NOICON); +SNC_MENU_ITEM(delete_list_item,"Delete-List...", delete_list_menu, Icon_file_view_menu); +SNC_MENU_ITEM(preview_mode_item,"Song-Preview", set_preview_song_mode, Icon_Config); + +/* scroll menu items */ +SNC_MENU_ITEM(aa_item,"Album Art...", switch_album_art, Icon_Config); +SNC_MENU_ITEM(peakmeter_item,"Peakmeter", switch_peakmeter, Icon_Config); +SNC_MENU_ITEM(scrolling_item,"Scrolling...", set_scrolling, Icon_Cursor); +#ifdef HAVE_LCD_INVERT + SNC_MENU_ITEM(invert_colors_item,"Invert Colors", invert_colors, Icon_Config); + #define INVERT_COLORS_ITEM &invert_colors_item, +#else + #define INVERT_COLORS_ITEM +#endif + +#define GENERAL_MENU_ITEMS \ + &translation_item, &auto_cue_item, &delete_list_item,\ + &time_offset_item, &repeat1_item,\ + &delete_file_item, &clean_blanks_item, &next_gap_item, &preview_mode_item + +#define SCROLL_MENU_ITEMS \ + &peakmeter_item, &scrolling_item, INVERT_COLORS_ITEM &aa_item + +#define EDIT_MENU_ITEMS \ + &dict_item, &delete_item, &insert_item, &join_item, &edit_all_item, &edit_item + +#ifdef CUSTOM_PLUGIN_PATCH + SNC_MENU_ITEM(id3_item,"ID3 Info", show_id3, Icon_Questionmark); + SNC_MENU_ITEM(playlist_item,"Playlist", show_playlist, Icon_Playlist); + SNC_MENU_ITEM(browse_dir_item,"Files", browse_dir, Icon_Folder); + + #define CUSTOM_MENU_ITEMS \ + &id3_item, &playlist_item, &browse_dir_item +#else + #define CUSTOM_MENU_ITEMS +#endif + +//MAKE_MENU +MAKE_MENU(scroll_menu, "", NULL, Icon_Audio, + GENERAL_MENU_ITEMS, SCROLL_MENU_ITEMS, CUSTOM_MENU_ITEMS +); + +MAKE_MENU(edit_menu, "EDIT", NULL, Icon_NOICON, + EDIT_MENU_ITEMS, GENERAL_MENU_ITEMS , CUSTOM_MENU_ITEMS +); + +static void show_menu(void) +{ + static int edit_selected=0; + static int scroll_selected=0; + auto_scroll ? + rb_menu(&scroll_menu,&scroll_selected): + rb_menu(&edit_menu,&edit_selected); + update_display(); +} + +static void set_time_tag(void) +{ + if(auto_scroll || current_snc_edit==0 || current_snc_edit+1==num_snc) + return; + unsigned long t = AUDIO_ELAPSED()-SET_TIME_LATENCY; /* reaction time */ + const int delta = prefs.load_translation!=TR_OK ? 1:2; + if(prefs.load_translation != TR_OK){ + sncs[current_snc_edit].time_in_ms=t; + } + else{/* synchronze all sncs with the same time */ + unsigned long ref = sncs[current_snc_edit].time_in_ms; + int i=0; + for(;iopen(SETTINGS_FILE, O_RDONLY); + if (settings_fd >= 0) + { + if((unsigned) rb->filesize(settings_fd)<=sizeof(struct Preferences)) + rb->read(settings_fd, &prefs, sizeof(struct Preferences)); + rb->close(settings_fd); + } + if(prefs.version!=SNC_CONFIG_VERSION){ + /* init default settings */ + prefs.version = SNC_CONFIG_VERSION; + prefs.load_bmp=0; + prefs.sxs_bmp=MIN(LCD_HEIGHT,LCD_WIDTH)>>1; + prefs.backlight=true; + prefs.save=PREF_SAVE; + prefs.load_translation=TR_OFF; + prefs.peakmeter=true; + prefs.invert_display=false; + //prefs.fontheight = 0; + prefs.cue_interval = 0; + } + if(prefs.backlight) force_backlight_on(); +#ifdef HAVE_LCD_INVERT + rb->lcd_set_invert_display(prefs.invert_display); +#endif + + rb->lcd_setfont(FONT_SYSFIXED); + rb->lcd_getstringsize("0", &indentwidth, &sysfont_height); + rb->lcd_setfont(FONT_UI); + /* init indentwidth, fontheight */ + int fontheight; + rb->lcd_getstringsize("0:00>", &indentwidth, &fontheight); + /* statusrows at top and bottom -> 2*sysfont_height */ + lcd_max_rows=(LCD_HEIGHT-2*sysfont_height)/fontheight; + bmp_width=NOT_INIT; + bmp_height=NOT_INIT; + scroll_y0=TIME_BAR_ROW*fontheight+(sysfont_height<<1)+TIMEBAR_LYRICS_SPACE; + if(fontheight != prefs.fontheight) { /* fontheight was changed! */ + prefs.fontheight = fontheight; + prefs.scroll_limit_y = scroll_y0; /* scroll_limit_y is dependent on fontheight */ + prefs.save = PREF_SAVE; /* save the new fontheight */ + } + auto_scroll = true; + delete_list.num = 0; + reset(true); +} + +static int handle_button_scroll(int button){ + switch (button) { + case SNC_TIMESTAMP_BL: + prefs.save^=PREF_BACKLIGHT; + prefs.backlight=!prefs.backlight; + prefs.backlight? force_backlight_on():default_backlight(); + break; + case SNC_DEC: + set_volume(rb->global_settings->volume - 1); + break; + case SNC_INC: + set_volume(rb->global_settings->volume + 1); + break; + case SNC_SELECT_OR_MODE: + switch_mode(false); + break; + default:button=ACTION_NONE; + } + return button; +} +static int handle_button_edit(int button){ + static const int TIMEOUT = HZ>>2; + switch (button) { + case SNC_TIMESTAMP_BL: + set_time_tag(); + break; + case SNC_LIST_DOWN: + if(++current_snc_edit>=num_snc){ + current_snc_edit = 1; + while(rb->button_get_w_tmo(TIMEOUT)&BUTTON_REPEAT){ + current_snc_edit=num_snc-1; + rb->yield(); + } + } + browse_snc(); + break; + case SNC_LIST_UP: + if(--current_snc_edit<0){ + current_snc_edit = num_snc-2; + while(rb->button_get_w_tmo(TIMEOUT)&BUTTON_REPEAT){ + current_snc_edit=0; + rb->yield(); + } + } + browse_snc(); + break; + case SNC_SELECT_OR_MODE: + ff_rew_snc(current_snc_edit); + break; + default: + button=ACTION_NONE; + } + return button; +} + +/* this is the plugin entry point */ +enum plugin_status plugin_start(const void* parameter) +{ + if((rb->audio_status() & AUDIO_STATUS_PLAY) == false){ + rb->splash(HZ,"No Audio Playing!"); + return PLUGIN_OK; + } + /* if you don't use the parameter, you can do like + this to avoid the compiler warning about it */ + (void)parameter; +#ifdef HAVE_LCD_COLOR + unsigned bg_color = rb->global_settings->bg_color; + unsigned fg_color = rb->global_settings->fg_color; + rb->global_settings->bg_color = SNC_BG_COLOR; + rb->global_settings->fg_color = SNC_FG_COLOR; + rb->lcd_set_background(SNC_BG_COLOR); + rb->lcd_set_foreground(SNC_FG_COLOR); +#endif +#if LCD_DEPTH > 1 + struct bitmap backdrop; + size_t buffer_size; + backdrop.data = rb->plugin_get_buffer(&buffer_size); + const int max_backdrop_size = LCD_HEIGHT * LCD_WIDTH * sizeof(fb_data); + int size = 0; + if(buffer_size > (size_t) max_backdrop_size){ + /* move pointer to the end of the buffer */ + backdrop.data += buffer_size - max_backdrop_size; + size = rb->read_bmp_file(SNCLRC_DATA_DIR"/"APP_NAME"_bd.bmp", + &backdrop, max_backdrop_size, FORMAT_NATIVE, NULL); + } + rb->lcd_set_backdrop(size > 0 ? (fb_data*) backdrop.data : NULL); +#endif + init(); + int button; + long newPos=NOT_INIT; + int timeout = HZ>>1; + #define PLUGIN_STAY 333 + int status=PLUGIN_STAY; /* see enum plugin_status */ + + do{ + button=rb->get_action(CONTEXT_SNC,timeout); + update_status_busy(button); + switch (button) { /* valid for both modes */ +#ifdef CUSTOM_PATCH + case SNC_PLIST: + show_playlist(); + update_display(); + break; +#endif + case SNC_AB: + IS_AUTOCUE() || NO_LYRICS() ? add2delete_list(true): set_ab_marker(); + update_display(); + break; + case SNC_AB_MENU: + ab_menu(); + update_display(); + break; + case SNC_SAVE: + save2file(); + break; + case SNC_PLAY_PAUSE: + (rb->audio_status() & AUDIO_STATUS_PAUSE) ? + rb->audio_resume():rb->audio_pause(); + exec_func(1,update_status_display); + break; + case SNC_STOP_SEEK: + ff_rew(newPos); + newPos=NOT_INIT; + break; + case SNC_VISUAL_REW: + case SNC_VISUAL_FF: + if(newPos==NOT_INIT) + newPos=AUDIO_ELAPSED(); + newPos+=(button==SNC_VISUAL_FF) ? 2000:-2000; + newPos=update_time_display(newPos,true); + break; + case SNC_NEXT: + (num_snc > 1 && rb->button_get_w_tmo(DOUBLE_CLICK_TICKS)!=BUTTON_NONE) ? + ff_rew_snc(current_snc+(prefs.load_translation!=TR_OK ? 1:2)): + rb->audio_next(); + break; + case SNC_PREV: + rb->button_get_w_tmo(DOUBLE_CLICK_TICKS)!=BUTTON_NONE? + rb->audio_prev(): + auto_scroll ? + ff_rew_snc(start_snc!=NOT_INIT?start_snc:0) : switch_mode(true); + break; + case SNC_QUIT: + status=PLUGIN_OK; + break; + case SNC_OPEN_MENU: show_menu(); break; + default: + if(rb->default_event_handler(button)==SYS_USB_CONNECTED) + status=PLUGIN_USB_CONNECTED; + if((rb->audio_status() & AUDIO_STATUS_PLAY)==false) + status=PLUGIN_OK; + if((auto_scroll ? handle_button_scroll(button): + handle_button_edit(button))==ACTION_NONE) + update(); /* only update if no button was pressed */ + if(prefs.peakmeter && NO_LYRICS()) peak_meter(); + update_time_display(AUDIO_ELAPSED(),!auto_scroll); + } + } while(status==PLUGIN_STAY); + /* clean up before quit */ + confirm_saving(); + if(delete_list.num>0) delete_list_menu(); + default_backlight(); /* restore backlight */ + set_repeat_mode(RMODE_OFF); + if(prefs.save){ + prefs.save=0; + int settings_fd = rb->creat(SETTINGS_FILE); /* create the settings file */ + rb->write(settings_fd, &prefs, sizeof(struct Preferences)); + rb->close(settings_fd); + } +#ifdef HAVE_LCD_COLOR + /* restore color settings */ + rb->global_settings->bg_color = bg_color; + rb->global_settings->fg_color = fg_color; + rb->lcd_set_background(rb->global_settings->bg_color); + rb->lcd_set_foreground(rb->global_settings->fg_color); +#endif +#ifdef HAVE_LCD_INVERT + if(prefs.invert_display) + rb->lcd_set_invert_display(rb->global_settings->invert); +#endif + /* consumes all button presses (button_clear_queue() doesn't really work */ + while(rb->get_action(CONTEXT_SNC,timeout) != ACTION_NONE); + return status; +}