| /**************************************************************************** |
| ** |
| ** Copyright (C) 2015 The Qt Company Ltd. |
| ** Contact: http://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Speech module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL3$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see http://www.qt.io/terms-conditions. For further |
| ** information use the contact form at http://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPLv3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or later as published by the Free |
| ** Software Foundation and appearing in the file LICENSE.GPL included in |
| ** the packaging of this file. Please review the following information to |
| ** ensure the GNU General Public License version 2.0 requirements will be |
| ** met: http://www.gnu.org/licenses/gpl-2.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qtexttospeech_sapi.h" |
| |
| #include <windows.h> |
| #include <sapi.h> |
| #include <sphelper.h> |
| #include <qdebug.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| #ifndef SPERR_NO_MORE_ITEMS |
| # define SPERR_NO_MORE_ITEMS MAKE_SAPI_ERROR(0x039) |
| #endif |
| |
| #ifdef Q_CC_MINGW // from sphelper.h |
| |
| static const GUID CLSD_SpVoice = {0x96749377, 0x3391, 0x11d2,{0x9e, 0xe3, 0x0, 0xc0, 0x4f, 0x79, 0x73, 0x96}}; |
| |
| static inline HRESULT SpGetTokenFromId(const WCHAR *pszTokenId, ISpObjectToken **cpToken, BOOL fCreateIfNotExist = FALSE) |
| { |
| LPUNKNOWN pUnkOuter = nullptr; |
| HRESULT hr = ::CoCreateInstance(CLSID_SpObjectToken, pUnkOuter, CLSCTX_ALL, |
| __uuidof(ISpObjectToken), reinterpret_cast<void **>(cpToken)); |
| if (SUCCEEDED(hr)) |
| hr = (*cpToken)->SetId(NULL, pszTokenId, fCreateIfNotExist); |
| return hr; |
| } |
| |
| static inline HRESULT SpCreateNewToken(const WCHAR *pszTokenId, ISpObjectToken **ppToken) |
| { |
| // Forcefully create the token |
| return SpGetTokenFromId(pszTokenId, ppToken, TRUE); |
| } |
| #endif // Q_CC_MINGW |
| |
| QTextToSpeechEngineSapi::QTextToSpeechEngineSapi(const QVariantMap &, QObject *) |
| : m_state(QTextToSpeech::BackendError), m_voice(nullptr), m_pitch(0.0), m_pauseCount(0) |
| { |
| init(); |
| } |
| |
| QTextToSpeechEngineSapi::~QTextToSpeechEngineSapi() |
| { |
| if (m_voice) |
| m_voice->Release(); |
| } |
| |
| void QTextToSpeechEngineSapi::init() |
| { |
| if (FAILED(::CoInitialize(NULL))) { |
| qWarning() << "Init of COM failed"; |
| return; |
| } |
| |
| HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&m_voice); |
| if (!SUCCEEDED(hr)) { |
| qWarning() << "Could not init voice"; |
| return; |
| } |
| |
| m_voice->SetInterest(SPFEI_ALL_TTS_EVENTS, SPFEI_ALL_TTS_EVENTS); |
| m_voice->SetNotifyCallbackInterface(this, 0, 0); |
| updateVoices(); |
| m_state = QTextToSpeech::Ready; |
| } |
| |
| bool QTextToSpeechEngineSapi::isSpeaking() const |
| { |
| SPVOICESTATUS eventStatus; |
| m_voice->GetStatus(&eventStatus, NULL); |
| return eventStatus.dwRunningState == SPRS_IS_SPEAKING; |
| } |
| |
| void QTextToSpeechEngineSapi::say(const QString &text) |
| { |
| if (text.isEmpty()) |
| return; |
| |
| QString textString = text; |
| if (m_state != QTextToSpeech::Ready) |
| stop(); |
| |
| textString.prepend(QString::fromLatin1("<pitch absmiddle=\"%1\"/>").arg(m_pitch * 10)); |
| |
| std::wstring wtext = textString.toStdWString(); |
| m_voice->Speak(wtext.data(), SPF_ASYNC, NULL); |
| } |
| |
| void QTextToSpeechEngineSapi::stop() |
| { |
| if (m_state == QTextToSpeech::Paused) |
| resume(); |
| m_voice->Speak(NULL, SPF_PURGEBEFORESPEAK, 0); |
| } |
| |
| void QTextToSpeechEngineSapi::pause() |
| { |
| if (!isSpeaking()) |
| return; |
| |
| if (m_pauseCount == 0) { |
| ++m_pauseCount; |
| m_voice->Pause(); |
| m_state = QTextToSpeech::Paused; |
| emit stateChanged(m_state); |
| } |
| } |
| |
| void QTextToSpeechEngineSapi::resume() |
| { |
| if (m_pauseCount > 0) { |
| --m_pauseCount; |
| m_voice->Resume(); |
| if (isSpeaking()) { |
| m_state = QTextToSpeech::Speaking; |
| } else { |
| m_state = QTextToSpeech::Ready; |
| } |
| emit stateChanged(m_state); |
| } |
| } |
| |
| bool QTextToSpeechEngineSapi::setPitch(double pitch) |
| { |
| m_pitch = pitch; |
| return true; |
| } |
| |
| double QTextToSpeechEngineSapi::pitch() const |
| { |
| return m_pitch; |
| } |
| |
| bool QTextToSpeechEngineSapi::setRate(double rate) |
| { |
| // -10 to 10 |
| m_voice->SetRate(long(rate*10)); |
| return true; |
| } |
| |
| double QTextToSpeechEngineSapi::rate() const |
| { |
| long rateValue; |
| if (m_voice->GetRate(&rateValue) == S_OK) |
| return rateValue / 10.0; |
| return -1; |
| } |
| |
| bool QTextToSpeechEngineSapi::setVolume(double volume) |
| { |
| // 0 to 1 |
| m_voice->SetVolume(volume * 100); |
| return true; |
| } |
| |
| double QTextToSpeechEngineSapi::volume() const |
| { |
| USHORT baseVolume; |
| if (m_voice->GetVolume(&baseVolume) == S_OK) |
| { |
| return baseVolume / 100.0; |
| } |
| return 0.0; |
| } |
| |
| QString QTextToSpeechEngineSapi::voiceId(ISpObjectToken *speechToken) const |
| { |
| HRESULT hr = S_OK; |
| LPWSTR vId = nullptr; |
| hr = speechToken->GetId(&vId); |
| if (FAILED(hr)) { |
| qWarning() << "ISpObjectToken::GetId failed"; |
| return QString(); |
| } |
| return QString::fromWCharArray(vId); |
| } |
| |
| QMap<QString, QString> QTextToSpeechEngineSapi::voiceAttributes(ISpObjectToken *speechToken) const |
| { |
| HRESULT hr = S_OK; |
| QMap<QString, QString> result; |
| |
| ISpDataKey *pAttrKey = nullptr; |
| hr = speechToken->OpenKey(L"Attributes", &pAttrKey); |
| if (FAILED(hr)) { |
| qWarning() << "ISpObjectToken::OpenKeys failed"; |
| return result; |
| } |
| |
| // enumerate values |
| for (ULONG v = 0; ; v++) { |
| LPWSTR val = nullptr; |
| hr = pAttrKey->EnumValues(v, &val); |
| if (SPERR_NO_MORE_ITEMS == hr) { |
| // done |
| break; |
| } else if (FAILED(hr)) { |
| qWarning() << "ISpDataKey::EnumValues failed"; |
| continue; |
| } |
| |
| // how do we know whether it's a string or a DWORD? |
| LPWSTR data = nullptr; |
| hr = pAttrKey->GetStringValue(val, &data); |
| if (FAILED(hr)) { |
| qWarning() << "ISpDataKey::GetStringValue failed"; |
| continue; |
| } |
| |
| if (0 != wcscmp(val, L"")) { |
| result[QString::fromWCharArray(val)] = QString::fromWCharArray(data); |
| } |
| |
| // FIXME: Do we need to free the memory here? |
| CoTaskMemFree(val); |
| CoTaskMemFree(data); |
| } |
| |
| return result; |
| } |
| |
| QLocale QTextToSpeechEngineSapi::lcidToLocale(const QString &lcid) const |
| { |
| bool ok; |
| LCID locale = lcid.toInt(&ok, 16); |
| if (!ok) { |
| qWarning() << "Could not convert language attribute to LCID"; |
| return QLocale(); |
| } |
| int nchars = GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, NULL, 0); |
| wchar_t* languageCode = new wchar_t[nchars]; |
| GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, languageCode, nchars); |
| QString iso = QString::fromWCharArray(languageCode); |
| delete[] languageCode; |
| return QLocale(iso); |
| } |
| |
| void QTextToSpeechEngineSapi::updateVoices() |
| { |
| HRESULT hr = S_OK; |
| ISpObjectToken *cpVoiceToken = nullptr; |
| IEnumSpObjectTokens *cpEnum = nullptr; |
| ULONG ulCount = 0; |
| |
| hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum); |
| if (SUCCEEDED(hr)) { |
| hr = cpEnum->GetCount(&ulCount); |
| } |
| |
| // Loop through all voices |
| while (SUCCEEDED(hr) && ulCount--) { |
| if (cpVoiceToken) { |
| cpVoiceToken->Release(); |
| cpVoiceToken = nullptr; |
| } |
| hr = cpEnum->Next(1, &cpVoiceToken, NULL); |
| |
| // Get attributes of the voice |
| QMap<QString, QString> vAttr = voiceAttributes(cpVoiceToken); |
| |
| // Transform Windows LCID to QLocale |
| QLocale vLocale = lcidToLocale(vAttr["Language"]); |
| if (!m_locales.contains(vLocale)) |
| m_locales.append(vLocale); |
| |
| // Create voice |
| |
| QString name = vAttr["Name"]; |
| QVoice::Age age = vAttr["Age"] == "Adult" ? QVoice::Adult : QVoice::Other; |
| QVoice::Gender gender = vAttr["Gender"] == "Male" ? QVoice::Male : |
| vAttr["Gender"] == "Female" ? QVoice::Female : |
| QVoice::Unknown; |
| // Getting the ID of the voice to set the voice later |
| QString vId = voiceId(cpVoiceToken); |
| QVoice voice = createVoice(name, gender, age, vId); |
| m_voices.insert(vLocale.name(), voice); |
| } |
| if (cpVoiceToken) |
| cpVoiceToken->Release(); |
| cpEnum->Release(); |
| } |
| |
| QVector<QLocale> QTextToSpeechEngineSapi::availableLocales() const |
| { |
| return m_locales; |
| } |
| |
| bool QTextToSpeechEngineSapi::setLocale(const QLocale &locale) |
| { |
| QList<QVoice> voicesForLocale = m_voices.values(locale.name()); |
| if (voicesForLocale.length() > 0) { |
| setVoice(voicesForLocale[0]); |
| return true; |
| } else { |
| qWarning() << "No voice found for given locale"; |
| } |
| return false; |
| } |
| |
| QLocale QTextToSpeechEngineSapi::locale() const |
| { |
| // Get current voice id |
| ISpObjectToken *cpVoiceToken = nullptr; |
| m_voice->GetVoice(&cpVoiceToken); |
| // read attributes |
| QMap<QString, QString> vAttr = voiceAttributes(cpVoiceToken); |
| cpVoiceToken->Release(); |
| return lcidToLocale(vAttr["Language"]); |
| } |
| |
| QVector<QVoice> QTextToSpeechEngineSapi::availableVoices() const |
| { |
| return m_voices.values(locale().name()).toVector(); |
| } |
| |
| bool QTextToSpeechEngineSapi::setVoice(const QVoice &voice) |
| { |
| // Convert voice id to null-terminated wide char string |
| QString vId = voiceData(voice).toString(); |
| wchar_t* tokenId = new wchar_t[vId.size()+1]; |
| vId.toWCharArray(tokenId); |
| tokenId[vId.size()] = 0; |
| |
| // create the voice token via the id |
| HRESULT hr = S_OK; |
| ISpObjectToken *cpVoiceToken = nullptr; |
| hr = SpCreateNewToken(tokenId, &cpVoiceToken); |
| if (FAILED(hr)) { |
| qWarning() << "Creating the voice token from ID failed"; |
| if (cpVoiceToken) |
| cpVoiceToken->Release(); |
| m_state = QTextToSpeech::BackendError; |
| emit stateChanged(m_state); |
| return false; |
| } |
| |
| if (m_state != QTextToSpeech::Ready) { |
| m_state = QTextToSpeech::Ready; |
| emit stateChanged(m_state); |
| } |
| |
| delete[] tokenId; |
| m_voice->SetVoice(cpVoiceToken); |
| cpVoiceToken->Release(); |
| return true; |
| } |
| |
| QVoice QTextToSpeechEngineSapi::voice() const |
| { |
| ISpObjectToken *cpVoiceToken = nullptr; |
| m_voice->GetVoice(&cpVoiceToken); |
| QString vId = voiceId(cpVoiceToken); |
| cpVoiceToken->Release(); |
| for (const QVoice &voice : m_voices) { |
| if (voiceData(voice).toString() == vId) { |
| return voice; |
| } |
| } |
| return QVoice(); |
| } |
| |
| QTextToSpeech::State QTextToSpeechEngineSapi::state() const |
| { |
| return m_state; |
| } |
| |
| HRESULT QTextToSpeechEngineSapi::NotifyCallback(WPARAM /*wParam*/, LPARAM /*lParam*/) |
| { |
| QTextToSpeech::State newState = QTextToSpeech::Ready; |
| if (isPaused()) { |
| newState = QTextToSpeech::Paused; |
| } else if (isSpeaking()) { |
| newState = QTextToSpeech::Speaking; |
| } else { |
| newState = QTextToSpeech::Ready; |
| } |
| |
| if (m_state != newState) { |
| m_state = newState; |
| emit stateChanged(newState); |
| } |
| |
| return S_OK; |
| } |
| |
| QT_END_NAMESPACE |