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 * Copyright (C) 2003-2005 Jörg Hohensohn
12 * Copyright (C) 2020 BILGUS
13 *
14 *
15 *
16 * Usage: Start plugin, it will stay in the background.
17 *
18 *
19 * This program is free software; you can redistribute it and/or
20 * modify it under the terms of the GNU General Public License
21 * as published by the Free Software Foundation; either version 2
22 * of the License, or (at your option) any later version.
23 *
24 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
25 * KIND, either express or implied.
26 *
27 ****************************************************************************/
28
29#include "plugin.h"
30#include "lib/helper.h"
31#include "lib/kbd_helper.h"
32#include "lib/configfile.h"
33
34/****************** constants ******************/
35#define MAX_GROUPS 7
36#define MAX_ANNOUNCE_WPS 63
37#define ANNOUNCEMENT_TIMEOUT 10
38#define GROUPING_CHAR ';'
39
40#define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF)
41#define EV_OTHINSTANCE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFE)
42#define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01)
43#define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02)
44
45#define CFG_FILE "/VoiceTSR.cfg"
46#define CFG_VER 1
47
48#define THREAD_STACK_SIZE 4*DEFAULT_STACK_SIZE
49
50#if CONFIG_RTC
51 #define K_TIME "DT D1;\n\n"
52 #define K_DATE "DD D2;\n\n"
53#else
54 #define K_TIME ""
55 #define K_DATE ""
56#endif
57
58#define K_TRACK_TA "TT TA;\n"
59#define K_TRACK "TE TL TR;\n"
60#define K_TRACK1 "T1 T2 T3;\n\n"
61#define K_PLAYLIST "PC PN PR P1 P2;\n"
62#define K_BATTERY "BP BM B1;\n"
63#define K_SLEEP "RS R2 R3;\n"
64#define K_RUNTIME "RT R1;"
65
66static const char keybd_layout[] =
67 K_TIME K_DATE K_TRACK_TA K_TRACK K_TRACK1 K_PLAYLIST K_BATTERY K_SLEEP K_RUNTIME;
68
69/* - each character in keybd_layout will consume one element
70 * - \n does not create a key, but it also consumes one element
71 * - the final null terminator is equivalent to \n
72 * - since sizeof includes the null terminator we don't need +1 for that. */
73static ucschar_t kbd_buf[sizeof(keybd_layout)];
74
75/****************** prototypes ******************/
76void print_scroll(char* string); /* implements a scrolling screen */
77
78int get_playtime(void); /* return the current track time in seconds */
79int get_tracklength(void); /* return the total length of the current track */
80int get_track(void); /* return the track number */
81void get_playmsg(void); /* update the play message with Rockbox info */
82
83void thread_create(void);
84void thread(void); /* the thread running it all */
85void thread_quit(void);
86static int voice_general_info(bool testing);
87static unsigned char* voice_info_group(unsigned char* current_token, bool testing);
88
89int plugin_main(const void* parameter); /* main loop */
90enum plugin_status plugin_start(const void* parameter); /* entry */
91
92
93/****************** data types ******************/
94
95/****************** globals ******************/
96/* communication to the worker thread */
97static struct
98{
99 bool exiting; /* signal to the thread that we want to exit */
100 bool resume;
101 unsigned int id; /* worker thread id */
102 struct event_queue queue; /* thread event queue */
103 long stack[THREAD_STACK_SIZE / sizeof(long)];
104} gThread;
105
106static struct
107{
108 int interval;
109 int announce_on;
110 int grouping;
111
112 int timeout;
113 int count;
114 unsigned int index;
115 int bin_added;
116
117 bool show_prompt;
118
119 unsigned char wps_fmt[MAX_ANNOUNCE_WPS+1];
120} gAnnounce;
121
122static struct configdata config[] =
123{
124 {TYPE_INT, 0, 10000, { .int_p = &gAnnounce.interval }, "Interval", NULL},
125 {TYPE_INT, 0, 2, { .int_p = &gAnnounce.announce_on }, "Announce", NULL},
126 {TYPE_INT, 0, 10, { .int_p = &gAnnounce.grouping }, "Grouping", NULL},
127 {TYPE_INT, 0, 10000, { .int_p = &gAnnounce.bin_added }, "Added", NULL},
128 {TYPE_BOOL, 0, 1, { .bool_p = &gAnnounce.show_prompt }, "Prompt", NULL},
129 {TYPE_STRING, 0, MAX_ANNOUNCE_WPS+1,
130 { .string = (char*)&gAnnounce.wps_fmt }, "Fmt", NULL},
131};
132
133const int gCfg_sz = sizeof(config)/sizeof(*config);
134/****************** communication with Rockbox playback ******************/
135
136#if 0
137/* return the track number */
138int get_track(void)
139{
140 //if (rb->audio_status() == (AUDIO_STATUS_PLAY | AUDIO_STATUS_PAUSE))
141 struct mp3entry* p_mp3entry;
142
143 p_mp3entry = rb->audio_current_track();
144 if (p_mp3entry == NULL)
145 return 0;
146
147 return p_mp3entry->index + 1; /* track numbers start with 1 */
148}
149#endif
150
151static void playback_event_callback(unsigned short id, void *data)
152{
153 (void)id;
154 (void)data;
155 if (gThread.id > 0)
156 rb->queue_post(&gThread.queue, EV_TRACKCHANGE, 0);
157}
158
159/****************** config functions *****************/
160static void config_set_defaults(void)
161{
162 gAnnounce.bin_added = 0;
163 gAnnounce.interval = ANNOUNCEMENT_TIMEOUT;
164 gAnnounce.announce_on = 0;
165 gAnnounce.grouping = 0;
166 gAnnounce.wps_fmt[0] = '\0';
167 gAnnounce.show_prompt = true;
168}
169
170static void config_reset_voice(void)
171{
172 /* don't want to change these so save a copy */
173 int interval = gAnnounce.interval;
174 int announce = gAnnounce.announce_on;
175 int grouping = gAnnounce.grouping;
176
177 if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0)
178 {
179 rb->splash(100, "ERROR!");
180 return;
181 }
182
183 /* restore other settings */
184 gAnnounce.interval = interval;
185 gAnnounce.announce_on = announce;
186 gAnnounce.grouping = grouping;
187}
188
189/****************** helper fuctions ******************/
190void announce(void)
191{
192 rb->talk_force_shutup();
193 rb->sleep(HZ / 2);
194 voice_general_info(false);
195 if (rb->talk_id(VOICE_PAUSE, true) < 0)
196 rb->beep_play(800, 100, 1000);
197 //rb->talk_force_enqueue_next();
198}
199
200static void announce_test(void)
201{
202 rb->talk_force_shutup();
203 rb->sleep(HZ / 2);
204 voice_info_group(gAnnounce.wps_fmt, true);
205 rb->splash(HZ, "...");
206 //rb->talk_force_enqueue_next();
207}
208
209static void announce_add(const char *str)
210{
211 int len_cur = rb->strlen(gAnnounce.wps_fmt);
212 int len_str = rb->strlen(str);
213 if (len_cur + len_str > MAX_ANNOUNCE_WPS)
214 return;
215 rb->strcpy(&gAnnounce.wps_fmt[len_cur], str);
216 announce_test();
217
218}
219
220static int _playlist_get_display_index(struct playlist_info *playlist)
221{
222 /* equivalent of the function found in playlist.c */
223 if(!playlist)
224 return -1;
225 /* first_index should always be index 0 for display purposes */
226 int index = playlist->index;
227 index -= playlist->first_index;
228 if (index < 0)
229 index += playlist->amount;
230
231 return index + 1;
232}
233
234static enum themable_icons icon_callback(int selected_item, void * data)
235{
236 (void)data;
237
238 if(selected_item < MAX_GROUPS && selected_item >= 0)
239 {
240 int bin = 1 << (selected_item);
241 if ((gAnnounce.bin_added & bin) == bin)
242 return Icon_Submenu;
243 }
244
245 return Icon_NOICON;
246}
247
248static int announce_menu_cb(int action,
249 const struct menu_item_ex *this_item,
250 struct gui_synclist *this_list)
251{
252 (void)this_item;
253 ucschar_t *kbd_p;
254
255 int selection = rb->gui_synclist_get_sel_pos(this_list);
256
257 if(action == ACTION_ENTER_MENUITEM)
258 {
259 rb->gui_synclist_set_icon_callback(this_list, icon_callback);
260 }
261 else if ((action == ACTION_STD_OK))
262 {
263 //rb->splashf(100, "%d", selection);
264 if (selection < MAX_GROUPS && selection >= 0) /* only add premade tags once */
265 {
266 int bin = 1 << (selection);
267 if ((gAnnounce.bin_added & bin) == bin)
268 return 0;
269
270 gAnnounce.bin_added |= bin;
271 }
272
273 switch(selection) {
274
275 case 0: /*Time*/
276 announce_add("D1Dt ;");
277 break;
278 case 1: /*Date*/
279 announce_add("D2Dd ;");
280 break;
281 case 2: /*Track*/
282 announce_add("TT TA T1TeT2Tr ;");
283 break;
284 case 3: /*Playlist*/
285 announce_add("P1PC P2PN ;");
286 break;
287 case 4: /*Battery*/
288 announce_add("B1Bp ;");
289 break;
290 case 5: /*Sleep*/
291 announce_add("R2RsR3 ;");
292 break;
293 case 6: /*Runtime*/
294 announce_add("R1Rt ;");
295 break;
296 case 7: /* sep */
297 break;
298 case 8: /*Clear All*/
299 gAnnounce.wps_fmt[0] = '\0';
300 gAnnounce.bin_added = 0;
301 rb->splash(HZ / 2, ID2P(LANG_RESET_DONE_CLEAR));
302 break;
303 case 9: /* inspect it */
304 kbd_p = kbd_buf;
305 if (!kbd_create_layout(keybd_layout, kbd_p, sizeof(kbd_buf)))
306 kbd_p = NULL;
307
308 rb->kbd_input(gAnnounce.wps_fmt, MAX_ANNOUNCE_WPS, kbd_p);
309 break;
310 case 10: /*test it*/
311 announce_test();
312 break;
313 case 11: /*cancel*/
314 config_reset_voice();
315 return ACTION_STD_CANCEL;
316 case 12: /* save */
317 return ACTION_STD_CANCEL;
318 default:
319 return action;
320 }
321 rb->gui_synclist_draw(this_list); /* redraw */
322 return 0;
323 }
324
325 return action;
326}
327
328static int announce_menu(void)
329{
330 int selection = 0;
331
332 MENUITEM_STRINGLIST(announce_menu, "Announcements", announce_menu_cb,
333 ID2P(LANG_TIME),
334 ID2P(LANG_DATE),
335 ID2P(LANG_TRACK),
336 ID2P(LANG_PLAYLIST),
337 ID2P(LANG_BATTERY_MENU),
338 ID2P(LANG_SLEEP_TIMER),
339 ID2P(LANG_RUNNING_TIME),
340 ID2P(VOICE_BLANK),
341 ID2P(LANG_CLEAR_ALL),
342 ID2P(LANG_ANNOUNCEMENT_FMT),
343 ID2P(LANG_VOICE),
344 ID2P(LANG_CANCEL_0),
345 ID2P(LANG_SAVE));
346
347 selection = rb->do_menu(&announce_menu, &selection, NULL, true);
348 if (selection == MENU_ATTACHED_USB)
349 return PLUGIN_USB_CONNECTED;
350
351 return 0;
352}
353
354/**
355 Shows the settings menu
356 */
357static int settings_menu(void)
358{
359 int selection = 0;
360 //bool old_val;
361
362 MENUITEM_STRINGLIST(settings_menu, "Announce Settings", NULL,
363 ID2P(LANG_TIMEOUT),
364 ID2P(LANG_ANNOUNCE_ON),
365 ID2P(LANG_GROUPING),
366 ID2P(LANG_ANNOUNCEMENT_FMT),
367 ID2P(VOICE_BLANK),
368 ID2P(LANG_MENU_QUIT),
369 ID2P(LANG_SAVE_EXIT));
370
371 static const struct opt_items announce_options[] = {
372 { STR(LANG_OFF)},
373 { STR(LANG_TRACK_CHANGE)},
374 };
375
376 do {
377 selection=rb->do_menu(&settings_menu,&selection, NULL, true);
378 switch(selection) {
379
380 case 0:
381 rb->set_int(rb->str(LANG_TIMEOUT), "s", UNIT_SEC,
382 &gAnnounce.interval, NULL, 1, 1, 360, NULL );
383 break;
384 case 1:
385 rb->set_option(rb->str(LANG_ANNOUNCE_ON),
386 &gAnnounce.announce_on, RB_INT, announce_options, 2, NULL);
387 break;
388 case 2:
389 rb->set_int(rb->str(LANG_GROUPING), "", 1,
390 &gAnnounce.grouping, NULL, 1, 0, 7, NULL );
391 break;
392 case 3:
393 announce_menu();
394 break;
395 case 4: /*sep*/
396 continue;
397 case 5: /* quit the plugin */
398 return -1;
399 break;
400 case 6:
401 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
402 return 0;
403 break;
404
405 case MENU_ATTACHED_USB:
406 return PLUGIN_USB_CONNECTED;
407 default:
408 return 0;
409 }
410 } while ( selection >= 0 );
411 return 0;
412}
413
414
415/****************** main thread + helper ******************/
416void thread(void)
417{
418 bool in_usb = false;
419 long interval;
420 long last_tick = *rb->current_tick; /* for 1 sec tick */
421
422 struct queue_event ev;
423 while (!gThread.exiting)
424 {
425 rb->queue_wait(&gThread.queue, &ev);
426 interval = gAnnounce.interval * HZ;
427 switch (ev.id)
428 {
429 case SYS_USB_CONNECTED:
430 rb->usb_acknowledge(SYS_USB_CONNECTED_ACK);
431 in_usb = true;
432 break;
433 case SYS_USB_DISCONNECTED:
434 in_usb = false;
435 /*fall through*/
436 case EV_STARTUP:
437 if (!gThread.resume)
438 rb->beep_play(1500, 100, 1000);
439 break;
440 case EV_EXIT:
441 return;
442 case EV_OTHINSTANCE:
443 if (*rb->current_tick - last_tick >= interval)
444 {
445 last_tick += interval;
446 rb->sleep(HZ / 10);
447 if (!in_usb) announce();
448 }
449 break;
450 case EV_TRACKCHANGE:
451 rb->sleep(HZ / 10);
452 if (!in_usb) announce();
453 break;
454 }
455 }
456}
457
458void thread_create(void)
459{
460 /* put the thread's queue in the bcast list */
461 rb->queue_init(&gThread.queue, true);
462 gThread.id = rb->create_thread(thread, gThread.stack, sizeof(gThread.stack),
463 0, "vTSR"
464 IF_PRIO(, PRIORITY_BACKGROUND)
465 IF_COP(, CPU));
466 rb->queue_post(&gThread.queue, EV_STARTUP, 0);
467 rb->yield();
468}
469
470void thread_quit(void)
471{
472 if (!gThread.exiting) {
473 rb->queue_post(&gThread.queue, EV_EXIT, 0);
474 rb->thread_wait(gThread.id);
475 /* we don't want any more events */
476 rb->remove_event(PLAYBACK_EVENT_TRACK_CHANGE, playback_event_callback);
477
478 /* remove the thread's queue from the broadcast list */
479 rb->queue_delete(&gThread.queue);
480 gThread.exiting = true;
481 }
482}
483
484static bool check_user_input(void)
485{
486 int i = 0;
487 rb->button_clear_queue();
488 if (rb->button_get_w_tmo(HZ) > BUTTON_NONE)
489 {
490 while ((rb->button_get(false) & BUTTON_REL) != BUTTON_REL)
491 {
492 if (i & 1)
493 rb->beep_play(800, 100, 1000 - i * (1000 / 15));
494
495 if (++i > 15)
496 {
497 return true;
498 }
499 rb->sleep(HZ / 5);
500 }
501 }
502 return false;
503}
504
505/* callback to end the TSR plugin, called before a new one gets loaded */
506static int exit_tsr(bool reenter)
507{
508 if (reenter)
509 {
510 rb->queue_post(&gThread.queue, EV_OTHINSTANCE, 0);
511
512 /* quit the plugin if user holds a button */
513 if (check_user_input() == true)
514 {
515 if (settings_menu() < 0)
516 return PLUGIN_TSR_TERMINATE; /*kill TSR dont let it start again */
517 }
518 return PLUGIN_TSR_CONTINUE; /* dont let new plugin start*/
519 }
520 thread_quit();
521
522 return PLUGIN_TSR_SUSPEND;
523}
524
525
526/****************** main ******************/
527
528int plugin_main(const void* parameter)
529{
530 rb->memset(&gThread, 0, sizeof(gThread));
531
532 gAnnounce.index = 0;
533 gAnnounce.timeout = 0;
534 /* Resume plugin ? */
535 if (parameter == rb->plugin_tsr)
536 {
537 gThread.resume = true;
538 }
539 else
540 {
541 rb->splash(HZ / 2, "Announce Status");
542 if (gAnnounce.show_prompt)
543 {
544 if (rb->mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK) != CHANNEL_PLAYING)
545 {
546 rb->talk_id(LANG_HOLD_FOR_SETTINGS, false);
547 }
548 rb->splash(HZ, ID2P(LANG_HOLD_FOR_SETTINGS));
549 }
550
551
552 if (check_user_input() == true)
553 {
554 rb->splash(100, ID2P(LANG_SETTINGS));
555 int ret = settings_menu();
556 if (ret < 0)
557 return 0;
558 }
559 }
560
561 gAnnounce.timeout = *rb->current_tick;
562 rb->plugin_tsr(exit_tsr); /* stay resident */
563
564 if (gAnnounce.announce_on == 1)
565 rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, playback_event_callback);
566
567 thread_create();
568
569 return 0;
570}
571
572
573/***************** Plugin Entry Point *****************/
574
575
576enum plugin_status plugin_start(const void* parameter)
577{
578 /* now go ahead and have fun! */
579 if (rb->usb_inserted() == true)
580 return PLUGIN_USB_CONNECTED;
581
582 config_set_defaults();
583 if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0)
584 {
585 /* If the loading failed, save a new config file */
586 config_set_defaults();
587 configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
588 rb->splash(HZ, ID2P(LANG_HOLD_FOR_SETTINGS));
589 }
590
591 int ret = plugin_main(parameter);
592 return (ret==0) ? PLUGIN_OK : PLUGIN_ERROR;
593}
594
595static int voice_general_info(bool testing)
596{
597 unsigned char* infotemplate = gAnnounce.wps_fmt;
598
599 if (gAnnounce.index >= rb->strlen(gAnnounce.wps_fmt))
600 gAnnounce.index = 0;
601
602 long current_tick = *rb->current_tick;
603
604 if (*infotemplate == 0)
605 {
606 #if CONFIG_RTC
607 /* announce the time */
608 voice_info_group("D1Dt ", false);
609 #else
610 /* announce elapsed play for this track */
611 voice_info_group("T1Te ", false);
612 #endif
613 return -1;
614 }
615
616 if (TIME_BEFORE(current_tick, gAnnounce.timeout))
617 {
618 return -2;
619 }
620
621 gAnnounce.timeout = current_tick + gAnnounce.interval * HZ;
622
623 rb->talk_shutup();
624
625 gAnnounce.count = 0;
626 infotemplate = voice_info_group(&infotemplate[gAnnounce.index], testing);
627 gAnnounce.index = (infotemplate - gAnnounce.wps_fmt) + 1;
628
629 return 0;
630}
631
632static unsigned char* voice_info_group(unsigned char* current_token, bool testing)
633{
634 unsigned char current_char;
635 bool skip_next_group = false;
636 gAnnounce.count = 0;
637
638 while (*current_token != 0)
639 {
640 //rb->splash(10, current_token);
641 current_char = toupper(*current_token);
642 if (current_char == 'D')
643 {
644 /*
645 Date and time functions
646 */
647 current_token++;
648
649 current_char = toupper(*current_token);
650
651#if CONFIG_RTC
652 struct tm *tm = rb->get_time();
653
654 if (true) //(valid_time(tm))
655 {
656 if (current_char == 'T')
657 {
658 rb->talk_time(tm, true);
659 }
660 else if (current_char == 'D')
661 {
662 rb->talk_date(tm, true);
663 }
664 /* prefix suffix connectives */
665 else if (current_char == '1')
666 {
667 rb->talk_id(LANG_TIME, true);
668 }
669 else if (current_char == '2')
670 {
671 rb->talk_id(LANG_DATE, true);
672 }
673 }
674#endif
675 }
676 else if (current_char == 'R')
677 {
678 /*
679 Sleep timer and runtime
680 */
681 int sleep_remaining = rb->get_sleep_timer();
682 int runtime;
683
684 current_token++;
685 current_char = toupper(*current_token);
686 if (current_char == 'T')
687 {
688 runtime = rb->global_status->runtime;
689 talk_val(runtime, UNIT_TIME, true);
690 }
691 /* prefix suffix connectives */
692 else if (current_char == '1')
693 {
694 rb->talk_id(LANG_RUNNING_TIME, true);
695 }
696 else if (testing || sleep_remaining > 0)
697 {
698 if (current_char == 'S')
699 {
700 talk_val(sleep_remaining, UNIT_TIME, true);
701 }
702 /* prefix suffix connectives */
703 else if (current_char == '2')
704 {
705 rb->talk_id(LANG_SLEEP_TIMER, true);
706 }
707 else if (current_char == '3')
708 {
709 rb->talk_id(LANG_REMAIN, true);
710 }
711 }
712 else if (sleep_remaining == 0)
713 {
714 skip_next_group = true;
715 }
716
717 }
718 else if (current_char == 'T')
719 {
720 /*
721 Current track information
722 */
723 current_token++;
724
725 current_char = toupper(*current_token);
726
727 struct mp3entry* id3 = rb->audio_current_track();
728
729 int elapsed_length = id3->elapsed / 1000;
730 int track_length = id3->length / 1000;
731 int track_remaining = track_length - elapsed_length;
732
733 if (current_char == 'E')
734 {
735 talk_val(elapsed_length, UNIT_TIME, true);
736 }
737 else if (current_char == 'L')
738 {
739 talk_val(track_length, UNIT_TIME, true);
740 }
741 else if (current_char == 'R')
742 {
743 talk_val(track_remaining, UNIT_TIME, true);
744 }
745 else if (current_char == 'T' && id3->title)
746 {
747 rb->talk_spell(id3->title, true);
748 }
749 else if (current_char == 'A' && id3->artist)
750 {
751 rb->talk_spell(id3->artist, true);
752 }
753 else if (current_char == 'A' && id3->albumartist)
754 {
755 rb->talk_spell(id3->albumartist, true);
756 }
757 /* prefix suffix connectives */
758 else if (current_char == '1')
759 {
760 rb->talk_id(LANG_ELAPSED, true);
761 }
762 else if (current_char == '2')
763 {
764 rb->talk_id(LANG_REMAIN, true);
765 }
766 else if (current_char == '3')
767 {
768 rb->talk_id(LANG_OF, true);
769 }
770 }
771 else if (current_char == 'P')
772 {
773 /*
774 Current playlist information
775 */
776 current_token++;
777
778 current_char = toupper(*current_token);
779 struct playlist_info *pl;
780 int current_track = 0;
781 int remaining_tracks = 0;
782 int total_tracks = rb->playlist_amount();
783
784 if (!isdigit(current_char)) {
785 pl = rb->playlist_get_current();
786 current_track = _playlist_get_display_index(pl);
787 remaining_tracks = total_tracks - current_track;
788 }
789
790 if (total_tracks > 0 || testing)
791 {
792 if (current_char == 'C')
793 {
794 rb->talk_number(current_track, true);
795 }
796 else if (current_char == 'N')
797 {
798 rb->talk_number(total_tracks, true);
799 }
800 else if (current_char == 'R')
801 {
802 rb->talk_number(remaining_tracks, true);
803 }
804 /* prefix suffix connectives */
805 else if (current_char == '1')
806 {
807 rb->talk_id(LANG_TRACK, true);
808 }
809 else if (current_char == '2')
810 {
811 rb->talk_id(LANG_OF, true);
812 }
813 }
814 else if (total_tracks == 0)
815 skip_next_group = true;
816 }
817 else if (current_char == 'B')
818 {
819 /*
820 Battery
821 */
822 current_token++;
823
824 current_char = toupper(*current_token);
825
826 if (current_char == 'P')
827 {
828 talk_val(rb->battery_level(), UNIT_PERCENT, true);
829 }
830 else if (current_char == 'M')
831 {
832 talk_val(rb->battery_time() * 60, UNIT_TIME, true);
833 }
834 /* prefix suffix connectives */
835 else if (current_char == '1')
836 {
837 rb->talk_id(LANG_BATTERY_TIME, true);
838 }
839 }
840 else if (current_char == ' ')
841 {
842 /*
843 Catch your breath
844 */
845 rb->talk_id(VOICE_PAUSE, true);
846 }
847 else if (current_char == GROUPING_CHAR && gAnnounce.grouping > 0)
848 {
849 gAnnounce.count++;
850
851 if (gAnnounce.count >= gAnnounce.grouping && !testing && !skip_next_group)
852 break;
853
854 skip_next_group = false;
855
856 }
857 current_token++;
858 }
859
860 return current_token;
861}