6#include <QtCore/QRegularExpression>
7#include <QtCore/QApplicationStatic>
8#include <QtCore/QTimer>
9#include <QtTextToSpeech/QTextToSpeech>
10#include <QtTextToSpeech/QVoice>
18const QHash<QString, QString> AudioOutput::_textHash = {
20 {
"POSCTL",
"Position Control" },
21 {
"ALTCTL",
"Altitude Control" },
22 {
"AUTO_RTL",
"auto return to launch" },
23 {
"RTL",
"return To launch" },
24 {
"ACCEL",
"accelerometer" },
25 {
"RC_MAP_MODE_SW",
"RC mode switch" },
26 {
"REJ",
"rejected" },
29 {
"COMPID",
"component eye dee" },
30 {
"PARAMS",
"parameters" },
32 {
"ADSB",
"A.D.S.B." },
34 {
"PREARM",
"pre arm" },
35 {
"PITOT",
"pee toe" },
36 {
"SERVOX_FUNCTION",
"Servo X Function" },
45 , _engine(
QGC::runningUnitTests()
46 ? new QTextToSpeech(QStringLiteral(
"none"), this)
47 : new QTextToSpeech(this))
59 return _audioOutput();
64 Q_CHECK_PTR(volumeFact);
65 Q_CHECK_PTR(mutedFact);
71 _volumeFact = volumeFact;
72 _mutedFact = mutedFact;
75 (void) connect(_engine, &QTextToSpeech::stateChanged,
this, [
this](QTextToSpeech::State state) {
76 if (state == QTextToSpeech::State::Ready) {
82 qCDebug(AudioOutputLog) <<
"TTS State changed to:" << state;
85 (void) connect(_engine, &QTextToSpeech::errorOccurred,
this, [
this](QTextToSpeech::ErrorReason reason,
const QString &
errorString) {
86 qCWarning(AudioOutputLog) <<
"TTS error occurred. Reason:" << reason <<
", Message:" <<
errorString;
91 (void) connect(_engine, &QTextToSpeech::aboutToSynthesize,
this, [
this](qsizetype) {
92 if (_textQueueSize > 0) {
97 (void) connect(_engine, &QTextToSpeech::engineChanged,
this, [
this](
const QString &engine) {
98 qCDebug(AudioOutputLog) <<
"TTS Engine set to:" << engine;
99 _applyEngineSettings();
102 if (_engine->state() == QTextToSpeech::State::Ready) {
107 qCDebug(AudioOutputLog) <<
"QTextToSpeech engine not ready; deferring init. State:" << _engine->state();
110 QTimer::singleShot(kEngineInitWarnTimeout,
this, [
this]() {
112 qCWarning(AudioOutputLog) <<
"No usable QTextToSpeech engine available. Engine:" << _engine->engine()
113 <<
"State:" << _engine->state()
114 <<
"Reason:" << _engine->errorReason() << _engine->errorString();
121void AudioOutput::_finishInit()
123 _applyEngineSettings();
133 if (AudioOutputLog().isDebugEnabled()) {
134 (void) connect(_engine, &QTextToSpeech::localeChanged,
this, [](
const QLocale &locale) {
135 qCDebug(AudioOutputLog) <<
"TTS Locale change to:" << locale;
137 (void) connect(_engine, &QTextToSpeech::volumeChanged,
this, [](
double volume) {
138 qCDebug(AudioOutputLog) <<
"TTS Volume changed to:" << volume;
140 (void) connect(_engine, &QTextToSpeech::sayingWord,
this, [](
const QString &word, qsizetype
id, qsizetype start, qsizetype length) {
141 qCDebug(AudioOutputLog) <<
"TTS Saying:" << word <<
"ID:" <<
id <<
"Start:" << start <<
"Length:" << length;
148 qCDebug(AudioOutputLog) <<
"AudioOutput initialized with volume:" << _volumeSetting() <<
"%";
151double AudioOutput::_volumeSetting()
const
153 return std::clamp(_volumeFact->
rawValue().toDouble(), 0.0, 100.0);
156bool AudioOutput::_mutedSetting()
const
158 return _mutedFact->
rawValue().toBool();
161void AudioOutput::_applyEngineSettings()
163 if (_engine->state() != QTextToSpeech::State::Ready) {
167 const QLocale defaultLocale(
"en_US");
168 if (_engine->availableLocales().contains(defaultLocale)) {
169 _engine->setLocale(defaultLocale);
173 const QList<QVoice> voices = _engine->availableVoices();
174 if (!voices.isEmpty()) {
175 _engine->setVoice(voices.constFirst());
178 _speakCapable = _engine->engineCapabilities().testFlag(QTextToSpeech::Capability::Speak);
181void AudioOutput::_setVolume()
183 const bool muted = _mutedSetting();
184 const double volume = muted ? 0.0 : _volumeSetting();
187 if (qFuzzyCompare(1.0 + volume, 1.0 + _lastVolume)) {
190 _lastVolume = volume;
193 const double normalizedVolume = volume / 100.0;
194 (void) QMetaObject::invokeMethod(_engine, [
this, volume, normalizedVolume]() {
197 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
200 _engine->setVolume(normalizedVolume);
202 qCDebug(AudioOutputLog) <<
"AudioOutput volume set to:" << volume <<
"%";
209 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using say().";
214 if (_volumeSetting() <= 0.0 || _mutedSetting()) {
218 if (!_speakCapable) {
219 qCWarning(AudioOutputLog) <<
"Speech Not Supported:" << text;
223 QString outText = _fixTextMessageForAudio(text);
226 outText = tr(
"%1").arg(outText);
229 if (outText.isEmpty()) {
234 (void) QMetaObject::invokeMethod(_engine, [
this, outText]() {
235 if (_textQueueSize >= kMaxTextQueueSize) {
236 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
238 qCWarning(AudioOutputLog) <<
"Text queue exceeded maximum size. Stopped current speech.";
241 const qsizetype index = _engine->enqueue(outText);
243 qCWarning(AudioOutputLog) <<
"Failed to enqueue speech. State:" << _engine->state()
244 <<
"Reason:" << _engine->errorReason();
249 qCDebug(AudioOutputLog) <<
"Enqueued text with index:" << index <<
", Queue Size:" << _textQueueSize;
256 qCWarning(AudioOutputLog) <<
"AudioOutput not initialized. Call init() before using testAudioOutput().";
261 _engine->stop(QTextToSpeech::BoundaryHint::Immediate);
264 const QString testText = tr(
"Audio test. Volume is %1 percent").arg(_volumeSetting(), 0,
'f', 1);
268QString AudioOutput::_fixTextMessageForAudio(
const QString &
string)
270 QString result = string;
271 result = _replaceAbbreviations(result);
272 result = _replaceNegativeSigns(result);
273 result = _replaceDecimalPoints(result);
274 result = _replaceMeters(result);
275 result = _convertMilliseconds(result);
279QString AudioOutput::_replaceAbbreviations(
const QString &input)
281 QStringList words = input.split(
' ');
282 for (QString &word : words) {
283 const auto it = _textHash.constFind(word.toUpper());
284 if (it != _textHash.constEnd()) {
289 return words.join(
' ');
292QString AudioOutput::_replaceNegativeSigns(
const QString &input)
294 static const QRegularExpression negNumRegex(QStringLiteral(
"-\\s*(?=\\d)"));
295 Q_ASSERT(negNumRegex.isValid());
297 QString output = input;
298 (void) output.replace(negNumRegex,
"negative ");
302QString AudioOutput::_replaceDecimalPoints(
const QString &input)
304 static const QRegularExpression realNumRegex(QStringLiteral(
"([0-9]+)(\\.)([0-9]+)"));
305 Q_ASSERT(realNumRegex.isValid());
307 QString output = input;
308 QRegularExpressionMatch realNumRegexMatch = realNumRegex.match(output);
309 while (realNumRegexMatch.hasMatch()) {
310 if (!realNumRegexMatch.captured(2).isNull()) {
311 (void) output.replace(realNumRegexMatch.capturedStart(2), realNumRegexMatch.capturedEnd(2) - realNumRegexMatch.capturedStart(2), QStringLiteral(
" point "));
313 realNumRegexMatch = realNumRegex.match(output);
319QString AudioOutput::_replaceMeters(
const QString &input)
321 static const QRegularExpression realNumMeterRegex(QStringLiteral(
"[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)"));
322 Q_ASSERT(realNumMeterRegex.isValid());
324 QString output = input;
325 QRegularExpressionMatch realNumMeterRegexMatch = realNumMeterRegex.match(output);
326 while (realNumMeterRegexMatch.hasMatch()) {
327 if (!realNumMeterRegexMatch.captured(1).isNull()) {
328 (void) output.replace(realNumMeterRegexMatch.capturedStart(1), realNumMeterRegexMatch.capturedEnd(1) - realNumMeterRegexMatch.capturedStart(1), QStringLiteral(
" meters"));
330 realNumMeterRegexMatch = realNumMeterRegex.match(output);
336QString AudioOutput::_convertMilliseconds(
const QString &input)
338 QString result = input;
342 if (_getMillisecondString(input, match, number) && (number >= 1000)) {
344 if (number < 60000) {
345 const int seconds = number / 1000;
346 const int ms = number - (seconds * 1000);
347 newNumber = QStringLiteral(
"%1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
"");
349 (void) newNumber.append(QStringLiteral(
" and %1 millisecond").arg(ms));
352 const int minutes = number / 60000;
353 const int seconds = (number - (minutes * 60000)) / 1000;
354 newNumber = QStringLiteral(
"%1 minute%2").arg(minutes).arg(minutes > 1 ?
"s" :
"");
356 (void) newNumber.append(QStringLiteral(
" and %1 second%2").arg(seconds).arg(seconds > 1 ?
"s" :
""));
359 (void) result.replace(match, newNumber);
365bool AudioOutput::_getMillisecondString(
const QString &
string, QString &match,
int &number)
367 static const QRegularExpression msRegex(
"((?<number>[0-9]+)ms)");
368 Q_ASSERT(msRegex.isValid());
372 QRegularExpressionMatch regexpMatch = msRegex.match(
string);
373 if (regexpMatch.hasMatch()) {
374 match = regexpMatch.captured(0);
375 const QString numberStr = regexpMatch.captured(
"number");
376 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 ...