QGCAudioWorker.cpp 9.6 KB
Newer Older
1 2
#include <QSettings>
#include <QDebug>
3 4
#include <QCoreApplication>
#include <QFile>
5
#include <QRegularExpression>
6 7 8

#include "QGC.h"
#include "QGCAudioWorker.h"
9
#include "GAudioOutput.h"
10

dogmaphobic's avatar
dogmaphobic committed
11
#if (defined __macos__) && defined QGC_SPEECH_ENABLED
12
#include <ApplicationServices/ApplicationServices.h>
13
#include <MacTypes.h>
dogmaphobic's avatar
dogmaphobic committed
14

15 16 17
void macSpeak(const char* words)
{
    static SpeechChannel sc = NULL;
dogmaphobic's avatar
dogmaphobic committed
18

19 20
    while (SpeechBusy()) {
        QGC::SLEEP::msleep(100);
dogmaphobic's avatar
dogmaphobic committed
21
    }
22 23
    if (sc == NULL) {
        Float32 volume = 1.0;
dogmaphobic's avatar
dogmaphobic committed
24 25

        NewSpeechChannel(NULL, &sc);
26 27 28
        CFNumberRef volumeRef = CFNumberCreate(NULL, kCFNumberFloat32Type, &volume);
        SetSpeechProperty(sc, kSpeechVolumeProperty, volumeRef);
        CFRelease(volumeRef);
dogmaphobic's avatar
dogmaphobic committed
29
    }
30 31 32 33
    CFStringRef strRef = CFStringCreateWithCString(NULL, words, kCFStringEncodingUTF8);
    SpeakCFString(sc, strRef, NULL);
    CFRelease(strRef);
}
34 35
#endif

dogmaphobic's avatar
dogmaphobic committed
36 37 38 39
#if (defined __ios__) && defined QGC_SPEECH_ENABLED
extern void iOSSpeak(QString msg);
#endif

40 41 42 43 44 45
// Speech synthesis is only supported with MSVC compiler
#if defined _MSC_VER && defined QGC_SPEECH_ENABLED
// Documentation: http://msdn.microsoft.com/en-us/library/ee125082%28v=VS.85%29.aspx
#include <sapi.h>
#endif

dogmaphobic's avatar
dogmaphobic committed
46
#if defined Q_OS_LINUX && !defined __android__ && defined QGC_SPEECH_ENABLED
47 48 49 50 51 52 53 54 55
// Using eSpeak for speech synthesis: following https://github.com/mondhs/espeak-sample/blob/master/sampleSpeak.cpp
#include <espeak/speak_lib.h>
#endif

#define QGC_GAUDIOOUTPUT_KEY QString("QGC_AUDIOOUTPUT_")

QGCAudioWorker::QGCAudioWorker(QObject *parent) :
    QObject(parent),
    voiceIndex(0),
56 57 58
    #if defined _MSC_VER && defined QGC_SPEECH_ENABLED
    pVoice(NULL),
    #endif
59 60
    emergency(false),
    muted(false)
61 62 63 64
{
    // Load settings
    QSettings settings;
    muted = settings.value(QGC_GAUDIOOUTPUT_KEY + "muted", muted).toBool();
65
}
66

67 68
void QGCAudioWorker::init()
{
dogmaphobic's avatar
dogmaphobic committed
69
#if defined Q_OS_LINUX && !defined __android__ && defined QGC_SPEECH_ENABLED
70
    espeak_Initialize(AUDIO_OUTPUT_PLAYBACK, 500, NULL, 0); // initialize for playback with 500ms buffer and no options (see speak_lib.h)
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    espeak_VOICE *espeak_voice = espeak_GetCurrentVoice();
    espeak_voice->languages = "en-uk"; // Default to British English
    espeak_voice->identifier = NULL; // no specific voice file specified
    espeak_voice->name = "klatt"; // espeak voice name
    espeak_voice->gender = 2; // Female
    espeak_voice->age = 0; // age not specified
    espeak_SetVoiceByProperties(espeak_voice);
#endif

#if defined _MSC_VER && defined QGC_SPEECH_ENABLED

    if (FAILED(::CoInitialize(NULL)))
    {
        qDebug() << "ERROR: Creating COM object for audio output failed!";
    }
    else
    {

        HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&pVoice);

        if (FAILED(hr))
        {
            qDebug() << "ERROR: Initializing voice for audio output failed!";
        }
    }

