rockbox/apps/cuesheet.c
Michael Sevakis 5ee13ebd39 Implements starting playback from a cuesheet.
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>
2014-03-10 04:13:53 +01:00

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;
}