kopia lustrzana https://github.com/OpenRTX/OpenRTX
697 wiersze
19 KiB
C
697 wiersze
19 KiB
C
/***************************************************************************
|
|
* Copyright (C) 2022 - 2023 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 <http://www.gnu.org/licenses/> *
|
|
***************************************************************************/
|
|
#include <interfaces/platform.h>
|
|
#include <interfaces/keyboard.h>
|
|
#include <interfaces/delays.h>
|
|
#include <voicePromptUtils.h>
|
|
#include <ui/ui_strings.h>
|
|
#include <voicePrompts.h>
|
|
#include <audio_codec.h>
|
|
#include <audio_path.h>
|
|
#include <strings.h> // For strncasecmp
|
|
#include <ctype.h>
|
|
#include <state.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <beeps.h>
|
|
#include <errno.h>
|
|
|
|
static const uint32_t VOICE_PROMPTS_DATA_MAGIC = 0x5056; //'VP'
|
|
static const uint32_t VOICE_PROMPTS_DATA_VERSION = 0x1000; // v1000 OpenRTX
|
|
|
|
#define VOICE_PROMPTS_TOC_SIZE 350
|
|
#define CODEC2_HEADER_SIZE 7
|
|
#define VP_SEQUENCE_BUF_SIZE 128
|
|
#define BEEP_SEQ_BUF_SIZE 256
|
|
|
|
typedef struct
|
|
{
|
|
uint32_t magic;
|
|
uint32_t version;
|
|
}
|
|
vpHeader_t;
|
|
|
|
typedef struct
|
|
{
|
|
const char* userWord;
|
|
const voicePrompt_t vp;
|
|
}
|
|
userDictEntry_t;
|
|
|
|
typedef struct
|
|
{
|
|
uint16_t buffer[VP_SEQUENCE_BUF_SIZE]; // Buffer of individual prompt indices.
|
|
uint16_t pos; // Index into above buffer.
|
|
uint16_t length; // Number of entries in above buffer.
|
|
uint32_t c2DataStart;
|
|
uint32_t c2DataIndex; // Index into current codec2 data
|
|
uint32_t c2DataLength; // Length of codec2 data for current prompt.
|
|
}
|
|
vpSequence_t;
|
|
|
|
typedef struct
|
|
{
|
|
uint16_t freq;
|
|
uint16_t duration;
|
|
}
|
|
beepData_t;
|
|
|
|
|
|
static const userDictEntry_t userDictionary[] =
|
|
{
|
|
{"hotspot", PROMPT_CUSTOM1}, // Hotspot
|
|
{"clearnode", PROMPT_CUSTOM2}, // ClearNode
|
|
{"sharinode", PROMPT_CUSTOM3}, // ShariNode
|
|
{"microhub", PROMPT_CUSTOM4}, // MicroHub
|
|
{"openspot", PROMPT_CUSTOM5}, // Openspot
|
|
{"repeater", PROMPT_CUSTOM6}, // repeater
|
|
{"blindhams", PROMPT_CUSTOM7}, // BlindHams
|
|
{"allstar", PROMPT_CUSTOM8}, // Allstar
|
|
{"parrot", PROMPT_CUSTOM9}, // Parrot
|
|
{"channel", PROMPT_CHANNEL}, // Channel
|
|
{0, 0}
|
|
};
|
|
|
|
static vpSequence_t vpCurrentSequence =
|
|
{
|
|
.pos = 0,
|
|
.length = 0,
|
|
.c2DataStart = 0,
|
|
.c2DataIndex = 0,
|
|
.c2DataLength = 0
|
|
};
|
|
|
|
static uint32_t tableOfContents[VOICE_PROMPTS_TOC_SIZE];
|
|
static bool vpDataLoaded = false;
|
|
static bool voicePromptActive = false;
|
|
|
|
static beepData_t beepSeriesBuffer[BEEP_SEQ_BUF_SIZE];
|
|
static uint16_t currentBeepDuration = 0;
|
|
static uint8_t beepSeriesIndex = 0;
|
|
static bool delayBeepUntilTick = false;
|
|
|
|
static pathId vpAudioPath;
|
|
static long long vpStartTime;
|
|
|
|
#ifdef VP_USE_FILESYSTEM
|
|
static FILE *vpFile = NULL;
|
|
#else
|
|
extern unsigned char _vpdata_start;
|
|
extern unsigned char _vpdata_end;
|
|
unsigned char *vpData = &_vpdata_start;
|
|
#endif
|
|
|
|
/**
|
|
* \internal
|
|
* Load voice prompts header.
|
|
*
|
|
* @param header: pointer toa vpHeader_t data structure.
|
|
*/
|
|
static void loadVpHeader(vpHeader_t *header)
|
|
{
|
|
#ifdef VP_USE_FILESYSTEM
|
|
fseek(vpFile, 0L, SEEK_SET);
|
|
fread(header, sizeof(vpHeader_t), 1, vpFile);
|
|
#else
|
|
memcpy(header, vpData, sizeof(vpHeader_t));
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Load voice prompts' table of contents.
|
|
*/
|
|
static void loadVpToC()
|
|
{
|
|
#ifdef VP_USE_FILESYSTEM
|
|
fread(&tableOfContents, sizeof(tableOfContents), 1, vpFile);
|
|
size_t vpDataOffset = ftell(vpFile);
|
|
|
|
if(vpDataOffset == (sizeof(vpHeader_t) + sizeof(tableOfContents)))
|
|
vpDataLoaded = true;
|
|
#else
|
|
uint8_t *tocPtr = vpData + sizeof(vpHeader_t);
|
|
memcpy(&tableOfContents, tocPtr, sizeof(tableOfContents));
|
|
vpDataLoaded = true;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Load Codec2 data for a voice prompt.
|
|
*
|
|
* @param offset: offset relative to the start of the voice prompt data.
|
|
* @param length: data length in bytes.
|
|
*/
|
|
static void fetchCodec2Data(uint8_t *data, const size_t offset)
|
|
{
|
|
if (vpDataLoaded == false)
|
|
return;
|
|
|
|
#ifdef VP_USE_FILESYSTEM
|
|
if (vpFile == NULL)
|
|
return;
|
|
|
|
size_t start = sizeof(vpHeader_t)
|
|
+ sizeof(tableOfContents)
|
|
+ CODEC2_HEADER_SIZE;
|
|
|
|
fseek(vpFile, start + offset, SEEK_SET);
|
|
fread(data, 8, 1, vpFile);
|
|
#else
|
|
uint8_t *dataPtr = vpData
|
|
+ sizeof(vpHeader_t)
|
|
+ sizeof(tableOfContents)
|
|
+ CODEC2_HEADER_SIZE;
|
|
|
|
if((dataPtr + 8) >= &_vpdata_end)
|
|
{
|
|
memset(data, 0x00, 8);
|
|
return;
|
|
}
|
|
|
|
memcpy(data, dataPtr + offset, 8);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Check validity of voice prompt header.
|
|
*
|
|
* @param header: voice prompt header to be checked.
|
|
* @return true if header is valid
|
|
*/
|
|
static inline bool checkVpHeader(const vpHeader_t* header)
|
|
{
|
|
return ((header->magic == VOICE_PROMPTS_DATA_MAGIC) &&
|
|
(header->version == VOICE_PROMPTS_DATA_VERSION));
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Perform a string lookup inside user dictionary.
|
|
*
|
|
* @param ptr: string to be searched.
|
|
* @param advanceBy: final offset with respect of dictionary beginning.
|
|
* @return index of user dictionary's voice prompt.
|
|
*/
|
|
static uint16_t userDictLookup(const char* ptr, int* advanceBy)
|
|
{
|
|
if ((ptr == NULL) || (*ptr == '\0'))
|
|
return 0;
|
|
|
|
for(uint32_t index = 0; userDictionary[index].userWord != 0; index++)
|
|
{
|
|
int len = strlen(userDictionary[index].userWord);
|
|
if (strncasecmp(userDictionary[index].userWord, ptr, len) == 0)
|
|
{
|
|
*advanceBy = len;
|
|
return userDictionary[index].vp;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
*
|
|
*/
|
|
static bool GetSymbolVPIfItShouldBeAnnounced(char symbol,
|
|
vpFlags_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));
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Function managing set up of audio path towards the speaker.
|
|
*/
|
|
static inline void enableSpkOutput()
|
|
{
|
|
// Set up a new audio path only if there is no other one already open to
|
|
// avoid overwriting the path ID with a -1, locking everything.
|
|
if(audioPath_getStatus(vpAudioPath) == PATH_CLOSED)
|
|
{
|
|
vpAudioPath = audioPath_request(SOURCE_MCU, SINK_SPK, PRIO_PROMPT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Function managing release of audio path towards the speaker.
|
|
*/
|
|
static inline void disableSpkOutput()
|
|
{
|
|
// Avoid chomping away a still in-progress beep or voice prompt.
|
|
if((currentBeepDuration != 0) || (voicePromptActive == true))
|
|
return;
|
|
|
|
audioPath_release(vpAudioPath);
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Stop an ongoing beep, if present, and clear all the beep management
|
|
* variables.
|
|
*/
|
|
static void beep_flush()
|
|
{
|
|
if (currentBeepDuration > 0)
|
|
platform_beepStop();
|
|
|
|
memset(beepSeriesBuffer, 0, sizeof(beepSeriesBuffer));
|
|
currentBeepDuration = 0;
|
|
beepSeriesIndex = 0;
|
|
disableSpkOutput();
|
|
}
|
|
|
|
/**
|
|
* \internal
|
|
* Function managing beep update.
|
|
*/
|
|
static bool beep_tick()
|
|
{
|
|
if (currentBeepDuration > 0)
|
|
{
|
|
if (delayBeepUntilTick)
|
|
{
|
|
platform_beepStart(beepSeriesBuffer[beepSeriesIndex].freq);
|
|
delayBeepUntilTick = false;
|
|
}
|
|
|
|
currentBeepDuration--;
|
|
if (currentBeepDuration == 0)
|
|
{
|
|
platform_beepStop();
|
|
|
|
// see if there are any more in the series to play.
|
|
if ((beepSeriesBuffer[beepSeriesIndex+1].freq != 0) &&
|
|
(beepSeriesBuffer[beepSeriesIndex+1].duration != 0))
|
|
{
|
|
beepSeriesIndex++;
|
|
currentBeepDuration = beepSeriesBuffer[beepSeriesIndex].duration;
|
|
platform_beepStart(beepSeriesBuffer[beepSeriesIndex].freq);
|
|
}
|
|
else
|
|
{
|
|
// Clear all variables for beep management
|
|
beep_flush();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
void vp_init()
|
|
{
|
|
#ifdef VP_USE_FILESYSTEM
|
|
if(vpFile == NULL)
|
|
vpFile = fopen("voiceprompts.vpc", "r");
|
|
|
|
if(vpFile == NULL)
|
|
return;
|
|
#else
|
|
if(&_vpdata_start == &_vpdata_end)
|
|
return;
|
|
#endif
|
|
|
|
// Read header
|
|
vpHeader_t header;
|
|
loadVpHeader(&header);
|
|
|
|
if(checkVpHeader(&header) == true)
|
|
{
|
|
loadVpToC();
|
|
}
|
|
|
|
if (vpDataLoaded)
|
|
{
|
|
// If the hash key is down, set vpLevel to high, if beep or less.
|
|
if ((kbd_getKeys() & KEY_HASH) && (state.settings.vpLevel <= vpBeep))
|
|
state.settings.vpLevel = vpHigh;
|
|
}
|
|
else
|
|
{
|
|
// ensure we at least have beeps in the event no voice prompts are
|
|
// loaded.
|
|
if (state.settings.vpLevel > vpBeep)
|
|
state.settings.vpLevel = vpBeep;
|
|
}
|
|
|
|
// Initialize codec2 module
|
|
codec_init();
|
|
}
|
|
|
|
void vp_terminate()
|
|
{
|
|
if (voicePromptActive)
|
|
vp_flush();
|
|
|
|
codec_terminate();
|
|
|
|
#ifdef VP_USE_FILESYSTEM
|
|
fclose(vpFile);
|
|
#endif
|
|
}
|
|
|
|
void vp_stop()
|
|
{
|
|
voicePromptActive = false;
|
|
// codec_stop(vpAudioPath);
|
|
disableSpkOutput();
|
|
|
|
// Clear voice prompt sequence data
|
|
vpCurrentSequence.pos = 0;
|
|
vpCurrentSequence.c2DataIndex = 0;
|
|
vpCurrentSequence.c2DataLength = 0;
|
|
|
|
// If any beep is playing, immediately stop it.
|
|
beep_flush();
|
|
}
|
|
|
|
void vp_flush()
|
|
{
|
|
// Stop the prompt and reset the codec data length
|
|
vp_stop();
|
|
vpCurrentSequence.length = 0;
|
|
}
|
|
|
|
void vp_queuePrompt(const uint16_t prompt)
|
|
{
|
|
if (state.settings.vpLevel < vpLow)
|
|
return;
|
|
|
|
if (voicePromptActive)
|
|
vp_flush();
|
|
|
|
if (vpCurrentSequence.length < VP_SEQUENCE_BUF_SIZE)
|
|
{
|
|
vpCurrentSequence.buffer[vpCurrentSequence.length] = prompt;
|
|
vpCurrentSequence.length++;
|
|
}
|
|
}
|
|
|
|
void vp_queueString(const char* string, vpFlags_t flags)
|
|
{
|
|
if (state.settings.vpLevel < vpLow)
|
|
return;
|
|
|
|
if (voicePromptActive)
|
|
vp_flush();
|
|
|
|
if (state.settings.vpPhoneticSpell)
|
|
flags |= vpAnnouncePhoneticRendering;
|
|
|
|
while (*string != '\0')
|
|
{
|
|
int advanceBy = 0;
|
|
voicePrompt_t vp = userDictLookup(string, &advanceBy);
|
|
|
|
if (vp != 0)
|
|
{
|
|
vp_queuePrompt(vp);
|
|
string += advanceBy;
|
|
continue;
|
|
}
|
|
else if ((*string >= '0') && (*string <= '9'))
|
|
{
|
|
vp_queuePrompt(*string - '0' + PROMPT_0);
|
|
}
|
|
else if ((*string >= 'A') && (*string <= 'Z'))
|
|
{
|
|
if (flags & vpAnnounceCaps)
|
|
vp_queuePrompt(PROMPT_CAP);
|
|
if (flags & vpAnnouncePhoneticRendering)
|
|
vp_queuePrompt((*string - 'A') + PROMPT_A_PHONETIC);
|
|
else
|
|
vp_queuePrompt(*string - 'A' + PROMPT_A);
|
|
}
|
|
else if ((*string >= 'a') && (*string <= 'z'))
|
|
{
|
|
if (flags & vpAnnouncePhoneticRendering)
|
|
vp_queuePrompt((*string - 'a') + PROMPT_A_PHONETIC);
|
|
else
|
|
vp_queuePrompt(*string - 'a' + PROMPT_A);
|
|
}
|
|
else if ((*string == ' ') && (flags & vpAnnounceSpace))
|
|
{
|
|
vp_queuePrompt(PROMPT_SPACE);
|
|
}
|
|
else if (GetSymbolVPIfItShouldBeAnnounced(*string, flags, &vp))
|
|
{
|
|
if (vp != PROMPT_SILENCE)
|
|
vp_queuePrompt(vp);
|
|
else
|
|
{
|
|
// announce ASCII
|
|
int32_t val = *string;
|
|
vp_queuePrompt(PROMPT_CHARACTER);
|
|
vp_queueInteger(val);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// otherwise just add silence
|
|
vp_queuePrompt(PROMPT_SILENCE);
|
|
}
|
|
|
|
string++;
|
|
}
|
|
|
|
if (flags & vpqAddSeparatingSilence)
|
|
vp_queuePrompt(PROMPT_SILENCE);
|
|
}
|
|
|
|
void vp_queueInteger(const int value)
|
|
{
|
|
if (state.settings.vpLevel < vpLow)
|
|
return;
|
|
|
|
char buf[12] = {0}; // min: -2147483648, max: 2147483647
|
|
if (value < 0)
|
|
vp_queuePrompt(PROMPT_MINUS);
|
|
|
|
sniprintf(buf, 12, "%d", value);
|
|
vp_queueString(buf, 0);
|
|
}
|
|
|
|
void vp_queueStringTableEntry(const char* const* stringTableStringPtr)
|
|
{
|
|
/*
|
|
* 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)
|
|
*/
|
|
|
|
if (state.settings.vpLevel < vpLow)
|
|
return;
|
|
|
|
if (stringTableStringPtr == NULL)
|
|
return;
|
|
|
|
uint16_t pos = NUM_VOICE_PROMPTS
|
|
+ (stringTableStringPtr - ¤tLanguage->languageName);
|
|
|
|
vp_queuePrompt(pos);
|
|
}
|
|
|
|
void vp_play()
|
|
{
|
|
if (state.settings.vpLevel < vpLow)
|
|
return;
|
|
|
|
if (voicePromptActive)
|
|
return;
|
|
|
|
if (vpCurrentSequence.length <= 0)
|
|
return;
|
|
|
|
// TODO: remove this once switching to hardware-based I2C driver for AT1846S
|
|
// management.
|
|
vpStartTime = getTick();
|
|
}
|
|
|
|
void vp_tick()
|
|
{
|
|
if (platform_getPttStatus() && (voicePromptActive || (currentBeepDuration > 0)))
|
|
{
|
|
vp_stop();
|
|
return;
|
|
}
|
|
|
|
if (beep_tick())
|
|
return;
|
|
|
|
// Temporary fix for the following bug: on MD-UV3x0 the configuration of
|
|
// the AT1846S chip may take more than 20ms, making the codec2 thread miss
|
|
// the syncronization point with the output stream. By delaying the start
|
|
// of the voice prompt by 50ms, a time span almost not noticeable, we avoid
|
|
// to incur in such a problem.
|
|
// TODO: remove this once switched to hardware-based I2C driver for AT1846S
|
|
// management.
|
|
if((vpStartTime > 0) && ((getTick() - vpStartTime) > 50))
|
|
{
|
|
vpStartTime = 0;
|
|
voicePromptActive = true;
|
|
enableSpkOutput();
|
|
codec_startDecode(vpAudioPath);
|
|
}
|
|
|
|
if (voicePromptActive == false)
|
|
return;
|
|
|
|
while(vpCurrentSequence.pos < vpCurrentSequence.length)
|
|
{
|
|
// get the codec2 data for the current prompt if needed.
|
|
if (vpCurrentSequence.c2DataLength == 0)
|
|
{
|
|
// obtain the data for the prompt.
|
|
int promptNumber = vpCurrentSequence.buffer[vpCurrentSequence.pos];
|
|
|
|
vpCurrentSequence.c2DataIndex = 0;
|
|
vpCurrentSequence.c2DataStart = tableOfContents[promptNumber];
|
|
vpCurrentSequence.c2DataLength = ((tableOfContents[promptNumber + 1]
|
|
- tableOfContents[promptNumber])/8 * 8);
|
|
}
|
|
|
|
while (vpCurrentSequence.c2DataIndex < vpCurrentSequence.c2DataLength)
|
|
{
|
|
// push the codec2 data in lots of 8 byte frames.
|
|
uint8_t c2Frame[8] = {0};
|
|
|
|
fetchCodec2Data(c2Frame, vpCurrentSequence.c2DataStart +
|
|
vpCurrentSequence.c2DataIndex);
|
|
|
|
// Do not push codec2 data if audio path is closed or suspended
|
|
if(audioPath_getStatus(vpAudioPath) != PATH_OPEN)
|
|
return;
|
|
|
|
if(codec_pushFrame(c2Frame, false) < 0)
|
|
return;
|
|
|
|
vpCurrentSequence.c2DataIndex += 8;
|
|
}
|
|
|
|
vpCurrentSequence.pos++; // ready for next prompt in sequence.
|
|
vpCurrentSequence.c2DataLength = 0; // flag that we need to get more data.
|
|
vpCurrentSequence.c2DataIndex = 0;
|
|
}
|
|
|
|
// see if we've finished.
|
|
if(vpCurrentSequence.pos == vpCurrentSequence.length)
|
|
{
|
|
voicePromptActive = false;
|
|
vpCurrentSequence.pos = 0;
|
|
vpCurrentSequence.c2DataIndex = 0;
|
|
vpCurrentSequence.c2DataLength = 0;
|
|
codec_stop(vpAudioPath);
|
|
disableSpkOutput();
|
|
}
|
|
}
|
|
|
|
bool vp_isPlaying()
|
|
{
|
|
return voicePromptActive;
|
|
}
|
|
|
|
bool vp_sequenceNotEmpty()
|
|
{
|
|
return (vpCurrentSequence.length > 0);
|
|
}
|
|
|
|
void vp_beep(uint16_t freq, uint16_t duration)
|
|
{
|
|
if (state.settings.vpLevel < vpBeep)
|
|
return;
|
|
|
|
// Do not play a new one if one is playing.
|
|
if (currentBeepDuration != 0)
|
|
return;
|
|
|
|
// avoid extra long beeps!
|
|
if (duration > 20)
|
|
duration = 20;
|
|
|
|
beepSeriesBuffer[0].freq = freq;
|
|
beepSeriesBuffer[0].duration = duration;
|
|
beepSeriesBuffer[1].freq = 0;
|
|
beepSeriesBuffer[1].duration = 0;
|
|
currentBeepDuration = duration;
|
|
beepSeriesIndex = 0;
|
|
platform_beepStart(freq);
|
|
enableSpkOutput();
|
|
}
|
|
|
|
void vp_beepSeries(const uint16_t* beepSeries)
|
|
{
|
|
if (state.settings.vpLevel < vpBeep)
|
|
return;
|
|
|
|
if (currentBeepDuration != 0)
|
|
return;
|
|
|
|
enableSpkOutput();
|
|
|
|
if (beepSeries == NULL)
|
|
return;
|
|
|
|
memcpy(beepSeriesBuffer, beepSeries, BEEP_SEQ_BUF_SIZE*sizeof(beepData_t));
|
|
// Always ensure that the array is terminated!
|
|
beepSeriesBuffer[BEEP_SEQ_BUF_SIZE-1].freq = 0;
|
|
beepSeriesBuffer[BEEP_SEQ_BUF_SIZE-1].duration = 0;
|
|
|
|
currentBeepDuration = beepSeriesBuffer[0].duration;
|
|
beepSeriesIndex = 0;
|
|
delayBeepUntilTick = true;
|
|
}
|