From be04c4be0afbdef36f2968dc291af54360de8e23 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Wed, 15 Jul 2020 12:00:09 -0400 Subject: 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 --- apps/lang/english.lang | 206 ++++++++++ apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 5 + apps/plugins/announce_status.c | 842 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1054 insertions(+) create mode 100644 apps/plugins/announce_status.c (limited to 'apps') diff --git a/apps/lang/english.lang b/apps/lang/english.lang index a424fd1e7f..8079884426 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -15904,3 +15904,209 @@ xduoox20,xduoox3,xduoox3ii: "Double tap HOME to cancel." + + id: LANG_DATE + desc: for constructing time and date announcements + user: core + + *: "Date" + + + *: "Date" + + + *: "Date" + + + + id: LANG_CLEAR_ALL + desc: + user: core + + *: "Clear all" + + + *: "Clear all" + + + *: "Clear all" + + + + id: LANG_CANCEL_0 + desc: CANCEL. + user: core + + *: "Cancel" + + + *: "Cancel" + + + *: "Cancel" + + + + id: LANG_SAVE + desc: + user: core + + *: "Save" + + + *: "Save" + + + *: "Save" + + + + id: LANG_TIMEOUT + desc: + user: core + + *: "Timeout" + + + *: "Timeout" + + + *: "Timeout" + + + + id: LANG_TRACK + desc: used in track x of y constructs + user: core + + *: none + hotkey: "Track" + + + *: none + hotkey: "Track" + + + *: none + hotkey: "Track" + + + + id: LANG_ELAPSED + desc: prefix for elapsed playtime announcement + user: core + + *: none + hotkey: "Elapsed" + + + *: none + hotkey: "Elapsed" + + + *: none + hotkey: "Elapsed" + + + + id: LANG_ANNOUNCEMENT_FMT + desc: format for wps hotkey announcement + user: core + + *: none + hotkey: "Announcement format" + + + *: none + hotkey: "Announcement format" + + + *: none + hotkey: "Announcement format" + + + + id: LANG_REMAIN + desc: for constructs such as number of tracks remaining etc + user: core + + *: none + hotkey: "Remain" + + + *: none + hotkey: "Remain" + + + *: none + hotkey: "Remain" + + + + id: LANG_GROUPING + desc: + user: core + + *: none + hotkey: "Grouping" + + + *: none + hotkey: "Grouping" + + + *: none + hotkey: "Grouping" + + + + id: LANG_ANNOUNCE_ON + desc: + user: core + + *: none + hotkey: "Announce on" + + + *: none + hotkey: "Announce on" + + + *: none + hotkey: "Announce on" + + + + id: LANG_TRACK_CHANGE + desc: + user: core + + *: none + hotkey: "Track change" + + + *: none + hotkey: "Track change" + + + *: none + hotkey: "Track change" + + + + id: LANG_HOLD_FOR_SETTINGS + desc: + user: core + + *: none + hotkey: "Hold for settings" + + + *: none + hotkey: "Hold for settings" + + + *: none + hotkey: "Hold for settings" + + diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index bb8688347d..07bc4df847 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -1,6 +1,7 @@ 2048,games alpine_cdc,apps alarmclock,apps +announce_status,demos autostart,apps battery_bench,apps bench_scaler,apps diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 5d24bee8f6..0180c56faf 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -151,6 +151,11 @@ starfield.c vu_meter.c wormlet.c +#ifdef HAVE_HOTKEY +announce_status.c +#endif + + /* Plugins needing the grayscale lib on low-depth LCDs */ fire.c plasma.c 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; +} -- cgit