Skip to content

Commit

Permalink
AudioOutput improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
HTRamsey committed Apr 16, 2024
1 parent e05473a commit 3d5db33
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 181 deletions.
1 change: 0 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ find_package(Qt6
Sql
Svg
Test
TextToSpeech
Widgets
Xml
OPTIONAL_COMPONENTS
Expand Down
1 change: 0 additions & 1 deletion src/AnalyzeView/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ target_link_libraries(AnalyzeView
PUBLIC
Qt6::Charts
Qt6::Location
Qt6::TextToSpeech
Qt6::Widgets
)

Expand Down
279 changes: 137 additions & 142 deletions src/Audio/AudioOutput.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,191 +7,186 @@
*
****************************************************************************/

#include <QDebug>
#include <QRegularExpression>

#include "AudioOutput.h"
#include "QGCApplication.h"
#include "SettingsManager.h"

AudioOutput::AudioOutput(QGCApplication* app, QGCToolbox* toolbox)
: QGCTool (app, toolbox)
, _tts (nullptr)
{
if (qgcApp()->runningUnitTests()) {
// Cloud based unit tests don't have speech capabilty. If you try to crank up
// speech engine it will pop a qWarning which prevents usage of QT_FATAL_WARNINGS
return;
}
#include <QtCore/QRegularExpression>

#define MAX_TEXT_QUEUE_SIZE 20U

Q_LOGGING_CATEGORY( AudioOutputLog, "qgc.audio.audiooutput" );
// qt.speech.tts.flite
// qt.speech.tts.android

const QHash<QString, QString> AudioOutput::s_textHash = {
{ "ERR", "error" },
{ "POSCTL", "Position Control" },
{ "ALTCTL", "Altitude Control" },
{ "AUTO_RTL", "auto return to launch" },
{ "RTL", "return To launch" },
{ "ACCEL", "accelerometer" },
{ "RC_MAP_MODE_SW", "RC mode switch" },
{ "REJ", "rejected" },
{ "WP", "waypoint" },
{ "CMD", "command" },
{ "COMPID", "component eye dee" },
{ "params", "parameters" },
{ "id", "eye dee" },
{ "ADSB", "Hey Dee Ess Bee" },
{ "EKF", "Eee Kay Eff" },
{ "PREARM", "pre arm" },
{ "PITOT", "pee toe" },
};

_tts = new QTextToSpeech(this);
Q_APPLICATION_STATIC( AudioOutput, s_audioOutput );

//-- Force TTS engine to English as all incoming messages from the autopilot
// are in English and not localized.
#ifdef Q_OS_LINUX
_tts->setLocale(QLocale("en_US"));
#endif
connect(_tts, &QTextToSpeech::stateChanged, this, &AudioOutput::_stateChanged);
AudioOutput* AudioOutput::instance()
{
return s_audioOutput();
}

void AudioOutput::say(const QString& inText)
AudioOutput::AudioOutput( QObject* parent )
: QTextToSpeech( QStringLiteral("none"), parent )
{
if (!_tts) {
qDebug() << "say" << inText;
return;
}
#ifdef QT_DEBUG
( void ) connect( this, &QTextToSpeech::stateChanged, []( QTextToSpeech::State state ) {
qCInfo( AudioOutputLog ) << Q_FUNC_INFO << "State:" << state;
});
( void ) connect( this, &QTextToSpeech::errorOccurred, []( QTextToSpeech::ErrorReason reason, const QString &errorString ) {
( void ) reason;
qCInfo( AudioOutputLog ) << Q_FUNC_INFO << "Error:" << errorString;
});
#endif

bool muted = qgcApp()->toolbox()->settingsManager()->appSettings()->audioMuted()->rawValue().toBool();
muted |= qgcApp()->runningUnitTests();
if (!muted && !qgcApp()->runningUnitTests()) {
QString text = fixTextMessageForAudio(inText);
if(_tts->state() == QTextToSpeech::Speaking) {
if(!_texts.contains(text)) {
//-- Some arbitrary limit
if(_texts.size() > 20) {
_texts.removeFirst();
}
_texts.append(text);
if ( !QTextToSpeech::availableEngines().isEmpty() ) {
if ( setEngine( QString() ) ) { // Autoselect engine by priority
if ( availableLocales().contains( QLocale( "en_US" ) ) ) {
setLocale( QLocale( "en_US" ) );
}
} else {
_tts->say(text);

( void ) connect( this, &QTextToSpeech::volumeChanged, [ this ]( double volume ) {
( void ) volume;
const bool muted = isMuted();
if ( muted != m_lastMuted ) {
emit mutedChanged( muted );
m_lastMuted = muted;
}
});
}
}
}

void AudioOutput::_stateChanged(QTextToSpeech::State state)
bool AudioOutput::isMuted() const
{
if(state == QTextToSpeech::Ready) {
if(_texts.size()) {
QString text = _texts.first();
_texts.removeFirst();
_tts->say(text);
}
}
return qFuzzyIsNull( volume() );
}

bool AudioOutput::getMillisecondString(const QString& string, QString& match, int& number) {
static QRegularExpression re("([0-9]+ms)");
QRegularExpressionMatchIterator i = re.globalMatch(string);
while (i.hasNext()) {
QRegularExpressionMatch qmatch = i.next();
if (qmatch.hasMatch()) {
match = qmatch.captured(0);
number = qmatch.captured(0).replace("ms", "").toInt();
return true;
}
void AudioOutput::setMuted( bool enable )
{
if ( enable != isMuted() ) {
( void ) QMetaObject::invokeMethod( this, "setVolume", Qt::AutoConnection, enable ? 0. : 100. );
}
return false;
}

QString AudioOutput::fixTextMessageForAudio(const QString& string) {
QString match;
QString newNumber;
QString result = string;

//-- Look for codified terms
if(result.contains("ERR ", Qt::CaseInsensitive)) {
result.replace("ERR ", "error ", Qt::CaseInsensitive);
}
if(result.contains("ERR:", Qt::CaseInsensitive)) {
result.replace("ERR:", "error.", Qt::CaseInsensitive);
}
if(result.contains("POSCTL", Qt::CaseInsensitive)) {
result.replace("POSCTL", "Position Control", Qt::CaseInsensitive);
}
if(result.contains("ALTCTL", Qt::CaseInsensitive)) {
result.replace("ALTCTL", "Altitude Control", Qt::CaseInsensitive);
}
if(result.contains("AUTO_RTL", Qt::CaseInsensitive)) {
result.replace("AUTO_RTL", "auto Return To Launch", Qt::CaseInsensitive);
} else if(result.contains("RTL", Qt::CaseInsensitive)) {
result.replace("RTL", "Return To Launch", Qt::CaseInsensitive);
}
if(result.contains("ACCEL ", Qt::CaseInsensitive)) {
result.replace("ACCEL ", "accelerometer ", Qt::CaseInsensitive);
}
if(result.contains("RC_MAP_MODE_SW", Qt::CaseInsensitive)) {
result.replace("RC_MAP_MODE_SW", "RC mode switch", Qt::CaseInsensitive);
}
if(result.contains("REJ.", Qt::CaseInsensitive)) {
result.replace("REJ.", "Rejected", Qt::CaseInsensitive);
}
if(result.contains("WP", Qt::CaseInsensitive)) {
result.replace("WP", "way point", Qt::CaseInsensitive);
}
if(result.contains("CMD", Qt::CaseInsensitive)) {
result.replace("CMD", "command", Qt::CaseInsensitive);
}
if(result.contains("COMPID", Qt::CaseInsensitive)) {
result.replace("COMPID", "component eye dee", Qt::CaseInsensitive);
}
if(result.contains(" params ", Qt::CaseInsensitive)) {
result.replace(" params ", " parameters ", Qt::CaseInsensitive);
void AudioOutput::say( const QString& text, AudioOutput::TextMods textMods )
{
if ( !engineCapabilities().testFlag( QTextToSpeech::Capability::Speak ) ) {
qCWarning( AudioOutputLog ) << Q_FUNC_INFO << "Speech Not Supported:" << text;
return;
}
if(result.contains(" id ", Qt::CaseInsensitive)) {
result.replace(" id ", " eye dee ", Qt::CaseInsensitive);

if ( m_textQueueSize > MAX_TEXT_QUEUE_SIZE ) {
( void ) QMetaObject::invokeMethod( this, "stop", Qt::AutoConnection, QTextToSpeech::BoundaryHint::Default );
}
if(result.contains(" ADSB ", Qt::CaseInsensitive)) {
result.replace(" ADSB ", " Hey Dee Ess Bee ", Qt::CaseInsensitive);

QString outText = AudioOutput::fixTextMessageForAudio( text );

if ( textMods.testFlag( AudioOutput::TextMod::Translate ) ) {
outText = QCoreApplication::translate("AudioOutput", outText.toStdString().c_str() );
}
if(result.contains(" EKF ", Qt::CaseInsensitive)) {
result.replace(" EKF ", " Eee Kay Eff ", Qt::CaseInsensitive);

qsizetype index = -1;
( void ) QMetaObject::invokeMethod( this, "enqueue", Qt::AutoConnection, qReturnArg( m_textQueueSize ), outText );
if ( index != -1 ) {
m_textQueueSize = index;
}
if(result.contains("PREARM", Qt::CaseInsensitive)) {
result.replace("PREARM", "pre arm", Qt::CaseInsensitive);
}

bool AudioOutput::getMillisecondString( const QString& string, QString& match, int& number )
{
static const QRegularExpression regexp( "([0-9]+ms)" );

bool result = false;

for ( const QRegularExpressionMatch &tempMatch : regexp.globalMatch( string ) ) {
if ( tempMatch.hasMatch() ) {
match = tempMatch.captured( 0 );
number = tempMatch.captured( 0 ).replace( "ms", "" ).toInt();
result = true;
break;
}
}
if(result.contains("PITOT", Qt::CaseInsensitive)) {
result.replace("PITOT", "pee toe", Qt::CaseInsensitive);

return result;
}

QString AudioOutput::fixTextMessageForAudio( const QString& string )
{
QString result = string;

for ( const QString& word: string.split( ' ', Qt::SkipEmptyParts ) ) {
if ( s_textHash.contains( word.toUpper() ) ) {
result.replace( word, s_textHash.value( word.toUpper() ) );
}
}

// Convert negative numbers
QRegularExpression re(QStringLiteral("(-)[0-9]*\\.?[0-9]"));
QRegularExpressionMatch reMatch = re.match(result);
while (reMatch.hasMatch()) {
if (!reMatch.captured(1).isNull()) {
// There is a negative prefix
result.replace(reMatch.capturedStart(1), reMatch.capturedEnd(1) - reMatch.capturedStart(1), tr(" negative "));
static const QRegularExpression negNumRegex( QStringLiteral( "(-)[0-9]*\\.?[0-9]" ) );
QRegularExpressionMatch negNumRegexMatch = negNumRegex.match( result );
while ( negNumRegexMatch.hasMatch() ) {
if ( !negNumRegexMatch.captured( 1 ).isNull() ) {
result.replace( negNumRegexMatch.capturedStart( 1 ), negNumRegexMatch.capturedEnd( 1 ) - negNumRegexMatch.capturedStart( 1 ), tr(" negative "));
}
reMatch = re.match(result);
negNumRegexMatch = negNumRegex.match( result );
}

// Convert real number with decimal point
re.setPattern(QStringLiteral("([0-9]+)(\\.)([0-9]+)"));
reMatch = re.match(result);
while (reMatch.hasMatch()) {
if (!reMatch.captured(2).isNull()) {
// There is a decimal point
result.replace(reMatch.capturedStart(2), reMatch.capturedEnd(2) - reMatch.capturedStart(2), tr(" point "));
static const QRegularExpression realNumRegex( QStringLiteral( "([0-9]+)(\\.)([0-9]+)" ) );
QRegularExpressionMatch realNumRegexMatch = realNumRegex.match( result );
while ( realNumRegexMatch.hasMatch() ) {
if ( !realNumRegexMatch.captured( 2 ).isNull() ) {
result.replace(realNumRegexMatch.capturedStart( 2 ), realNumRegexMatch.capturedEnd( 2 ) - realNumRegexMatch.capturedStart( 2 ), tr( " point " ));
}
reMatch = re.match(result);
realNumRegexMatch = realNumRegex.match( result );
}

// Convert meter postfix after real number
re.setPattern(QStringLiteral("[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)"));
reMatch = re.match(result);
while (reMatch.hasMatch()) {
if (!reMatch.captured(1).isNull()) {
// There is a meter postfix
result.replace(reMatch.capturedStart(1), reMatch.capturedEnd(1) - reMatch.capturedStart(1), tr(" meters"));
static const QRegularExpression realNumMeterRegex( QStringLiteral( "[0-9]*\\.?[0-9]\\s?(m)([^A-Za-z]|$)" ) );
QRegularExpressionMatch realNumMeterRegexMatch = realNumMeterRegex.match( result );
while ( realNumMeterRegexMatch.hasMatch() ) {
if ( !realNumMeterRegexMatch.captured( 1 ).isNull( ) ) {
result.replace( realNumMeterRegexMatch.capturedStart( 1 ), realNumMeterRegexMatch.capturedEnd( 1 ) - realNumMeterRegexMatch.capturedStart( 1 ), tr(" meters"));
}
reMatch = re.match(result);
realNumMeterRegexMatch = realNumMeterRegex.match( result );
}

QString match;
int number;
if(getMillisecondString(string, match, number) && number > 1000) {
if(number < 60000) {
int seconds = number / 1000;
newNumber = QString("%1 second%2").arg(seconds).arg(seconds > 1 ? "s" : "");
if ( getMillisecondString( string, match, number ) && ( number > 1000 ) ) {
QString newNumber;
if ( number < 60000 ) {
const int seconds = number / 1000;
newNumber = QString( "%1 second%2" ).arg( seconds ).arg( seconds > 1 ? "s" : "" );
} else {
int minutes = number / 60000;
int seconds = (number - (minutes * 60000)) / 1000;
if (!seconds) {
newNumber = QString("%1 minute%2").arg(minutes).arg(minutes > 1 ? "s" : "");
const int minutes = number / 60000;
const int seconds = ( number - ( minutes * 60000 ) ) / 1000;
if ( !seconds ) {
newNumber = QString( "%1 minute%2" ).arg( minutes ).arg( minutes > 1 ? "s" : "" );
} else {
newNumber = QString("%1 minute%2 and %3 second%4").arg(minutes).arg(minutes > 1 ? "s" : "").arg(seconds).arg(seconds > 1 ? "s" : "");
newNumber = QString( "%1 minute%2 and %3 second%4" ).arg( minutes ).arg( minutes > 1 ? "s" : "" ).arg( seconds ).arg( seconds > 1 ? "s" : "" );
}
}
result.replace(match, newNumber);
result.replace( match, newNumber );
}

return result;
}
Loading

0 comments on commit 3d5db33

Please sign in to comment.