QGCAudioWorker.cpp 8.13 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 11 12

#if defined Q_OS_MAC && defined QGC_SPEECH_ENABLED
#include <ApplicationServices/ApplicationServices.h>
dogmaphobic's avatar
dogmaphobic committed
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

static SpeechChannel sc;
static Fixed volume;

static void speechDone(SpeechChannel sc2, void *) {
    if (sc2 == sc)
    {
        DisposeSpeechChannel(sc);
    }
}

class MacSpeech
{
public:
    MacSpeech()
    {
        setVolume(100);
    }
    ~MacSpeech()
    {
    }
    void say(const char* words)
    {
        while (SpeechBusy()) {
            QGC::SLEEP::msleep(100);
        }
        NewSpeechChannel(NULL, &sc);
        SetSpeechInfo(sc, soVolume, &volume);
        SetSpeechInfo(sc, soSpeechDoneCallBack, reinterpret_cast<void *>(speechDone));
        CFStringRef cfstr = CFStringCreateWithCString(NULL, words, kCFStringEncodingUTF8);
        SpeakCFString(sc, cfstr, NULL);
    }
    void setVolume(int v)
    {
        volume = FixRatio(v, 100);
    }
};

//-- Singleton
MacSpeech macSpeech;

54 55 56 57 58 59 60 61
#endif

// 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
62
#if defined Q_OS_LINUX && !defined __android__ && defined QGC_SPEECH_ENABLED
63 64 65 66 67 68 69 70 71
// 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),
72 73 74
    #if defined _MSC_VER && defined QGC_SPEECH_ENABLED
    pVoice(NULL),
    #endif
75 76 77 78 79
    #ifdef QGC_NOTIFY_TUNES_ENABLED
    sound(NULL),
    #endif
    emergency(false),
    muted(false)
80 81 82 83
{
    // Load settings
    QSettings settings;
    muted = settings.value(QGC_GAUDIOOUTPUT_KEY + "muted", muted).toBool();
84
}
85

86 87
void QGCAudioWorker::init()
{
88
#ifdef QGC_NOTIFY_TUNES_ENABLED
Don Gagne's avatar
Don Gagne committed
89
    sound = new QSound(":/res/Alert");
90
#endif
91

dogmaphobic's avatar
dogmaphobic committed
92
#if defined Q_OS_LINUX && !defined __android__ && defined QGC_SPEECH_ENABLED
93
    espeak_Initialize(AUDIO_OUTPUT_SYNCH_PLAYBACK, 500, NULL, 0); // initialize for playback with 500ms buffer and no options (see speak_lib.h)
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
    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
126 127 128 129
    if (pVoice) {
        pVoice->Release();
        pVoice = NULL;
    }
130 131 132 133
    ::CoUninitialize();
#endif
}

134
void QGCAudioWorker::say(QString inText, int severity)
135
{
dogmaphobic's avatar
dogmaphobic committed
136 137 138 139 140 141 142 143 144
#ifdef __android__
    Q_UNUSED(inText);
    Q_UNUSED(severity);
#else
    static bool threadInit = false;
    if (!threadInit) {
        threadInit = true;
        init();
    }
Don Gagne's avatar
Don Gagne committed
145

146 147
    if (!muted)
    {
dogmaphobic's avatar
dogmaphobic committed
148
        QString text = fixTextMessageForAudio(inText);
149 150 151 152
        // Prepend high priority text with alert beep
        if (severity < GAudioOutput::AUDIO_SEVERITY_CRITICAL) {
            beep();
        }
153

154
#ifdef QGC_NOTIFY_TUNES_ENABLED
155 156 157 158
        // Wait for the last sound to finish
        while (!sound->isFinished()) {
            QGC::SLEEP::msleep(100);
        }
159
#endif
160

161
#if defined _MSC_VER && defined QGC_SPEECH_ENABLED
Don Gagne's avatar
Don Gagne committed
162
        HRESULT hr = pVoice->Speak(text.toStdWString().c_str(), SPF_DEFAULT, NULL);
dogmaphobic's avatar
dogmaphobic committed
163 164 165
        if (FAILED(hr)) {
            qDebug() << "Speak failed, HR:" << QString("%1").arg(hr, 0, 16);
        }
166 167 168 169 170 171
#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);

#elif defined Q_OS_MAC && defined QGC_SPEECH_ENABLED
dogmaphobic's avatar
dogmaphobic committed
172
        macSpeech.say(text.toStdString().c_str());
173 174
#else
        // Make sure there isn't an unused variable warning when speech output is disabled
175
        Q_UNUSED(inText);
176
#endif
177
    }
dogmaphobic's avatar
dogmaphobic committed
178
#endif // __android__
179 180 181 182 183 184 185 186 187 188 189 190 191
}

void QGCAudioWorker::mute(bool mute)
{
    if (mute != muted)
    {
        this->muted = mute;
        QSettings settings;
        settings.setValue(QGC_GAUDIOOUTPUT_KEY + "muted", this->muted);
//        emit mutedChanged(muted);
    }
}

192 193 194 195 196
void QGCAudioWorker::beep()
{

    if (!muted)
    {
197
#ifdef QGC_NOTIFY_TUNES_ENABLED
Don Gagne's avatar
Don Gagne committed
198
        sound->play(":/res/Alert");
199
#endif
200 201 202
    }
}

203 204 205 206
bool QGCAudioWorker::isMuted()
{
    return this->muted;
}
207 208

bool QGCAudioWorker::_getMillisecondString(const QString& string, QString& match, int& number) {
dogmaphobic's avatar
dogmaphobic committed
209
    static QRegularExpression re("([0-9]+ms)");
210 211 212 213 214 215 216 217 218 219 220 221
    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;
}

dogmaphobic's avatar
dogmaphobic committed
222
QString QGCAudioWorker::fixTextMessageForAudio(const QString& string) {
223 224 225
    QString match;
    QString newNumber;
    QString result = string;
226 227 228 229 230 231 232
    //-- 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);
    }
233 234
    if(result.contains("POSCTL", Qt::CaseInsensitive)) {
        result.replace("POSCTL", "Position Control", Qt::CaseInsensitive);
235 236
    }
    if(result.contains("ALTCTL", Qt::CaseInsensitive)) {
237
        result.replace("ALTCTL", "Altitude Control", Qt::CaseInsensitive);
238
    }
dogmaphobic's avatar
dogmaphobic committed
239 240 241
    if(result.contains("AUTO_RTL", Qt::CaseInsensitive)) {
        result.replace("AUTO_RTL", "auto Return To Land", Qt::CaseInsensitive);
    } else if(result.contains("RTL", Qt::CaseInsensitive)) {
242 243
        result.replace("RTL", "Return To Land", Qt::CaseInsensitive);
    }
244 245 246 247 248 249
    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);
    }
250 251 252 253 254 255 256 257 258 259 260 261
    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(" id ", Qt::CaseInsensitive)) {
        result.replace(" id ", " eye dee ", Qt::CaseInsensitive);
    }
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
    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);
    }
278
    // qDebug() << "Speech: " << result;
279 280
    return result;
}