A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 861 lines 25 kB view raw
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}