summaryrefslogtreecommitdiffstats
path: root/apps/plugins/announce_status.c
diff options
context:
space:
mode:
authorWilliam Wilgus <wilgus.william@gmail.com>2020-07-15 12:00:09 -0400
committerWilliam Wilgus <wilgus.william@gmail.com>2020-07-27 20:47:03 -0400
commitbe04c4be0afbdef36f2968dc291af54360de8e23 (patch)
tree8b4e87d48b38bf98f4f7eb8bcbd62cc196cbbaff /apps/plugins/announce_status.c
parent3f828e9244140e7860e961917fc96e42b2a866ca (diff)
downloadrockbox-be04c4be0a.tar.gz
rockbox-be04c4be0a.tar.bz2
rockbox-be04c4be0a.zip
Voice TSR Plugin Demo
allows user to run plugin in background that voices status messages grouping is now working it counts ; as the end of a group sleep timer remaining is not voiced if sleep timer is not active TODO manual entries Change-Id: I39e8500df6440c07d2a3347513c749d5e155d1cc
Diffstat (limited to 'apps/plugins/announce_status.c')
-rw-r--r--apps/plugins/announce_status.c842
1 files changed, 842 insertions, 0 deletions
diff --git a/apps/plugins/announce_status.c b/apps/plugins/announce_status.c
new file mode 100644
index 0000000000..e46899f8cd
--- /dev/null
+++ b/apps/plugins/announce_status.c
@@ -0,0 +1,842 @@
+/***************************************************************************
+ * __________ __ ___.
+ * Open \______ \ ____ ____ | | _\_ |__ _______ ___
+ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
+ * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
+ * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
+ * \/ \/ \/ \/ \/
+ * $Id$
+ *
+ *
+ * Copyright (C) 2003-2005 Jörg Hohensohn
+ * Copyright (C) 2020 BILGUS
+ *
+ *
+ *
+ * Usage: Start plugin, it will stay in the background.
+ *
+ *
+ * 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"
+#include "lib/kbd_helper.h"
+#include "lib/configfile.h"
+
+/****************** constants ******************/
+#define MAX_GROUPS 7
+#define MAX_ANNOUNCE_WPS 63
+#define ANNOUNCEMENT_TIMEOUT 10
+#define GROUPING_CHAR ';'
+
+#define EV_EXIT MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFF)
+#define EV_OTHINSTANCE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0xFE)
+#define EV_STARTUP MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x01)
+#define EV_TRACKCHANGE MAKE_SYS_EVENT(SYS_EVENT_CLS_PRIVATE, 0x02)
+
+#define CFG_FILE "/VoiceTSR.cfg"
+#define CFG_VER 1
+
+#if CONFIG_RTC
+ #define K_TIME "DT D1;\n\n"
+ #define K_DATE "DD D2;\n\n"
+#else
+ #define K_TIME ""
+ #define K_DATE ""
+#endif
+
+#define K_TRACK_TA "TT TA;\n"
+#define K_TRACK "TE TL TR;\n"
+#define K_TRACK1 "T1 T2 T3;\n\n"
+#define K_PLAYLIST "PC PN PR P1 P2;\n"
+#define K_BATTERY "BP BM B1;\n"
+#define K_SLEEP "RS R2 R3;\n"
+#define K_RUNTIME "RT R1;"
+#define KEYBD_LAYOUT (K_TIME K_DATE K_TRACK_TA K_TRACK K_TRACK1 K_PLAYLIST K_BATTERY K_SLEEP K_RUNTIME)
+
+/****************** prototypes ******************/
+void print_scroll(char* string); /* implements a scrolling screen */
+
+int get_playtime(void); /* return the current track time in seconds */
+int get_tracklength(void); /* return the total length of the current track */
+int get_track(void); /* return the track number */
+void get_playmsg(void); /* update the play message with Rockbox info */
+
+void thread_create(void);
+void thread(void); /* the thread running it all */
+void thread_quit(void);
+static int voice_general_info(bool testing);
+static unsigned char* voice_info_group(unsigned char* current_token, bool testing);
+
+int main(const void* parameter); /* main loop */
+enum plugin_status plugin_start(const void* parameter); /* entry */
+
+
+/****************** data types ******************/
+
+/****************** globals ******************/
+/* communication to the worker thread */
+static struct
+{
+ bool exiting; /* signal to the thread that we want to exit */
+ unsigned int id; /* worker thread id */
+ struct event_queue queue; /* thread event queue */
+ long *stack;
+ ssize_t stacksize;
+ void *buf;
+ size_t buf_size;
+
+} gThread;
+
+static struct
+{
+ int interval;
+ int announce_on;
+ int grouping;
+
+ int timeout;
+ int count;
+ unsigned int index;
+ int bin_added;
+
+ unsigned char wps_fmt[MAX_ANNOUNCE_WPS+1];
+} gAnnounce;
+
+static struct configdata config[] =
+{
+ {TYPE_INT, 0, 10000, { .int_p = &gAnnounce.interval }, "Interval", NULL},
+ {TYPE_INT, 0, 2, { .int_p = &gAnnounce.announce_on }, "Announce", NULL},
+ {TYPE_INT, 0, 10, { .int_p = &gAnnounce.grouping }, "Grouping", NULL},
+ {TYPE_INT, 0, 10000, { .int_p = &gAnnounce.bin_added }, "Added", NULL},
+ {TYPE_STRING, 0, MAX_ANNOUNCE_WPS+1,
+ { .string = (char*)&gAnnounce.wps_fmt }, "Fmt", NULL},
+};
+
+const int gCfg_sz = sizeof(config)/sizeof(*config);
+/****************** communication with Rockbox playback ******************/
+
+#if 0
+/* return the track number */
+int get_track(void)
+{
+ //if (rb->audio_status() == (AUDIO_STATUS_PLAY | AUDIO_STATUS_PAUSE))
+ struct mp3entry* p_mp3entry;
+
+ p_mp3entry = rb->audio_current_track();
+ if (p_mp3entry == NULL)
+ return 0;
+
+ return p_mp3entry->index + 1; /* track numbers start with 1 */
+}
+#endif
+
+static void playback_event_callback(unsigned short id, void *data)
+{
+ (void)id;
+ (void)data;
+ rb->queue_post(&gThread.queue, EV_TRACKCHANGE, 0);
+}
+
+/****************** config functions *****************/
+static void config_set_defaults(void)
+{
+ gAnnounce.bin_added = 0;
+ gAnnounce.interval = ANNOUNCEMENT_TIMEOUT;
+ gAnnounce.announce_on = 0;
+ gAnnounce.grouping = 0;
+ gAnnounce.wps_fmt[0] = '\0';
+}
+
+static void config_reset_voice(void)
+{
+ /* don't want to change these so save a copy */
+ int interval = gAnnounce.interval;
+ int announce = gAnnounce.announce_on;
+ int grouping = gAnnounce.grouping;
+
+ if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0)
+ {
+ rb->splash(100, "ERROR!");
+ return;
+ }
+
+ /* restore other settings */
+ gAnnounce.interval = interval;
+ gAnnounce.announce_on = announce;
+ gAnnounce.grouping = grouping;
+}
+
+/****************** helper fuctions ******************/
+
+void announce(void)
+{
+ rb->talk_force_shutup();
+ rb->sleep(HZ / 2);
+ voice_general_info(false);
+ //rb->talk_force_enqueue_next();
+}
+
+static void announce_test(void)
+{
+ rb->talk_force_shutup();
+ rb->sleep(HZ / 2);
+ voice_info_group(gAnnounce.wps_fmt, true);
+
+ //rb->talk_force_enqueue_next();
+}
+
+static void announce_add(const char *str)
+{
+ int len_cur = rb->strlen(gAnnounce.wps_fmt);
+ int len_str = rb->strlen(str);
+ if (len_cur + len_str > MAX_ANNOUNCE_WPS)
+ return;
+ rb->strcpy(&gAnnounce.wps_fmt[len_cur], str);
+ announce_test();
+
+}
+
+static int _playlist_get_display_index(struct playlist_info *playlist)
+{
+ /* equivalent of the function found in playlist.c */
+ if(!playlist)
+ return -1;
+ /* first_index should always be index 0 for display purposes */
+ int index = playlist->index;
+ index -= playlist->first_index;
+ if (index < 0)
+ index += playlist->amount;
+
+ return index + 1;
+}
+
+static enum themable_icons icon_callback(int selected_item, void * data)
+{
+ (void)data;
+
+ if(selected_item < MAX_GROUPS && selected_item >= 0)
+ {
+ int bin = 1 << (selected_item);
+ if ((gAnnounce.bin_added & bin) == bin)
+ return Icon_Submenu;
+ }
+
+ return Icon_NOICON;
+}
+
+static int announce_menu_cb(int action,
+ const struct menu_item_ex *this_item,
+ struct gui_synclist *this_list)
+{
+ (void)this_item;
+ unsigned short *kbd_p = gThread.buf;
+ size_t kbd_bufsz = gThread.buf_size;
+
+ int selection = rb->gui_synclist_get_sel_pos(this_list);
+
+ if(action == ACTION_ENTER_MENUITEM)
+ {
+ rb->gui_synclist_set_icon_callback(this_list, icon_callback);
+ }
+ else if ((action == ACTION_STD_OK))
+ {
+ //rb->splashf(100, "%d", selection);
+ if (selection < MAX_GROUPS && selection >= 0) /* only add premade tags once */
+ {
+ int bin = 1 << (selection);
+ if ((gAnnounce.bin_added & bin) == bin)
+ return 0;
+
+ gAnnounce.bin_added |= bin;
+ }
+
+ switch(selection) {
+
+ case 0: /*Time*/
+ announce_add("D1Dt ;");
+ break;
+ case 1: /*Date*/
+ announce_add("D2Dd ;");
+ break;
+ case 2: /*Track*/
+ announce_add("TT T1TeT2Tr ;");
+ break;
+ case 3: /*Playlist*/
+ announce_add("P1PC P2PN ;");
+ break;
+ case 4: /*Battery*/
+ announce_add("B1Bp ;");
+ break;
+ case 5: /*Sleep*/
+ announce_add("R2RsR3 ;");
+ break;
+ case 6: /*Runtime*/
+ announce_add("R1Rt ;");
+ break;
+ case 7: /* sep */
+ break;
+ case 8: /*Clear All*/
+ gAnnounce.wps_fmt[0] = '\0';
+ gAnnounce.bin_added = 0;
+ rb->splash(HZ / 2, ID2P(LANG_RESET_DONE_CLEAR));
+ break;
+ case 9: /* inspect it */
+ if (!kbd_create_layout(KEYBD_LAYOUT, kbd_p, kbd_bufsz))
+ kbd_p = NULL;
+
+ rb->kbd_input(gAnnounce.wps_fmt, MAX_ANNOUNCE_WPS, kbd_p);
+ break;
+ case 10: /*test it*/
+ announce_test();
+ break;
+ case 11: /*cancel*/
+ config_reset_voice();
+ return ACTION_STD_CANCEL;
+ case 12: /* save */
+ return ACTION_STD_CANCEL;
+ default:
+ return action;
+ }
+ rb->gui_synclist_draw(this_list); /* redraw */
+ return 0;
+ }
+
+ return action;
+}
+
+static int announce_menu(void)
+{
+ int selection = 0;
+
+ MENUITEM_STRINGLIST(announce_menu, "Announcements", announce_menu_cb,
+ ID2P(LANG_TIME),
+ ID2P(LANG_DATE),
+ ID2P(LANG_TRACK),
+ ID2P(LANG_PLAYLIST),
+ ID2P(LANG_BATTERY_MENU),
+ ID2P(LANG_SLEEP_TIMER),
+ ID2P(LANG_RUNNING_TIME),
+ ID2P(VOICE_BLANK),
+ ID2P(LANG_CLEAR_ALL),
+ ID2P(LANG_ANNOUNCEMENT_FMT),
+ ID2P(LANG_VOICE),
+ ID2P(LANG_CANCEL_0),
+ ID2P(LANG_SAVE));
+
+ selection = rb->do_menu(&announce_menu, &selection, NULL, true);
+ if (selection == MENU_ATTACHED_USB)
+ return PLUGIN_USB_CONNECTED;
+
+ return 0;
+}
+
+/**
+ Shows the settings menu
+ */
+static int settings_menu(void)
+{
+ int selection = 0;
+ //bool old_val;
+
+ MENUITEM_STRINGLIST(settings_menu, "Announce Settings", NULL,
+ ID2P(LANG_TIMEOUT),
+ ID2P(LANG_ANNOUNCE_ON),
+ ID2P(LANG_GROUPING),
+ ID2P(LANG_ANNOUNCEMENT_FMT),
+ ID2P(VOICE_BLANK),
+ ID2P(LANG_MENU_QUIT),
+ ID2P(LANG_SAVE_EXIT));
+
+ static const struct opt_items announce_options[] = {
+ { STR(LANG_OFF)},
+ { STR(LANG_TRACK_CHANGE)},
+ };
+
+ do {
+ selection=rb->do_menu(&settings_menu,&selection, NULL, true);
+ switch(selection) {
+
+ case 0:
+ rb->set_int(rb->str(LANG_TIMEOUT), "s", UNIT_SEC,
+ &gAnnounce.interval, NULL, 1, 1, 360, NULL );
+ break;
+ case 1:
+ rb->set_option(rb->str(LANG_ANNOUNCE_ON),
+ &gAnnounce.announce_on, INT, announce_options, 2, NULL);
+ break;
+ case 2:
+ rb->set_int(rb->str(LANG_GROUPING), "", 1,
+ &gAnnounce.grouping, NULL, 1, 0, 7, NULL );
+ break;
+ case 3:
+ announce_menu();
+ break;
+ case 4: /*sep*/
+ continue;
+ case 5:
+ return -1;
+ break;
+ case 6:
+ configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
+ return 0;
+ break;
+
+ case MENU_ATTACHED_USB:
+ return PLUGIN_USB_CONNECTED;
+ default:
+ return 0;
+ }
+ } while ( selection >= 0 );
+ return 0;
+}
+
+
+/****************** main thread + helper ******************/
+void thread(void)
+{
+ long interval;
+ long last_tick = *rb->current_tick; /* for 1 sec tick */
+
+ struct queue_event ev;
+ while (!gThread.exiting)
+ {
+ rb->queue_wait(&gThread.queue, &ev);
+ interval = gAnnounce.interval * HZ;
+ switch (ev.id)
+ {
+ case EV_EXIT:
+ return;
+ case EV_OTHINSTANCE:
+ if (*rb->current_tick - last_tick >= interval)
+ {
+ last_tick += interval;
+ rb->sleep(0);
+ announce();
+ }
+ break;
+ case EV_STARTUP:
+ rb->beep_play(1500, 100, 1000);
+ break;
+ case EV_TRACKCHANGE:
+ rb->sleep(0);
+ announce();
+ break;
+ }
+ }
+}
+
+void plugin_buffer_init(void)
+{
+ if (gThread.buf == 0)
+ {
+ rb->memset(&gThread, 0, sizeof(gThread));
+ gThread.buf = rb->plugin_get_buffer(&gThread.buf_size);
+ ALIGN_BUFFER(gThread.buf, gThread.buf_size, 4);
+ }
+}
+
+void thread_create(void)
+{
+ /* init the worker thread */
+ gThread.stacksize = gThread.buf_size;
+ gThread.buf_size -= gThread.stacksize;
+
+ gThread.stack = (long *) gThread.buf + gThread.buf_size;
+
+ ALIGN_BUFFER(gThread.stack, gThread.stacksize, 4);
+
+ if (gThread.stacksize < DEFAULT_STACK_SIZE)
+ {
+ rb->splash(HZ*2, "Out of memory");
+ gThread.exiting = true;
+ gThread.id = UINT_MAX;
+ return;
+ }
+
+
+ /* put the thread's queue in the bcast list */
+ rb->queue_init(&gThread.queue, true);
+
+ gThread.id = rb->create_thread(thread, gThread.stack, gThread.stacksize,
+ 0, "vTSR"
+ IF_PRIO(, PRIORITY_BACKGROUND)
+ IF_COP(, CPU));
+ rb->queue_post(&gThread.queue, EV_STARTUP, 0);
+ rb->yield();
+
+}
+
+void thread_quit(void)
+{
+ if (!gThread.exiting) {
+ rb->queue_post(&gThread.queue, EV_EXIT, 0);
+ rb->thread_wait(gThread.id);
+ /* remove the thread's queue from the broadcast list */
+ rb->queue_delete(&gThread.queue);
+ gThread.exiting = true;
+ }
+}
+
+/* callback to end the TSR plugin, called before a new one gets loaded */
+static bool exit_tsr(bool reenter)
+{
+ if (reenter)
+ {
+ rb->queue_post(&gThread.queue, EV_OTHINSTANCE, 0);
+ return false; /* dont let it start again */
+ }
+ thread_quit();
+
+ return true;
+}
+
+
+/****************** main ******************/
+
+int plugin_main(const void* parameter)
+{
+ (void)parameter;
+ bool settings = false;
+ int i = 0;
+
+ gAnnounce.index = 0;
+ gAnnounce.timeout = 0;
+
+ rb->talk_id(LANG_HOLD_FOR_SETTINGS, false);
+ rb->splash(HZ / 2, "Announce Status");
+
+ if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0)
+ {
+ /* If the loading failed, save a new config file */
+ config_set_defaults();
+ configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
+
+ rb->splash(HZ, ID2P(LANG_HOLD_FOR_SETTINGS));
+ }
+
+ rb->splash(HZ, ID2P(LANG_HOLD_FOR_SETTINGS));
+
+ rb->button_clear_queue();
+ if (rb->button_get_w_tmo(HZ) > BUTTON_NONE)
+ {
+ while ((rb->button_get(false) & BUTTON_REL) != BUTTON_REL)
+ {
+ if (i & 1)
+ rb->beep_play(800, 100, 1000);
+
+ if (++i > 15)
+ {
+ settings = true;
+ break;
+ }
+ sleep(HZ / 5);
+ }
+ }
+
+ plugin_buffer_init(); /* need buffer for custom keyboard layout */
+
+ if (settings)
+ {
+ rb->splash(100, ID2P(LANG_SETTINGS));
+ int ret = settings_menu();
+ if (ret < 0)
+ return 0;
+ }
+
+ gAnnounce.timeout = *rb->current_tick;
+ rb->plugin_tsr(exit_tsr); /* stay resident */
+
+ if (gAnnounce.announce_on == 1)
+ rb->add_event(PLAYBACK_EVENT_TRACK_CHANGE, playback_event_callback);
+
+ thread_create();
+#ifdef DEBUG
+ return rb->default_event_handler(button);
+#else
+ return 0;
+#endif
+}
+
+
+/***************** Plugin Entry Point *****************/
+
+
+enum plugin_status plugin_start(const void* parameter)
+{
+ /* now go ahead and have fun! */
+ int ret = plugin_main(parameter);
+
+ rb->remove_event(PLAYBACK_EVENT_START_PLAYBACK, playback_event_callback);
+ return (ret==0) ? PLUGIN_OK : PLUGIN_ERROR;
+}
+
+static int voice_general_info(bool testing)
+{
+ unsigned char* infotemplate = gAnnounce.wps_fmt;
+
+ if (gAnnounce.index >= rb->strlen(gAnnounce.wps_fmt))
+ gAnnounce.index = 0;
+
+ long current_tick = *rb->current_tick;
+
+ if (*infotemplate == 0)
+ {
+ #if CONFIG_RTC
+ /* announce the time */
+ voice_info_group("D1Dt ", false);
+ #else
+ /* announce elapsed play for this track */
+ voice_info_group("T1Te ", false);
+ #endif
+ return -1;
+ }
+
+ if (TIME_BEFORE(current_tick, gAnnounce.timeout))
+ {
+ return -2;
+ }
+
+ gAnnounce.timeout = current_tick + gAnnounce.interval * HZ;
+
+ rb->talk_shutup();
+
+ gAnnounce.count = 0;
+ infotemplate = voice_info_group(&infotemplate[gAnnounce.index], testing);
+ gAnnounce.index = (infotemplate - gAnnounce.wps_fmt) + 1;
+
+ return 0;
+}
+
+static unsigned char* voice_info_group(unsigned char* current_token, bool testing)
+{
+ unsigned char current_char;
+ bool skip_next_group = false;
+ gAnnounce.count = 0;
+
+ while (*current_token != 0)
+ {
+ //rb->splash(10, current_token);
+ current_char = toupper(*current_token);
+ if (current_char == 'D')
+ {
+ /*
+ Date and time functions
+ */
+ current_token++;
+
+ current_char = toupper(*current_token);
+
+#if CONFIG_RTC
+ struct tm *tm = rb->get_time();
+
+ if (true) //(valid_time(tm))
+ {
+ if (current_char == 'T')
+ {
+ rb->talk_time(tm, true);
+ }
+ else if (current_char == 'D')
+ {
+ rb->talk_date(tm, true);
+ }
+ /* prefix suffix connectives */
+ else if (current_char == '1')
+ {
+ rb->talk_id(LANG_TIME, true);
+ }
+ else if (current_char == '2')
+ {
+ rb->talk_id(LANG_DATE, true);
+ }
+ }
+#endif
+ }
+ else if (current_char == 'R')
+ {
+ /*
+ Sleep timer and runtime
+ */
+ int sleep_remaining = sleep_remaining = rb->get_sleep_timer();
+ int runtime;
+
+ current_token++;
+ current_char = toupper(*current_token);
+ if (current_char == 'T')
+ {
+ runtime = rb->global_status->runtime;
+ rb->talk_value(runtime, UNIT_TIME, true);
+ }
+ /* prefix suffix connectives */
+ else if (current_char == '1')
+ {
+ rb->talk_id(LANG_RUNNING_TIME, true);
+ }
+ else if (testing || sleep_remaining > 0)
+ {
+ if (current_char == 'S')
+ {
+ rb->talk_value(sleep_remaining, UNIT_TIME, true);
+ }
+ /* prefix suffix connectives */
+ else if (current_char == '2')
+ {
+ rb->talk_id(LANG_SLEEP_TIMER, true);
+ }
+ else if (current_char == '3')
+ {
+ rb->talk_id(LANG_REMAIN, true);
+ }
+ }
+ else if (sleep_remaining == 0)
+ {
+ skip_next_group = true;
+ }
+
+ }
+ else if (current_char == 'T')
+ {
+ /*
+ Current track information
+ */
+ current_token++;
+
+ current_char = toupper(*current_token);
+
+ struct mp3entry* id3 = rb->audio_current_track();
+
+ int elapsed_length = id3->elapsed / 1000;
+ int track_length = id3->length / 1000;
+ int track_remaining = track_length - elapsed_length;
+
+ if (current_char == 'E')
+ {
+ rb->talk_value(elapsed_length, UNIT_TIME, true);
+ }
+ else if (current_char == 'L')
+ {
+ rb->talk_value(track_length, UNIT_TIME, true);
+ }
+ else if (current_char == 'R')
+ {
+ rb->talk_value(track_remaining, UNIT_TIME, true);
+ }
+ else if (current_char == 'T' && id3->title)
+ {
+ rb->talk_spell(id3->title, true);
+ }
+ else if (current_char == 'A' && id3->albumartist)
+ {
+ rb->talk_spell(id3->albumartist, true);
+ }
+ /* prefix suffix connectives */
+ else if (current_char == '1')
+ {
+ rb->talk_id(LANG_ELAPSED, true);
+ }
+ else if (current_char == '2')
+ {
+ rb->talk_id(LANG_REMAIN, true);
+ }
+ else if (current_char == '3')
+ {
+ rb->talk_id(LANG_OF, true);
+ }
+ }
+ else if (current_char == 'P')
+ {
+ /*
+ Current playlist information
+ */
+ current_token++;
+
+ current_char = toupper(*current_token);
+ struct playlist_info *pl;
+ int current_track = 0;
+ int remaining_tracks = 0;
+ int total_tracks = rb->playlist_amount();
+
+ if (!isdigit(current_char)) {
+ pl = rb->playlist_get_current();
+ current_track = _playlist_get_display_index(pl);
+ remaining_tracks = total_tracks - current_track;
+ }
+
+ if (total_tracks > 0 || testing)
+ {
+ if (current_char == 'C')
+ {
+ rb->talk_number(current_track, true);
+ }
+ else if (current_char == 'N')
+ {
+ rb->talk_number(total_tracks, true);
+ }
+ else if (current_char == 'R')
+ {
+ rb->talk_number(remaining_tracks, true);
+ }
+ /* prefix suffix connectives */
+ else if (current_char == '1')
+ {
+ rb->talk_id(LANG_TRACK, true);
+ }
+ else if (current_char == '2')
+ {
+ rb->talk_id(LANG_OF, true);
+ }
+ }
+ else if (total_tracks == 0)
+ skip_next_group = true;
+ }
+ else if (current_char == 'B')
+ {
+ /*
+ Battery
+ */
+ current_token++;
+
+ current_char = toupper(*current_token);
+
+ if (current_char == 'P')
+ {
+ rb->talk_value(rb->battery_level(), UNIT_PERCENT, true);
+ }
+ else if (current_char == 'M')
+ {
+ rb->talk_value(rb->battery_time() * 60, UNIT_TIME, true);
+ }
+ /* prefix suffix connectives */
+ else if (current_char == '1')
+ {
+ rb->talk_id(LANG_BATTERY_TIME, true);
+ }
+ }
+ else if (current_char == ' ')
+ {
+ /*
+ Catch your breath
+ */
+ rb->talk_id(VOICE_PAUSE, true);
+ }
+ else if (current_char == GROUPING_CHAR && gAnnounce.grouping > 0)
+ {
+ gAnnounce.count++;
+
+ if (gAnnounce.count >= gAnnounce.grouping && !testing && !skip_next_group)
+ break;
+
+ skip_next_group = false;
+
+ }
+ current_token++;
+ }
+
+ return current_token;
+}