QGCAudioWorker.cpp 7.98 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
    emergency(false),
    muted(false)
81 82 83 84
{
    // Load settings
    QSettings settings;
    muted = settings.value(QGC_GAUDIOOUTPUT_KEY + "muted", muted).toBool();
85
}
86

87 88
void QGCAudioWorker::init()
{
dogmaphobic's avatar
dogmaphobic committed
89
#if defined Q_OS_LINUX && !defined __android__ && defined QGC_SPEECH_ENABLED
90
    espeak_Initialize(AUDIO_OUTPUT_PLAYBACK, 500, NULL, 0); // initialize for playback with 500ms buffer and no options (see speak_lib.h)
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
    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
123 124 125 126
    if (pVoice) {
        pVoice->Release();
        pVoice = NULL;
    }
127 128 129 130
    ::CoUninitialize();
#endif
}

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

142 143
    if (!muted)
    {
dogmaphobic's avatar
dogmaphobic committed
144
        QString text = fixTextMessageForAudio(inText);
145

146
#if defined _MSC_VER && defined QGC_SPEECH_ENABLED
Don Gagne's avatar
Don Gagne committed
147
        HRESULT hr = pVoice->Speak(text.toStdWString().c_str(), SPF_DEFAULT, NULL);
dogmaphobic's avatar
dogmaphobic committed
148 149 150
        if (FAILED(hr)) {
            qDebug() << "Speak failed, HR:" << QString("%1").arg(hr, 0, 16);
        }
151 152 153 154
#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);
155
        espeak_Synchronize();
dogmaphobic's avatar
dogmaphobic committed
156
#elif (defined __macos__) && defined QGC_SPEECH_ENABLED
dogmaphobic's avatar
dogmaphobic committed
157
        macSpeech.say(text.toStdString().c_str());
dogmaphobic's avatar
dogmaphobic committed
158 159
#elif (defined __ios__) && defined QGC_SPEECH_ENABLED
        iOSSpeak(text);
160 161
#else
        // Make sure there isn't an unused variable warning when speech output is disabled
162
        Q_UNUSED(inText);
163
#endif
164
    }
dogmaphobic's avatar
dogmaphobic committed
165
#endif // __android__
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
}

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

bool QGCAudioWorker::isMuted()
{
    return this->muted;
}
183 184

bool QGCAudioWorker::_getMillisecondString(const QString& string, QString& match, int& number) {
dogmaphobic's avatar
dogmaphobic committed
185
    static QRegularExpression re("([0-9]+ms)");
186 187 188 189 190 191 192 193 194 195 196 197
    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
198
QString QGCAudioWorker::fixTextMessageForAudio(const QString& string) {
199 200 201
    QString match;
    QString newNumber;
    QString result = string;
202 203 204 205 206 207 208
    //-- 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);
    }
209 210
    if(result.contains("POSCTL", Qt::CaseInsensitive)) {
        result.replace("POSCTL", "Position Control", Qt::CaseInsensitive);
211 212
    }
    if(result.contains("ALTCTL", Qt::CaseInsensitive)) {
213
        result.replace("ALTCTL", "Altitude Control", Qt::CaseInsensitive);
214
    }
dogmaphobic's avatar
dogmaphobic committed
215 216 217
    if(result.contains("AUTO_RTL", Qt::CaseInsensitive)) {
        result.replace("AUTO_RTL", "auto Return To Land", Qt::CaseInsensitive);
    } else if(result.contains("RTL", Qt::CaseInsensitive)) {
218 219
        result.replace("RTL", "Return To Land", Qt::CaseInsensitive);
    }
220 221 222 223 224 225
    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);
    }
226 227 228 229 230 231 232 233 234
    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
235 236 237 238 239 240
    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);
    }
241 242 243
    if(result.contains(" id ", Qt::CaseInsensitive)) {
        result.replace(" id ", " eye dee ", Qt::CaseInsensitive);
    }
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
    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);
    }
260
    // qDebug() << "Speech: " << result;
261 262
    return result;
}