rockbox/apps/plugins/lrcplayer.c
Aidan MacDonald 5b27e2255a Add perceptual volume adjustment
The perceived loudness change of a change in volume depends
on the listening volume: at high volumes a 1 dB increment is
noticeable, but at low volumes a larger increment is needed
to get a comparable change in loudness.

Perceptual volume adjustment accounts for this fact, and
divides the hardware volume range into a number of steps.
Each step changes the dB volume by a variable amount, with
most of the steps concentrated at higher volumes. This
makes it possible to sweep over the entire hardware volume
range quickly, without losing the ability to finely adjust
the volume at normal listening levels.

Use "Volume Adjustment Mode" in the system settings menu
to select perceptual volume mode. The number of steps used
is controlled by "Number of Volume Steps". (Number of steps
has no effect in direct adjustment mode.)

It's still possible to set a specific dB volume level from
the sound settings menu when perceptual volume is enabled,
and perceptual volume does not affect the volume displayed
by themes.

Change-Id: I6f91fd3f7c5e2d323a914e47b5653033e92b4b3b
2023-01-22 21:19:57 +00:00

2838 lines
83 KiB
C

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