mirror of
https://github.com/Rockbox/rockbox.git
synced 2026-07-10 13:29:52 -04:00
skin: add %pX tag for time-based playlist progress
%pP reports playlist progress by position index, which treats every track as equally long. For playlists with tracks of unequal length -- audiobooks with chapters anywhere from two minutes to an hour are the motivating case -- position is a poor proxy for listening progress. %pX reports the played percentage of the whole playlist by time: the summed length of all preceding tracks plus the elapsed time in the current one, relative to the playlist's total duration. It can be used as a value, in a conditional, with %if(), or as a bar tag like %pb. If a playlist contains more than 500 tracks or the scan is taking too long and the user aborts the tag will fallback to the behavior of %pP except the progress through the current track will be included in the returned percentage -------------------------------------- Computing this needs every track's length, and reading metadata for every track is too slow and disk-heavy for a tag that refreshes on the WPS. Instead each track's length is estimated from its file size: the skin engine scans the playlist in the background, a batch of files each skin refresh, opening each file only to read its size (directory metadata, no header parse). Size is turned into time by calibrating one file of each type -- the first file of each extension is parsed once with get_metadata to learn its bytes-per-second, and every later file of that type reuses it. A single-format playlist, the usual audiobook case, parses exactly one file and stat's the rest. The result is an estimate -- bitrate varies within a type, especially for VBR -- but it is cheap and accurate enough for a progress indicator, and the playing track always contributes its exact elapsed time. Per-track lengths are stored as two bytes of minutes each in a movable buffer sized to the track count and allocated only while the tag is in use; the cache is keyed on the track count and a crc of the first, middle and last filenames, so a playlist swap or reshuffle is caught. If the buffer cannot be allocated (a very large playlist on a low-memory target) the tag falls back to position-based progress. The buffer is allocated when playback starts (and when the now-playing screen is opened with playback already active) rather than lazily on the first WPS refresh, so the one-time allocation happens at a playback boundary instead of during steady-state playback; it falls back to allocating on first use if that point is missed. Until the scan finishes the tag does not blank: it returns an instant equal-weight estimate -- (completed tracks + fraction through the current one) / track count -- which sharpens into the size-based value as the scan fills in. So a theme can just use %pX and it is correct from the first frame, and %?pX is true whenever a playlist is loaded. Tested on a Sansa Clip Zip and in the simulator. On a 216-track, ~745 MB single-format audiobook playlist the length scan dropped from ~4150 ms (get_metadata on every track) to ~174 ms (one parse plus 215 file-size stats), roughly 24x lighter. Change-Id: I6e572e78a10444bd513ddc77e30da04aa5153ef2
This commit is contained in:
parent
02f7dabc67
commit
a824085057
10 changed files with 233 additions and 1 deletions
|
|
@ -210,6 +210,23 @@ void draw_progressbar(struct gui_wps *gwps, struct skin_viewport* skin_viewport,
|
|||
length = 100;
|
||||
end = battery_level();
|
||||
}
|
||||
else if (pb->type == SKIN_TOKEN_PLAYLIST_PROGRESSBAR)
|
||||
{
|
||||
unsigned long pl_elapsed, pl_total;
|
||||
if (id3 && wps_get_playlist_percent(id3,
|
||||
id3->elapsed + state->ff_rewind_count,
|
||||
&pl_elapsed, &pl_total)
|
||||
&& pl_total > 0)
|
||||
{
|
||||
length = pl_total;
|
||||
end = pl_elapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
length = 1;
|
||||
end = 0;
|
||||
}
|
||||
}
|
||||
else if (pb->type == SKIN_TOKEN_PEAKMETER_LEFTBAR ||
|
||||
pb->type == SKIN_TOKEN_PEAKMETER_RIGHTBAR)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1234,6 +1234,11 @@ static int parse_progressbar_tag(struct skin_element* element,
|
|||
token->type = SKIN_TOKEN_VOLUMEBAR;
|
||||
else if (token->type == SKIN_TOKEN_BATTERY_PERCENT)
|
||||
token->type = SKIN_TOKEN_BATTERY_PERCENTBAR;
|
||||
else if (token->type == SKIN_TOKEN_PLAYLIST_ELAPSED_PERCENT)
|
||||
{
|
||||
wps_playlist_percent_enable(); /* enable scanning track lengths */
|
||||
token->type = SKIN_TOKEN_PLAYLIST_PROGRESSBAR;
|
||||
}
|
||||
else if (token->type == SKIN_TOKEN_TUNER_RSSI)
|
||||
token->type = SKIN_TOKEN_TUNER_RSSI_BAR;
|
||||
else if (token->type == SKIN_TOKEN_PEAKMETER_LEFT)
|
||||
|
|
@ -2416,6 +2421,7 @@ static int skin_element_callback(struct skin_element* element, void* data)
|
|||
case SKIN_TOKEN_PROGRESSBAR:
|
||||
case SKIN_TOKEN_VOLUME:
|
||||
case SKIN_TOKEN_BATTERY_PERCENT:
|
||||
case SKIN_TOKEN_PLAYLIST_ELAPSED_PERCENT:
|
||||
case SKIN_TOKEN_PLAYER_PROGRESSBAR:
|
||||
case SKIN_TOKEN_PEAKMETER_LEFT:
|
||||
case SKIN_TOKEN_PEAKMETER_RIGHT:
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ static bool do_non_text_tags(struct gui_wps *gwps, struct skin_draw_info *info,
|
|||
case SKIN_TOKEN_PLAYLIST_PERCENTBAR:
|
||||
case SKIN_TOKEN_SETTINGBAR:
|
||||
case SKIN_TOKEN_PROGRESSBAR:
|
||||
case SKIN_TOKEN_PLAYLIST_PROGRESSBAR:
|
||||
case SKIN_TOKEN_TUNER_RSSI_BAR:
|
||||
case SKIN_TOKEN_LIST_SCROLLBAR:
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,10 +41,15 @@
|
|||
#include "misc.h"
|
||||
#include "led.h"
|
||||
#include "peakmeter.h"
|
||||
#include "splash.h"
|
||||
/* Image stuff */
|
||||
#include "albumart.h"
|
||||
#include "playlist.h"
|
||||
#include "playback.h"
|
||||
#include "metadata.h"
|
||||
#include "crc32.h"
|
||||
#include "file.h"
|
||||
#include "core_alloc.h"
|
||||
#include "tdspeed.h"
|
||||
#include "viewport.h"
|
||||
#include "tagcache.h"
|
||||
|
|
@ -198,6 +203,167 @@ const char *get_cuesheetid3_token(struct wps_token *token, struct mp3entry *id3,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
/* Whole-playlist progress by time (%pX). if track count exceeds MAX_TRACKS
|
||||
* the tag falls back to position-based progress.
|
||||
* The cache is keyed on the track count and a crc of the
|
||||
* first, middle and last filenames, so a playlist change, same-length swap,
|
||||
* or reshuffle causes a reload of the length data. */
|
||||
#define PLPCT_MAX_TRACKS 500
|
||||
|
||||
static struct {
|
||||
uint32_t sig; /* signature of the playlist to detect changed playlist or reshuffle*/
|
||||
int amount;
|
||||
bool nomem; /* allocation failed -> position fallback */
|
||||
bool enabled; /* tag was parsed */
|
||||
uint16_t track_mins[PLPCT_MAX_TRACKS];
|
||||
unsigned long total_min;
|
||||
} plpct = { .amount = -1, .enabled = false };
|
||||
|
||||
static uint32_t plpct_sig(void)
|
||||
{
|
||||
struct playlist_info *pl = playlist_get_current();
|
||||
/* changes on any playlist swap or reshuffle */
|
||||
return pl->seed ^ pl->first_index ^ pl->amount ^ pl->last_insert_pos ^ pl->dirlen
|
||||
^ pl->created_tick ^ pl->max_playlist_size ^ pl->last_shuffled_start;
|
||||
}
|
||||
|
||||
/* Equal-weight estimate (every track the same length)
|
||||
scaled by 1000 as %pX is percent-only. */
|
||||
static void plpct_estimate(const struct mp3entry *id3, unsigned long elapsed_ms,
|
||||
int index, int amount,
|
||||
unsigned long *elapsed_s, unsigned long *total_s)
|
||||
{
|
||||
unsigned long frac = 0; /* 0..1000 through the current track */
|
||||
if (id3->length)
|
||||
frac = MIN(1000UL, 1000UL * elapsed_ms / id3->length);
|
||||
*elapsed_s = (unsigned long)(index - 1) * 1000 + frac;
|
||||
*total_s = (unsigned long)amount * 1000;
|
||||
}
|
||||
|
||||
void wps_playlist_percent_enable(void)
|
||||
{
|
||||
plpct.enabled = true;
|
||||
}
|
||||
|
||||
void wps_playlist_percent_prepare(void)
|
||||
{
|
||||
if (!plpct.enabled)
|
||||
return;
|
||||
|
||||
plpct.nomem = true;
|
||||
|
||||
int amount = playlist_amount();
|
||||
plpct.sig = plpct_sig();
|
||||
plpct.amount = amount;
|
||||
if (amount <= 0 || amount > PLPCT_MAX_TRACKS) /* too many tracks abort */
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
struct playlist_track_info info;
|
||||
|
||||
struct mp3entry *tmp = get_temp_mp3entry(NULL); /* shared scratch */
|
||||
|
||||
if (!tmp)
|
||||
return;
|
||||
#if (CONFIG_STORAGE & STORAGE_ATA) /* Harddrive estimate length based on filesize */
|
||||
unsigned int last_afmt = AFMT_UNKNOWN;
|
||||
uint32_t last_bps = 0;
|
||||
#endif
|
||||
for (int n = 0; n < amount; n++)
|
||||
{
|
||||
unsigned long secs = 0;
|
||||
uint16_t mins = 0;
|
||||
if (playlist_get_track_info(NULL, n, &info) < 0) /* couldn't get track filename abort */
|
||||
goto abort;
|
||||
|
||||
int slot = info.display_index - 1;
|
||||
if (slot < 0 || slot >= amount)
|
||||
goto abort;
|
||||
|
||||
#if (CONFIG_STORAGE & STORAGE_ATA) /* Harddrive */
|
||||
int fd = open(info.filename, O_RDONLY);
|
||||
if (fd >= 0)
|
||||
{
|
||||
unsigned int afmt = probe_file_format(info.filename);
|
||||
off_t size = filesize(fd);
|
||||
|
||||
if (size > 0)
|
||||
{
|
||||
if (afmt != last_afmt || last_bps == 0
|
||||
|| amount <= 50 || (amount <= 250 && ata_disk_isssd()) // TODO tune this for harddisk devices
|
||||
{
|
||||
if (get_metadata_ex(tmp, fd, info.filename,
|
||||
METADATA_EXCLUDE_ID3_PATH | METADATA_EXCLUDE_NORMALIZE))
|
||||
{
|
||||
secs = tmp->length / 1000;
|
||||
last_bps = (uint32_t)((uint64_t)size * 1000 / tmp->length);
|
||||
last_afmt = afmt;
|
||||
}
|
||||
}
|
||||
else /* file size only -- no per-track metadata parse */
|
||||
{
|
||||
secs = (unsigned long)((uint64_t)size / last_bps);
|
||||
}
|
||||
mins = MIN(MAX(1, (secs + 30) / 60), 65535ul);/* minutes, rounded to nearest */
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
#else
|
||||
if (get_metadata_ex(tmp, -1, info.filename,
|
||||
METADATA_EXCLUDE_ID3_PATH | METADATA_EXCLUDE_NORMALIZE))
|
||||
{
|
||||
secs = tmp->length / 1000;
|
||||
mins = MIN(MAX(1, (secs + 30) / 60), 65535ul);/* minutes, rounded to nearest */
|
||||
}
|
||||
#endif
|
||||
/* unreadable tracks count as zero length */
|
||||
plpct.track_mins[slot] = mins;
|
||||
}
|
||||
|
||||
plpct.total_min = 0;
|
||||
for (int t = 0; t < plpct.amount; t++)
|
||||
plpct.total_min += plpct.track_mins[t];
|
||||
plpct.nomem = false;
|
||||
abort:
|
||||
get_temp_mp3entry(tmp); /* release scratch */
|
||||
}
|
||||
|
||||
bool wps_get_playlist_percent(struct mp3entry *id3, unsigned long elapsed_ms,
|
||||
unsigned long *elapsed_s, unsigned long *total_s)
|
||||
{
|
||||
int amount = playlist_amount();
|
||||
int index = playlist_get_display_index(); /* 1-based */
|
||||
|
||||
if (!id3 || amount <= 0 || index < 1 || index > amount)
|
||||
return false;
|
||||
|
||||
uint32_t sig = plpct_sig();
|
||||
if (amount != plpct.amount || sig != plpct.sig)
|
||||
{
|
||||
splashf(0, ID2P(LANG_WAIT));
|
||||
wps_playlist_percent_enable();
|
||||
wps_playlist_percent_prepare();
|
||||
}
|
||||
/* error parsing playlist or more than MAX_TRACKS fall back to
|
||||
position-based progress rather than blank */
|
||||
if (plpct.nomem)
|
||||
{
|
||||
plpct_estimate(id3, elapsed_ms, index, amount, elapsed_s, total_s);
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned long before_min = 0;
|
||||
for (int t = 0; t < index - 1; t++)
|
||||
before_min += plpct.track_mins[t];
|
||||
|
||||
*elapsed_s = before_min * 60 + elapsed_ms / 1000;
|
||||
*total_s = plpct.total_min * 60;
|
||||
if (*elapsed_s > *total_s)
|
||||
*elapsed_s = *total_s;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* get_filename_token(struct wps_token *token, char* filename,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
|
|
@ -333,6 +499,24 @@ const char *get_id3_token(struct wps_token *token, struct mp3entry *id3,
|
|||
snprintf(buf, buf_size, "%lu", 100 * elapsed / length);
|
||||
return buf;
|
||||
|
||||
case SKIN_TOKEN_PLAYLIST_ELAPSED_PERCENT:
|
||||
{
|
||||
unsigned long pl_elapsed, pl_total;
|
||||
if (!wps_get_playlist_percent(id3, elapsed,
|
||||
&pl_elapsed, &pl_total)
|
||||
|| pl_total == 0)
|
||||
return NULL;
|
||||
|
||||
/* %pX always yields a value for a valid playlist now (the
|
||||
equal-weight estimate until the scan finishes, then the exact
|
||||
time-weighted figure), so it behaves like any percent tag;
|
||||
%?pX<...> is true whenever the tag applies (a sized playlist). */
|
||||
if (intval && limit == TOKEN_VALUE_ONLY)
|
||||
*intval = 100 * pl_elapsed / pl_total;
|
||||
snprintf(buf, buf_size, "%lu", 100 * pl_elapsed / pl_total);
|
||||
return buf;
|
||||
}
|
||||
|
||||
case SKIN_TOKEN_TRACK_STARTING:
|
||||
{
|
||||
unsigned long time = token->value.i * (HZ/TIMEOUT_UNIT);
|
||||
|
|
|
|||
|
|
@ -428,6 +428,12 @@ const char *get_cuesheetid3_token(struct wps_token *token, struct mp3entry *id3,
|
|||
int offset_tracks, char *buf, int buf_size);
|
||||
const char *get_id3_token(struct wps_token *token, struct mp3entry *id3,
|
||||
char *filename, char *buf, int buf_size, int limit, int *intval);
|
||||
/* whole-playlist progress backing %pX (number and bar forms) */
|
||||
bool wps_get_playlist_percent(struct mp3entry *id3, unsigned long elapsed_ms,
|
||||
unsigned long *elapsed_s, unsigned long *total_s);
|
||||
void wps_playlist_percent_enable(void);
|
||||
void wps_playlist_percent_prepare(void);
|
||||
|
||||
#if CONFIG_TUNER
|
||||
const char *get_radio_token(struct wps_token *token, int preset_offset,
|
||||
char *buf, int buf_size, int limit, int *intval);
|
||||
|
|
|
|||
|
|
@ -178,6 +178,9 @@
|
|||
static struct playlist_info current_playlist;
|
||||
static struct playlist_info on_disk_playlist;
|
||||
|
||||
/* playlist elapsed percent function from skin_tokens.c */
|
||||
extern void wps_playlist_percent_prepare(void);
|
||||
|
||||
/* REPEAT_ONE support function from playback.c */
|
||||
extern bool audio_pending_track_skip_is_manual(void);
|
||||
static inline bool is_manual_skip(void)
|
||||
|
|
@ -491,7 +494,7 @@ static void empty_playlist_unlocked(struct playlist_info* playlist, bool resume)
|
|||
playlist->first_index = 0;
|
||||
playlist->amount = 0;
|
||||
playlist->last_insert_pos = -1;
|
||||
|
||||
playlist->created_tick = current_tick;
|
||||
playlist->started = false;
|
||||
|
||||
if (!resume && playlist == ¤t_playlist)
|
||||
|
|
@ -3755,6 +3758,8 @@ void playlist_start(int start_index, unsigned long elapsed,
|
|||
|
||||
playlist_write_unlock(playlist);
|
||||
|
||||
wps_playlist_percent_prepare();
|
||||
|
||||
audio_play(elapsed, offset);
|
||||
audio_resume();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ struct playlist_info
|
|||
int last_shuffled_start; /* number of tracks when insert last
|
||||
shuffled command start */
|
||||
int seed; /* shuffle seed */
|
||||
unsigned long created_tick;
|
||||
struct mutex mutex; /* mutex for control file access */
|
||||
#ifdef HAVE_DIRCACHE
|
||||
int dcfrefs_handle;
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ static const struct tag_info legal_tags[] =
|
|||
TAG(SKIN_TOKEN_PLAYLIST_ENTRIES, "pe", "", SKIN_REFRESH_STATIC),
|
||||
TAG(SKIN_TOKEN_PLAYLIST_NAME, "pn", "", SKIN_REFRESH_STATIC),
|
||||
TAG(SKIN_TOKEN_PLAYLIST_SHUFFLE, "ps", "", SKIN_REFRESH_DYNAMIC),
|
||||
TAG(SKIN_TOKEN_PLAYLIST_ELAPSED_PERCENT, "pX", BAR_PARAMS, SKIN_REFRESH_DYNAMIC),
|
||||
|
||||
TAG(SKIN_TOKEN_DATABASE_PLAYCOUNT, "rp", "", SKIN_REFRESH_DYNAMIC),
|
||||
TAG(SKIN_TOKEN_DATABASE_RATING, "rr", "", SKIN_REFRESH_DYNAMIC),
|
||||
|
|
|
|||
|
|
@ -213,6 +213,8 @@ enum skin_token_type {
|
|||
SKIN_TOKEN_PLAYLIST_PERCENT,
|
||||
SKIN_TOKEN_PLAYLIST_PERCENTBAR,
|
||||
SKIN_TOKEN_PLAYLIST_SHUFFLE,
|
||||
SKIN_TOKEN_PLAYLIST_ELAPSED_PERCENT,
|
||||
SKIN_TOKEN_PLAYLIST_PROGRESSBAR,
|
||||
|
||||
|
||||
SKIN_TOKEN_ENABLE_THEME,
|
||||
|
|
|
|||
|
|
@ -176,6 +176,15 @@ or \config{\%D(2)}), produce the information for the next file to be played.
|
|||
\config{\%pp} & Playlist position as index\\
|
||||
\config{\%pP} & Playlist position as percentage. Can be used as a value, %
|
||||
a conditional tag or a bar tag.\\
|
||||
\config{\%pX} & Percentage of the whole playlist played by time. Unlike
|
||||
\config{\%pP} this reflects tracks of differing length when 500 or less,
|
||||
such as audiobook chapters. Othewise acts like \config{\%pP} except with
|
||||
also including progress through the current track in the percentage.
|
||||
\opt{HAVE_DISK_STORAGE}{
|
||||
Track lengths are estimated from file size to reduce disk use, so
|
||||
the value may be approximate.
|
||||
}
|
||||
Can be used as a value, a conditional tag or a bar tag.\\
|
||||
\config{\%pr} & Remaining time in song\\
|
||||
\config{\%ps} & ``s'' if shuffle mode is enabled\\
|
||||
\config{\%pt} & Total track time\\
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue