/*************************************************************************** * Copyright (C) 2022 by Federico Amedeo Izzo IU2NUO, * * Niccolò Izzo IU2KIN, * * Silvano Seva IU2KWO * * Joseph Stephen VK7JS * * Roger Clark VK3KYY * * 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 * ***************************************************************************/ #include #include "core/voicePrompts.h" #include "ui/UIStrings.h" const uint32_t VOICE_PROMPTS_DATA_MAGIC = 0x5056;//'VP' const uint32_t VOICE_PROMPTS_DATA_VERSION = 0x1000; // v1000 OpenRTX #define VOICE_PROMPTS_TOC_SIZE 350 static void getM17Data(int offset,int length); typedef struct { uint32_t magic; uint32_t version; } voicePromptsDataHeader_t; const uint32_t VOICE_PROMPTS_FLASH_HEADER_ADDRESS = 0x8F400; // todo figure this out for OpenRTX static uint32_t vpFlashDataAddress;// = VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(voicePromptsDataHeader_t) + sizeof(uint32_t)*VOICE_PROMPTS_TOC_SIZE ; // TODO figure out M17 frame equivalent. // 76 x 27 byte ambe frames #define M17_DATA_BUFFER_SIZE 2052 bool voicePromptDataIsLoaded = false; static bool voicePromptIsActive = false; static int promptDataPosition = -1; static int currentPromptLength = -1; #define PROMPT_TAIL 30 static int promptTail = 0; static uint8_t M17Data[M17_DATA_BUFFER_SIZE]; #define VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE 128 typedef struct { uint16_t Buffer[VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE]; int Pos; int Length; } vpSequence_t; static vpSequence_t vpCurrentSequence = { .Pos = 0, .Length = 0 }; uint32_t tableOfContents[VOICE_PROMPTS_TOC_SIZE]; void vpCacheInit(void) { voicePromptsDataHeader_t header; SPI_Flash_read(VOICE_PROMPTS_FLASH_HEADER_ADDRESS,(uint8_t *)&header,sizeof(voicePromptsDataHeader_t)); if (vpCheckHeader((uint32_t *)&header)) { voicePromptDataIsLoaded = SPI_Flash_read(VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(voicePromptsDataHeader_t), (uint8_t *)&tableOfContents, sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE); vpFlashDataAddress = VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(voicePromptsDataHeader_t) + sizeof(uint32_t)*VOICE_PROMPTS_TOC_SIZE ; } } bool vpCheckHeader(uint32_t *bufferAddress) { voicePromptsDataHeader_t *header = (voicePromptsDataHeader_t *)bufferAddress; return ((header->magic == VOICE_PROMPTS_DATA_MAGIC) && (header->version == VOICE_PROMPTS_DATA_VERSION)); } static void getM17Data(int offset,int length) { if (length <= M17_DATA_BUFFER_SIZE) { SPI_Flash_read(vpFlashDataAddress + offset, (uint8_t *)&M17Data, length); } } void vpTick(void) { if (voicePromptIsActive) { if (promptDataPosition < currentPromptLength) { //if (wavbuffer_count <= (WAV_BUFFER_COUNT / 2)) { // codecDecode((uint8_t *)&M17Data[promptDataPosition], 3); promptDataPosition += 27; } //soundTickRXBuffer(); } else { if ( vpCurrentSequence.Pos < (vpCurrentSequence.Length - 1)) { vpCurrentSequence.Pos++; promptDataPosition = 0; int promptNumber = vpCurrentSequence.Buffer[vpCurrentSequence.Pos]; currentPromptLength = tableOfContents[promptNumber + 1] - tableOfContents[promptNumber]; getM17Data(tableOfContents[promptNumber], currentPromptLength); } else { // wait for wave buffer to empty when prompt has finished playing // if (wavbuffer_count == 0) { vpTerminate(); } } } } else { if (promptTail > 0) { promptTail--; if ((promptTail == 0) && trxCarrierDetected() && (trxGetMode() == RADIO_MODE_ANALOG)) { //GPIO_PinWrite(GPIO_RX_audio_mux, Pin_RX_audio_mux, 1); // Set the audio path to AT1846 -> audio amp. } } } } void vpTerminate(void) { if (voicePromptIsActive) { //disableAudioAmp(AUDIO_AMP_MODE_PROMPT); vpCurrentSequence.Pos = 0; //soundTerminateSound(); //soundInit(); promptTail = PROMPT_TAIL; voicePromptIsActive = false; } } void vpInit(void) { if (voicePromptIsActive) { vpTerminate(); } vpCurrentSequence.Length = 0; vpCurrentSequence.Pos = 0; } void vpQueuePrompt(uint16_t prompt) { if (voicePromptIsActive) { vpInit(); } if (vpCurrentSequence.Length < VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE) { vpCurrentSequence.Buffer[vpCurrentSequence.Length] = prompt; vpCurrentSequence.Length++; } } static bool GetSymbolVPIfItShouldBeAnnounced(char symbol, VoicePromptFlags_T flags, voicePrompt_t* vp) { *vp=PROMPT_SILENCE; const char indexedSymbols[] = "%.+-*#!,@:?()~/[]<>=$'`&|_^{}"; // Must match order of symbols in voicePrompt_t enum. const char commonSymbols[] = "%.+-*#"; bool announceCommonSymbols = (flags & vpAnnounceCommonSymbols) ? true : false; bool announceLessCommonSymbols=(flags & vpAnnounceLessCommonSymbols) ? true : false; char* symbolPtr = strchr(indexedSymbols, symbol); if (symbolPtr == NULL) {// we don't have a prompt for this character. return (flags&vpAnnounceASCIIValueForUnknownChars) ? true : false; } bool commonSymbol= strchr(commonSymbols, symbol) != NULL; *vp = PROMPT_PERCENT+(symbolPtr-indexedSymbols); return ((commonSymbol && announceCommonSymbols) || (!commonSymbol && announceLessCommonSymbols)); } // This function spells out a string letter by letter. void vpQueueString(char *promptString, VoicePromptFlags_T flags) { if (voicePromptIsActive) { vpInit(); } while (*promptString != 0) { voicePrompt_t vp = PROMPT_SILENCE; if ((*promptString >= '0') && (*promptString <= '9')) { vpQueuePrompt(*promptString - '0' + PROMPT_0); } else if ((*promptString >= 'A') && (*promptString <= 'Z')) { if (flags&vpAnnounceCaps) vpQueuePrompt(PROMPT_CAP); if (flags&vpAnnouncePhoneticRendering) vpQueuePrompt((*promptString - 'A') + PROMPT_A_PHONETIC); else vpQueuePrompt(*promptString - 'A' + PROMPT_A); } else if ((*promptString >= 'a') && (*promptString <= 'z')) { if (flags&vpAnnouncePhoneticRendering) vpQueuePrompt((*promptString - 'a') + PROMPT_A_PHONETIC); else vpQueuePrompt(*promptString - 'a' + PROMPT_A); } else if ((*promptString==' ') && (flags&vpAnnounceSpace)) { vpQueuePrompt(PROMPT_SPACE); } else if (GetSymbolVPIfItShouldBeAnnounced(*promptString, flags, &vp)) { if (vp != PROMPT_SILENCE) vpQueuePrompt(vp); else // announce ASCII { int32_t val = *promptString; vpQueueLanguageString(¤tLanguage->dtmf_code); // just the word "code" as we don't have character. vpQueueInteger(val); } } else { // otherwise just add silence vpQueuePrompt(PROMPT_SILENCE); } promptString++; } } void vpQueueInteger(int32_t value) { char buf[12] = {0}; // min: -2147483648, max: 2147483647 itoa(value, buf, 10); vpQueueString(buf, 0); } // This function looks up a voice prompt corresponding to a string table entry. // These are stored in the voice data after the voice prompts with no corresponding string table entry, hence the offset calculation: // NUM_VOICE_PROMPTS + (stringTableStringPtr - currentLanguage->languageName) void vpQueueStringTableEntry(const char * const *stringTableStringPtr) { if (stringTableStringPtr == NULL) { return; } vpQueuePrompt(NUM_VOICE_PROMPTS + (stringTableStringPtr - currentLanguage->languageName)); } void vpPlay(void) { if ((voicePromptIsActive == false) && (vpCurrentSequence.Length > 0)) { voicePromptIsActive = true;// Start the playback int promptNumber = vpCurrentSequence.Buffer[0]; vpCurrentSequence.Pos = 0; currentPromptLength = tableOfContents[promptNumber + 1] - tableOfContents[promptNumber]; getM17Data(tableOfContents[promptNumber], currentPromptLength); // GPIO_PinWrite(GPIO_RX_audio_mux, Pin_RX_audio_mux, 0);// set the audio mux HR-C6000 -> audio amp //enableAudioAmp(AUDIO_AMP_MODE_PROMPT); //codecInit(true); promptDataPosition = 0; } } inline bool vpIsPlaying(void) { return (voicePromptIsActive || (promptTail > 0)); } bool vpHasDataToPlay(void) { return (vpCurrentSequence.Length > 0); }