Something went wrong. Try again.
A modern, network-enabled music player platform built on Rockbox technology. rockboxd.tsiry-sandratraina.com
rust deno navidrome airplay libadwaita zig mpris snapcast mpd rockbox audio subsonic
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * * * 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"
#define rb_talk_ids(enqueue, ids...) rb->talk_idarray(TALK_IDARRAY(ids), enqueue)
/* units used with output_dyn_value */const unsigned char * const byte_units[] ={ ID2P(LANG_BYTE), ID2P(LANG_KIBIBYTE), ID2P(LANG_MEBIBYTE), ID2P(LANG_GIBIBYTE)};
const int menu_items[] = { LANG_REMAINING, LANG_ELAPSED, LANG_PLAYTIME_TRK_REMAINING, LANG_PLAYTIME_TRK_ELAPSED, LANG_PLAYTIME_TRACK, LANG_PLAYTIME_STORAGE, LANG_PLAYTIME_AVG_TRACK_SIZE, LANG_PLAYTIME_AVG_BITRATE,};
const unsigned char * const * const kibyte_units = &byte_units[1];
enum ePT_SUM { /* Note: Order matters (voicing order of LANG_PLAYTIME_STORAGE) */ ePT_TOTAL = 0, ePT_ELAPSED, ePT_REMAINING,
ePT_COUNT};
struct playing_time_info { char single_mode_tag[MAX_PATH]; /* Relevant tag when single mode enabled */ char error_str[16]; /* Error message to display to user */ unsigned long long size[ePT_COUNT]; /* File size of tracks */ unsigned long long length[ePT_COUNT]; /* Length of tracks */ unsigned long curr_track_length[ePT_COUNT]; /* Current track length */ int curr_track_index; /* Index of currently playing track in playlist */ int curr_display_index; /* Display index of currently playing track */ int actual_index; /* Display index in actually counted tracks */ int counted; /* Number of tracks already added up */ int nb_tracks; /* Number of tracks in playlist */ int error_count; /* Number of tracks whose data couldn't be retrieved */ bool remaining_only; /* Whether to ignore elapsed tracks */};
static int32_t single_mode_lang(void){ switch (rb->global_settings->single_mode) { case SINGLE_MODE_ALBUM: return LANG_ID3_ALBUM; case SINGLE_MODE_ALBUM_ARTIST: return LANG_ID3_ALBUMARTIST; case SINGLE_MODE_ARTIST: return LANG_ID3_ARTIST; case SINGLE_MODE_COMPOSER: return LANG_ID3_COMPOSER; case SINGLE_MODE_GROUPING: return LANG_ID3_GROUPING; case SINGLE_MODE_GENRE: return LANG_ID3_GENRE; case SINGLE_MODE_TRACK: return LANG_TRACK; } return LANG_OFF;}
static char* single_mode_id3_tag(struct mp3entry *id3){ switch (rb->global_settings->single_mode) { case SINGLE_MODE_ALBUM: return id3->album; case SINGLE_MODE_ALBUM_ARTIST: return id3->albumartist; case SINGLE_MODE_ARTIST: return id3->artist; case SINGLE_MODE_COMPOSER: return id3->composer; case SINGLE_MODE_GROUPING: return id3->grouping; case SINGLE_MODE_GENRE: return id3->genre_string; } return NULL;}
static char* get_percent_str(long percents){ static char val[10]; rb->snprintf(val, sizeof(val), rb->str(LANG_PERCENT_FORMAT), percents); return val;}
static inline void prepare_time_string(char *buf, size_t buffer_len, long elapsed_pct, const char *timestr1, const char *timestr2){ if (rb->lang_is_rtl()) rb->snprintf(buf, buffer_len, "%s %s / %s", get_percent_str(elapsed_pct), timestr2, timestr1); else rb->snprintf(buf, buffer_len, "%s / %s %s", timestr1, timestr2, get_percent_str(elapsed_pct));}
/* list callback for playing_time screen */static const char * pt_get_or_speak_info(int selected_item, void * data, char *buf, size_t buffer_len, bool say_it){ long elapsed_pct; /* percentage of duration elapsed */ struct playing_time_info *pti = (struct playing_time_info *)data; int info_no = selected_item/2; const int menu_name_id = menu_items[info_no];
/* header */ if (!say_it && !(selected_item % 2)) return rb->str(menu_name_id);
/* data */ switch(info_no) { case 0: { /* playlist remaining time */ char timestr[25]; rb->format_time_auto(timestr, sizeof(timestr), pti->length[ePT_REMAINING], UNIT_SEC, false); rb->snprintf(buf, buffer_len, "%s", timestr);
if (say_it) rb_talk_ids(false, menu_name_id, TALK_ID(pti->length[ePT_REMAINING], UNIT_TIME)); break; } case 1: { /* elapsed and total time */ char timestr1[25], timestr2[25]; rb->format_time_auto(timestr1, sizeof(timestr1), pti->length[ePT_ELAPSED], UNIT_SEC, true);
rb->format_time_auto(timestr2, sizeof(timestr2), pti->length[ePT_TOTAL], UNIT_SEC, true);
if (pti->length[ePT_TOTAL] == 0) elapsed_pct = 0; else if (pti->length[ePT_TOTAL] <= 0xFFFFFF) { elapsed_pct = (pti->length[ePT_ELAPSED] * 100 / pti->length[ePT_TOTAL]); } else /* sacrifice some precision to avoid overflow */ { elapsed_pct = (pti->length[ePT_ELAPSED] >> 7) * 100 / (pti->length[ePT_TOTAL] >> 7); } prepare_time_string(buf, buffer_len, elapsed_pct, timestr1, timestr2);
if (say_it) rb_talk_ids(false, menu_name_id, TALK_ID(pti->length[ePT_ELAPSED], UNIT_TIME), VOICE_OF, TALK_ID(pti->length[ePT_TOTAL], UNIT_TIME), VOICE_PAUSE, TALK_ID(elapsed_pct, UNIT_PERCENT)); break; } case 2: { /* track remaining time */ char timestr[25]; rb->format_time_auto(timestr, sizeof(timestr), pti->curr_track_length[ePT_REMAINING], UNIT_SEC, false); rb->snprintf(buf, buffer_len, "%s", timestr);
if (say_it) rb_talk_ids(false, menu_name_id, TALK_ID(pti->curr_track_length[ePT_REMAINING], UNIT_TIME)); break; } case 3: { /* track elapsed and duration */ char timestr1[25], timestr2[25];
rb->format_time_auto(timestr1, sizeof(timestr1), pti->curr_track_length[ePT_ELAPSED], UNIT_SEC, true); rb->format_time_auto(timestr2, sizeof(timestr2), pti->curr_track_length[ePT_TOTAL], UNIT_SEC, true);
if (pti->curr_track_length[ePT_TOTAL] == 0) elapsed_pct = 0; else if (pti->curr_track_length[ePT_TOTAL] <= 0xFFFFFF) { elapsed_pct = (pti->curr_track_length[ePT_ELAPSED] * 100 / pti->curr_track_length[ePT_TOTAL]); } else /* sacrifice some precision to avoid overflow */ { elapsed_pct = (pti->curr_track_length[ePT_ELAPSED] >> 7) * 100 / (pti->curr_track_length[ePT_TOTAL] >> 7); } prepare_time_string(buf, buffer_len, elapsed_pct, timestr1, timestr2);
if (say_it) rb_talk_ids(false, menu_name_id, TALK_ID(pti->curr_track_length[ePT_ELAPSED], UNIT_TIME), VOICE_OF, TALK_ID(pti->curr_track_length[ePT_TOTAL], UNIT_TIME), VOICE_PAUSE, TALK_ID(elapsed_pct, UNIT_PERCENT)); break; } case 4: { /* track index */ int track_pct = pti->actual_index * 100 / pti->counted;
if (rb->lang_is_rtl()) rb->snprintf(buf, buffer_len, "%s %d / %d", get_percent_str(track_pct), pti->counted, pti->actual_index); else rb->snprintf(buf, buffer_len, "%d / %d %s", pti->actual_index, pti->counted, get_percent_str(track_pct));
if (say_it) rb_talk_ids(false, menu_name_id, TALK_ID(pti->actual_index, UNIT_INT), VOICE_OF, TALK_ID(pti->counted, UNIT_INT), VOICE_PAUSE, TALK_ID(track_pct, UNIT_PERCENT)); break; } case 5: { /* storage size */ int i; char kbstr[ePT_COUNT][20];
for (i = 0; i < ePT_COUNT; i++) { rb->output_dyn_value(kbstr[i], sizeof(kbstr[i]), pti->size[i], kibyte_units, 3, true); } rb->snprintf(buf, buffer_len, "%s (%s / %s)", kbstr[ePT_TOTAL], kbstr[ePT_ELAPSED], kbstr[ePT_REMAINING]);
if (say_it) { int32_t voice_ids[ePT_COUNT]; voice_ids[ePT_TOTAL] = menu_name_id; voice_ids[ePT_ELAPSED] = VOICE_PLAYTIME_DONE; voice_ids[ePT_REMAINING] = LANG_REMAINING;
for (i = 0; i < ePT_COUNT; i++) { rb_talk_ids(i > 0, VOICE_PAUSE, voice_ids[i]); rb->output_dyn_value(NULL, 0, pti->size[i], kibyte_units, 3, true); } } break; } case 6: { /* Average track file size */ char str[20]; long avg_track_size = pti->size[ePT_TOTAL] / pti->counted; rb->output_dyn_value(str, sizeof(str), avg_track_size, kibyte_units, 3, true); rb->snprintf(buf, buffer_len, "%s", str);
if (say_it) { rb->talk_id(menu_name_id, false); rb->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->size[ePT_TOTAL] / pti->length[ePT_TOTAL] * 1024 * 8 / 1000); rb->snprintf(buf, buffer_len, "%ld kbps", avg_bitrate);
if (say_it) rb_talk_ids(false, menu_name_id, TALK_ID(avg_bitrate, UNIT_KBIT)); break; } } return buf;}
static const char * pt_get_info(int selected_item, void * data, char *buffer, size_t buffer_len){ return pt_get_or_speak_info(selected_item, data, buffer, buffer_len, false);}
static int pt_speak_info(int selected_item, void * data){ static char buffer[MAX_PATH]; pt_get_or_speak_info(selected_item, data, buffer, sizeof(buffer), true); return 0;}
static bool pt_display_stats(struct playing_time_info *pti){ struct gui_synclist pt_lists; rb->gui_synclist_init(&pt_lists, &pt_get_info, pti, true, 2, NULL); if (rb->global_settings->talk_menu) rb->gui_synclist_set_voice_callback(&pt_lists, pt_speak_info); rb->gui_synclist_set_nb_items(&pt_lists, pti->remaining_only ? 2 : 8*2); rb->gui_synclist_set_title(&pt_lists, *pti->single_mode_tag ? rb->str(single_mode_lang()) : rb->str(LANG_PLAYLIST), NOICON); rb->gui_synclist_draw(&pt_lists); rb->gui_synclist_speak_item(&pt_lists); while (true) { int action = rb->get_action(CONTEXT_LIST, HZ/2); if (rb->gui_synclist_do_button(&pt_lists, &action) == 0 && action != ACTION_NONE && action != ACTION_UNKNOWN) { bool usb = rb->default_event_handler(action) == SYS_USB_CONNECTED;
if (!usb && IS_SYSEVENT(action)) continue;
rb->talk_force_shutup(); return usb; } } return false;}
static const char *pt_options_name(int selected_item, void * data, char *buf, size_t buf_size){ (void) data; (void) buf; (void) buf_size; return selected_item == 0 ? rb->str(LANG_ALL) : selected_item == 1 ? rb->str(LANG_REMAINING) : rb->str(single_mode_lang());}
static int pt_options_speak(int selected_item, void * data){ (void) data; rb->talk_id(selected_item == 0 ? LANG_ALL : selected_item == 1 ? LANG_REMAINING : single_mode_lang(), false); return 0;}
static int pt_options(struct playing_time_info *pti){ struct gui_synclist pt_options; rb->gui_synclist_init(&pt_options, &pt_options_name, NULL, true, 1, NULL); if (rb->global_settings->talk_menu) rb->gui_synclist_set_voice_callback(&pt_options, pt_options_speak); rb->gui_synclist_set_nb_items(&pt_options, *pti->single_mode_tag ? 3 : 2); rb->gui_synclist_set_title(&pt_options, rb->str(LANG_PLAYING_TIME), NOICON); rb->gui_synclist_draw(&pt_options); rb->gui_synclist_speak_item(&pt_options);
while(true) { int button = rb->get_action(CONTEXT_LIST, HZ); if (rb->gui_synclist_do_button(&pt_options, &button)) continue; switch(button) { case ACTION_STD_OK: { int sel = rb->gui_synclist_get_sel_pos(&pt_options); if (sel < 2) *pti->single_mode_tag = 0; if (sel == 1) pti->remaining_only = true; return -1; } case ACTION_STD_CANCEL: return 0; default: if (rb->default_event_handler(button) == SYS_USB_CONNECTED) return 1; } }}
static void pt_store_converted_totals(struct playing_time_info *pti){ /* convert units from ms to s */ pti->length[ePT_ELAPSED] /= 1000; pti->length[ePT_REMAINING] /= 1000; pti->curr_track_length[ePT_ELAPSED] /= 1000; pti->curr_track_length[ePT_REMAINING] /= 1000; /* convert units from Bytes to KiB */ pti->size[ePT_ELAPSED] >>= 10; pti->size[ePT_REMAINING] >>= 10;
pti->length[ePT_TOTAL] = pti->length[ePT_ELAPSED] + pti->length[ePT_REMAINING]; pti->curr_track_length[ePT_TOTAL] = pti->curr_track_length[ePT_ELAPSED] + pti->curr_track_length[ePT_REMAINING]; pti->size[ePT_TOTAL] = pti->size[ePT_ELAPSED] + pti->size[ePT_REMAINING];}
static int pt_add_track(int i, enum ePT_SUM section, struct playing_time_info *pti){ static struct mp3entry id3; static struct playlist_track_info pl_track; int progress_total = pti->remaining_only ? (pti->nb_tracks - pti->curr_display_index) + 1 : pti->nb_tracks;
/* (voiced) */ rb->splash_progress(pti->counted, progress_total, "%s (%s)", rb->str(LANG_WAIT), rb->str(LANG_OFF_ABORT));
if (rb->action_userabort(TIMEOUT_NOBLOCK)) return -1; else if (rb->playlist_get_track_info(NULL, i, &pl_track) < 0 || !rb->get_metadata(&id3, -1, pl_track.filename)) { pti->error_count++; return -2; } else if(*pti->single_mode_tag && /* single mode tag doesn't match */ rb->strcmp(pti->single_mode_tag, single_mode_id3_tag(&id3) ?: "")) return 1;
pti->length[section] += id3.length; pti->size[section] += id3.filesize; pti->counted++; return 0;}
static bool pt_add_remaining(struct playing_time_info *pti){ int display_index = pti->curr_display_index + 1; for (int i = pti->curr_track_index + 1; display_index <= pti->nb_tracks; i++, display_index++) { if (i == pti->nb_tracks) i = 0;
int ret = pt_add_track(i, ePT_REMAINING, pti); if (ret == 1) break; else if (ret == -1) return false; } return true;}
static bool pt_add_elapsed(struct playing_time_info *pti){ int display_index = pti->curr_display_index - 1; for (int i = pti->curr_track_index - 1; display_index > 0; i--, display_index--) { if (i < 0) i = pti->nb_tracks - 1;
int ret = pt_add_track(i, ePT_ELAPSED, pti); if (ret == 1) break; else if (ret == -1) return false; else if (ret == 0) pti->actual_index++; } return true;}
static bool pt_add_curr_track(struct playing_time_info *pti){ struct mp3entry *curr_id3 = rb->audio_current_track(); rb->playlist_get_resume_info(&pti->curr_track_index);
if (pti->curr_track_index == -1 || !curr_id3) return false;
pti->curr_display_index = rb->playlist_get_display_index(); pti->length[ePT_ELAPSED] = pti->curr_track_length[ePT_ELAPSED] = curr_id3->elapsed; pti->length[ePT_REMAINING] = pti->curr_track_length[ePT_REMAINING] = curr_id3->length - curr_id3->elapsed; pti->size[ePT_ELAPSED] = curr_id3->offset; pti->size[ePT_REMAINING] = curr_id3->filesize - curr_id3->offset; pti->actual_index = pti->counted = 1; rb->strlcpy(pti->single_mode_tag, single_mode_id3_tag(curr_id3) ?: "", sizeof(pti->single_mode_tag)); return true;}
/* playing time screen: shows total and elapsed playlist duration and other stats */static bool playing_time(void){ struct playing_time_info pti; rb->memset(&pti, 0, sizeof(struct playing_time_info));
if (!pt_add_curr_track(&pti)) return false;
int opt = pt_options(&pti); if (opt > -1) return opt;
#ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(true);#endif rb->splash_progress_set_delay(HZ/2); pti.nb_tracks = rb->playlist_amount(); int success = (pti.remaining_only || pt_add_elapsed(&pti)) && pt_add_remaining(&pti);#ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(false);#endif if (!success) return false; if (pti.error_count > 0) { rb->snprintf(pti.error_str, sizeof pti.error_str, "%d %s", pti.error_count, rb->str(LANG_TRACKS)); if (rb->global_settings->talk_menu) { rb->talk_id(LANG_ERROR_FORMATSTR, false); rb->talk_number(pti.error_count, true); rb->talk_id(LANG_TRACKS, true); rb->talk_force_enqueue_next(); } /* (voiced above) */ rb->splashf(HZ, rb->str(LANG_ERROR_FORMATSTR), pti.error_str); }
pt_store_converted_totals(&pti); return pt_display_stats(&pti);}
/* this is the plugin entry point */enum plugin_status plugin_start(const void* parameter){ (void)parameter; enum plugin_status status = PLUGIN_OK;
if (!rb->audio_status()) { rb->splash(HZ*2, "Nothing Playing"); return status; }
FOR_NB_SCREENS(i) { rb->sb_set_persistent_title(rb->str(LANG_PLAYING_TIME), Icon_NOICON, i); rb->viewportmanager_theme_enable(i, true, NULL); }
if (playing_time()) status = PLUGIN_USB_CONNECTED;
FOR_NB_SCREENS(i) rb->viewportmanager_theme_undo(i, false);
return status;}