AudioOutput.cc 6.71 KB
Newer Older
1 2 3 4 5 6 7 8
/****************************************************************************
 *
 *   (c) 2009-2016 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/
Lorenz Meier's avatar
Lorenz Meier committed
9 10 11

#include <QApplication>
#include <QDebug>
12
#include <QRegularExpression>
Lorenz Meier's avatar
Lorenz Meier committed
13

14
#include "AudioOutput.h"
15 16
#include "QGCApplication.h"
#include "QGC.h"
17
#include "SettingsManager.h"
Lorenz Meier's avatar
Lorenz Meier committed
18

19
AudioOutput::AudioOutput(QGCApplication* app, QGCToolbox* toolbox)
20
    : QGCTool(app, toolbox)
21
    , _tts(new QTextToSpeech(this))
Lorenz Meier's avatar
Lorenz Meier committed
22
{
23 24 25 26 27
    //-- 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
28
    connect(_tts, &QTextToSpeech::stateChanged, this, &AudioOutput::_stateChanged);
Lorenz Meier's avatar
Lorenz Meier committed
29 30
}

31
bool AudioOutput::say(const QString& inText)
Lorenz Meier's avatar
Lorenz Meier committed
32
{
33
    bool muted = qgcApp()->toolbox()->settingsManager()->appSettings()->audioMuted()->rawValue().toBool();
34
    muted |= qgcApp()->runningUnitTests();
Don Gagne's avatar
Don Gagne committed
35
    if (!muted && !qgcApp()->runningUnitTests()) {
36 37 38 39 40 41 42 43 44 45 46
        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);
            }
        } else {
            _tts->say(text);
dogmaphobic's avatar
dogmaphobic committed
47
        }
48
    }
49
    return true;
Lorenz Meier's avatar
Lorenz Meier committed
50
}
51

52
void AudioOutput::_stateChanged(QTextToSpeech::State state)
53 54 55 56 57 58 59 60 61 62
{
    if(state == QTextToSpeech::Ready) {
        if(_texts.size()) {
            QString text = _texts.first();
            _texts.removeFirst();
            _tts->say(text);
        }
    }
}

63
bool AudioOutput::getMillisecondString(const QString& string, QString& match, int& number) {
64 65 66 67 68 69 70 71 72 73 74 75 76
    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;
        }
    }
    return false;
}

77
QString AudioOutput::fixTextMessageForAudio(const QString& string) {
78 79 80
    QString match;
    QString newNumber;
    QString result = string;
81

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    //-- 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);
    }
    if(result.contains(" id ", Qt::CaseInsensitive)) {
        result.replace(" id ", " eye dee ", Qt::CaseInsensitive);
    }
    if(result.contains(" ADSB ", Qt::CaseInsensitive)) {
        result.replace(" ADSB ", " Hey Dee Ess Bee ", Qt::CaseInsensitive);
    }
127 128 129
    if(result.contains(" EKF ", Qt::CaseInsensitive)) {
        result.replace(" EKF ", " Eee Kay Eff ", Qt::CaseInsensitive);
    }
Don Gagne's avatar
Don Gagne committed
130 131 132
    if(result.contains("PREARM", Qt::CaseInsensitive)) {
        result.replace("PREARM", "pre arm", Qt::CaseInsensitive);
    }
133 134 135 136 137 138 139 140

    // 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 "));
141 142 143 144 145
        }
        reMatch = re.match(result);
    }

    // Convert real number with decimal point
146
    re.setPattern(QStringLiteral("([0-9]+)(\\.)([0-9]+)"));
147 148 149 150
    reMatch = re.match(result);
    while (reMatch.hasMatch()) {
        if (!reMatch.captured(2).isNull()) {
            // There is a decimal point
151
            result.replace(reMatch.capturedStart(2), reMatch.capturedEnd(2) - reMatch.capturedStart(2), tr(" point "));
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
        }
        reMatch = re.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"));
        }
        reMatch = re.match(result);
    }

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
    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" : "");
        } else {
            int minutes = number / 60000;
            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" : "");
            }
        }
        result.replace(match, newNumber);
    }
    return result;
}