QGCAudioWorker.cpp 8.58 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>
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
#endif

dogmaphobic's avatar
dogmaphobic committed
56 57 58 59
#if (defined __ios__) && defined QGC_SPEECH_ENABLED
extern void iOSSpeak(QString msg);
#endif

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

90 91
void QGCAudioWorker::init()
{
92
#ifdef QGC_NOTIFY_TUNES_ENABLED
Don Gagne's avatar
Don Gagne committed
93
    sound = new QSound(":/res/Alert");
94
#endif
95

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

138
void QGCAudioWorker::say(QString inText, int severity)
139
{
dogmaphobic's avatar
dogmaphobic committed
140 141 142 143 144 145 146 147 148
#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
149

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

158
#ifdef QGC_NOTIFY_TUNES_ENABLED
159 160 161 162
        // Wait for the last sound to finish
        while (!sound->isFinished()) {
            QGC::SLEEP::msleep(100);
        }
163
#endif
164

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

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

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

198 199 200 201 202
void QGCAudioWorker::beep()
{

    if (!muted)
    {
203
#ifdef QGC_NOTIFY_TUNES_ENABLED
Don Gagne's avatar
Don Gagne committed
204
        sound->play(":/res/Alert");
205
#endif
206 207 208
    }
}

209 210 211 212
bool QGCAudioWorker::isMuted()
{
    return this->muted;
}
213 214

bool QGCAudioWorker::_getMillisecondString(const QString& string, QString& match, int& number) {
dogmaphobic's avatar
dogmaphobic committed
215
    static QRegularExpression re("([0-9]+ms)");
216 217 218 219 220 221 222 223 224 225 226 227
    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
228
QString QGCAudioWorker::fixTextMessageForAudio(const QString& string) {
229 230 231
    QString match;
    QString newNumber;
    QString result = string;
232 233 234 235 236 237 238
    //-- 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);
    }
239 240
    if(result.contains("POSCTL", Qt::CaseInsensitive)) {
        result.replace("POSCTL", "Position Control", Qt::CaseInsensitive);
241 242
    }
    if(result.contains("ALTCTL", Qt::CaseInsensitive)) {
243
        result.replace("ALTCTL", "Altitude Control", Qt::CaseInsensitive);
244
    }
dogmaphobic's avatar
dogmaphobic committed
245 246 247
    if(result.contains("AUTO_RTL", Qt::CaseInsensitive)) {
        result.replace("AUTO_RTL", "auto Return To Land", Qt::CaseInsensitive);
    } else if(result.contains("RTL", Qt::CaseInsensitive)) {
248 249
        result.replace("RTL", "Return To Land", Qt::CaseInsensitive);
    }
250 251 252 253 254 255
    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);
    }
256 257 258 259 260 261 262 263 264
    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);
    }
dogmaphobic's avatar
dogmaphobic committed
265 266 267 268 269 270
    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);
    }
271 272 273
    if(result.contains(" id ", Qt::CaseInsensitive)) {
        result.replace(" id ", " eye dee ", Qt::CaseInsensitive);
    }
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
    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);
    }
290
    // qDebug() << "Speech: " << result;
291 292
    return result;
}