QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
AudioOutput.cc
Go to the documentation of this file.
1#include "AudioOutput.h"
2#include "Fact.h"
4#include "QGCApplication.h"
5
6#include <QtCore/QRegularExpression>
7#include <QtCore/QApplicationStatic>
8#include <QtTextToSpeech/QTextToSpeech>
9
10QGC_LOGGING_CATEGORY(AudioOutputLog, "Utilities.AudioOutput");
11// qt.speech.tts.flite
12// qt.speech.tts.android
13
14const QHash<QString, QString> AudioOutput::_textHash = {
15 { "ERR", "error" },
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" },
23 { "WP", "waypoint" },
24 { "CMD", "command" },
25 { "COMPID", "component eye dee" },
26 { "PARAMS", "parameters" },
27 { "ID", "I.D." },
28 { "ADSB", "A.D.S.B." },
29 { "EKF", "E.K.F." },
30 { "PREARM", "pre arm" },
31 { "PITOT", "pee toe" },
32 { "SERVOX_FUNCTION","Servo X Function" },
33};
34
36
38 : QObject(parent)
39 , _engine(new QTextToSpeech(QStringLiteral("none"), this))
40{
41 // qCDebug(AudioOutputLog) << this;
42}
43
45{
46 // qCDebug(AudioOutputLog) << this;
47}
48
50{
51 return _audioOutput();
52}
53
54void AudioOutput::init(Fact *mutedFact)
55{
56 Q_CHECK_PTR(mutedFact);
57
58 if (_initialized) {
59 return;
60 }
61
62 if (QTextToSpeech::availableEngines().isEmpty()) {
63 qCWarning(AudioOutputLog) << "No available QTextToSpeech engines found.";
64 return;
65 }
66
67 // Autoselect engine by priority
68 if (!_engine->setEngine(QString())) {
69 qCWarning(AudioOutputLog) << "Failed to set the TTS engine.";
70 return;
71 }
72
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);
78 }
79 });
80
81 (void) connect(_engine, &QTextToSpeech::aboutToSynthesize, this, [this](qsizetype id) {
82 qCDebug(AudioOutputLog) << "TTS About To Synthesize ID:" << id;
83 _textQueueSize--;
84 qCDebug(AudioOutputLog) << "Queue Size:" << _textQueueSize;
85 });
86
87 (void) connect(mutedFact, &Fact::valueChanged, this, [this](QVariant value) {
88 setMuted(value.toBool());
89 });
90
91 if (AudioOutputLog().isDebugEnabled()) {
92 (void) connect(_engine, &QTextToSpeech::stateChanged, this, [](QTextToSpeech::State state) {
93 qCDebug(AudioOutputLog) << "TTS State changed to:" << state;
94 });
95 (void) connect(_engine, &QTextToSpeech::errorOccurred, this, [](QTextToSpeech::ErrorReason reason, const QString &errorString) {
96 qCDebug(AudioOutputLog) << "TTS Error occurred. Reason:" << reason << ", Message:" << errorString;
97 });
98 (void) connect(_engine, &QTextToSpeech::localeChanged, this, [](const QLocale &locale) {
99 qCDebug(AudioOutputLog) << "TTS Locale change to:" << locale;
100 });
101 (void) connect(_engine, &QTextToSpeech::volumeChanged, this, [](double volume) {
102 qCDebug(AudioOutputLog) << "TTS Volume changed to:" << volume;
103 });
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;
106 });
107 }
108
109 setMuted(mutedFact->rawValue().toBool());
110 _initialized = true;
111
112 qCDebug(AudioOutputLog) << "AudioOutput initialized with muted state:" << _muted;
113}
114
115void AudioOutput::setMuted(bool muted)
116{
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;
120 }
121}
122
123void AudioOutput::say(const QString &text, TextMods textMods)
124{
125 if (!_initialized) {
126 if (!qgcApp()->runningUnitTests()) {
127 qCWarning(AudioOutputLog) << "AudioOutput not initialized. Call init() before using say().";
128 }
129 return;
130 }
131
132 if (_muted) {
133 return;
134 }
135
136 if (!_engine->engineCapabilities().testFlag(QTextToSpeech::Capability::Speak)) {
137 qCWarning(AudioOutputLog) << "Speech Not Supported:" << text;
138 return;
139 }
140
141 if (_textQueueSize >= kMaxTextQueueSize) {
142 (void) QMetaObject::invokeMethod(_engine, "stop", Qt::AutoConnection, QTextToSpeech::BoundaryHint::Default);
143 _textQueueSize = 0;
144 qCWarning(AudioOutputLog) << "Text queue exceeded maximum size. Stopped current speech.";
145 }
146
147 QString outText = _fixTextMessageForAudio(text);
148
149 if (textMods.testFlag(TextMod::Translate)) {
150 outText = tr("%1").arg(outText);
151 }
152
153 qsizetype index;
154 if (QMetaObject::invokeMethod(_engine, "enqueue", Qt::AutoConnection, qReturnArg(index), outText)) {
155 if (index != -1) {
156 _textQueueSize++;
157 qCDebug(AudioOutputLog) << "Enqueued text with index:" << index << ", Queue Size:" << _textQueueSize;
158 }
159 } else {
160 qCWarning(AudioOutputLog) << "Failed to invoke Enqueue method.";
161 }
162}
163
164QString AudioOutput::_fixTextMessageForAudio(const QString &string)
165{
166 QString result = string;
167 result = _replaceAbbreviations(result);
168 result = _replaceNegativeSigns(result);
169 result = _replaceDecimalPoints(result);
170 result = _replaceMeters(result);
171 result = _convertMilliseconds(result);
172 return result;
173}
174
175QString AudioOutput::_replaceAbbreviations(const QString &input)
176{
177 QString output = input;
178
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));
184 }
185 }
186
187 return output;
188}
189
190QString AudioOutput::_replaceNegativeSigns(const QString &input)
191{
192 static const QRegularExpression negNumRegex(QStringLiteral("-\\s*(?=\\d)"));
193 Q_ASSERT(negNumRegex.isValid());
194
195 QString output = input;
196 (void) output.replace(negNumRegex, "negative ");
197 return output;
198}
199
200QString AudioOutput::_replaceDecimalPoints(const QString &input)
201{
202 static const QRegularExpression realNumRegex(QStringLiteral("([0-9]+)(\\.)([0-9]+)"));
203 Q_ASSERT(realNumRegex.isValid());
204
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 "));
210 }
211 realNumRegexMatch = realNumRegex.match(output);
212 }
213
214 return output;
215}
216
217QString AudioOutput::_replaceMeters(const QString &input)
218{
219 static const QRegularExpression realNumMeterRegex(QStringLiteral("[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)"));
220 Q_ASSERT(realNumMeterRegex.isValid());
221
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"));
227 }
228 realNumMeterRegexMatch = realNumMeterRegex.match(output);
229 }
230
231 return output;
232}
233
234QString AudioOutput::_convertMilliseconds(const QString &input)
235{
236 QString result = input;
237
238 QString match;
239 int number;
240 if (_getMillisecondString(input, match, number) && (number >= 1000)) {
241 QString newNumber;
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" : "");
246 if (ms > 0) {
247 (void) newNumber.append(QStringLiteral(" and %1 millisecond").arg(ms));
248 }
249 } else {
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" : "");
253 if (seconds > 0) {
254 (void) newNumber.append(QStringLiteral(" and %1 second%2").arg(seconds).arg(seconds > 1 ? "s" : ""));
255 }
256 }
257 (void) result.replace(match, newNumber);
258 }
259
260 return result;
261}
262
263bool AudioOutput::_getMillisecondString(const QString &string, QString &match, int &number)
264{
265 static const QRegularExpression msRegex("((?<number>[0-9]+)ms)");
266 Q_ASSERT(msRegex.isValid());
267
268 bool result = false;
269
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();
275 result = true;
276 }
277
278 return result;
279}
Q_APPLICATION_STATIC(AudioOutput, _audioOutput)
#define qgcApp()
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
The AudioOutput class provides functionality for audio output using text-to-speech.
Definition AudioOutput.h:14
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.
Definition Fact.h:19
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 ...