#endif
}

QGCAudioWorker::~QGCAudioWorker()
{
#if defined _MSC_VER && defined QGC_SPEECH_ENABLED
dogmaphobic's avatar
dogmaphobic committed
103 104 105 106
    if (pVoice) {
        pVoice->Release();
        pVoice = NULL;
    }
107 108 109 110
    ::CoUninitialize();
#endif
}

Don Gagne's avatar
Don Gagne committed
111
void QGCAudioWorker::say(QString inText)
112
{
dogmaphobic's avatar
dogmaphobic committed
113 114 115 116 117 118 119 120
#ifdef __android__
    Q_UNUSED(inText);
#else
    static bool threadInit = false;
    if (!threadInit) {
        threadInit = true;
        init();
    }
Don Gagne's avatar
Don Gagne committed
121

122 123
    if (!muted)
    {
dogmaphobic's avatar
dogmaphobic committed
124
        QString text = fixTextMessageForAudio(inText);
125

126
#if defined _MSC_VER && defined QGC_SPEECH_ENABLED
Don Gagne's avatar
Don Gagne committed
127
        HRESULT hr = pVoice->Speak(text.toStdWString().c_str(), SPF_DEFAULT, NULL);
dogmaphobic's avatar
dogmaphobic committed
128 129 130
        if (FAILED(hr)) {
            qDebug() << "Speak failed, HR:" << QString("%1").arg(hr, 0, 16);
        }
131 132 133 134
#elif defined Q_OS_LINUX && defined QGC_SPEECH_ENABLED
        // Set size of string for espeak: +1 for the null-character
        unsigned int espeak_size = strlen(text.toStdString().c_str()) + 1;
        espeak_Synth(text.toStdString().c_str(), espeak_size, 0, POS_CHARACTER, 0, espeakCHARS_AUTO, NULL, NULL);
135
        espeak_Synchronize();
dogmaphobic's avatar
dogmaphobic committed
136
#elif (defined __macos__) && defined QGC_SPEECH_ENABLED
137
        macSpeak(text.toStdString().c_str());
dogmaphobic's avatar
dogmaphobic committed
138 139
#elif (defined __ios__) && defined QGC_SPEECH_ENABLED
        iOSSpeak(text);
140 141
#else
        // Make sure there isn't an unused variable warning when speech output is disabled
142
        Q_UNUSED(inText);
143
#endif
144
    }
dogmaphobic's avatar
dogmaphobic committed
145
#endif // __android__
146 147 148 149 150 151 152 153 154
}

void QGCAudioWorker::mute(bool mute)
{
    if (mute != muted)
    {
        this->muted = mute;
        QSettings settings;
        settings.setValue(QGC_GAUDIOOUTPUT_KEY + "muted", this->muted);
155
        //        emit mutedChanged(muted);
156 157 158 159 160 161 162
    }
}

bool QGCAudioWorker::isMuted()
{
    return this->muted;
}
163 164

bool QGCAudioWorker::_getMillisecondString(const QString& string, QString& match, int& number) {
dogmaphobic's avatar
dogmaphobic committed
165
    static QRegularExpression re("([0-9]+ms)");
166 167
    QRegularExpressionMatchIterator i = re.globalMatch(string);
    while (i.hasNext()) {
168 169 170 171
        QRegularExpressionMatch reMatch = i.next();
        if (reMatch.hasMatch()) {
            match = reMatch.captured(0);
            number = reMatch.captured(0).replace("ms", "").toInt();
172 173 174 175 176 177
            return true;
        }
    }
    return false;
}

