AudioOutput.cc 7.16 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

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

DonLakeFlyer's avatar
DonLakeFlyer committed
19
AudioOutput::AudioOutput(QGCApplication* app, QGCToolbox* toolbox)
Don Gagne's avatar
   
Don Gagne committed
20
21
    : QGCTool   (app, toolbox)
    , _tts      (nullptr)
Lorenz Meier's avatar
Lorenz Meier committed
22
{
Don Gagne's avatar
   
Don Gagne committed
23
24
25
26
27
28
29
30
    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;
    }

    _tts = new QTextToSpeech(this);

31
32
33
34
35
    //-- 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
DonLakeFlyer's avatar
DonLakeFlyer committed
36
    connect(_tts, &QTextToSpeech::stateChanged, this, &AudioOutput::_stateChanged);
Lorenz Meier's avatar
Lorenz Meier committed
37
38
}

Don Gagne's avatar
   
Don Gagne committed
39
void AudioOutput::say(const QString& inText)
Lorenz Meier's avatar
Lorenz Meier committed
40
{
Don Gagne's avatar
   
Don Gagne committed
41
42
43
44
45
    if (!_tts) {
        qDebug() << "say" << inText;
        return;
    }

Don Gagne's avatar
Don Gagne committed
46
    bool muted = qgcApp()->toolbox()->settingsManager()->appSettings()->audioMuted()->rawValue().toBool();
Don Gagne's avatar
Don Gagne committed
47
    muted |= qgcApp()->runningUnitTests();
Don Gagne's avatar
Don Gagne committed
48
    if (!muted && !qgcApp()->runningUnitTests()) {
49
50
51
52
53
54
55
56
57
58
59
        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
60
        }
61
    }
Lorenz Meier's avatar
Lorenz Meier committed
62
}
63

DonLakeFlyer's avatar
DonLakeFlyer committed
64
void AudioOutput::_stateChanged(QTextToSpeech::State state)
65
66
67
68
69
70
71
72
73
74
{
    if(state == QTextToSpeech::Ready) {
        if(_texts.size()) {
            QString text = _texts.first();
            _texts.removeFirst();
            _tts->say(text);
        }
    }
}

DonLakeFlyer's avatar
DonLakeFlyer committed
75
bool AudioOutput::getMillisecondString(const QString& string, QString& match, int& number) {
76
77
78
79
80
81
82
83
84
85
86
87
88
    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;
}

DonLakeFlyer's avatar
DonLakeFlyer committed
89
QString AudioOutput::fixTextMessageForAudio(const QString& string) {
90
91
92
    QString match;
    QString newNumber;
    QString result = string;
DonLakeFlyer's avatar
DonLakeFlyer committed
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
127
128
129
130
131
132
133
134
135
136
137
138
    //-- 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);
    }
DonLakeFlyer's avatar
DonLakeFlyer committed
139
140
141
    if(result.contains(" EKF ", Qt::CaseInsensitive)) {
        result.replace(" EKF ", " Eee Kay Eff ", Qt::CaseInsensitive);
    }
Don Gagne's avatar
   
Don Gagne committed
142
143
144
    if(result.contains("PREARM", Qt::CaseInsensitive)) {
        result.replace("PREARM", "pre arm", Qt::CaseInsensitive);
    }
Don Gagne's avatar
   
Don Gagne committed
145
146
147
    if(result.contains("PITOT", Qt::CaseInsensitive)) {
        result.replace("PITOT", "pee toe", Qt::CaseInsensitive);
    }
DonLakeFlyer's avatar
DonLakeFlyer committed
148
149
150
151
152
153
154
155

    // 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 "));
DonLakeFlyer's avatar
DonLakeFlyer committed
156
157
158
159
160
        }
        reMatch = re.match(result);
    }

    // Convert real number with decimal point
161
    re.setPattern(QStringLiteral("([0-9]+)(\\.)([0-9]+)"));
DonLakeFlyer's avatar
DonLakeFlyer committed
162
163
164
165
    reMatch = re.match(result);
    while (reMatch.hasMatch()) {
        if (!reMatch.captured(2).isNull()) {
            // There is a decimal point
166
            result.replace(reMatch.capturedStart(2), reMatch.capturedEnd(2) - reMatch.capturedStart(2), tr(" point "));
DonLakeFlyer's avatar
DonLakeFlyer committed
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
        }
        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);
    }

182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
    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;
}