6#include <QtCore/QRegularExpression>
7#include <QtCore/QApplicationStatic>
8#include <QtCore/QTimer>
9#include <QtTextToSpeech/QTextToSpeech>
10#include <QtTextToSpeech/QVoice>
19const QHash<QString, QString> AudioOutput::_textHash = {
21 {
"POSCTL",
"Position Control"},
22 {
"ALTCTL",
"Altitude Control"},
23 {
"AUTO_RTL",
"auto return to launch"},
24 {
"RTL",
"return To launch"},
25 {
"ACCEL",
"accelerometer"},
26 {
"RC_MAP_MODE_SW",
"RC mode switch"},
30 {
"COMPID",
"component eye dee"},
31 {
"PARAMS",
"parameters"},
35 {
"PREARM",
"pre arm"},
37 {
"SERVOX_FUNCTION",
"Servo X Function"},
42 {
"AROT",
"autorotation"},
43 {
"TCAL",
"temperature calibration"},
45 {
"PERF",
"performance"},
47 {
"FBWA",
"fly-by-wire A"},
51const QSet<QString> AudioOutput::_spelledAcronyms = {
52 "GPS",
"GNSS",
"GCS",
"ESC",
"VTOL",
"RC",
"IMU",
"AHRS",
"INS",
"CPU",
"RPM",
"SD",
"USB",
53 "PWM",
"PPS",
"CRC",
"ICAO",
"SIH",
"HAGL",
"FMU",
"UTM",
"MAV",
"FFT",
"AFS",
"AMSL",
"MSL",
54 "HDOP",
"GPIO",
"SITL",
"ODID",
"TCP",
"UDP",
"UART",
"I2C",
"IOMCU",
"FTP",
"NACK",
"NED",
"RSSI",
55 "OSD",
"MSP",
"DMA",
"DFU",
"DNA",
"EFI",
"ISR",
"WDG",
"NMEA",
"RTCM",
"VTX",
"WMM",
"HW",
56 "SW",
"FW",
"RX",
"LLH",
"IP",
"IO",
"RF",
"LQ",
"DEVID",
65 , _engine(
QGC::runningUnitTests()
66 ? new QTextToSpeech(QStringLiteral(
"none"), this)
67 : new QTextToSpeech(this))
79 return _audioOutput();
84 Q_CHECK_PTR(volumeFact);
85 Q_CHECK_PTR(mutedFact);
91 _volumeFact = volumeFact;
92 _mutedFact = mutedFact;
95 (void) connect(_engine, &QTextToSpeech::stateChanged,
this, [
this](QTextToSpeech::State state) {
96 if (state == QTextToSpeech::State::Ready) {
102 qCDebug(AudioOutputLog) <<
"TTS State changed to:" << state;
105 (void) connect(_engine, &QTextToSpeech::errorOccurred,
this, [
this](QTextToSpeech::ErrorReason reason,
const QString &
errorString) {
106 qCWarning(AudioOutputLog) <<
"TTS error occurred. Reason:" << reason <<
", Message:" <<
errorString;
111 (void) connect(_engine, &QTextToSpeech::aboutToSynthesize,
this, [
this](qsizetype) {
112 if (_textQueueSize > 0) {
117 (void) connect(_engine, &QTextToSpeech::engineChanged,
this, [
this](
const QString &engine) {
118 qCDebug(AudioOutputLog) <<
"TTS Engine set to:" << engine;
119 _applyEngineSettings();
122 if (_engine->state() == QTextToSpeech::State::Ready) {
127 qCDebug(AudioOutputLog) <<
"QTextToSpeech engine not ready; deferring init. State:" << _engine->state();
130 QTimer::singleShot(kEngineInitWarnTimeout,
this, [
this]() {
132 qCWarning(AudioOutputLog) <<
"No usable QTextToSpeech engine available. Engine:" << _engine->engine()
133 <<
"State:" << _engine->state()
134 <<
"Reason:" << _engine->errorReason() << _engine->errorString();
141void AudioOutput::_finishInit()
143 _applyEngineSettings();
153 if (AudioOutputLog().isDebugEnabled()) {
154 (void) connect(_engine, &QTextToSpeech::localeChanged,
this, [](
const QLocale &locale) {
155 qCDebug(AudioOutputLog) <<
"TTS Locale change to:" << locale;
157 (void) connect(_engine, &QTextToSpeech::volumeChanged,
this, [](
double volume) {
158 qCDebug(AudioOutputLog) <<
"TTS Volume changed to:" << volume;
160 (void) connect(_engine, &QTextToSpeech::sayingWord,
this, [](
const QString &word, qsizetype
id, qsizetype start, qsizetype length) {
161 qCDebug(AudioOutputLog) <<
"TTS Saying:" << word <<
"ID:" <<
id <<
"Start:" << start <<
"Length:" << length;
168 qCDebug(AudioOutputLog) <<
"AudioOutput initialized with volume:" << _volumeSetting() <<
"%";
171double AudioOutput::_volumeSetting()
const
173 return std::clamp(_volumeFact->
rawValue().toDouble(), 0.0, 100.0);
176bool AudioOutput::_mutedSetting()
const
178 return _mutedFact->
rawValue().toBool();
181void AudioOutput::_applyEngineSettings()
183 if (_engine->state() != QTextToSpeech::State::Ready) {
187 const QLocale defaultLocale(
"en_US");
188 if (_engine->availableLocales().contains(defaultLocale)) {
189 _engine->setLocale(defaultLocale);
193 const QList<QVoice> voices = _engine->availableVoices();
194 if (!voices.isEmpty()) {
195 _engine->setVoice(voices.constFirst());
198 _speakCapable = _engine->engineCapabilities().testFlag(QTextToSpeech::Capability::Speak);
201void AudioOutput::_setVolume()
203 const bool muted = _mutedSetting();
204 const double volume = muted ? 0.0 : _volumeSetting();
207 if (qFuzzyCompare(1.0 + volume, 1.0 + _lastVolume)) {
210 _lastVolume = volume;
213 const double normalizedVolume = volume / 100.0;
214 (void) QMetaObject::invokeMethod(_engine, [
this, volume, normalizedVolume]() {
217 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
220 _engine->setVolume(normalizedVolume);
222 qCDebug(AudioOutputLog) <<
"AudioOutput volume set to:" << volume <<
"%";
229 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using say().";
234 if (_volumeSetting() <= 0.0 || _mutedSetting()) {
238 if (!_speakCapable) {
239 qCWarning(AudioOutputLog) <<
"Speech Not Supported:" << text;
243 QString outText = _fixTextMessageForAudio(text);
246 outText = tr(
"%1").arg(outText);
249 if (outText.isEmpty()) {
254 (void) QMetaObject::invokeMethod(_engine, [
this, outText]() {
255 if (_textQueueSize >= kMaxTextQueueSize) {
256 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
258 qCWarning(AudioOutputLog) <<
"Text queue exceeded maximum size. Stopped current speech.";
261 const qsizetype index = _engine->enqueue(outText);
263 qCWarning(AudioOutputLog) <<
"Failed to enqueue speech. State:" << _engine->state()
264 <<
"Reason:" << _engine->errorReason();
269 qCDebug(AudioOutputLog) <<
"Enqueued text with index:" << index <<
", Queue Size:" << _textQueueSize;
276 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using testAudioOutput().";
281 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
284 const QString testText = tr(
"Audio test. Volume is %1 percent").arg(_volumeSetting(), 0,
'f', 1);
288QString AudioOutput::_fixTextMessageForAudio(
const QString &
string)
290 QString result = string;
292 result = _replaceNegativeSigns(result);
293 result = _replaceAbbreviations(result);
294 result = _replaceDecimalPoints(result);
295 result = _replaceMeters(result);
296 result = _convertMilliseconds(result);
300QString AudioOutput::_spokenAbbreviation(
const QString& token)
302 const auto it = _textHash.constFind(token.toUpper());
303 if (it != _textHash.constEnd()) {
307 if (!_spelledAcronyms.contains(token)) {
312 spelled.reserve(token.size() * 2);
313 for (
const QChar c : token) {
315 spelled.append(QLatin1Char(
'.'));
320QString AudioOutput::_replaceAbbreviations(
const QString &input)
323 static const QRegularExpression tokenRegex(
324 QStringLiteral(
"(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9_]*?)([0-9]*)(?![A-Za-z0-9_])"));
327 output.reserve(input.size());
330 QRegularExpressionMatchIterator it = tokenRegex.globalMatch(input);
331 while (it.hasNext()) {
332 const QRegularExpressionMatch match = it.next();
333 const QString spoken = _spokenAbbreviation(match.captured(1));
334 if (spoken.isEmpty()) {
338 output.append(QStringView(input).mid(last, match.capturedStart() - last));
339 output.append(spoken);
340 if (match.capturedLength(2) > 0) {
341 output.append(QLatin1Char(
' '));
342 output.append(match.capturedView(2));
344 last = match.capturedEnd();
346 output.append(QStringView(input).mid(last));
351QString AudioOutput::_replaceNegativeSigns(
const QString &input)
353 static const QRegularExpression negNumRegex(QStringLiteral(
"(?<![A-Za-z0-9_])-\\s*(?=\\d)"));
355 QString output = input;
356 (void) output.replace(negNumRegex,
"negative ");
360QString AudioOutput::_replaceDecimalPoints(
const QString &input)
362 static const QRegularExpression realNumRegex(QStringLiteral(
"([0-9]+)(\\.)([0-9]+)"));
364 QString output = input;
365 QRegularExpressionMatch realNumRegexMatch = realNumRegex.match(output);
366 while (realNumRegexMatch.hasMatch()) {
367 if (!realNumRegexMatch.captured(2).isNull()) {
368 (void) output.replace(realNumRegexMatch.capturedStart(2), realNumRegexMatch.capturedEnd(2) - realNumRegexMatch.capturedStart(2), QStringLiteral(
" point "));
370 realNumRegexMatch = realNumRegex.match(output);
376QString AudioOutput::_replaceMeters(
const QString &input)
378 static const QRegularExpression realNumMeterRegex(QStringLiteral(
"[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)"));
380 QString output = input;
381 QRegularExpressionMatch realNumMeterRegexMatch = realNumMeterRegex.match(output);
382 while (realNumMeterRegexMatch.hasMatch()) {
383 if (!realNumMeterRegexMatch.captured(1).isNull()) {
384 (void) output.replace(realNumMeterRegexMatch.capturedStart(1), realNumMeterRegexMatch.capturedEnd(1) - realNumMeterRegexMatch.capturedStart(1), QStringLiteral(
" meters"));
386 realNumMeterRegexMatch = realNumMeterRegex.match(output);
392QString AudioOutput::_convertMilliseconds(
const QString &input)
394 QString result = input;
398 if (_getMillisecondString(input, match, number) && (number >= 1000)) {
400 if (number < 60000) {
401 const int seconds =
number / 1000;
402 const int ms =
number - (seconds * 1000);
403 newNumber = QStringLiteral(
"%1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
"");
405 (void) newNumber.append(QStringLiteral(
" and %1 millisecond").arg(ms));
408 const int minutes =
number / 60000;
409 const int seconds = (
number - (minutes * 60000)) / 1000;
410 newNumber = QStringLiteral(
"%1 minute%2").arg(minutes).arg(minutes > 1 ?
"s" :
"");
412 (void) newNumber.append(QStringLiteral(
" and %1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
""));
415 (void) result.replace(match, newNumber);
421bool AudioOutput::_getMillisecondString(
const QString &
string, QString &match,
int &number)
423 static const QRegularExpression msRegex(
"((?<number>[0-9]+)ms)");
427 QRegularExpressionMatch regexpMatch = msRegex.match(
string);
428 if (regexpMatch.hasMatch()) {
429 match = regexpMatch.captured(0);
430 const QString numberStr = regexpMatch.captured(
"number");
431 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 testAudioOutput()
Tests the audio output. Will stop current output before test.
static AudioOutput * instance()
~AudioOutput()
Destructor for the AudioOutput class.
void init(Fact *volumeFact, Fact *mutedFact)
Initialize the Singleton.
A Fact is used to hold a single value within the system.
QVariant rawValue() const
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 ...
std::optional< T > number(std::string_view field, int base=DECIMAL_BASE)