6#include <QtCore/QRegularExpression>
7#include <QtCore/QApplicationStatic>
8#include <QtTextToSpeech/QTextToSpeech>
14const QHash<QString, QString> AudioOutput::_textHash = {
16 {
"POSCTL",
"Position Control" },
17 {
"ALTCTL",
"Altitude Control" },
18 {
"AUTO_RTL",
"auto return to launch" },
19 {
"RTL",
"return To launch" },
20 {
"ACCEL",
"accelerometer" },
21 {
"RC_MAP_MODE_SW",
"RC mode switch" },
22 {
"REJ",
"rejected" },
25 {
"COMPID",
"component eye dee" },
26 {
"PARAMS",
"parameters" },
28 {
"ADSB",
"A.D.S.B." },
30 {
"PREARM",
"pre arm" },
31 {
"PITOT",
"pee toe" },
32 {
"SERVOX_FUNCTION",
"Servo X Function" },
39 , _engine(new QTextToSpeech(QStringLiteral(
"none"), this))
51 return _audioOutput();
56 Q_CHECK_PTR(mutedFact);
62 if (QTextToSpeech::availableEngines().isEmpty()) {
63 qCWarning(AudioOutputLog) <<
"No available QTextToSpeech engines found.";
68 if (!_engine->setEngine(QString())) {
69 qCWarning(AudioOutputLog) <<
"Failed to set the TTS engine.";
73 (void) connect(_engine, &QTextToSpeech::engineChanged,
this, [
this](
const QString &engine) {
74 qCDebug(AudioOutputLog) <<
"TTS Engine set to:" << engine;
75 const QLocale defaultLocale = QLocale(
"en_US");
76 if (_engine->availableLocales().contains(defaultLocale)) {
77 _engine->setLocale(defaultLocale);
81 (void) connect(_engine, &QTextToSpeech::aboutToSynthesize,
this, [
this](qsizetype
id) {
82 qCDebug(AudioOutputLog) <<
"TTS About To Synthesize ID:" << id;
84 qCDebug(AudioOutputLog) <<
"Queue Size:" << _textQueueSize;
91 if (AudioOutputLog().isDebugEnabled()) {
92 (void) connect(_engine, &QTextToSpeech::stateChanged,
this, [](QTextToSpeech::State state) {
93 qCDebug(AudioOutputLog) <<
"TTS State changed to:" << state;
95 (void) connect(_engine, &QTextToSpeech::errorOccurred,
this, [](QTextToSpeech::ErrorReason reason,
const QString &
errorString) {
96 qCDebug(AudioOutputLog) <<
"TTS Error occurred. Reason:" << reason <<
", Message:" <<
errorString;
98 (void) connect(_engine, &QTextToSpeech::localeChanged,
this, [](
const QLocale &locale) {
99 qCDebug(AudioOutputLog) <<
"TTS Locale change to:" << locale;
101 (void) connect(_engine, &QTextToSpeech::volumeChanged,
this, [](
double volume) {
102 qCDebug(AudioOutputLog) <<
"TTS Volume changed to:" << volume;
104 (void) connect(_engine, &QTextToSpeech::sayingWord,
this, [](
const QString &word, qsizetype
id, qsizetype start, qsizetype length) {
105 qCDebug(AudioOutputLog) <<
"TTS Saying:" << word <<
"ID:" <<
id <<
"Start:" << start <<
"Length:" << length;
109 setMuted(mutedFact->rawValue().toBool());
112 qCDebug(AudioOutputLog) <<
"AudioOutput initialized with muted state:" << _muted;
117 if (_muted.exchange(muted) != muted) {
118 (void) QMetaObject::invokeMethod(_engine,
"setVolume", Qt::AutoConnection, muted ? 0.0 : 1.0);
119 qCDebug(AudioOutputLog) <<
"AudioOutput muted state set to:" << muted;
126 if (!
qgcApp()->runningUnitTests()) {
127 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using say().";
136 if (!_engine->engineCapabilities().testFlag(QTextToSpeech::Capability::Speak)) {
137 qCWarning(AudioOutputLog) <<
"Speech Not Supported:" << text;
141 if (_textQueueSize >= kMaxTextQueueSize) {
142 (void) QMetaObject::invokeMethod(_engine,
"stop", Qt::AutoConnection, QTextToSpeech::BoundaryHint::Default);
144 qCWarning(AudioOutputLog) <<
"Text queue exceeded maximum size. Stopped current speech.";
147 QString outText = _fixTextMessageForAudio(text);
150 outText = tr(
"%1").arg(outText);
154 if (QMetaObject::invokeMethod(_engine,
"enqueue", Qt::AutoConnection, qReturnArg(index), outText)) {
157 qCDebug(AudioOutputLog) <<
"Enqueued text with index:" << index <<
", Queue Size:" << _textQueueSize;
160 qCWarning(AudioOutputLog) <<
"Failed to invoke Enqueue method.";
164QString AudioOutput::_fixTextMessageForAudio(
const QString &
string)
166 QString result = string;
167 result = _replaceAbbreviations(result);
168 result = _replaceNegativeSigns(result);
169 result = _replaceDecimalPoints(result);
170 result = _replaceMeters(result);
171 result = _convertMilliseconds(result);
175QString AudioOutput::_replaceAbbreviations(
const QString &input)
177 QString output = input;
179 const QStringList wordList = input.split(
' ', Qt::SkipEmptyParts);
180 for (
const QString &word : wordList) {
181 const QString upperWord = word.toUpper();
182 if (_textHash.contains(upperWord)) {
183 (void) output.replace(word, _textHash.value(upperWord));
190QString AudioOutput::_replaceNegativeSigns(
const QString &input)
192 static const QRegularExpression negNumRegex(QStringLiteral(
"-\\s*(?=\\d)"));
193 Q_ASSERT(negNumRegex.isValid());
195 QString output = input;
196 (void) output.replace(negNumRegex,
"negative ");
200QString AudioOutput::_replaceDecimalPoints(
const QString &input)
202 static const QRegularExpression realNumRegex(QStringLiteral(
"([0-9]+)(\\.)([0-9]+)"));
203 Q_ASSERT(realNumRegex.isValid());
205 QString output = input;
206 QRegularExpressionMatch realNumRegexMatch = realNumRegex.match(output);
207 while (realNumRegexMatch.hasMatch()) {
208 if (!realNumRegexMatch.captured(2).isNull()) {
209 (void) output.replace(realNumRegexMatch.capturedStart(2), realNumRegexMatch.capturedEnd(2) - realNumRegexMatch.capturedStart(2), QStringLiteral(
" point "));
211 realNumRegexMatch = realNumRegex.match(output);
217QString AudioOutput::_replaceMeters(
const QString &input)
219 static const QRegularExpression realNumMeterRegex(QStringLiteral(
"[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)"));
220 Q_ASSERT(realNumMeterRegex.isValid());
222 QString output = input;
223 QRegularExpressionMatch realNumMeterRegexMatch = realNumMeterRegex.match(output);
224 while (realNumMeterRegexMatch.hasMatch()) {
225 if (!realNumMeterRegexMatch.captured(1).isNull()) {
226 (void) output.replace(realNumMeterRegexMatch.capturedStart(1), realNumMeterRegexMatch.capturedEnd(1) - realNumMeterRegexMatch.capturedStart(1), QStringLiteral(
" meters"));
228 realNumMeterRegexMatch = realNumMeterRegex.match(output);
234QString AudioOutput::_convertMilliseconds(
const QString &input)
236 QString result = input;
240 if (_getMillisecondString(input, match, number) && (number >= 1000)) {
242 if (number < 60000) {
243 const int seconds = number / 1000;
244 const int ms = number - (seconds * 1000);
245 newNumber = QStringLiteral(
"%1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
"");
247 (void) newNumber.append(QStringLiteral(
" and %1 millisecond").arg(ms));
250 const int minutes = number / 60000;
251 const int seconds = (number - (minutes * 60000)) / 1000;
252 newNumber = QStringLiteral(
"%1 minute%2").arg(minutes).arg(minutes > 1 ?
"s" :
"");
254 (void) newNumber.append(QStringLiteral(
" and %1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
""));
257 (void) result.replace(match, newNumber);
263bool AudioOutput::_getMillisecondString(
const QString &
string, QString &match,
int &number)
265 static const QRegularExpression msRegex(
"((?<number>[0-9]+)ms)");
266 Q_ASSERT(msRegex.isValid());
270 QRegularExpressionMatch regexpMatch = msRegex.match(
string);
271 if (regexpMatch.hasMatch()) {
272 match = regexpMatch.captured(0);
273 const QString numberStr = regexpMatch.captured(
"number");
274 number = numberStr.toInt();
Q_APPLICATION_STATIC(AudioOutput, _audioOutput)
#define QGC_LOGGING_CATEGORY(name, categoryStr)
The AudioOutput class provides functionality for audio output using text-to-speech.
void say(const QString &text, TextMods textMods=TextMod::None)
AudioOutput(QObject *parent=nullptr)
void setMuted(bool muted)
static AudioOutput * instance()
void init(Fact *mutedFact)
Initialize the Singleton.
~AudioOutput()
Destructor for the AudioOutput class.
A Fact is used to hold a single value within the system.
void valueChanged(const QVariant &value)
This signal is only meant for use by the QT property system. It should not be connected to by client ...