6#include <QtCore/QRegularExpression>
7#include <QtCore/QApplicationStatic>
8#include <QtTextToSpeech/QTextToSpeech>
9#include <QtTextToSpeech/QVoice>
17const QHash<QString, QString> AudioOutput::_textHash = {
19 {
"POSCTL",
"Position Control" },
20 {
"ALTCTL",
"Altitude Control" },
21 {
"AUTO_RTL",
"auto return to launch" },
22 {
"RTL",
"return To launch" },
23 {
"ACCEL",
"accelerometer" },
24 {
"RC_MAP_MODE_SW",
"RC mode switch" },
25 {
"REJ",
"rejected" },
28 {
"COMPID",
"component eye dee" },
29 {
"PARAMS",
"parameters" },
31 {
"ADSB",
"A.D.S.B." },
33 {
"PREARM",
"pre arm" },
34 {
"PITOT",
"pee toe" },
35 {
"SERVOX_FUNCTION",
"Servo X Function" },
44 , _engine(
QGC::runningUnitTests()
45 ? new QTextToSpeech(QStringLiteral(
"none"), this)
46 : new QTextToSpeech(this))
58 return _audioOutput();
63 Q_CHECK_PTR(volumeFact);
64 Q_CHECK_PTR(mutedFact);
70 _volumeFact = volumeFact;
71 _mutedFact = mutedFact;
74 (void) connect(_engine, &QTextToSpeech::stateChanged,
this, [
this](QTextToSpeech::State state) {
75 if (state == QTextToSpeech::State::Ready) {
81 qCDebug(AudioOutputLog) <<
"TTS State changed to:" << state;
84 (void) connect(_engine, &QTextToSpeech::errorOccurred,
this, [
this](QTextToSpeech::ErrorReason reason,
const QString &
errorString) {
85 qCWarning(AudioOutputLog) <<
"TTS error occurred. Reason:" << reason <<
", Message:" <<
errorString;
90 (void) connect(_engine, &QTextToSpeech::aboutToSynthesize,
this, [
this](qsizetype) {
91 if (_textQueueSize > 0) {
96 (void) connect(_engine, &QTextToSpeech::engineChanged,
this, [
this](
const QString &engine) {
97 qCDebug(AudioOutputLog) <<
"TTS Engine set to:" << engine;
98 _applyEngineSettings();
101 switch (_engine->state()) {
102 case QTextToSpeech::State::Ready:
105 case QTextToSpeech::State::Error:
106 qCWarning(AudioOutputLog) <<
"No usable QTextToSpeech engine available.";
109 qCDebug(AudioOutputLog) <<
"QTextToSpeech engine not ready; deferring init. State:" << _engine->state();
114void AudioOutput::_finishInit()
116 _applyEngineSettings();
126 if (AudioOutputLog().isDebugEnabled()) {
127 (void) connect(_engine, &QTextToSpeech::localeChanged,
this, [](
const QLocale &locale) {
128 qCDebug(AudioOutputLog) <<
"TTS Locale change to:" << locale;
130 (void) connect(_engine, &QTextToSpeech::volumeChanged,
this, [](
double volume) {
131 qCDebug(AudioOutputLog) <<
"TTS Volume changed to:" << volume;
133 (void) connect(_engine, &QTextToSpeech::sayingWord,
this, [](
const QString &word, qsizetype
id, qsizetype start, qsizetype length) {
134 qCDebug(AudioOutputLog) <<
"TTS Saying:" << word <<
"ID:" <<
id <<
"Start:" << start <<
"Length:" << length;
141 qCDebug(AudioOutputLog) <<
"AudioOutput initialized with volume:" << _volumeSetting() <<
"%";
144double AudioOutput::_volumeSetting()
const
146 return std::clamp(_volumeFact->
rawValue().toDouble(), 0.0, 100.0);
149bool AudioOutput::_mutedSetting()
const
151 return _mutedFact->
rawValue().toBool();
154void AudioOutput::_applyEngineSettings()
156 if (_engine->state() != QTextToSpeech::State::Ready) {
160 const QLocale defaultLocale(
"en_US");
161 if (_engine->availableLocales().contains(defaultLocale)) {
162 _engine->setLocale(defaultLocale);
166 const QList<QVoice> voices = _engine->availableVoices();
167 if (!voices.isEmpty()) {
168 _engine->setVoice(voices.constFirst());
171 _speakCapable = _engine->engineCapabilities().testFlag(QTextToSpeech::Capability::Speak);
174void AudioOutput::_setVolume()
176 const bool muted = _mutedSetting();
177 const double volume = muted ? 0.0 : _volumeSetting();
180 if (qFuzzyCompare(1.0 + volume, 1.0 + _lastVolume)) {
183 _lastVolume = volume;
186 const double normalizedVolume = volume / 100.0;
187 (void) QMetaObject::invokeMethod(_engine, [
this, volume, normalizedVolume]() {
190 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
193 _engine->setVolume(normalizedVolume);
195 qCDebug(AudioOutputLog) <<
"AudioOutput volume set to:" << volume <<
"%";
202 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using say().";
207 if (_volumeSetting() <= 0.0 || _mutedSetting()) {
211 if (!_speakCapable) {
212 qCWarning(AudioOutputLog) <<
"Speech Not Supported:" << text;
216 QString outText = _fixTextMessageForAudio(text);
219 outText = tr(
"%1").arg(outText);
222 if (outText.isEmpty()) {
227 (void) QMetaObject::invokeMethod(_engine, [
this, outText]() {
228 if (_textQueueSize >= kMaxTextQueueSize) {
229 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
231 qCWarning(AudioOutputLog) <<
"Text queue exceeded maximum size. Stopped current speech.";
234 const qsizetype index = _engine->enqueue(outText);
236 qCWarning(AudioOutputLog) <<
"Failed to enqueue speech. State:" << _engine->state()
237 <<
"Reason:" << _engine->errorReason();
242 qCDebug(AudioOutputLog) <<
"Enqueued text with index:" << index <<
", Queue Size:" << _textQueueSize;
249 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using testAudioOutput().";
254 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
257 const QString testText = tr(
"Audio test. Volume is %1 percent").arg(_volumeSetting(), 0,
'f', 1);
261QString AudioOutput::_fixTextMessageForAudio(
const QString &
string)
263 QString result = string;
264 result = _replaceAbbreviations(result);
265 result = _replaceNegativeSigns(result);
266 result = _replaceDecimalPoints(result);
267 result = _replaceMeters(result);
268 result = _convertMilliseconds(result);
272QString AudioOutput::_replaceAbbreviations(
const QString &input)
274 QStringList words = input.split(
' ');
275 for (QString &word : words) {
276 const auto it = _textHash.constFind(word.toUpper());
277 if (it != _textHash.constEnd()) {
282 return words.join(
' ');
285QString AudioOutput::_replaceNegativeSigns(
const QString &input)
287 static const QRegularExpression negNumRegex(QStringLiteral(
"-\\s*(?=\\d)"));
288 Q_ASSERT(negNumRegex.isValid());
290 QString output = input;
291 (void) output.replace(negNumRegex,
"negative ");
295QString AudioOutput::_replaceDecimalPoints(
const QString &input)
297 static const QRegularExpression realNumRegex(QStringLiteral(
"([0-9]+)(\\.)([0-9]+)"));
298 Q_ASSERT(realNumRegex.isValid());
300 QString output = input;
301 QRegularExpressionMatch realNumRegexMatch = realNumRegex.match(output);
302 while (realNumRegexMatch.hasMatch()) {
303 if (!realNumRegexMatch.captured(2).isNull()) {
304 (void) output.replace(realNumRegexMatch.capturedStart(2), realNumRegexMatch.capturedEnd(2) - realNumRegexMatch.capturedStart(2), QStringLiteral(
" point "));
306 realNumRegexMatch = realNumRegex.match(output);
312QString AudioOutput::_replaceMeters(
const QString &input)
314 static const QRegularExpression realNumMeterRegex(QStringLiteral(
"[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)"));
315 Q_ASSERT(realNumMeterRegex.isValid());
317 QString output = input;
318 QRegularExpressionMatch realNumMeterRegexMatch = realNumMeterRegex.match(output);
319 while (realNumMeterRegexMatch.hasMatch()) {
320 if (!realNumMeterRegexMatch.captured(1).isNull()) {
321 (void) output.replace(realNumMeterRegexMatch.capturedStart(1), realNumMeterRegexMatch.capturedEnd(1) - realNumMeterRegexMatch.capturedStart(1), QStringLiteral(
" meters"));
323 realNumMeterRegexMatch = realNumMeterRegex.match(output);
329QString AudioOutput::_convertMilliseconds(
const QString &input)
331 QString result = input;
335 if (_getMillisecondString(input, match, number) && (number >= 1000)) {
337 if (number < 60000) {
338 const int seconds = number / 1000;
339 const int ms = number - (seconds * 1000);
340 newNumber = QStringLiteral(
"%1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
"");
342 (void) newNumber.append(QStringLiteral(
" and %1 millisecond").arg(ms));
345 const int minutes = number / 60000;
346 const int seconds = (number - (minutes * 60000)) / 1000;
347 newNumber = QStringLiteral(
"%1 minute%2").arg(minutes).arg(minutes > 1 ?
"s" :
"");
349 (void) newNumber.append(QStringLiteral(
" and %1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
""));
352 (void) result.replace(match, newNumber);
358bool AudioOutput::_getMillisecondString(
const QString &
string, QString &match,
int &number)
360 static const QRegularExpression msRegex(
"((?<number>[0-9]+)ms)");
361 Q_ASSERT(msRegex.isValid());
365 QRegularExpressionMatch regexpMatch = msRegex.match(
string);
366 if (regexpMatch.hasMatch()) {
367 match = regexpMatch.captured(0);
368 const QString numberStr = regexpMatch.captured(
"number");
369 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
Value after translation.
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 ...