voicePromptsAppendLanguageString() turns a language-string pointer into a prompt index (NUM_VOICE_PROMPTS + offset), and voicePromptsPlay() then reads tableOfContents[index + 1]. Strings added after the voice-prompt pack was built (the SMS menu entries) land past the 368-entry table, so the player read off the end of the array and the radio crashed when the Messages menu announced a submenu name with voice prompts enabled. Clamp any index at or beyond the last table slot to PROMPT_SILENCE, so such strings are simply not spoken. Also protects the pre-existing strings sitting at the very end of the table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
441 lines
13 KiB
C
441 lines
13 KiB
C
/*
|
|
* Copyright (C) 2019-2025 Roger Clark, VK3KYY / G4KYF
|
|
* Daniel Caujolle-Bert, F1RMB
|
|
*
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
|
|
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
*/
|
|
#include "dmr_codec/codec.h"
|
|
#include "functions/sound.h"
|
|
#include "functions/voicePrompts.h"
|
|
#include "functions/settings.h"
|
|
#include "user_interface/uiLocalisation.h"
|
|
#include "functions/rxPowerSaving.h"
|
|
#include "hardware/radioHardwareInterface.h"
|
|
|
|
const uint32_t VOICE_PROMPTS_DATA_MAGIC = 0x5056;//'VP'
|
|
const uint32_t VOICE_PROMPTS_DATA_VERSION =
|
|
#if defined(HAS_COLOURS)
|
|
0x0009; // Version 9 TOC increased to 368 (10 slots free)
|
|
// Version 7 TOC increased to 350.
|
|
#define VOICE_PROMPTS_TOC_SIZE 368
|
|
#else
|
|
0x0008; // Version 8 TOC increased to 331 (10 slots free)
|
|
// Version 6 TOC increased to 320. Added PROMOT_VOX and PROMPT_UNUSED_1 to PROMPT_UNUSED_10
|
|
#define VOICE_PROMPTS_TOC_SIZE 331
|
|
#endif
|
|
// Version 5 TOC increased to 300
|
|
// Version 4 does not have unused items
|
|
// Version 3 does not have the PROMPT_TBD items in it
|
|
|
|
typedef struct
|
|
{
|
|
uint32_t magic;
|
|
uint32_t version;
|
|
} VoicePromptsDataHeader_t;
|
|
|
|
const uint32_t VOICE_PROMPTS_FLASH_HEADER_ADDRESS = 0x8F400 + FLASH_ADDRESS_OFFSET;
|
|
const uint32_t VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS = 0xE0000 + FLASH_ADDRESS_OFFSET;
|
|
static uint32_t voicePromptsFlashDataAddress;// = VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t) + sizeof(uint32_t)*VOICE_PROMPTS_TOC_SIZE ;
|
|
// 76 x 27 byte ambe frames
|
|
#define AMBE_DATA_BUFFER_SIZE 2052
|
|
bool voicePromptDataIsLoaded = false;
|
|
static volatile bool voicePromptIsActive = false; // used within ISR
|
|
static int promptDataPosition = -1;
|
|
static int currentPromptLength = -1;
|
|
|
|
#define PROMPT_TAIL 30
|
|
static volatile uint32_t promptTail = 0; // used within ISR
|
|
|
|
static __attribute__((section(".ccmram"))) uint8_t ambeData[AMBE_DATA_BUFFER_SIZE];
|
|
|
|
#define VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE 128
|
|
|
|
typedef struct
|
|
{
|
|
uint16_t Buffer[VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE];
|
|
uint32_t Pos;
|
|
uint32_t Length;
|
|
} VoicePromptsSequence_t;
|
|
|
|
static __attribute__((section(".ccmram"))) VoicePromptsSequence_t voicePromptsCurrentSequence =
|
|
{
|
|
.Pos = 0,
|
|
.Length = 0
|
|
};
|
|
|
|
static __attribute__((section(".ccmram"))) uint32_t tableOfContents[VOICE_PROMPTS_TOC_SIZE];
|
|
|
|
static bool temporaryOverride = false;
|
|
|
|
void voicePromptsCacheInit(void)
|
|
{
|
|
VoicePromptsDataHeader_t header;
|
|
|
|
SPI_Flash_read(VOICE_PROMPTS_FLASH_HEADER_ADDRESS, (uint8_t *)&header, sizeof(VoicePromptsDataHeader_t));
|
|
|
|
if (voicePromptsCheckMagicAndVersion((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);
|
|
voicePromptsFlashDataAddress = VOICE_PROMPTS_FLASH_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t) + (sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
|
|
}
|
|
|
|
if (!voicePromptDataIsLoaded)
|
|
{
|
|
SPI_Flash_read(VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS, (uint8_t *)&header, sizeof(VoicePromptsDataHeader_t));
|
|
if (voicePromptsCheckMagicAndVersion((uint32_t *)&header))
|
|
{
|
|
voicePromptDataIsLoaded = SPI_Flash_read(VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t), (uint8_t *)&tableOfContents, sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
|
|
voicePromptsFlashDataAddress = VOICE_PROMPTS_FLASH_OLD_HEADER_ADDRESS + sizeof(VoicePromptsDataHeader_t) + (sizeof(uint32_t) * VOICE_PROMPTS_TOC_SIZE);
|
|
}
|
|
}
|
|
|
|
// DMR (digital) is disabled.
|
|
if (uiDataGlobal.dmrDisabled)
|
|
{
|
|
voicePromptDataIsLoaded = false;
|
|
}
|
|
|
|
// is data is not loaded change prompt mode back to beep.
|
|
if ((nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && (voicePromptDataIsLoaded == false))
|
|
{
|
|
settingsSet(nonVolatileSettings.audioPromptMode, AUDIO_PROMPT_MODE_BEEP);
|
|
}
|
|
}
|
|
|
|
bool voicePromptsCheckMagicAndVersion(uint32_t *bufferAddress)
|
|
{
|
|
VoicePromptsDataHeader_t *header = (VoicePromptsDataHeader_t *)bufferAddress;
|
|
|
|
return ((header->magic == VOICE_PROMPTS_DATA_MAGIC) && (header->version == VOICE_PROMPTS_DATA_VERSION));
|
|
}
|
|
|
|
static void getAmbeData(int offset, int length)
|
|
{
|
|
if (length <= AMBE_DATA_BUFFER_SIZE)
|
|
{
|
|
SPI_Flash_read(voicePromptsFlashDataAddress + offset, (uint8_t *)&ambeData, length);
|
|
}
|
|
}
|
|
|
|
static void voicePromptsTerminateOptionalTail(bool withTail)
|
|
{
|
|
if (voicePromptIsActive)
|
|
{
|
|
audioAmpDisable(AUDIO_AMP_CHANNEL_PROMPT);
|
|
|
|
voicePromptsCurrentSequence.Pos = 0;
|
|
promptTail = (withTail ? PROMPT_TAIL : 0);
|
|
|
|
taskENTER_CRITICAL();
|
|
soundTerminateSound();
|
|
codecInit(true);
|
|
voicePromptIsActive = false;
|
|
temporaryOverride = false;
|
|
taskEXIT_CRITICAL();
|
|
}
|
|
}
|
|
|
|
void voicePromptsTick(void)
|
|
{
|
|
if (voicePromptIsActive)
|
|
{
|
|
if (promptDataPosition < currentPromptLength)
|
|
{
|
|
taskENTER_CRITICAL();
|
|
if (wavbuffer_count <= WAV_BUFFER_AMBE_PREBUFFERING_COUNT)
|
|
{
|
|
codecDecode((uint8_t *)&ambeData[promptDataPosition], 3);
|
|
promptDataPosition += AMBE_AUDIO_LENGTH;
|
|
}
|
|
|
|
soundTickRXBuffer();
|
|
taskEXIT_CRITICAL();
|
|
}
|
|
else
|
|
{
|
|
if (voicePromptsCurrentSequence.Pos < (voicePromptsCurrentSequence.Length - 1))
|
|
{
|
|
voicePromptsCurrentSequence.Pos++;
|
|
promptDataPosition = 0;
|
|
|
|
int promptNumber = voicePromptsCurrentSequence.Buffer[voicePromptsCurrentSequence.Pos];
|
|
|
|
if ((tableOfContents[promptNumber + 1] == 0) || (tableOfContents[promptNumber] == 0))
|
|
{
|
|
promptNumber = PROMPT_SILENCE;
|
|
}
|
|
|
|
currentPromptLength = tableOfContents[promptNumber + 1] - tableOfContents[promptNumber];
|
|
getAmbeData(tableOfContents[promptNumber], currentPromptLength);
|
|
}
|
|
else
|
|
{
|
|
// wait for wave buffer to empty when prompt has finished playing
|
|
|
|
if (wavbuffer_count == 0)
|
|
{
|
|
voicePromptsTerminateOptionalTail(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (promptTail > 0)
|
|
{
|
|
promptTail--;
|
|
|
|
if ((promptTail == 0) && trxCarrierDetected(RADIO_DEVICE_PRIMARY) && (trxGetMode() == RADIO_MODE_ANALOG))
|
|
{
|
|
radioSetAudioPath(true); // Set the audio path to FM -> audio amp.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void voicePromptsTerminate(void)
|
|
{
|
|
voicePromptsTerminateOptionalTail(true);
|
|
}
|
|
|
|
void voicePromptsTerminateNoTail(void)
|
|
{
|
|
voicePromptsTerminateOptionalTail(false);
|
|
}
|
|
|
|
void voicePromptsInit(void)
|
|
{
|
|
if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (voicePromptIsActive)
|
|
{
|
|
voicePromptsTerminateOptionalTail(false);
|
|
}
|
|
|
|
voicePromptsCurrentSequence.Length = 0;
|
|
voicePromptsCurrentSequence.Pos = 0;
|
|
}
|
|
|
|
void voicePromptsInitWithOverride(void)
|
|
{
|
|
temporaryOverride = voicePromptDataIsLoaded;// only allow override if prompts are actually loaded
|
|
voicePromptsInit();
|
|
}
|
|
|
|
void voicePromptsAppendPrompt(voicePrompt_t prompt)
|
|
{
|
|
if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (voicePromptIsActive)
|
|
{
|
|
voicePromptsInit();
|
|
}
|
|
|
|
if (voicePromptsCurrentSequence.Length < VOICE_PROMPTS_SEQUENCE_BUFFER_SIZE)
|
|
{
|
|
voicePromptsCurrentSequence.Buffer[voicePromptsCurrentSequence.Length] = prompt;
|
|
voicePromptsCurrentSequence.Length++;
|
|
}
|
|
}
|
|
|
|
void voicePromptsAppendString(const char *promptString)
|
|
{
|
|
if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (voicePromptIsActive)
|
|
{
|
|
voicePromptsInit();
|
|
}
|
|
|
|
while (*promptString != 0)
|
|
{
|
|
if ((*promptString >= '0') && (*promptString <= '9'))
|
|
{
|
|
voicePromptsAppendPrompt(*promptString - '0' + PROMPT_0);
|
|
}
|
|
else if ((*promptString >= 'A') && (*promptString <= 'Z'))
|
|
{
|
|
voicePromptsAppendPrompt(*promptString - 'A' + PROMPT_A);
|
|
}
|
|
else if ((*promptString >= 'a') && (*promptString <= 'z'))
|
|
{
|
|
voicePromptsAppendPrompt(*promptString - 'a' + PROMPT_A);
|
|
}
|
|
else if (*promptString == '.')
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_POINT);
|
|
}
|
|
else if (*promptString == '+')
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_PLUS);
|
|
}
|
|
else if (*promptString == '-')
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_MINUS);
|
|
}
|
|
else if (*promptString == '%')
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_PERCENT);
|
|
}
|
|
else if (*promptString == '*')
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_STAR);
|
|
}
|
|
else if (*promptString == '#')
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_HASH);
|
|
}
|
|
else if (*promptString == 176)
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_DEGREES);
|
|
}
|
|
else
|
|
{
|
|
// otherwise just add silence
|
|
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
|
}
|
|
|
|
promptString++;
|
|
}
|
|
}
|
|
|
|
void voicePromptsAppendInteger(int32_t value)
|
|
{
|
|
char buf[12] = {0}; // min: -2147483648, max: 2147483647
|
|
itoa(value, buf, 10);
|
|
voicePromptsAppendString(buf);
|
|
}
|
|
|
|
void voicePromptsAppendLanguageString(const char *languageStringAdd)
|
|
{
|
|
if (((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride) || (languageStringAdd == NULL))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The prompt index is derived from the pointer's offset within the currentLanguage
|
|
// table, so a pointer that isn't actually inside that table (e.g. a raw string
|
|
// literal passed by mistake) would yield an out-of-range prompt and crash. Ignore it.
|
|
const char *languageBase = ¤tLanguage->LANGUAGE_NAME[0];
|
|
if ((languageStringAdd < languageBase) || (languageStringAdd >= (languageBase + sizeof(*currentLanguage))))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int promptNumber = NUM_VOICE_PROMPTS +
|
|
((languageStringAdd - currentLanguage->LANGUAGE_NAME)
|
|
#if ! defined(HAS_COLOURS)
|
|
- ((languageStringAdd >= currentLanguage->theme_chooser) ?
|
|
((currentLanguage->theme_colour_picker_blue - currentLanguage->theme_chooser) + LANGUAGE_TEXTS_LENGTH) : 0)
|
|
#endif
|
|
) / LANGUAGE_TEXTS_LENGTH;
|
|
|
|
// Language strings added after the voice prompt pack was built have no
|
|
// entry in the table of contents. voicePromptsPlay() reads TOC[promptNumber + 1],
|
|
// so anything at or past the last slot would read past the array and crash;
|
|
// play silence for those instead.
|
|
if ((promptNumber < 0) || (promptNumber >= (VOICE_PROMPTS_TOC_SIZE - 1)))
|
|
{
|
|
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
|
return;
|
|
}
|
|
|
|
voicePromptsAppendPrompt(promptNumber);
|
|
}
|
|
|
|
void voicePromptsPlay(void)
|
|
{
|
|
if ((nonVolatileSettings.audioPromptMode < AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && !temporaryOverride)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((voicePromptIsActive == false) && (voicePromptsCurrentSequence.Length > 0))
|
|
{
|
|
rxPowerSavingSetState(ECOPHASE_POWERSAVE_INACTIVE);
|
|
|
|
if (nonVolatileSettings.dmrRxAGC != 0)
|
|
{
|
|
soundResetDMRRxAGCGain();
|
|
}
|
|
|
|
taskENTER_CRITICAL();
|
|
|
|
// Early state change to lock out HR-C6000.
|
|
voicePromptIsActive = true;// Start the playback
|
|
if (melody_play && (melody_play != MELODY_ERROR_BEEP) && (melody_play != MELODY_ACK_BEEP))
|
|
{
|
|
soundStopMelody();
|
|
}
|
|
|
|
int promptNumber = voicePromptsCurrentSequence.Buffer[0];
|
|
|
|
voicePromptsCurrentSequence.Pos = 0;
|
|
|
|
if ((tableOfContents[promptNumber + 1] == 0) || (tableOfContents[promptNumber] == 0))
|
|
{
|
|
promptNumber = PROMPT_SILENCE;
|
|
}
|
|
|
|
currentPromptLength = tableOfContents[promptNumber + 1] - tableOfContents[promptNumber];
|
|
getAmbeData(tableOfContents[promptNumber], currentPromptLength);
|
|
|
|
radioSetAudioPath(false); // set the audio path to HR-C6000 -> audio amp (Actually this is always on, this call just disables the FM audio)
|
|
audioAmpEnable(AUDIO_AMP_CHANNEL_PROMPT);
|
|
|
|
codecInit(true);
|
|
promptDataPosition = 0;
|
|
promptTail = 0;
|
|
|
|
taskEXIT_CRITICAL();
|
|
}
|
|
}
|
|
|
|
inline bool voicePromptsIsPlaying(void)
|
|
{
|
|
return (voicePromptIsActive || (promptTail > 0));
|
|
}
|
|
|
|
bool voicePromptsHasDataToPlay(void)
|
|
{
|
|
return (voicePromptsCurrentSequence.Length > 0);
|
|
}
|
|
|
|
bool voicePromptsDoesItContainPrompt(voicePrompt_t prompt)
|
|
{
|
|
if (voicePromptsCurrentSequence.Length > 0)
|
|
{
|
|
return (memchr(&voicePromptsCurrentSequence.Buffer[0], (int)prompt, voicePromptsCurrentSequence.Length) != NULL);
|
|
}
|
|
return false;
|
|
}
|