2019-07-15 21:09:36 -07:00
|
|
|
/* ScummVM - Graphic Adventure Engine
|
|
|
|
*
|
|
|
|
* ScummVM is the legal property of its developers, whose names
|
|
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
|
|
* file distributed with this source distribution.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Disable symbol overrides so that we can use system headers.
|
|
|
|
#define FORBIDDEN_SYMBOL_ALLOW_ALL
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
#if defined(USE_WINDOWS_TTS)
|
|
|
|
#include <basetyps.h>
|
|
|
|
#include <windows.h>
|
|
|
|
#include <Servprov.h>
|
|
|
|
#include <sapi.h>
|
|
|
|
#include "backends/text-to-speech/windows/sphelper-scummvm.h"
|
2019-07-16 22:09:05 -07:00
|
|
|
#include "backends/platform/sdl/win32/win32_wrapper.h"
|
2019-07-15 21:09:36 -07:00
|
|
|
|
|
|
|
#include "backends/text-to-speech/windows/windows-text-to-speech.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include "common/translation.h"
|
|
|
|
#include "common/debug.h"
|
|
|
|
#include "common/system.h"
|
|
|
|
#include "common/ustr.h"
|
|
|
|
#include "common/config-manager.h"
|
|
|
|
|
2019-07-15 13:50:21 -07:00
|
|
|
ISpVoice *_voice;
|
|
|
|
|
2019-07-16 22:09:05 -07:00
|
|
|
// We need this pointer to be able to stop speech immediately.
|
|
|
|
ISpAudio *_audio;
|
|
|
|
|
2019-07-15 13:50:21 -07:00
|
|
|
WindowsTextToSpeechManager::WindowsTextToSpeechManager()
|
|
|
|
: _speechState(BROKEN){
|
2019-07-17 13:33:42 +02:00
|
|
|
ConfMan.setInt("tts_voice", 0);
|
2019-07-15 21:09:36 -07:00
|
|
|
init();
|
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::init() {
|
2019-07-16 22:09:05 -07:00
|
|
|
// init COM
|
2019-07-15 13:50:21 -07:00
|
|
|
if (FAILED(::CoInitialize(NULL)))
|
|
|
|
return;
|
|
|
|
|
2019-07-16 22:09:05 -07:00
|
|
|
// init voice
|
2019-07-15 13:50:21 -07:00
|
|
|
HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&_voice);
|
|
|
|
if (!SUCCEEDED(hr)) {
|
|
|
|
warning("Could not initialize TTS voice");
|
|
|
|
return;
|
|
|
|
}
|
2019-07-16 22:09:05 -07:00
|
|
|
setLanguage("en");
|
|
|
|
|
|
|
|
// init audio
|
|
|
|
CSpStreamFormat format;
|
|
|
|
format.AssignFormat(SPSF_11kHz8BitMono);
|
|
|
|
ISpObjectToken *pToken;
|
|
|
|
hr = SpGetDefaultTokenFromCategoryId(SPCAT_AUDIOOUT, &pToken);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
warning("Could not initialize TTS audio");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
pToken->CreateInstance(NULL, CLSCTX_ALL, IID_ISpAudio, (void **)&_audio);
|
|
|
|
_audio->SetFormat(format.FormatId(), format.WaveFormatExPtr());
|
|
|
|
_voice->SetOutput(_audio, FALSE);
|
|
|
|
|
|
|
|
if(_ttsState->_availaibleVoices.size() > 0)
|
|
|
|
_speechState = READY;
|
|
|
|
else
|
|
|
|
_speechState = NO_VOICE;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
WindowsTextToSpeechManager::~WindowsTextToSpeechManager() {
|
2019-07-16 22:09:05 -07:00
|
|
|
freeVoices();
|
2019-07-15 13:50:21 -07:00
|
|
|
if (_voice)
|
|
|
|
_voice->Release();
|
|
|
|
::CoUninitialize();
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::say(Common::String str) {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE) {
|
|
|
|
warning("The tts cannot speak in this state");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (isPaused()) {
|
|
|
|
resume();
|
|
|
|
}
|
|
|
|
_audio->SetState(SPAS_STOP, 0);
|
|
|
|
_audio->SetState(SPAS_RUN, 0);
|
|
|
|
// We have to set the pitch by prepending xml code at the start of the said string;
|
|
|
|
Common::String pitch= Common::String::format("<pitch absmiddle=\"%d\">", _ttsState->_pitch);
|
|
|
|
str.replace((uint32)0, 0, pitch);
|
|
|
|
|
|
|
|
WCHAR *strW = Win32::ansiToUnicode(str.c_str());
|
|
|
|
bool result = _voice->Speak(strW, SPF_ASYNC | SPF_PURGEBEFORESPEAK, NULL) != S_OK;
|
|
|
|
free(strW);
|
|
|
|
_speechState = SPEAKING;
|
|
|
|
return result;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::stop() {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return true;
|
|
|
|
if (isPaused())
|
|
|
|
resume();
|
|
|
|
_audio->SetState(SPAS_STOP, 0);
|
|
|
|
_audio->SetState(SPAS_RUN, 0);
|
|
|
|
_voice->Speak(NULL, SPF_PURGEBEFORESPEAK | SPF_ASYNC | SPF_IS_NOT_XML, 0);
|
|
|
|
_speechState = READY;
|
|
|
|
return false;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::pause() {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return true;
|
|
|
|
if (isPaused())
|
|
|
|
return false;
|
|
|
|
_voice->Pause();
|
|
|
|
_speechState = PAUSED;
|
|
|
|
return false;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::resume() {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return true;
|
|
|
|
if (!isPaused())
|
|
|
|
return false;
|
|
|
|
_voice->Resume();
|
|
|
|
if (isSpeaking())
|
|
|
|
_speechState = SPEAKING;
|
|
|
|
else
|
|
|
|
_speechState = READY;
|
|
|
|
return false;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::isSpeaking() {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return false;
|
|
|
|
SPVOICESTATUS eventStatus;
|
|
|
|
_voice->GetStatus(&eventStatus, NULL);
|
|
|
|
return eventStatus.dwRunningState == SPRS_IS_SPEAKING;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::isPaused() {
|
2019-07-16 22:09:05 -07:00
|
|
|
return _speechState == PAUSED;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::isReady() {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return false;
|
|
|
|
if (_speechState != PAUSED && !isSpeaking())
|
|
|
|
return true;
|
|
|
|
else
|
|
|
|
return false;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::setVoice(unsigned index) {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return;
|
|
|
|
_voice->SetVoice((ISpObjectToken *) _ttsState->_availaibleVoices[index].getData());
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::setRate(int rate) {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return;
|
|
|
|
assert(rate >= -10 && rate <= 10);
|
|
|
|
_voice->SetRate(rate);
|
|
|
|
_ttsState->_rate = rate;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::setPitch(int pitch) {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return;
|
|
|
|
_ttsState->_pitch = pitch;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::setVolume(unsigned volume) {
|
2019-07-16 22:09:05 -07:00
|
|
|
if(_speechState == BROKEN || _speechState == NO_VOICE)
|
|
|
|
return;
|
|
|
|
assert(volume <= 100);
|
|
|
|
_voice->SetVolume(volume);
|
|
|
|
_ttsState->_volume = volume;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
int WindowsTextToSpeechManager::getVolume() {
|
2019-07-16 22:09:05 -07:00
|
|
|
return _ttsState->_volume;
|
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::freeVoices() {
|
|
|
|
for(Common::TTSVoice *i = _ttsState->_availaibleVoices.begin(); i < _ttsState->_availaibleVoices.end(); i++) {
|
|
|
|
ISpObjectToken *voiceData = (ISpObjectToken *)i->getData();
|
|
|
|
voiceData->Release();
|
|
|
|
}
|
|
|
|
_ttsState->_availaibleVoices.clear();
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::setLanguage(Common::String language) {
|
2019-07-16 22:09:05 -07:00
|
|
|
if (language == "C")
|
|
|
|
language = "en";
|
|
|
|
_ttsState->_language = language;
|
|
|
|
updateVoices();
|
|
|
|
setVoice(0);
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
2019-07-16 22:09:05 -07:00
|
|
|
void WindowsTextToSpeechManager::createVoice(void *cpVoiceToken) {
|
|
|
|
ISpObjectToken *voiceToken = (ISpObjectToken *) cpVoiceToken;
|
|
|
|
|
|
|
|
// description
|
|
|
|
WCHAR *descW;
|
|
|
|
SpGetDescription(voiceToken, &descW);
|
|
|
|
char *buffer = Win32::unicodeToAnsi(descW);
|
|
|
|
Common::String desc = buffer;
|
|
|
|
free(buffer);
|
|
|
|
|
|
|
|
// voice attributes
|
|
|
|
HRESULT hr = S_OK;
|
|
|
|
ISpDataKey *key = nullptr;
|
|
|
|
hr = voiceToken->OpenKey(L"Attributes", &key);
|
|
|
|
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
voiceToken->Release();
|
|
|
|
warning("Could not open attribute key for voice: %s", desc.c_str());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
LPWSTR data;
|
|
|
|
|
|
|
|
// language
|
|
|
|
hr = key->GetStringValue(L"Language", &data);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
voiceToken->Release();
|
|
|
|
warning("Could not get the language attribute for voice: %s", desc.c_str());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
buffer = Win32::unicodeToAnsi(data);
|
|
|
|
Common::String language = lcidToLocale(buffer);
|
|
|
|
free(buffer);
|
|
|
|
CoTaskMemFree(data);
|
|
|
|
|
|
|
|
// only get the voices for the current language
|
|
|
|
if (language != _ttsState->_language) {
|
|
|
|
voiceToken->Release();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// gender
|
|
|
|
hr = key->GetStringValue(L"Gender", &data);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
voiceToken->Release();
|
|
|
|
warning("Could not get the gender attribute for voice: %s", desc.c_str());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
buffer = Win32::unicodeToAnsi(data);
|
|
|
|
Common::TTSVoice::Gender gender = !strcmp(buffer, "Male") ? Common::TTSVoice::MALE : Common::TTSVoice::FEMALE;
|
|
|
|
free(buffer);
|
|
|
|
CoTaskMemFree(data);
|
|
|
|
|
2019-07-17 13:33:42 +02:00
|
|
|
// age
|
|
|
|
hr = key->GetStringValue(L"Age", &data);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
voiceToken->Release();
|
|
|
|
warning("Could not get the age attribute for voice: %s", desc.c_str());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
buffer = Win32::unicodeToAnsi(data);
|
|
|
|
Common::TTSVoice::Age age = !strcmp(buffer, "Adult") ? Common::TTSVoice::ADULT : Common::TTSVoice::UNKNOWN_AGE;
|
|
|
|
free(buffer);
|
|
|
|
CoTaskMemFree(data);
|
|
|
|
|
|
|
|
_ttsState->_availaibleVoices.push_back(Common::TTSVoice(gender, Common::TTSVoice::ADULT, (void *) voiceToken, desc));
|
2019-07-16 22:09:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
int strToInt(Common::String str) {
|
|
|
|
str.toUppercase();
|
|
|
|
int result = 0;
|
|
|
|
for(unsigned i = 0; i < str.size(); i++) {
|
|
|
|
if (str[i] < '0' || (str[i] > '9' && str[i] < 'A') || str[i] > 'F')
|
|
|
|
break;
|
|
|
|
int num = (str[i] <= '9') ? str[i] - '0' : str[i] - 55;
|
|
|
|
result = result * 16 + num;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
Common::String WindowsTextToSpeechManager::lcidToLocale(Common::String lcid) {
|
|
|
|
LCID locale = strToInt(lcid);
|
|
|
|
int nchars = GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, NULL, 0);
|
|
|
|
wchar_t *languageCode = new wchar_t[nchars];
|
|
|
|
GetLocaleInfoW(locale, LOCALE_SISO639LANGNAME, languageCode, nchars);
|
|
|
|
char *resultTmp = Win32::unicodeToAnsi(languageCode);
|
|
|
|
Common::String result = resultTmp;
|
|
|
|
delete[] languageCode;
|
|
|
|
free(resultTmp);
|
|
|
|
return result;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
void WindowsTextToSpeechManager::updateVoices() {
|
2019-07-16 22:09:05 -07:00
|
|
|
freeVoices();
|
|
|
|
HRESULT hr = S_OK;
|
|
|
|
ISpObjectToken *cpVoiceToken = nullptr;
|
|
|
|
IEnumSpObjectTokens *cpEnum = nullptr;
|
|
|
|
unsigned long ulCount = 0;
|
|
|
|
|
|
|
|
hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &cpEnum);
|
|
|
|
if (SUCCEEDED(hr)) {
|
|
|
|
hr = cpEnum->GetCount(&ulCount);
|
|
|
|
}
|
|
|
|
_voice->SetVolume(0);
|
|
|
|
while (SUCCEEDED(hr) && ulCount--) {
|
|
|
|
hr = cpEnum->Next(1, &cpVoiceToken, NULL);
|
|
|
|
_voice->SetVoice(cpVoiceToken);
|
|
|
|
if(SUCCEEDED(_voice->Speak(L"hi, this is test", SPF_PURGEBEFORESPEAK | SPF_ASYNC | SPF_IS_NOT_XML, 0)))
|
|
|
|
createVoice(cpVoiceToken);
|
|
|
|
else
|
|
|
|
cpVoiceToken->Release();
|
|
|
|
}
|
|
|
|
_voice->SetVolume(_ttsState->_volume);
|
|
|
|
cpEnum->Release();
|
|
|
|
|
|
|
|
if(_ttsState->_availaibleVoices.size() == 0) {
|
|
|
|
_speechState = NO_VOICE;
|
|
|
|
warning("No voice is availaible");
|
|
|
|
} else if (_speechState == NO_VOICE)
|
|
|
|
_speechState = READY;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool WindowsTextToSpeechManager::popState() {
|
|
|
|
if (_ttsState->_next == nullptr)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
for (Common::TTSVoice *i = _ttsState->_availaibleVoices.begin(); i < _ttsState->_availaibleVoices.end(); i++) {
|
|
|
|
ISpObjectToken *voiceToken = (ISpObjectToken *) i->getData();
|
|
|
|
voiceToken->Release();
|
|
|
|
}
|
|
|
|
|
|
|
|
Common::TTSState *oldState = _ttsState;
|
|
|
|
_ttsState = _ttsState->_next;
|
|
|
|
|
|
|
|
delete oldState;
|
2019-07-15 21:09:36 -07:00
|
|
|
|
2019-07-16 22:09:05 -07:00
|
|
|
setLanguage(_ttsState->_language);
|
|
|
|
setPitch(_ttsState->_pitch);
|
|
|
|
setVolume(_ttsState->_volume);
|
|
|
|
setRate(_ttsState->_rate);
|
|
|
|
setVoice(_ttsState->_activeVoice);
|
|
|
|
return false;
|
2019-07-15 21:09:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#endif
|