diff --git a/apps/lang/english.lang b/apps/lang/english.lang
index ed1cadfb9e..1f5afa0a50 100644
--- a/apps/lang/english.lang
+++ b/apps/lang/english.lang
@@ -237,7 +237,7 @@
*: "Loading... %d%% done (%s)"
- *: ""
+ *: "Loading"
@@ -13829,3 +13829,157 @@
*: "Never"
+
+ id: LANG_PLAYTIME_ELAPSED
+ desc: playing time screen
+ user: core
+
+ *: "Playlist elapsed: %s / %s %ld%%"
+
+
+ *: "Playlist elapsed: %s / %s %ld%%"
+
+
+ *: "Playlist elapsed"
+
+
+
+ id: LANG_PLAYTIME_TRK_ELAPSED
+ desc: playing time screen
+ user: core
+
+ *: "Track elapsed: %s / %s %ld%%"
+
+
+ *: "Track elapsed: %s / %s %ld%%"
+
+
+ *: "Track elapsed"
+
+
+
+ id: LANG_PLAYTIME_REMAINING
+ desc: playing time screen
+ user: core
+
+ *: "Playlist remaining: %s"
+
+
+ *: "Playlist remaining: %s"
+
+
+ *: "Playlist remaining"
+
+
+
+ id: LANG_PLAYTIME_TRK_REMAINING
+ desc: playing time screen
+ user: core
+
+ *: "Track remaining: %s"
+
+
+ *: "Track remaining: %s"
+
+
+ *: "Track remaining"
+
+
+
+ id: LANG_PLAYTIME_TRACK
+ desc: playing time screen
+ user: core
+
+ *: "Track %d / %d %d%%"
+
+
+ *: "Track %d / %d %d%%"
+
+
+ *: "Track"
+
+
+
+ id: LANG_PLAYTIME_STORAGE
+ desc: playing time screen
+ user: core
+
+ *: "Storage: %s (done %s, remaining %s)"
+
+
+ *: "Storage: %s (done %s, remaining %s)"
+
+
+ *: "Storage"
+
+
+
+ id: VOICE_PLAYTIME_DONE
+ desc: playing time screen
+ user: core
+
+ *: ""
+
+
+ *: ""
+
+
+ *: "Done"
+
+
+
+ id: LANG_PLAYTIME_AVG_TRACK_SIZE
+ desc: playing time screen
+ user: core
+
+ *: "Average track size: %s"
+
+
+ *: "Average track size: %s"
+
+
+ *: "Average track size"
+
+
+
+ id: LANG_PLAYTIME_AVG_BITRATE
+ desc: playing time screen
+ user: core
+
+ *: "Average bitrate: %ld kbps"
+
+
+ *: "Average bitrate: %ld kbps"
+
+
+ *: "Average bit rate"
+
+
+
+ id: LANG_PLAYTIME_ERROR
+ desc: playing time screen
+ user: core
+
+ *: "Error while gathering info"
+
+
+ *: "Error while gathering info"
+
+
+ *: "Error while gathering info"
+
+
+
+ id: LANG_PLAYING_TIME
+ desc: onplay menu
+ user: core
+
+ *: "Playing time"
+
+
+ *: "Playing time"
+
+
+ *: "Playing time"
+
+
diff --git a/apps/onplay.c b/apps/onplay.c
index 7c4c83a991..092d23406b 100644
--- a/apps/onplay.c
+++ b/apps/onplay.c
@@ -192,6 +192,263 @@ static int bookmark_menu_callback(int action,
return action;
}
+/* playing_time screen context */
+struct playing_time_info {
+ int curr_playing; /* index of currently playing track in playlist */
+ int nb_tracks; /* how many tracks in playlist */
+ /* seconds before and after current position, and total. Datatype
+ allows for values up to 68years. If I had kept it in ms
+ though, it would have overflowed at 24days, which takes
+ something like 8.5GB at 32kbps, and so we could conceivably
+ have playlists lasting longer than that. */
+ long secs_bef, secs_aft, secs_ttl;
+ long trk_secs_bef, trk_secs_aft, trk_secs_ttl;
+ /* kilobytes played before and after current pos, and total.
+ Kilobytes because bytes would overflow. Data type range is up
+ to 2TB. */
+ long kbs_bef, kbs_aft, kbs_ttl;
+};
+
+/* list callback for playing_time screen */
+static const char * playing_time_get_or_speak_info(int selected_item, void * data,
+ char *buf, size_t buffer_len,
+ bool say_it)
+{
+ struct playing_time_info *pti = (struct playing_time_info *)data;
+ switch(selected_item) {
+ case 0: { /* elapsed and total time */
+ char timestr1[25], timestr2[25];
+ format_time_auto(timestr1, sizeof(timestr1), pti->secs_bef,
+ UNIT_SEC, false);
+ format_time_auto(timestr2, sizeof(timestr2), pti->secs_ttl,
+ UNIT_SEC, false);
+ long elapsed_perc; /* percentage of duration elapsed */
+ if (pti->secs_ttl == 0)
+ elapsed_perc = 0;
+ else if (pti->secs_ttl <= 0xFFFFFF)
+ elapsed_perc = pti->secs_bef *100 / pti->secs_ttl;
+ else /* sacrifice some precision to avoid overflow */
+ elapsed_perc = (pti->secs_bef>>7) *100 /(pti->secs_ttl>>7);
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_ELAPSED),
+ timestr1, timestr2, elapsed_perc);
+ if (say_it)
+ talk_ids(false, LANG_PLAYTIME_ELAPSED,
+ TALK_ID(pti->secs_bef, UNIT_TIME),
+ VOICE_OF,
+ TALK_ID(pti->secs_ttl, UNIT_TIME),
+ VOICE_PAUSE,
+ TALK_ID(elapsed_perc, UNIT_PERCENT));
+ break;
+ }
+ case 1: { /* playlist remaining time */
+ char timestr[25];
+ format_time_auto(timestr, sizeof(timestr), pti->secs_aft,
+ UNIT_SEC, false);
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_REMAINING),
+ timestr);
+ if (say_it)
+ talk_ids(false, LANG_PLAYTIME_REMAINING,
+ TALK_ID(pti->secs_aft, UNIT_TIME));
+ break;
+ }
+ case 2: { /* track elapsed and duration */
+ char timestr1[25], timestr2[25];
+ format_time_auto(timestr1, sizeof(timestr1), pti->trk_secs_bef,
+ UNIT_SEC, false);
+ format_time_auto(timestr2, sizeof(timestr2), pti->trk_secs_ttl,
+ UNIT_SEC, false);
+ long elapsed_perc; /* percentage of duration elapsed */
+ if (pti->trk_secs_ttl == 0)
+ elapsed_perc = 0;
+ else if (pti->trk_secs_ttl <= 0xFFFFFF)
+ elapsed_perc = pti->trk_secs_bef *100 / pti->trk_secs_ttl;
+ else /* sacrifice some precision to avoid overflow */
+ elapsed_perc = (pti->trk_secs_bef>>7) *100 /(pti->trk_secs_ttl>>7);
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_TRK_ELAPSED),
+ timestr1, timestr2, elapsed_perc);
+ if (say_it)
+ talk_ids(false, LANG_PLAYTIME_TRK_ELAPSED,
+ TALK_ID(pti->trk_secs_bef, UNIT_TIME),
+ VOICE_OF,
+ TALK_ID(pti->trk_secs_ttl, UNIT_TIME),
+ VOICE_PAUSE,
+ TALK_ID(elapsed_perc, UNIT_PERCENT));
+ break;
+ }
+ case 3: { /* track remaining time */
+ char timestr[25];
+ format_time_auto(timestr, sizeof(timestr), pti->trk_secs_aft,
+ UNIT_SEC, false);
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_TRK_REMAINING),
+ timestr);
+ if (say_it)
+ talk_ids(false, LANG_PLAYTIME_TRK_REMAINING,
+ TALK_ID(pti->trk_secs_aft, UNIT_TIME));
+ break;
+ }
+ case 4: { /* track index */
+ int track_perc = (pti->curr_playing+1) *100 / pti->nb_tracks;
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_TRACK),
+ pti->curr_playing, pti->nb_tracks, track_perc);
+ if (say_it)
+ talk_ids(false, LANG_PLAYTIME_TRACK,
+ TALK_ID(pti->curr_playing+1, UNIT_INT),
+ VOICE_OF,
+ TALK_ID(pti->nb_tracks, UNIT_INT),
+ VOICE_PAUSE,
+ TALK_ID(track_perc, UNIT_PERCENT));
+ break;
+ }
+ case 5: { /* storage size */
+ char str1[10], str2[10], str3[10];
+ output_dyn_value(str1, sizeof(str1), pti->kbs_ttl, kibyte_units, 3, true);
+ output_dyn_value(str2, sizeof(str2), pti->kbs_bef, kibyte_units, 3, true);
+ output_dyn_value(str3, sizeof(str3), pti->kbs_aft, kibyte_units, 3, true);
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_STORAGE),
+ str1,str2,str3);
+ if (say_it) {
+ talk_id(LANG_PLAYTIME_STORAGE, false);
+ output_dyn_value(NULL, 0, pti->kbs_ttl, kibyte_units, 3, true);
+ talk_ids(true, VOICE_PAUSE, VOICE_PLAYTIME_DONE);
+ output_dyn_value(NULL, 0, pti->kbs_bef, kibyte_units, 3, true);
+ talk_id(LANG_PLAYTIME_REMAINING, true);
+ output_dyn_value(NULL, 0, pti->kbs_aft, kibyte_units, 3, true);
+ }
+ break;
+ }
+ case 6: { /* Average track file size */
+ char str[10];
+ long avg_track_size = pti->kbs_ttl /pti->nb_tracks;
+ output_dyn_value(str, sizeof(str), avg_track_size, kibyte_units, 3, true);
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_AVG_TRACK_SIZE),
+ str);
+ if (say_it) {
+ talk_id(LANG_PLAYTIME_AVG_TRACK_SIZE, false);
+ output_dyn_value(NULL, 0, avg_track_size, kibyte_units, 3, true);
+ }
+ break;
+ }
+ case 7: { /* Average bitrate */
+ /* Convert power of 2 kilobytes to power of 10 kilobits */
+ long avg_bitrate = pti->kbs_ttl / pti->secs_ttl *1024 *8 /1000;
+ snprintf(buf, buffer_len, str(LANG_PLAYTIME_AVG_BITRATE),
+ avg_bitrate);
+ if (say_it)
+ talk_ids(false, LANG_PLAYTIME_AVG_BITRATE,
+ TALK_ID(avg_bitrate, UNIT_KBIT));
+ break;
+ }
+ }
+ return buf;
+}
+
+static const char * playing_time_get_info(int selected_item, void * data,
+ char *buffer, size_t buffer_len)
+{
+ return playing_time_get_or_speak_info(selected_item, data,
+ buffer, buffer_len, false);
+}
+
+static int playing_time_speak_info(int selected_item, void * data)
+{
+ static char buffer[MAX_PATH];
+ playing_time_get_or_speak_info(selected_item, data,
+ buffer, MAX_PATH, true);
+ return 0;
+}
+
+/* playing time screen: shows total and elapsed playlist duration and
+ other stats */
+static bool playing_time(void)
+{
+ unsigned long talked_tick = current_tick;
+ struct playing_time_info pti;
+ struct playlist_track_info pltrack;
+ struct mp3entry id3;
+ int i, fd, ret;
+
+ pti.nb_tracks = playlist_amount();
+ playlist_get_resume_info(&pti.curr_playing);
+ struct mp3entry *curr_id3 = audio_current_track();
+ if (pti.curr_playing == -1 || !curr_id3)
+ return false;
+ pti.secs_bef = pti.trk_secs_bef = curr_id3->elapsed/1000;
+ pti.secs_aft = pti.trk_secs_aft
+ = (curr_id3->length -curr_id3->elapsed)/1000;
+ pti.kbs_bef = curr_id3->offset/1024;
+ pti.kbs_aft = (curr_id3->filesize -curr_id3->offset)/1024;
+
+ splash(0, ID2P(LANG_WAIT));
+
+ /* Go through each file in the playlist and get its stats. For
+ huge playlists this can take a while... The reference position
+ is the position at the moment this function was invoked,
+ although playback continues forward. */
+ for (i = 0; i < pti.nb_tracks; i++) {
+ /* Show a splash while we are loading. */
+ splashf(0, str(LANG_LOADING_PERCENT),
+ i*100/pti.nb_tracks, str(LANG_OFF_ABORT));
+ /* Voice equivalent */
+ if (TIME_AFTER(current_tick, talked_tick+5*HZ)) {
+ talked_tick = current_tick;
+ talk_ids(false, LANG_LOADING_PERCENT,
+ TALK_ID(i*100/pti.nb_tracks, UNIT_PERCENT));
+ }
+ if (action_userabort(TIMEOUT_NOBLOCK))
+ goto exit;
+
+ if (i == pti.curr_playing)
+ continue;
+
+ if (playlist_get_track_info(NULL, i, &pltrack) < 0)
+ goto error;
+ if ((fd = open(pltrack.filename, O_RDONLY)) < 0)
+ goto error;
+ ret = get_metadata(&id3, fd, pltrack.filename);
+ close(fd);
+ if (!ret)
+ goto error;
+
+ if (i < pti.curr_playing) {
+ pti.secs_bef += id3.length/1000;
+ pti.kbs_bef += id3.filesize/1024;
+ } else {
+ pti.secs_aft += id3.length/1000;
+ pti.kbs_aft += id3.filesize/1024;
+ }
+ }
+
+ pti.secs_ttl = pti.secs_bef +pti.secs_aft;
+ pti.trk_secs_ttl = pti.trk_secs_bef +pti.trk_secs_aft;
+ pti.kbs_ttl = pti.kbs_bef +pti.kbs_aft;
+
+ struct gui_synclist pt_lists;
+ int key;
+
+ gui_synclist_init(&pt_lists, &playing_time_get_info, &pti, true, 1, NULL);
+ if (global_settings.talk_menu)
+ gui_synclist_set_voice_callback(&pt_lists, playing_time_speak_info);
+ gui_synclist_set_nb_items(&pt_lists, 8);
+ gui_synclist_draw(&pt_lists);
+ gui_syncstatusbar_draw(&statusbars, true);
+ gui_synclist_speak_item(&pt_lists);
+ while (true) {
+ gui_syncstatusbar_draw(&statusbars, false);
+ if (list_do_action(CONTEXT_LIST, HZ/2,
+ &pt_lists, &key, LIST_WRAP_UNLESS_HELD) == 0
+ && key!=ACTION_NONE && key!=ACTION_UNKNOWN)
+ {
+ talk_force_shutup();
+ return(default_event_handler(key) == SYS_USB_CONNECTED);
+ }
+ }
+ error:
+ splash(HZ, ID2P(LANG_PLAYTIME_ERROR));
+ exit:
+ return false;
+}
+
+
/* CONTEXT_WPS playlist options */
static bool shuffle_playlist(void)
{
@@ -213,10 +470,12 @@ MENUITEM_FUNCTION(playlist_save_item, 0, ID2P(LANG_SAVE_DYNAMIC_PLAYLIST),
save_playlist, NULL, NULL, Icon_Playlist);
MENUITEM_FUNCTION(reshuffle_item, 0, ID2P(LANG_SHUFFLE_PLAYLIST),
shuffle_playlist, NULL, NULL, Icon_Playlist);
+MENUITEM_FUNCTION(playing_time_item, 0, ID2P(LANG_PLAYING_TIME),
+ playing_time, NULL, NULL, Icon_Playlist);
MAKE_ONPLAYMENU( wps_playlist_menu, ID2P(LANG_PLAYLIST),
NULL, Icon_Playlist,
&view_cur_playlist, &search_playlist_item,
- &playlist_save_item, &reshuffle_item
+ &playlist_save_item, &reshuffle_item, &playing_time_item
);
/* CONTEXT_[TREE|ID3DB] playlist options */
@@ -381,18 +640,18 @@ MENUITEM_FUNCTION(view_playlist_item, 0, ID2P(LANG_VIEW),
MAKE_ONPLAYMENU( tree_playlist_menu, ID2P(LANG_CURRENT_PLAYLIST),
treeplaylist_callback, Icon_Playlist,
-
+
/* view */
&view_playlist_item,
-
+
/* insert */
&i_pl_item, &i_first_pl_item, &i_last_pl_item,
&i_shuf_pl_item, &i_last_shuf_pl_item,
/* queue */
-
+
&q_pl_item, &q_first_pl_item, &q_last_pl_item,
&q_shuf_pl_item, &q_last_shuf_pl_item,
-
+
/* replace */
&replace_pl_item
);
@@ -941,7 +1200,7 @@ static int clipboard_pastedirectory(struct dirrecurse_params *src,
a move instead */
flags |= PASTE_EXDEV;
break;
- }
+ }
#endif /* HAVE_MULTIVOLUME */
}
}
@@ -999,7 +1258,7 @@ static int clipboard_pastedirectory(struct dirrecurse_params *src,
rc = OPRC_CANCELLED;
break;
}
-
+
DEBUGF("Copy %s to %s\n", src->path, target->path);
if (info.attribute & ATTR_DIRECTORY) {
@@ -1151,16 +1410,16 @@ static int ratingitem_callback(int action,const struct menu_item_ex *this_item)
MENUITEM_FUNCTION(rating_item, 0, ID2P(LANG_MENU_SET_RATING),
set_rating_inline, NULL,
ratingitem_callback, Icon_Questionmark);
-#endif
-#ifdef HAVE_PICTUREFLOW_INTEGRATION
-MENUITEM_RETURNVALUE(pictureflow_item, ID2P(LANG_ONPLAY_PICTUREFLOW),
- GO_TO_PICTUREFLOW, NULL, Icon_NOICON);
+#endif
+#ifdef HAVE_PICTUREFLOW_INTEGRATION
+MENUITEM_RETURNVALUE(pictureflow_item, ID2P(LANG_ONPLAY_PICTUREFLOW),
+ GO_TO_PICTUREFLOW, NULL, Icon_NOICON);
#endif
static bool view_cue(void)
{
struct mp3entry* id3 = audio_current_track();
- if(id3 && id3->cuesheet)
+ if (id3 && id3->cuesheet)
{
browse_cuesheet(id3->cuesheet);
}
@@ -1274,7 +1533,7 @@ MENUITEM_FUNCTION(set_recdir_item, 0, ID2P(LANG_SET_AS_REC_DIR),
#endif
static bool set_startdir(void)
{
- snprintf(global_settings.start_directory,
+ snprintf(global_settings.start_directory,
sizeof(global_settings.start_directory),
"%s/", selected_file);
settings_save();
@@ -1375,10 +1634,10 @@ MAKE_ONPLAYMENU( wps_onplay_menu, ID2P(LANG_ONPLAY_MENU_TITLE),
#ifdef HAVE_TAGCACHE
&rating_item,
#endif
- &bookmark_menu,
+ &bookmark_menu,
#ifdef HAVE_PICTUREFLOW_INTEGRATION
&pictureflow_item,
-#endif
+#endif
&browse_id3_item, &list_viewers_item,
&delete_file_item, &view_cue_item,
#ifdef HAVE_PITCHCONTROL
@@ -1451,7 +1710,7 @@ static int playlist_insert_shuffled(void)
playlist_insert_func((intptr_t*)PLAYLIST_INSERT_SHUFFLED);
return ONPLAY_START_PLAY;
}
-
+
return ONPLAY_RELOAD_DIR;
}
@@ -1465,7 +1724,7 @@ struct hotkey_assignment {
#define HOTKEY_FUNC(func, param) {{(void *)func}, param}
/* Any desired hotkey functions go here, in the enum in onplay.h,
- and in the settings menu in settings_list.c. The order here
+ and in the settings menu in settings_list.c. The order here
is not important. */
static struct hotkey_assignment hotkey_items[] = {
{ HOTKEY_VIEW_PLAYLIST, LANG_VIEW_DYNAMIC_PLAYLIST,
@@ -1510,7 +1769,7 @@ int get_hotkey_lang_id(int action)
if (hotkey_items[i].action == action)
return hotkey_items[i].lang_id;
}
-
+
return LANG_OFF;
}
@@ -1521,7 +1780,7 @@ static int execute_hotkey(bool is_wps)
struct hotkey_assignment *this_item;
const int action = (is_wps ? global_settings.hotkey_wps :
global_settings.hotkey_tree);
-
+
/* search assignment struct for a match for the hotkey setting */
while (i--)
{
@@ -1546,7 +1805,7 @@ static int execute_hotkey(bool is_wps)
return return_code;
}
}
-
+
/* no valid hotkey set, ignore hotkey */
return ONPLAY_RELOAD_DIR;
}
diff --git a/docs/CREDITS b/docs/CREDITS
index 0fcf8c66ae..6df7e58bca 100644
--- a/docs/CREDITS
+++ b/docs/CREDITS
@@ -673,6 +673,8 @@ Malik Enes Şafak
Roman Levkin-Taymenev
Nathan Follens
Gergely Békési
+Stephane Doyon
+Alex Wallis
The libmad team
The wavpack team
diff --git a/manual/rockbox_interface/wps.tex b/manual/rockbox_interface/wps.tex
index af594d0cd3..c82a9cb9f4 100644
--- a/manual/rockbox_interface/wps.tex
+++ b/manual/rockbox_interface/wps.tex
@@ -213,20 +213,20 @@ your WPS (While Playing Screen).
\end{description}
}
\subsection{\label{sec:contextmenu}The WPS Context Menu}
-Like the context menu for the \setting{File Browser}, the \setting{WPS Context Menu}
+Like the context menu for the \setting{File Browser}, the \setting{WPS Context Menu}
allows you quick access to some often used functions.
\subsubsection{Playlist}
-The \setting{Playlist} submenu allows you to view, save, search and
-reshuffle the current playlist. These and other operations are detailed in
-\reference{ref:working_with_playlists}. To change settings for the
-\setting{Playlist Viewer} press \ActionStdContext{} while viewing the current
-playlist to bring up the \setting{Playlist Viewer Menu}. In this menu, you
-can find the \setting{Playlist Viewer Settings}.
+The \setting{Playlist} submenu allows you to view, save, search, reshuffle,
+and display the play time of the current playlist. These and other operations
+are detailed in \reference{ref:working_with_playlists}. To change settings for
+the \setting{Playlist Viewer} press \ActionStdContext{} while viewing the
+current playlist to bring up the \setting{Playlist Viewer Menu}. In this
+menu, you can find the \setting{Playlist Viewer Settings}.
\paragraph{Playlist Viewer Settings}
\begin{description}
- \item[Show Icons.] This toggles display of the icon for the currently
+ \item[Show Icons.] This toggles display of the icon for the currently
selected playlist entry and the icon for moving a playlist entry
\item[Show Indices.] This toggles display of the line numbering for
the playlist
diff --git a/manual/working_with_playlists/main.tex b/manual/working_with_playlists/main.tex
index 412f77a234..a7c2bc1299 100644
--- a/manual/working_with_playlists/main.tex
+++ b/manual/working_with_playlists/main.tex
@@ -137,10 +137,11 @@ playlist.
Dynamic playlists are saved so resume will restore them exactly as they
were before shutdown.
-\note{To view, save or reshuffle the current dynamic playlist use the
+\note{To view, save, reshuffle, or display the play time of the current
+ dynamic playlist use the
\setting{Playlist} sub menu in the WPS context menu or in the
\setting{Main Menu}.}
-
+
\subsection{Modifying playlists}
\subsubsection{Reshuffling}
Reshuffling the current playlist is easily done from the \setting{Playlist}