5ee13ebd39
Before time-based resume this was impossible since playback could not be started at a specified elapsed time, only seeked with playback already running. Right now the "FILE" field is used, if present, to do the lookup from from the .cue to the audio file when it is separate from the audio file. If no path is specified, the .cue and audio file must be in the same directory. When the cuesheet is embedded, the containing file is used and the FILE field is ignored. Supports starting playback and seeking to cue points from the cuesheet browser even without Cuesheet Support turned on. Change-Id: Ib5b534c406f179a7f8c7042a31572b24a62c0731 Reviewed-on: http://gerrit.rockbox.org/522 Reviewed-by: Michael Sevakis <jethead71@rockbox.org> Tested: Michael Sevakis <jethead71@rockbox.org>
521 lines
15 KiB
C
521 lines
15 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
|
|
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
|
|
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
|
|
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|
* \/ \/ \/ \/ \/
|
|
* $Id$
|
|
*
|
|
* Copyright (C) 2007 Nicolas Pennequin, Jonathan Gordon
|
|
*
|
|
* 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include "system.h"
|
|
#include "audio.h"
|
|
#include "kernel.h"
|
|
#include "logf.h"
|
|
#include "misc.h"
|
|
#include "screens.h"
|
|
#include "list.h"
|
|
#include "action.h"
|
|
#include "lang.h"
|
|
#include "debug.h"
|
|
#include "settings.h"
|
|
#include "plugin.h"
|
|
#include "playback.h"
|
|
#include "cuesheet.h"
|
|
#include "gui/wps.h"
|
|
|
|
#define CUE_DIR ROCKBOX_DIR "/cue"
|
|
|
|
static bool search_for_cuesheet(const char *path, struct cuesheet_file *cue_file)
|
|
{
|
|
size_t len;
|
|
char cuepath[MAX_PATH];
|
|
char *dot, *slash, *slash_cuepath;
|
|
|
|
cue_file->pos = 0;
|
|
cue_file->size = 0;
|
|
cue_file->path[0] = '\0';
|
|
slash = strrchr(path, '/');
|
|
if (!slash)
|
|
return false;
|
|
len = strlcpy(cuepath, path, MAX_PATH);
|
|
slash_cuepath = &cuepath[slash - path];
|
|
dot = strrchr(slash_cuepath, '.');
|
|
if (dot)
|
|
strlcpy(dot, ".cue", MAX_PATH - (dot-cuepath));
|
|
|
|
if (!dot || !file_exists(cuepath))
|
|
{
|
|
strcpy(cuepath, CUE_DIR);
|
|
if (strlcat(cuepath, slash, MAX_PATH) >= MAX_PATH)
|
|
goto skip; /* overflow */
|
|
char *dot = strrchr(cuepath, '.');
|
|
strcpy(dot, ".cue");
|
|
if (!file_exists(cuepath))
|
|
{
|
|
skip:
|
|
if ((len+4) >= MAX_PATH)
|
|
return false;
|
|
strlcpy(cuepath, path, MAX_PATH);
|
|
strlcat(cuepath, ".cue", MAX_PATH);
|
|
if (!file_exists(cuepath))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
strlcpy(cue_file->path, cuepath, MAX_PATH);
|
|
return true;
|
|
}
|
|
|
|
bool look_for_cuesheet_file(struct mp3entry *track_id3, struct cuesheet_file *cue_file)
|
|
{
|
|
/* DEBUGF("look for cue file\n"); */
|
|
if (track_id3->has_embedded_cuesheet)
|
|
{
|
|
cue_file->pos = track_id3->embedded_cuesheet.pos;
|
|
cue_file->size = track_id3->embedded_cuesheet.size;
|
|
cue_file->encoding = track_id3->embedded_cuesheet.encoding;
|
|
strlcpy(cue_file->path, track_id3->path, MAX_PATH);
|
|
return true;
|
|
}
|
|
|
|
return search_for_cuesheet(track_id3->path, cue_file);
|
|
}
|
|
|
|
static char *get_string(const char *line)
|
|
{
|
|
char *start, *end;
|
|
|
|
start = strchr(line, '"');
|
|
if (!start)
|
|
{
|
|
start = strchr(line, ' ');
|
|
|
|
if (!start)
|
|
return NULL;
|
|
}
|
|
|
|
end = strchr(++start, '"');
|
|
if (end)
|
|
*end = '\0';
|
|
|
|
return start;
|
|
}
|
|
|
|
/* parse cuesheet "cue_file" and store the information in "cue" */
|
|
bool parse_cuesheet(struct cuesheet_file *cue_file, struct cuesheet *cue)
|
|
{
|
|
char line[MAX_PATH];
|
|
char *s;
|
|
unsigned char char_enc = CHAR_ENC_ISO_8859_1;
|
|
bool is_embedded = false;
|
|
int line_len;
|
|
int bytes_left = 0;
|
|
int read_bytes = MAX_PATH;
|
|
unsigned char utf16_buf[MAX_PATH];
|
|
|
|
int fd = open(cue_file->path, O_RDONLY, 0644);
|
|
if(fd < 0)
|
|
return false;
|
|
if (cue_file->pos > 0)
|
|
{
|
|
is_embedded = true;
|
|
lseek(fd, cue_file->pos, SEEK_SET);
|
|
bytes_left = cue_file->size;
|
|
char_enc = cue_file->encoding;
|
|
}
|
|
|
|
/* Look for a Unicode BOM */
|
|
unsigned char bom_read = 0;
|
|
read(fd, line, BOM_UTF_8_SIZE);
|
|
if(!memcmp(line, BOM_UTF_8, BOM_UTF_8_SIZE))
|
|
{
|
|
char_enc = CHAR_ENC_UTF_8;
|
|
bom_read = BOM_UTF_8_SIZE;
|
|
}
|
|
else if(!memcmp(line, BOM_UTF_16_LE, BOM_UTF_16_SIZE))
|
|
{
|
|
char_enc = CHAR_ENC_UTF_16_LE;
|
|
bom_read = BOM_UTF_16_SIZE;
|
|
}
|
|
else if(!memcmp(line, BOM_UTF_16_BE, BOM_UTF_16_SIZE))
|
|
{
|
|
char_enc = CHAR_ENC_UTF_16_BE;
|
|
bom_read = BOM_UTF_16_SIZE;
|
|
}
|
|
if (bom_read < BOM_UTF_8_SIZE)
|
|
lseek(fd, cue_file->pos + bom_read, SEEK_SET);
|
|
if (is_embedded)
|
|
{
|
|
if (bom_read > 0)
|
|
bytes_left -= bom_read;
|
|
if (read_bytes > bytes_left)
|
|
read_bytes = bytes_left;
|
|
}
|
|
|
|
/* Initialization */
|
|
memset(cue, 0, sizeof(struct cuesheet));
|
|
strcpy(cue->path, cue_file->path);
|
|
cue->curr_track = cue->tracks;
|
|
|
|
if (is_embedded)
|
|
strcpy(cue->file, cue->path);
|
|
|
|
while ((line_len = read_line(fd, line, read_bytes)) > 0
|
|
&& cue->track_count < MAX_TRACKS )
|
|
{
|
|
if (char_enc == CHAR_ENC_UTF_16_LE)
|
|
{
|
|
s = utf16LEdecode(line, utf16_buf, line_len);
|
|
/* terminate the string at the newline */
|
|
*s = '\0';
|
|
strcpy(line, utf16_buf);
|
|
/* chomp the trailing 0 after the newline */
|
|
lseek(fd, 1, SEEK_CUR);
|
|
line_len++;
|
|
}
|
|
else if (char_enc == CHAR_ENC_UTF_16_BE)
|
|
{
|
|
s = utf16BEdecode(line, utf16_buf, line_len);
|
|
*s = '\0';
|
|
strcpy(line, utf16_buf);
|
|
}
|
|
s = skip_whitespace(line);
|
|
|
|
if (!strncmp(s, "TRACK", 5))
|
|
{
|
|
cue->track_count++;
|
|
}
|
|
else if (!strncmp(s, "INDEX 01", 8))
|
|
{
|
|
s = strchr(s,' ');
|
|
s = skip_whitespace(s);
|
|
s = strchr(s,' ');
|
|
s = skip_whitespace(s);
|
|
cue->tracks[cue->track_count-1].offset = 60*1000 * atoi(s);
|
|
s = strchr(s,':') + 1;
|
|
cue->tracks[cue->track_count-1].offset += 1000 * atoi(s);
|
|
s = strchr(s,':') + 1;
|
|
cue->tracks[cue->track_count-1].offset += 13 * atoi(s);
|
|
}
|
|
else if (!strncmp(s, "TITLE", 5)
|
|
|| !strncmp(s, "PERFORMER", 9)
|
|
|| !strncmp(s, "SONGWRITER", 10)
|
|
|| !strncmp(s, "FILE", 4))
|
|
{
|
|
char *dest = NULL;
|
|
char *string = get_string(s);
|
|
if (!string)
|
|
break;
|
|
|
|
size_t count = MAX_NAME*3 + 1;
|
|
size_t count8859 = MAX_NAME;
|
|
|
|
switch (*s)
|
|
{
|
|
case 'T': /* TITLE */
|
|
dest = (cue->track_count <= 0) ? cue->title :
|
|
cue->tracks[cue->track_count-1].title;
|
|
break;
|
|
|
|
case 'P': /* PERFORMER */
|
|
dest = (cue->track_count <= 0) ? cue->performer :
|
|
cue->tracks[cue->track_count-1].performer;
|
|
break;
|
|
|
|
case 'S': /* SONGWRITER */
|
|
dest = (cue->track_count <= 0) ? cue->songwriter :
|
|
cue->tracks[cue->track_count-1].songwriter;
|
|
break;
|
|
|
|
case 'F': /* FILE */
|
|
if (is_embedded || cue->track_count > 0)
|
|
break;
|
|
|
|
dest = cue->file;
|
|
count = MAX_PATH;
|
|
count8859 = MAX_PATH/3;
|
|
break;
|
|
}
|
|
|
|
if (dest)
|
|
{
|
|
if (char_enc == CHAR_ENC_ISO_8859_1)
|
|
{
|
|
dest = iso_decode(string, dest, -1,
|
|
MIN(strlen(string), count8859));
|
|
*dest = '\0';
|
|
}
|
|
else
|
|
{
|
|
strlcpy(dest, string, count);
|
|
}
|
|
}
|
|
}
|
|
if (is_embedded)
|
|
{
|
|
bytes_left -= line_len;
|
|
if (bytes_left <= 0)
|
|
break;
|
|
if (bytes_left < read_bytes)
|
|
read_bytes = bytes_left;
|
|
}
|
|
}
|
|
close(fd);
|
|
|
|
/* If just a filename, add path information from cuesheet path */
|
|
if (*cue->file && !strrchr(cue->file, '/'))
|
|
{
|
|
strcpy(line, cue->file);
|
|
strcpy(cue->file, cue->path);
|
|
char *slash = strrchr(cue->file, '/');
|
|
if (!slash++) slash = cue->file;
|
|
strlcpy(slash, line, MAX_PATH - (slash - cue->file));
|
|
}
|
|
|
|
/* If some songs don't have performer info, we copy the cuesheet performer */
|
|
int i;
|
|
for (i = 0; i < cue->track_count; i++)
|
|
{
|
|
if (*(cue->tracks[i].performer) == '\0')
|
|
strlcpy(cue->tracks[i].performer, cue->performer, MAX_NAME*3);
|
|
|
|
if (*(cue->tracks[i].songwriter) == '\0')
|
|
strlcpy(cue->tracks[i].songwriter, cue->songwriter, MAX_NAME*3);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* takes care of seeking to a track in a playlist
|
|
* returns false if audio isn't playing */
|
|
static bool seek(unsigned long pos)
|
|
{
|
|
if (!(audio_status() & AUDIO_STATUS_PLAY))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
#if (CONFIG_CODEC == SWCODEC)
|
|
audio_pre_ff_rewind();
|
|
audio_ff_rewind(pos);
|
|
#else
|
|
audio_pause();
|
|
audio_ff_rewind(pos);
|
|
audio_resume();
|
|
#endif
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* returns the index of the track currently being played
|
|
and updates the information about the current track. */
|
|
int cue_find_current_track(struct cuesheet *cue, unsigned long curpos)
|
|
{
|
|
int i=0;
|
|
while (i < cue->track_count-1 && cue->tracks[i+1].offset < curpos)
|
|
i++;
|
|
|
|
cue->curr_track_idx = i;
|
|
cue->curr_track = cue->tracks + i;
|
|
return i;
|
|
}
|
|
|
|
/* callback that gives list item titles for the cuesheet browser */
|
|
static const char* list_get_name_cb(int selected_item,
|
|
void *data,
|
|
char *buffer,
|
|
size_t buffer_len)
|
|
{
|
|
struct cuesheet *cue = (struct cuesheet *)data;
|
|
|
|
if (selected_item & 1)
|
|
strlcpy(buffer, cue->tracks[selected_item/2].title, buffer_len);
|
|
else
|
|
snprintf(buffer, buffer_len, "%02d. %s", selected_item/2+1,
|
|
cue->tracks[selected_item/2].performer);
|
|
|
|
return buffer;
|
|
}
|
|
|
|
void browse_cuesheet(struct cuesheet *cue)
|
|
{
|
|
struct gui_synclist lists;
|
|
int action;
|
|
bool done = false;
|
|
char title[MAX_PATH];
|
|
struct cuesheet_file cue_file;
|
|
struct mp3entry *id3 = audio_current_track();
|
|
|
|
snprintf(title, MAX_PATH, "%s: %s", cue->performer, cue->title);
|
|
gui_synclist_init(&lists, list_get_name_cb, cue, false, 2, NULL);
|
|
gui_synclist_set_nb_items(&lists, 2*cue->track_count);
|
|
gui_synclist_set_title(&lists, title, 0);
|
|
|
|
|
|
if (id3)
|
|
{
|
|
gui_synclist_select_item(&lists,
|
|
2*cue_find_current_track(cue, id3->elapsed));
|
|
}
|
|
|
|
while (!done)
|
|
{
|
|
gui_synclist_draw(&lists);
|
|
action = get_action(CONTEXT_LIST,TIMEOUT_BLOCK);
|
|
if (gui_synclist_do_button(&lists, &action, LIST_WRAP_UNLESS_HELD))
|
|
continue;
|
|
switch (action)
|
|
{
|
|
case ACTION_STD_OK:
|
|
{
|
|
bool startit = true;
|
|
unsigned long elapsed =
|
|
cue->tracks[gui_synclist_get_sel_pos(&lists)/2].offset;
|
|
|
|
id3 = audio_current_track();
|
|
if (id3 && *id3->path)
|
|
{
|
|
look_for_cuesheet_file(id3, &cue_file);
|
|
if (!strcmp(cue->path, cue_file.path))
|
|
startit = false;
|
|
}
|
|
|
|
if (!startit)
|
|
startit = !seek(elapsed);
|
|
|
|
if (!startit || !*cue->file)
|
|
break;
|
|
|
|
/* check that this cue is the same one that would be found by
|
|
a search from playback */
|
|
char file[MAX_PATH];
|
|
strlcpy(file, cue->file, MAX_PATH);
|
|
|
|
if (!strcmp(cue->path, file) || /* if embedded */
|
|
(search_for_cuesheet(file, &cue_file) &&
|
|
!strcmp(cue->path, cue_file.path)))
|
|
{
|
|
char *fname = strrsplt(file, '/');
|
|
char *dirname = fname <= file + 1 ? "/" : file;
|
|
bookmark_play(dirname, 0, elapsed, 0, current_tick, fname);
|
|
}
|
|
break;
|
|
} /* ACTION_STD_OK */
|
|
|
|
case ACTION_STD_CANCEL:
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool display_cuesheet_content(char* filename)
|
|
{
|
|
size_t bufsize = 0;
|
|
struct cuesheet_file cue_file;
|
|
struct cuesheet *cue = (struct cuesheet *)plugin_get_buffer(&bufsize);
|
|
if (!cue || bufsize < sizeof(struct cuesheet))
|
|
return false;
|
|
|
|
strlcpy(cue_file.path, filename, MAX_PATH);
|
|
cue_file.pos = 0;
|
|
cue_file.size = 0;
|
|
|
|
if (!parse_cuesheet(&cue_file, cue))
|
|
return false;
|
|
|
|
browse_cuesheet(cue);
|
|
return true;
|
|
}
|
|
|
|
/* skips backwards or forward in the current cuesheet
|
|
* the return value indicates whether we're still in a cusheet after skipping
|
|
* it also returns false if we weren't in a cuesheet.
|
|
* direction should be 1 or -1.
|
|
*/
|
|
bool curr_cuesheet_skip(struct cuesheet *cue, int direction, unsigned long curr_pos)
|
|
{
|
|
int track = cue_find_current_track(cue, curr_pos);
|
|
|
|
if (direction >= 0 && track == cue->track_count - 1)
|
|
{
|
|
/* we want to get out of the cuesheet */
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!(direction <= 0 && track == 0))
|
|
{
|
|
/* If skipping forward, skip to next cuesheet segment. If skipping
|
|
backward before DEFAULT_SKIP_TRESH milliseconds have elapsed, skip
|
|
to previous cuesheet segment. If skipping backward after
|
|
DEFAULT_SKIP_TRESH seconds have elapsed, skip to the start of the
|
|
current cuesheet segment */
|
|
if (direction == 1 ||
|
|
((curr_pos - cue->tracks[track].offset) < DEFAULT_SKIP_TRESH))
|
|
{
|
|
track += direction;
|
|
}
|
|
}
|
|
|
|
seek(cue->tracks[track].offset);
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
#ifdef HAVE_LCD_BITMAP
|
|
static inline void draw_veritcal_line_mark(struct screen * screen,
|
|
int x, int y, int h)
|
|
{
|
|
screen->set_drawmode(DRMODE_COMPLEMENT);
|
|
screen->vline(x, y, y+h-1);
|
|
}
|
|
|
|
/* draw the cuesheet markers for a track of length "tracklen",
|
|
between (x,y) and (x+w,y) */
|
|
void cue_draw_markers(struct screen *screen, struct cuesheet *cue,
|
|
unsigned long tracklen,
|
|
int x, int y, int w, int h)
|
|
{
|
|
int i,xi;
|
|
unsigned long tracklen_seconds = tracklen/1000; /* duration in seconds */
|
|
|
|
for (i=1; i < cue->track_count; i++)
|
|
{
|
|
/* Convert seconds prior to multiplication to avoid overflow. */
|
|
xi = x + (w * (cue->tracks[i].offset/1000)) / tracklen_seconds;
|
|
draw_veritcal_line_mark(screen, xi, y, h);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool cuesheet_subtrack_changed(struct mp3entry *id3)
|
|
{
|
|
struct cuesheet *cue = id3->cuesheet;
|
|
if (cue && (id3->elapsed < cue->curr_track->offset
|
|
|| (cue->curr_track_idx < cue->track_count - 1
|
|
&& id3->elapsed >= (cue->curr_track+1)->offset)))
|
|
{
|
|
cue_find_current_track(cue, id3->elapsed);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|