dogmaphobic's avatar
dogmaphobic committed
178
QString QGCAudioWorker::fixTextMessageForAudio(const QString& string) {
179 180 181
    QString match;
    QString newNumber;
    QString result = string;
182

183
    //-- Look for codified terms
184 185 186 187 188
    if(result.contains(QStringLiteral("ERR "), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("ERR "), tr("error "), Qt::CaseInsensitive);
    }
    if(result.contains(QStringLiteral("ERR:"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("ERR:"), tr("error."), Qt::CaseInsensitive);
189
    }
190 191
    if(result.contains(QStringLiteral("POSCTL"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("POSCTL"), tr("Position Control"), Qt::CaseInsensitive);
192
    }
193 194
    if(result.contains(QStringLiteral("ALTCTL"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("ALTCTL"), tr("Altitude Control"), Qt::CaseInsensitive);
195
    }
196 197 198 199
    if(result.contains(QStringLiteral("AUTO_RTL"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("AUTO_RTL"), tr("auto Return To Launch"), Qt::CaseInsensitive);
    } else if(result.contains(QStringLiteral("RTL"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("RTL"), tr("Return To Launch"), Qt::CaseInsensitive);
200
    }
201 202
    if(result.contains(QStringLiteral("ACCEL "), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("ACCEL "), tr("accelerometer "), Qt::CaseInsensitive);
203
    }
204 205
    if(result.contains(QStringLiteral("RC_MAP_MODE_SW"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("RC_MAP_MODE_SW"), tr("RC mode switch"), Qt::CaseInsensitive);
206
    }
207 208
    if(result.contains(QStringLiteral("REJ."), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("REJ."), tr("Rejected"), Qt::CaseInsensitive);
209
    }
210 211
    if(result.contains(QStringLiteral("WP"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("WP"), tr("way point"), Qt::CaseInsensitive);
212
    }
213 214
    if(result.contains(QStringLiteral("CMD"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("CMD"), tr("command"), Qt::CaseInsensitive);
215
    }
216 217
    if(result.contains(QStringLiteral("COMPID"), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral("COMPID"), tr("component eye dee"), Qt::CaseInsensitive);
218
    }
219 220
    if(result.contains(QStringLiteral(" params "), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral(" params "), tr(" parameters "), Qt::CaseInsensitive);
dogmaphobic's avatar
dogmaphobic committed
221
    }
222 223
    if(result.contains(QStringLiteral(" id "), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral(" id "), QStringLiteral(" eye dee "), Qt::CaseInsensitive);
dogmaphobic's avatar
dogmaphobic committed
224
    }
225 226
    if(result.contains(QStringLiteral(" ADSB "), Qt::CaseInsensitive)) {
        result.replace(QStringLiteral(" ADSB "), QStringLiteral(" Hey Dee Ess Bee "), Qt::CaseInsensitive);
227
    }
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252

    // 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
            qDebug() << "negative" << reMatch.captured(1) << reMatch.capturedStart(1) << reMatch.capturedEnd(1);
            result.replace(reMatch.capturedStart(1), reMatch.capturedEnd(1) - reMatch.capturedStart(1), tr(" negative "));
            qDebug() << result;
        }
        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
            qDebug() << "meters" << reMatch.captured(1) << reMatch.capturedStart(1) << reMatch.capturedEnd(1);
            result.replace(reMatch.capturedStart(1), reMatch.capturedEnd(1) - reMatch.capturedStart(1), tr(" meters"));
            qDebug() << result;
        }
        reMatch = re.match(result);
253 254
    }

255 256 257 258
    int number;
    if(_getMillisecondString(string, match, number) && number > 1000) {
        if(number < 60000) {
            int seconds = number / 1000;
259
            newNumber = tr("%1 second%2").arg(seconds).arg(seconds > 1 ? "s" : "");
260 261 262 263
        } else {
            int minutes = number / 60000;
            int seconds = (number - (minutes * 60000)) / 1000;
            if (!seconds) {
264
                newNumber = tr("%1 minute%2").arg(minutes).arg(minutes > 1 ? "s" : "");
265
            } else {
266
                newNumber = tr("%1 minute%2 and %3 second%4").arg(minutes).arg(minutes > 1 ? "s" : "").arg(seconds).arg(seconds > 1 ? "s" : "");
267 268 269 270
            }
        }
        result.replace(match, newNumber);
    }
271
    // qDebug() << "Speech: " << result;
272 273
    return result;
}