A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 *
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU General Public License
13 * as published by the Free Software Foundation; either version 2
14 * of the License, or (at your option) any later version.
15 *
16 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
17 * KIND, either express or implied.
18 *
19 ****************************************************************************/
20
21#include "plugin.h"
22
23#define rb_talk_ids(enqueue, ids...) rb->talk_idarray(TALK_IDARRAY(ids), enqueue)
24
25/* units used with output_dyn_value */
26const unsigned char * const byte_units[] =
27{
28 ID2P(LANG_BYTE),
29 ID2P(LANG_KIBIBYTE),
30 ID2P(LANG_MEBIBYTE),
31 ID2P(LANG_GIBIBYTE)
32};
33
34const int menu_items[] = {
35 LANG_REMAINING,
36 LANG_ELAPSED,
37 LANG_PLAYTIME_TRK_REMAINING,
38 LANG_PLAYTIME_TRK_ELAPSED,
39 LANG_PLAYTIME_TRACK,
40 LANG_PLAYTIME_STORAGE,
41 LANG_PLAYTIME_AVG_TRACK_SIZE,
42 LANG_PLAYTIME_AVG_BITRATE,
43};
44
45const unsigned char * const * const kibyte_units = &byte_units[1];
46
47enum ePT_SUM {
48 /* Note: Order matters (voicing order of LANG_PLAYTIME_STORAGE) */
49 ePT_TOTAL = 0,
50 ePT_ELAPSED,
51 ePT_REMAINING,
52
53 ePT_COUNT
54};
55
56struct playing_time_info {
57 char single_mode_tag[MAX_PATH]; /* Relevant tag when single mode enabled */
58 unsigned long long size[ePT_COUNT]; /* File size of tracks */
59 unsigned long long length[ePT_COUNT]; /* Length of tracks */
60 unsigned long curr_track_length[ePT_COUNT]; /* Current track length */
61 int curr_track_index; /* Index of currently playing track in playlist */
62 int curr_display_index; /* Display index of currently playing track */
63 int actual_index; /* Display index in actually counted tracks */
64 int counted; /* Number of tracks already added up */
65 int nb_tracks; /* Number of tracks in playlist */
66 int error_count; /* Number of tracks whose data couldn't be retrieved */
67 bool remaining_only; /* Whether to ignore elapsed tracks */
68};
69
70static int32_t single_mode_lang(void)
71{
72 switch (rb->global_settings->single_mode)
73 {
74 case SINGLE_MODE_ALBUM:
75 return LANG_ID3_ALBUM;
76 case SINGLE_MODE_ALBUM_ARTIST:
77 return LANG_ID3_ALBUMARTIST;
78 case SINGLE_MODE_ARTIST:
79 return LANG_ID3_ARTIST;
80 case SINGLE_MODE_COMPOSER:
81 return LANG_ID3_COMPOSER;
82 case SINGLE_MODE_GROUPING:
83 return LANG_ID3_GROUPING;
84 case SINGLE_MODE_GENRE:
85 return LANG_ID3_GENRE;
86 case SINGLE_MODE_TRACK:
87 return LANG_TRACK;
88 }
89 return LANG_OFF;
90}
91
92static char* single_mode_id3_tag(struct mp3entry *id3)
93{
94 switch (rb->global_settings->single_mode)
95 {
96 case SINGLE_MODE_ALBUM:
97 return id3->album;
98 case SINGLE_MODE_ALBUM_ARTIST:
99 return id3->albumartist;
100 case SINGLE_MODE_ARTIST:
101 return id3->artist;
102 case SINGLE_MODE_COMPOSER:
103 return id3->composer;
104 case SINGLE_MODE_GROUPING:
105 return id3->grouping;
106 case SINGLE_MODE_GENRE:
107 return id3->genre_string;
108 }
109 return NULL;
110}
111
112static char* get_percent_str(long percents)
113{
114 static char val[10];
115 rb->snprintf(val, sizeof(val), rb->str(LANG_PERCENT_FORMAT), percents);
116 return val;
117}
118
119static inline void prepare_time_string(char *buf, size_t buffer_len,
120 long elapsed_pct, const char *timestr1,
121 const char *timestr2)
122{
123 if (rb->lang_is_rtl())
124 rb->snprintf(buf, buffer_len, "%s %s / %s",
125 get_percent_str(elapsed_pct), timestr2, timestr1);
126 else
127 rb->snprintf(buf, buffer_len, "%s / %s %s",
128 timestr1, timestr2, get_percent_str(elapsed_pct));
129}
130
131/* list callback for playing_time screen */
132static const char * pt_get_or_speak_info(int selected_item, void * data,
133 char *buf, size_t buffer_len,
134 bool say_it)
135{
136 long elapsed_pct; /* percentage of duration elapsed */
137 struct playing_time_info *pti = (struct playing_time_info *)data;
138 int info_no = selected_item/2;
139 const int menu_name_id = menu_items[info_no];
140
141 /* header */
142 if (!say_it && !(selected_item % 2))
143 return rb->str(menu_name_id);
144
145 /* data */
146 switch(info_no) {
147 case 0: { /* playlist remaining time */
148 char timestr[25];
149 rb->format_time_auto(timestr, sizeof(timestr),
150 pti->length[ePT_REMAINING], UNIT_SEC, false);
151 rb->snprintf(buf, buffer_len, "%s", timestr);
152
153 if (say_it)
154 rb_talk_ids(false, menu_name_id,
155 TALK_ID(pti->length[ePT_REMAINING], UNIT_TIME));
156 break;
157 }
158 case 1: { /* elapsed and total time */
159 char timestr1[25], timestr2[25];
160 rb->format_time_auto(timestr1, sizeof(timestr1),
161 pti->length[ePT_ELAPSED], UNIT_SEC, true);
162
163 rb->format_time_auto(timestr2, sizeof(timestr2),
164 pti->length[ePT_TOTAL], UNIT_SEC, true);
165
166 if (pti->length[ePT_TOTAL] == 0)
167 elapsed_pct = 0;
168 else if (pti->length[ePT_TOTAL] <= 0xFFFFFF)
169 {
170 elapsed_pct = (pti->length[ePT_ELAPSED] * 100
171 / pti->length[ePT_TOTAL]);
172 }
173 else /* sacrifice some precision to avoid overflow */
174 {
175 elapsed_pct = (pti->length[ePT_ELAPSED] >> 7) * 100
176 / (pti->length[ePT_TOTAL] >> 7);
177 }
178 prepare_time_string(buf, buffer_len, elapsed_pct, timestr1, timestr2);
179
180 if (say_it)
181 rb_talk_ids(false, menu_name_id,
182 TALK_ID(pti->length[ePT_ELAPSED], UNIT_TIME),
183 VOICE_OF,
184 TALK_ID(pti->length[ePT_TOTAL], UNIT_TIME),
185 VOICE_PAUSE,
186 TALK_ID(elapsed_pct, UNIT_PERCENT));
187 break;
188 }
189 case 2: { /* track remaining time */
190 char timestr[25];
191 rb->format_time_auto(timestr, sizeof(timestr),
192 pti->curr_track_length[ePT_REMAINING], UNIT_SEC, false);
193 rb->snprintf(buf, buffer_len, "%s", timestr);
194
195 if (say_it)
196 rb_talk_ids(false, menu_name_id,
197 TALK_ID(pti->curr_track_length[ePT_REMAINING], UNIT_TIME));
198 break;
199 }
200 case 3: { /* track elapsed and duration */
201 char timestr1[25], timestr2[25];
202
203 rb->format_time_auto(timestr1, sizeof(timestr1),
204 pti->curr_track_length[ePT_ELAPSED], UNIT_SEC, true);
205 rb->format_time_auto(timestr2, sizeof(timestr2),
206 pti->curr_track_length[ePT_TOTAL], UNIT_SEC, true);
207
208 if (pti->curr_track_length[ePT_TOTAL] == 0)
209 elapsed_pct = 0;
210 else if (pti->curr_track_length[ePT_TOTAL] <= 0xFFFFFF)
211 {
212 elapsed_pct = (pti->curr_track_length[ePT_ELAPSED] * 100
213 / pti->curr_track_length[ePT_TOTAL]);
214 }
215 else /* sacrifice some precision to avoid overflow */
216 {
217 elapsed_pct = (pti->curr_track_length[ePT_ELAPSED] >> 7) * 100
218 / (pti->curr_track_length[ePT_TOTAL] >> 7);
219 }
220 prepare_time_string(buf, buffer_len, elapsed_pct, timestr1, timestr2);
221
222 if (say_it)
223 rb_talk_ids(false, menu_name_id,
224 TALK_ID(pti->curr_track_length[ePT_ELAPSED], UNIT_TIME),
225 VOICE_OF,
226 TALK_ID(pti->curr_track_length[ePT_TOTAL], UNIT_TIME),
227 VOICE_PAUSE,
228 TALK_ID(elapsed_pct, UNIT_PERCENT));
229 break;
230 }
231 case 4: { /* track index */
232 int track_pct = pti->actual_index * 100 / pti->counted;
233
234 if (rb->lang_is_rtl())
235 rb->snprintf(buf, buffer_len, "%s %d / %d", get_percent_str(track_pct),
236 pti->counted, pti->actual_index);
237 else
238 rb->snprintf(buf, buffer_len, "%d / %d %s", pti->actual_index,
239 pti->counted, get_percent_str(track_pct));
240
241 if (say_it)
242 rb_talk_ids(false, menu_name_id,
243 TALK_ID(pti->actual_index, UNIT_INT),
244 VOICE_OF,
245 TALK_ID(pti->counted, UNIT_INT),
246 VOICE_PAUSE,
247 TALK_ID(track_pct, UNIT_PERCENT));
248 break;
249 }
250 case 5: { /* storage size */
251 int i;
252 char kbstr[ePT_COUNT][20];
253
254 for (i = 0; i < ePT_COUNT; i++) {
255 rb->output_dyn_value(kbstr[i], sizeof(kbstr[i]),
256 pti->size[i], kibyte_units, 3, true);
257 }
258 rb->snprintf(buf, buffer_len, "%s (%s / %s)", kbstr[ePT_TOTAL],
259 kbstr[ePT_ELAPSED], kbstr[ePT_REMAINING]);
260
261 if (say_it) {
262 int32_t voice_ids[ePT_COUNT];
263 voice_ids[ePT_TOTAL] = menu_name_id;
264 voice_ids[ePT_ELAPSED] = VOICE_PLAYTIME_DONE;
265 voice_ids[ePT_REMAINING] = LANG_REMAINING;
266
267 for (i = 0; i < ePT_COUNT; i++)
268 {
269 rb_talk_ids(i > 0, VOICE_PAUSE, voice_ids[i]);
270 rb->output_dyn_value(NULL, 0, pti->size[i], kibyte_units, 3, true);
271 }
272 }
273 break;
274 }
275 case 6: { /* Average track file size */
276 char str[20];
277 long avg_track_size = pti->size[ePT_TOTAL] / pti->counted;
278 rb->output_dyn_value(str, sizeof(str), avg_track_size, kibyte_units, 3, true);
279 rb->snprintf(buf, buffer_len, "%s", str);
280
281 if (say_it) {
282 rb->talk_id(menu_name_id, false);
283 rb->output_dyn_value(NULL, 0, avg_track_size, kibyte_units, 3, true);
284 }
285 break;
286 }
287 case 7: { /* Average bitrate */
288 /* Convert power of 2 kilobytes to power of 10 kilobits */
289 long avg_bitrate = (pti->size[ePT_TOTAL] / pti->length[ePT_TOTAL]
290 * 1024 * 8 / 1000);
291 rb->snprintf(buf, buffer_len, "%ld kbps", avg_bitrate);
292
293 if (say_it)
294 rb_talk_ids(false, menu_name_id,
295 TALK_ID(avg_bitrate, UNIT_KBIT));
296 break;
297 }
298 }
299 return buf;
300}
301
302static const char * pt_get_info(int selected_item, void * data,
303 char *buffer, size_t buffer_len)
304{
305 return pt_get_or_speak_info(selected_item, data,
306 buffer, buffer_len, false);
307}
308
309static int pt_speak_info(int selected_item, void * data)
310{
311 static char buffer[MAX_PATH];
312 pt_get_or_speak_info(selected_item, data, buffer, sizeof(buffer), true);
313 return 0;
314}
315
316static bool pt_display_stats(struct playing_time_info *pti)
317{
318 struct gui_synclist pt_lists;
319 rb->gui_synclist_init(&pt_lists, &pt_get_info, pti, true, 2, NULL);
320 if (rb->global_settings->talk_menu)
321 rb->gui_synclist_set_voice_callback(&pt_lists, pt_speak_info);
322 rb->gui_synclist_set_nb_items(&pt_lists, pti->remaining_only ? 2 : 8*2);
323 rb->gui_synclist_set_title(&pt_lists, *pti->single_mode_tag ?
324 rb->str(single_mode_lang()) :
325 rb->str(LANG_PLAYLIST), NOICON);
326 rb->gui_synclist_draw(&pt_lists);
327 rb->gui_synclist_speak_item(&pt_lists);
328 while (true)
329 {
330 int action = rb->get_action(CONTEXT_LIST, HZ/2);
331 if (rb->gui_synclist_do_button(&pt_lists, &action) == 0
332 && action != ACTION_NONE && action != ACTION_UNKNOWN)
333 {
334 bool usb = rb->default_event_handler(action) == SYS_USB_CONNECTED;
335
336 if (!usb && IS_SYSEVENT(action))
337 continue;
338
339 rb->talk_force_shutup();
340 return usb;
341 }
342 }
343 return false;
344}
345
346static const char *pt_options_name(int selected_item, void * data,
347 char *buf, size_t buf_size)
348{
349 (void) data;
350 (void) buf;
351 (void) buf_size;
352 return selected_item == 0 ? rb->str(LANG_ALL) :
353 selected_item == 1 ? rb->str(LANG_REMAINING) :
354 rb->str(single_mode_lang());
355}
356
357static int pt_options_speak(int selected_item, void * data)
358{
359 (void) data;
360 rb->talk_id(selected_item == 0 ? LANG_ALL :
361 selected_item == 1 ? LANG_REMAINING :
362 single_mode_lang(), false);
363 return 0;
364}
365
366static int pt_options(struct playing_time_info *pti)
367{
368 struct gui_synclist pt_options;
369 rb->gui_synclist_init(&pt_options, &pt_options_name, NULL, true, 1, NULL);
370 if (rb->global_settings->talk_menu)
371 rb->gui_synclist_set_voice_callback(&pt_options, pt_options_speak);
372 rb->gui_synclist_set_nb_items(&pt_options, *pti->single_mode_tag ? 3 : 2);
373 rb->gui_synclist_set_title(&pt_options, rb->str(LANG_PLAYING_TIME), NOICON);
374 rb->gui_synclist_draw(&pt_options);
375 rb->gui_synclist_speak_item(&pt_options);
376
377 while(true)
378 {
379 int button = rb->get_action(CONTEXT_LIST, HZ);
380 if (rb->gui_synclist_do_button(&pt_options, &button))
381 continue;
382 switch(button)
383 {
384 case ACTION_STD_OK:
385 {
386 int sel = rb->gui_synclist_get_sel_pos(&pt_options);
387 if (sel < 2)
388 *pti->single_mode_tag = 0;
389 if (sel == 1)
390 pti->remaining_only = true;
391 return -1;
392 }
393 case ACTION_STD_CANCEL:
394 return 0;
395 default:
396 if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
397 return 1;
398 }
399 }
400}
401
402static void pt_store_converted_totals(struct playing_time_info *pti)
403{
404 /* convert units from ms to s */
405 pti->length[ePT_ELAPSED] /= 1000;
406 pti->length[ePT_REMAINING] /= 1000;
407 pti->curr_track_length[ePT_ELAPSED] /= 1000;
408 pti->curr_track_length[ePT_REMAINING] /= 1000;
409 /* convert units from Bytes to KiB */
410 pti->size[ePT_ELAPSED] >>= 10;
411 pti->size[ePT_REMAINING] >>= 10;
412
413 pti->length[ePT_TOTAL] = pti->length[ePT_ELAPSED] + pti->length[ePT_REMAINING];
414 pti->curr_track_length[ePT_TOTAL] = pti->curr_track_length[ePT_ELAPSED]
415 + pti->curr_track_length[ePT_REMAINING];
416 pti->size[ePT_TOTAL] = pti->size[ePT_ELAPSED] + pti->size[ePT_REMAINING];
417}
418
419static int pt_add_track(int i, enum ePT_SUM section, struct playing_time_info *pti)
420{
421 static struct mp3entry id3;
422 static struct playlist_track_info pl_track;
423 int progress_total = pti->remaining_only ?
424 (pti->nb_tracks - pti->curr_display_index) + 1 :
425 pti->nb_tracks;
426
427 /* (voiced) */
428 rb->splash_progress(pti->counted, progress_total, "%s (%s)",
429 rb->str(LANG_WAIT), rb->str(LANG_OFF_ABORT));
430
431 if (rb->action_userabort(TIMEOUT_NOBLOCK))
432 return -1;
433 else if (rb->playlist_get_track_info(NULL, i, &pl_track) < 0
434 || !rb->get_metadata(&id3, -1, pl_track.filename))
435 {
436 pti->error_count++;
437 return -2;
438 }
439 else if(*pti->single_mode_tag && /* single mode tag doesn't match */
440 rb->strcmp(pti->single_mode_tag, single_mode_id3_tag(&id3) ?: ""))
441 return 1;
442
443 pti->length[section] += id3.length;
444 pti->size[section] += id3.filesize;
445 pti->counted++;
446 return 0;
447}
448
449static bool pt_add_remaining(struct playing_time_info *pti)
450{
451 int display_index = pti->curr_display_index + 1;
452 for (int i = pti->curr_track_index + 1; display_index <= pti->nb_tracks; i++, display_index++)
453 {
454 if (i == pti->nb_tracks)
455 i = 0;
456
457 int ret = pt_add_track(i, ePT_REMAINING, pti);
458 if (ret == 1)
459 break;
460 else if (ret == -1)
461 return false;
462 }
463 return true;
464}
465
466static bool pt_add_elapsed(struct playing_time_info *pti)
467{
468 int display_index = pti->curr_display_index - 1;
469 for (int i = pti->curr_track_index - 1; display_index > 0; i--, display_index--)
470 {
471 if (i < 0)
472 i = pti->nb_tracks - 1;
473
474 int ret = pt_add_track(i, ePT_ELAPSED, pti);
475 if (ret == 1)
476 break;
477 else if (ret == -1)
478 return false;
479 else if (ret == 0)
480 pti->actual_index++;
481 }
482 return true;
483}
484
485static bool pt_add_curr_track(struct playing_time_info *pti)
486{
487 struct mp3entry *curr_id3 = rb->audio_current_track();
488 rb->playlist_get_resume_info(&pti->curr_track_index);
489
490 if (pti->curr_track_index == -1 || !curr_id3)
491 return false;
492
493 pti->curr_display_index = rb->playlist_get_display_index();
494 pti->length[ePT_ELAPSED] = pti->curr_track_length[ePT_ELAPSED]
495 = curr_id3->elapsed;
496 pti->length[ePT_REMAINING] = pti->curr_track_length[ePT_REMAINING]
497 = curr_id3->length - curr_id3->elapsed;
498 pti->size[ePT_ELAPSED] = curr_id3->offset;
499 pti->size[ePT_REMAINING] = curr_id3->filesize - curr_id3->offset;
500 pti->actual_index = pti->counted = 1;
501 rb->strlcpy(pti->single_mode_tag, single_mode_id3_tag(curr_id3) ?: "",
502 sizeof(pti->single_mode_tag));
503 return true;
504}
505
506/* playing time screen: shows total and elapsed playlist duration and
507 other stats */
508static bool playing_time(void)
509{
510 struct playing_time_info pti;
511 rb->memset(&pti, 0, sizeof(struct playing_time_info));
512
513 if (!pt_add_curr_track(&pti))
514 return false;
515
516 int opt = pt_options(&pti);
517 if (opt > -1)
518 return opt;
519
520#ifdef HAVE_ADJUSTABLE_CPU_FREQ
521 rb->cpu_boost(true);
522#endif
523 rb->splash_progress_set_delay(HZ/2);
524 pti.nb_tracks = rb->playlist_amount();
525 int success = (pti.remaining_only || pt_add_elapsed(&pti)) && pt_add_remaining(&pti);
526#ifdef HAVE_ADJUSTABLE_CPU_FREQ
527 rb->cpu_boost(false);
528#endif
529 if (!success)
530 return false;
531 if (pti.error_count > 0)
532 rb->splash(HZ, ID2P(LANG_PLAYTIME_ERROR));
533
534 pt_store_converted_totals(&pti);
535 return pt_display_stats(&pti);
536}
537
538/* this is the plugin entry point */
539enum plugin_status plugin_start(const void* parameter)
540{
541 (void)parameter;
542 enum plugin_status status = PLUGIN_OK;
543
544 if (!rb->audio_status())
545 {
546 rb->splash(HZ*2, "Nothing Playing");
547 return status;
548 }
549
550 FOR_NB_SCREENS(i)
551 rb->viewportmanager_theme_enable(i, true, NULL);
552
553 if (playing_time())
554 status = PLUGIN_USB_CONNECTED;
555
556 FOR_NB_SCREENS(i)
557 rb->viewportmanager_theme_undo(i, false);
558
559 return status;
560}