/*************************************************************************** * Copyright (C) 2022 by Federico Amedeo Izzo IU2NUO, * * Niccolò Izzo IU2KIN, * * Silvano Seva IU2KWO * * Joseph Stephen VK7JS * * 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 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see * ***************************************************************************/ /** * This file contains functions for announcing radio functions using the * building blocks in voicePrompts.h/c. */ #include "core/voicePromptUtils.h" #include #include #include #include #include #include #include #include "interfaces/cps_io.h" static void clearCurrPromptIfNeeded(const vpQueueFlags_t flags) { if (flags & vpqInit) vp_flush(); } static void playIfNeeded(const vpQueueFlags_t flags) { uint8_t vpLevel = state.settings.vpLevel; if ((flags & vpqPlayImmediately) || ((flags & vpqPlayImmediatelyAtMediumOrHigher) && (vpLevel >= vpMedium))) vp_play(); } static void addSilenceIfNeeded(const vpQueueFlags_t flags) { if ((flags & vpqAddSeparatingSilence) == 0) return; vp_queuePrompt(PROMPT_SILENCE); vp_queuePrompt(PROMPT_SILENCE); } static void removeUnnecessaryZerosFromVoicePrompts(char* str) { const int NUM_DECIMAL_PLACES = 1; int len = strlen(str); for (int i = len; i > 2; i--) { if ((str[i - 1] != '0') || (str[i - (NUM_DECIMAL_PLACES + 1)] == '.')) { str[i] = '\0'; return; } } } void vp_announceChannelName(const channel_t* channel, const uint16_t channelNumber, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_CHANNEL); } vp_queueInteger(channelNumber); // Only queue the name if it is not the same as the raw number. // Otherwise the radio will repeat channel 1 channel 1 for channel 1. char numAsStr[16] = "\0"; snprintf(numAsStr, 16, "Channel%d", channelNumber); if (strcmp(numAsStr, channel->name) != 0) { vp_queueString(channel->name, vpAnnounceCommonSymbols); } playIfNeeded(flags); } void vp_queueFrequency(const freq_t freq) { char buffer[16]; int MHz = (freq / 1000000); int kHz = ((freq % 1000000) / 10); snprintf(buffer, 16, "%d.%05d", MHz, kHz); removeUnnecessaryZerosFromVoicePrompts(buffer); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_MEGAHERTZ); } void vp_announceFrequencies(const freq_t rx, const freq_t tx, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); // If rx and tx frequencies differ, announce both, otherwise just one if (rx == tx) { vp_queueFrequency(rx); } else { vp_queuePrompt(PROMPT_RECEIVE); vp_queueFrequency(rx); vp_queuePrompt(PROMPT_TRANSMIT); vp_queueFrequency(tx); } playIfNeeded(flags); } void vp_announceRadioMode(const uint8_t mode, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_MODE); } switch (mode) { case OPMODE_DMR: vp_queueStringTableEntry(¤tLanguage->dmr); break; case OPMODE_FM: vp_queueStringTableEntry(¤tLanguage->fm); break; case OPMODE_M17: vp_queueStringTableEntry(¤tLanguage->m17); break; } playIfNeeded(flags); } void vp_announceBandwidth(const uint8_t bandwidth, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_BANDWIDTH); } char* bandwidths[] = {"12.5", "20", "25"}; vp_queueString(bandwidths[bandwidth], vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_KILOHERTZ); playIfNeeded(flags); } void vp_anouncePower(const float power, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_POWER); } char buffer[16] = "\0"; snprintf(buffer, 16, "%1.1f", power); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_WATTS); playIfNeeded(flags); } void vp_announceChannelSummary(const channel_t* channel, const uint16_t channelNumber, const uint16_t bank, const vpSummaryInfoFlags_t infoFlags) { if (channel == NULL) return; vp_flush(); vpQueueFlags_t localFlags = vpqAddSeparatingSilence; // Force on the descriptions for level 3. if (state.settings.vpLevel == vpHigh) { localFlags |= vpqIncludeDescriptions; } if (infoFlags&vpSplashInfo) vp_queueStringTableEntry(¤tLanguage->openRTX); // If VFO mode, announce VFO. // channelNumber will be 0 if called from VFO mode. if (infoFlags&vpChannelNameOrVFO) { if (channelNumber == 0) { vp_queuePrompt(PROMPT_VFO); } else { vp_announceChannelName(channel, channelNumber, localFlags); } addSilenceIfNeeded(localFlags); } if (infoFlags&vpFrequencies) vp_announceFrequencies(channel->rx_frequency, channel->tx_frequency, localFlags); if (infoFlags&vpRadioMode) { vp_announceRadioMode(channel->mode, localFlags); addSilenceIfNeeded(localFlags); } if (infoFlags&vpModeSpecificInfo) { if (channel->mode == OPMODE_FM) { vp_announceBandwidth(channel->bandwidth, localFlags); addSilenceIfNeeded(localFlags); if (channel->fm.rxToneEn || channel->fm.txToneEn) { vp_announceCTCSS(channel->fm.rxToneEn, channel->fm.rxTone, channel->fm.txToneEn, channel->fm.txTone, localFlags); } } else if (channel->mode == OPMODE_M17) { vp_announceM17Info(channel, false, localFlags); } else if (channel->mode == OPMODE_DMR) { vp_announceContactWithIndex(channel->dmr.contact_index, localFlags); // Force announcement of the words timeslot and colorcode to avoid // ambiguity. vp_announceTimeslot(channel->dmr.dmr_timeslot, (localFlags | vpqIncludeDescriptions)); vp_announceColorCode(channel->dmr.rxColorCode, channel->dmr.txColorCode, (localFlags | vpqIncludeDescriptions)); } addSilenceIfNeeded(localFlags); } if (infoFlags&vpPower) { float power = dBmToWatt(channel->power); vp_anouncePower(power, localFlags); addSilenceIfNeeded(localFlags); } if ((infoFlags&vpBankNameOrAllChannels) &&(channelNumber > 0)) // i.e. not called from VFO. { vp_announceBank(bank, localFlags); } vp_play(); } void vp_announceInputChar(const char ch) { char buf[2] = "\0"; buf[0] = ch; vp_flush(); uint8_t flags = vpAnnounceSpace | vpAnnounceCommonSymbols | vpAnnounceLessCommonSymbols; vp_queueString(buf, flags); vp_play(); } void vp_announceInputReceiveOrTransmit(const bool tx, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (tx) vp_queuePrompt(PROMPT_TRANSMIT); else vp_queuePrompt(PROMPT_RECEIVE); playIfNeeded(flags); } void vp_replayLastPrompt() { if (vp_isPlaying()) vp_stop(); else vp_play(); } void vp_announceError(const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); vp_queueStringTableEntry(¤tLanguage->error); playIfNeeded(flags); } void vp_announceText(const char* text, const vpQueueFlags_t flags) { if ((text == NULL) || (*text == '\0')) return; clearCurrPromptIfNeeded(flags); // See if we have a prompt for this string. int offset = GetEnglishStringTableOffset(text); if (offset != -1) vp_queueStringTableEntry( (const char* const*)(¤tLanguage->languageName + offset)); else // Just spell it out vp_queueString(text, vpAnnounceCommonSymbols); playIfNeeded(flags); } void vp_announceCTCSS(const bool rxToneEnabled, const uint8_t rxTone, const bool txToneEnabled, const uint8_t txTone, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if ((rxToneEnabled == false) && (txToneEnabled == false)) { if (flags & vpqIncludeDescriptions) vp_queuePrompt(PROMPT_TONE); vp_queueStringTableEntry(¤tLanguage->off); playIfNeeded(flags); return; } char buffer[16] = "\0"; // If the rx and tx tones are the same and both are enabled, just say Tone. if ((rxToneEnabled && txToneEnabled) && (rxTone == txTone)) { if (flags & vpqIncludeDescriptions) vp_queuePrompt(PROMPT_TONE); snprintf(buffer, 16, "%3.1f", ctcss_tone[rxTone] / 10.0f); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_HERTZ); playIfNeeded(flags); return; } // Speak the individual rx and tx tones. if (rxToneEnabled) { if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_RECEIVE); vp_queuePrompt(PROMPT_TONE); } snprintf(buffer, 16, "%3.1f", ctcss_tone[rxTone] / 10.0f); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_HERTZ); } if (txToneEnabled) { if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_TRANSMIT); vp_queuePrompt(PROMPT_TONE); } snprintf(buffer, 16, "%3.1f", ctcss_tone[txTone] / 10.0f); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_HERTZ); } playIfNeeded(flags); } void vp_announceSquelch(const uint8_t squelch, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_SQUELCH); } vp_queueInteger(squelch); playIfNeeded(flags); } void vp_announceContact(const contact_t* contact, const vpQueueFlags_t flags) { if (contact == NULL) return; clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_CONTACT); } if (contact->name[0] != '\0') { vp_queueString(contact->name, vpAnnounceCommonSymbols); } playIfNeeded(flags); } void vp_announceContactWithIndex(const uint16_t index, const vpQueueFlags_t flags) { if (index == 0) return; contact_t contact; if (cps_readContact(&contact, index) == -1) return; vp_announceContact(&contact, flags); } void vp_announceTimeslot(const uint8_t timeslot, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_TIMESLOT); } vp_queueInteger(timeslot); playIfNeeded(flags); } void vp_announceColorCode(const uint8_t rxColorCode, const uint8_t txColorCode, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_COLORCODE); } if (rxColorCode == txColorCode) { vp_queueInteger(rxColorCode); } else { vp_queuePrompt(PROMPT_RECEIVE); vp_queueInteger(rxColorCode); vp_queuePrompt(PROMPT_TRANSMIT); vp_queueInteger(txColorCode); } playIfNeeded(flags); } void vp_announceBank(const uint16_t bank, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (state.bank_enabled) { bankHdr_t bank_hdr = {0}; cps_readBankHeader(&bank_hdr, bank); vp_queueString(bank_hdr.name, vpAnnounceCommonSymbols); } else { vp_queueStringTableEntry(¤tLanguage->allChannels); } playIfNeeded(flags); } void vp_announceM17Info(const channel_t* channel, bool isEditing, const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_DEST_ID); } if (isEditing) { vp_queuePrompt(PROMPT_EDIT); } else if (state.m17_data.dst_addr[0] != '\0') { vp_queueString(state.m17_data.dst_addr, vpAnnounceCommonSymbols); } else if ((channel != NULL) && (channel->m17.contact_index != 0)) { vp_announceContactWithIndex(channel->m17.contact_index, flags); } else { vp_queueStringTableEntry(¤tLanguage->broadcast); } playIfNeeded(flags); } #ifdef GPS_PRESENT // cardinal point plus or minus this value is still considered cardinal point. #define margin 3 static bool IsCompassCloseEnoughToCardinalPoint() { float tmg_true = state.gps_data.tmg_true; return (tmg_true < (0+margin) || tmg_true > (360-margin)) || // north (tmg_true > (90-margin) && tmg_true < (90+margin)) || // east (tmg_true > (180-margin) && tmg_true < (180+margin)) || // south (tmg_true > (270-margin) && tmg_true < (270+margin)) || // west (tmg_true > (45-margin) && tmg_true < (45+margin)) || // n.w. (tmg_true > (135-margin) && tmg_true < (135+margin)) || // s.e. (tmg_true > (225-margin) && tmg_true < (225+margin)) || // s.w. (tmg_true > (315-margin) && tmg_true < (315+margin)); // n.w. } void vp_announceGPSInfo(vpGPSInfoFlags_t gpsInfoFlags) { vp_flush(); vpQueueFlags_t flags = vpqIncludeDescriptions | vpqAddSeparatingSilence; if (gpsInfoFlags & vpGPSIntro) { vp_queueStringTableEntry(¤tLanguage->gps); if (!state.settings.gps_enabled) { vp_queueStringTableEntry(¤tLanguage->off); vp_play(); return; } } if (gpsInfoFlags & vpGPSFixQuality) { switch (state.gps_data.fix_quality) { case 0: vp_queueStringTableEntry(¤tLanguage->noFix); vp_play(); return; case 1: vp_queueString("SPS", vpAnnounceCommonSymbols); break; case 2: vp_queueString("DGPS", vpAnnounceCommonSymbols); break; case 3: vp_queueString("PPS", vpAnnounceCommonSymbols); break; case 6: vp_queueStringTableEntry(¤tLanguage->fixLost); break; default: vp_queueStringTableEntry(¤tLanguage->error); vp_play(); return; } addSilenceIfNeeded(flags); } if (gpsInfoFlags & vpGPSFixType) { switch (state.gps_data.fix_type) { case 2: vp_queueString("2D", vpAnnounceCommonSymbols); break; case 3: vp_queueString("3D", vpAnnounceCommonSymbols); break; } addSilenceIfNeeded(flags); } char buffer[17] = "\0"; if (gpsInfoFlags & vpGPSDirection) { vp_queuePrompt(PROMPT_COMPASS); if (!IsCompassCloseEnoughToCardinalPoint()) { snprintf(buffer, 16, "%3.1f", state.gps_data.tmg_true); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_DEGREES); } if (state.gps_data.tmg_true < (45+margin) || state.gps_data.tmg_true > (315-margin)) { vp_queuePrompt(PROMPT_NORTH); } if (state.gps_data.tmg_true > (45-margin) && state.gps_data.tmg_true < (135+margin)) { vp_queuePrompt(PROMPT_EAST); } if (state.gps_data.tmg_true > (135-margin) && state.gps_data.tmg_true < (225+margin)) { vp_queuePrompt(PROMPT_SOUTH); } if (state.gps_data.tmg_true > (225-margin) && state.gps_data.tmg_true < (315+margin)) { vp_queuePrompt(PROMPT_WEST); } addSilenceIfNeeded(flags); } if (gpsInfoFlags & vpGPSSpeed) { // speed/altitude: snprintf(buffer, 16, "%4.1fkm/h", state.gps_data.speed); vp_queuePrompt(PROMPT_SPEED); vp_queueString(buffer, vpAnnounceCommonSymbols|vpAnnounceLessCommonSymbols); } if (gpsInfoFlags & vpGPSAltitude) { vp_queuePrompt(PROMPT_ALTITUDE); snprintf(buffer, 16, "%4.1fm", state.gps_data.altitude); vp_queueString(buffer, vpAnnounceCommonSymbols); addSilenceIfNeeded(flags); } if (gpsInfoFlags & vpGPSLatitude) { // lat/long snprintf(buffer, 16, "%8.6f", state.gps_data.latitude); removeUnnecessaryZerosFromVoicePrompts(buffer); vp_queuePrompt(PROMPT_LATITUDE); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(PROMPT_NORTH); } if (gpsInfoFlags & vpGPSLongitude) { float longitude = state.gps_data.longitude; voicePrompt_t direction = (longitude < 0) ? PROMPT_WEST : PROMPT_EAST; longitude = (longitude < 0) ? -longitude : longitude; snprintf(buffer, 16, "%8.6f", longitude); removeUnnecessaryZerosFromVoicePrompts(buffer); vp_queuePrompt(PROMPT_LONGITUDE); vp_queueString(buffer, vpAnnounceCommonSymbols); vp_queuePrompt(direction); addSilenceIfNeeded(flags); } if (gpsInfoFlags & vpGPSSatCount) { vp_queuePrompt(PROMPT_SATELLITES); vp_queueInteger(state.gps_data.satellites_in_view); } vp_play(); } #endif // GPS_PRESENT void vp_announceAboutScreen() { vp_flush(); vp_queueStringTableEntry(¤tLanguage->openRTX); vp_queueStringTableEntry(¤tLanguage->Niccolo); vp_queueStringTableEntry(¤tLanguage->Silvano); vp_queueStringTableEntry(¤tLanguage->Federico); vp_queueStringTableEntry(¤tLanguage->Fred); vp_queueStringTableEntry(¤tLanguage->Joseph); vp_play(); } void vp_announceBackupScreen() { vp_flush(); vp_queueStringTableEntry(¤tLanguage->flashBackup); vp_queueStringTableEntry(¤tLanguage->connectToRTXTool); vp_queueStringTableEntry(¤tLanguage->toBackupFlashAnd); vp_queueStringTableEntry(¤tLanguage->pressPTTToStart); vp_queuePrompt(PROMPT_VP_UNAVAILABLE); vp_play(); } void vp_announceRestoreScreen() { vp_flush(); vp_queueStringTableEntry(¤tLanguage->flashRestore); vp_queueStringTableEntry(¤tLanguage->connectToRTXTool); vp_queueStringTableEntry(¤tLanguage->toRestoreFlashAnd); vp_queueStringTableEntry(¤tLanguage->pressPTTToStart); vp_queuePrompt(PROMPT_VP_UNAVAILABLE); vp_play(); } #ifdef RTC_PRESENT void vp_announceSettingsTimeDate() { vp_flush(); vp_queueStringTableEntry(¤tLanguage->timeAndDate); datetime_t local_time = utcToLocalTime(state.time, state.settings.utc_timezone); char buffer[16] = "\0"; snprintf(buffer, 16, "%02d/%02d/%02d", local_time.date, local_time.month, local_time.year); vp_queueString(buffer, vpAnnounceCommonSymbols | vpAnnounceLessCommonSymbols); vp_queuePrompt(PROMPT_SILENCE); vp_queuePrompt(PROMPT_SILENCE); snprintf(buffer, 16, "%02d:%02d:%02d", local_time.hour, local_time.minute, local_time.second); vp_queueString(buffer, vpAnnounceCommonSymbols | vpAnnounceLessCommonSymbols); vp_play(); } #endif // RTC_PRESENT void vp_announceSettingsVoiceLevel(const vpQueueFlags_t flags) { clearCurrPromptIfNeeded(flags); switch (state.settings.vpLevel) { case vpNone: vp_queueStringTableEntry(¤tLanguage->off); break; case vpBeep: vp_queueStringTableEntry(¤tLanguage->beep); break; default: if (flags & vpqIncludeDescriptions) { vp_queuePrompt(PROMPT_VOICE_NAME); vp_queueStringTableEntry(¤tLanguage->level); } vp_queueInteger(state.settings.vpLevel-vpBeep); break; } playIfNeeded(flags); } void vp_announceSettingsOnOffToggle(const char* const* stringTableStringPtr, const vpQueueFlags_t flags, bool val) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) vp_queueStringTableEntry(stringTableStringPtr); vp_queueStringTableEntry(val ? ¤tLanguage->on : ¤tLanguage->off); playIfNeeded(flags); } void vp_announceSettingsInt(const char* const* stringTableStringPtr, const vpQueueFlags_t flags, int val) { clearCurrPromptIfNeeded(flags); if (flags & vpqIncludeDescriptions) vp_queueStringTableEntry(stringTableStringPtr); vp_queueInteger(val); playIfNeeded(flags); } void vp_announceScreen(uint8_t ui_screen) { const vpSummaryInfoFlags_t infoFlags = vpChannelNameOrVFO|vpFrequencies | vpRadioMode; switch (ui_screen) { case MAIN_VFO: vp_announceChannelSummary(&state.channel, 0, state.bank, infoFlags); break; case MAIN_MEM: vp_announceChannelSummary(&state.channel, state.channel_index+1, state.bank, infoFlags); break; #ifdef GPS_PRESENT case MENU_GPS: vp_announceGPSInfo(vpGPSAll); break; #endif // GPS_PRESENT case MENU_BACKUP: vp_announceBackupScreen(); break; case MENU_RESTORE: vp_announceRestoreScreen(); break; case MENU_ABOUT: vp_announceAboutScreen(); break; case SETTINGS_TIMEDATE: vp_announceSettingsTimeDate(); break; case SETTINGS_M17: vp_announceBuffer(¤tLanguage->callsign, false, true, state.settings.callsign); break; } } void vp_announceBuffer(const char* const* stringTableStringPtr, bool editMode, bool callsign, const char* buffer) { bool isPlaying=vp_isPlaying(); vp_flush(); if (!isPlaying) { vp_queueStringTableEntry(stringTableStringPtr); if (editMode) vp_queuePrompt(PROMPT_EDIT); } vpFlags_t flags= vpAnnounceCommonSymbols; // add edit mode flags to adjust what is spoken. // extra symbols not relevant when entering callsign. if (editMode && !callsign) flags |= vpAnnounceLessCommonSymbols | vpAnnounceSpace | vpAnnounceASCIIValueForUnknownChars; vp_queueString(buffer, flags); vp_play(); } void vp_announceDisplayTimer() { bool isPlaying=vp_isPlaying(); vp_flush(); if (!isPlaying) vp_queueStringTableEntry(¤tLanguage->timer); uint8_t seconds = 0; uint8_t minutes = 0; switch (state.settings.display_timer) { case TIMER_OFF: seconds=0; break; case TIMER_5S: case TIMER_10S: case TIMER_15S: case TIMER_20S: case TIMER_25S: case TIMER_30S: seconds=state.settings.display_timer*5; break; case TIMER_1M: case TIMER_2M: case TIMER_3M: case TIMER_4M: case TIMER_5M: minutes = (state.settings.display_timer - (TIMER_1M - 1)); break; case TIMER_15M: case TIMER_30M: case TIMER_45M: minutes = 15 * (state.settings.display_timer - (TIMER_15M - 1)); break; case TIMER_1H: minutes = 60; break; } if (seconds==0 && minutes==0) { vp_queueStringTableEntry(¤tLanguage->off); } else if (seconds > 0) { vp_queueInteger(seconds); vp_queuePrompt(PROMPT_SECONDS); } else if (minutes > 0) { vp_queueInteger(minutes); vp_queuePrompt(PROMPT_MINUTES); } vp_play(); } vpQueueFlags_t vp_getVoiceLevelQueueFlags() { uint8_t vpLevel = state.settings.vpLevel; vpQueueFlags_t flags = vpqInit | vpqAddSeparatingSilence; switch (vpLevel) { case vpNone: case vpBeep: return vpqDefault; case vpLow: // Play some immediately, other things on demand. flags |= vpqPlayImmediatelyAtMediumOrHigher; break; case vpMedium: // Play all immediately but without extra descriptions flags |= vpqPlayImmediately; break; case vpHigh: // Play immediately with descriptions unless speech is in progress. flags |= vpqPlayImmediately; if (!vp_isPlaying()) flags |= vpqIncludeDescriptions; break; } return flags; } void vp_playMenuBeepIfNeeded(bool firstItem) { // Since menus talk at levels above beep, there's no need to run this or you'll // get an unwanted click. if (state.settings.vpLevel != vpBeep) return; if (firstItem) vp_beep(BEEP_MENU_FIRST_ITEM, SHORT_BEEP); else vp_beep(BEEP_MENU_ITEM, SHORT_BEEP); }