385 lines
11 KiB
C++
385 lines
11 KiB
C++
/* 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
|
|
|
|
#include "backends/text-to-speech/linux/linux-text-to-speech.h"
|
|
|
|
#if defined(USE_TTS) && defined(USE_SPEECH_DISPATCHER) && defined(POSIX)
|
|
#include <speech-dispatcher/libspeechd.h>
|
|
|
|
#include "common/translation.h"
|
|
#include "common/system.h"
|
|
#include "common/ustr.h"
|
|
#include "common/config-manager.h"
|
|
#include "common/encoding.h"
|
|
|
|
SPDConnection *_connection;
|
|
|
|
void speech_begin_callback(size_t msg_id, size_t client_id, SPDNotificationType state){
|
|
SpeechDispatcherManager *manager =
|
|
static_cast<SpeechDispatcherManager *> (g_system->getTextToSpeechManager());
|
|
manager->updateState(SpeechDispatcherManager::SPEECH_BEGUN);
|
|
}
|
|
|
|
void speech_end_callback(size_t msg_id, size_t client_id, SPDNotificationType state){
|
|
SpeechDispatcherManager *manager =
|
|
static_cast<SpeechDispatcherManager *> (g_system->getTextToSpeechManager());
|
|
manager->updateState(SpeechDispatcherManager::SPEECH_ENDED);
|
|
}
|
|
|
|
void speech_cancel_callback(size_t msg_id, size_t client_id, SPDNotificationType state){
|
|
SpeechDispatcherManager *manager =
|
|
static_cast<SpeechDispatcherManager *> (g_system->getTextToSpeechManager());
|
|
manager->updateState(SpeechDispatcherManager::SPEECH_CANCELED);
|
|
}
|
|
|
|
void speech_resume_callback(size_t msg_id, size_t client_id, SPDNotificationType state){
|
|
SpeechDispatcherManager *manager =
|
|
static_cast<SpeechDispatcherManager *> (g_system->getTextToSpeechManager());
|
|
manager->updateState(SpeechDispatcherManager::SPEECH_RESUMED);
|
|
}
|
|
|
|
void speech_pause_callback(size_t msg_id, size_t client_id, SPDNotificationType state){
|
|
SpeechDispatcherManager *manager =
|
|
static_cast<SpeechDispatcherManager *> (g_system->getTextToSpeechManager());
|
|
manager->updateState(SpeechDispatcherManager::SPEECH_PAUSED);
|
|
}
|
|
|
|
|
|
void *SpeechDispatcherManager::startSpeech(void *p) {
|
|
StartSpeechParams *params = (StartSpeechParams *) p;
|
|
pthread_mutex_lock(params->mutex);
|
|
if (!_connection || g_system->getTextToSpeechManager()->isPaused() ||
|
|
params->speechQueue->front().empty()) {
|
|
pthread_mutex_unlock(params->mutex);
|
|
return NULL;
|
|
}
|
|
if (spd_say(_connection, SPD_MESSAGE, params->speechQueue->front().c_str()) == -1) {
|
|
// close the connection
|
|
if (_connection != 0) {
|
|
spd_close(_connection);
|
|
_connection = 0;
|
|
}
|
|
}
|
|
pthread_mutex_unlock(params->mutex);
|
|
return NULL;
|
|
}
|
|
|
|
SpeechDispatcherManager::SpeechDispatcherManager()
|
|
: _speechState(READY) {
|
|
pthread_mutex_init(&_speechMutex, NULL);
|
|
_params.mutex = &_speechMutex;
|
|
_params.speechQueue = &_speechQueue;
|
|
_threadCreated = false;
|
|
init();
|
|
}
|
|
|
|
void SpeechDispatcherManager::init() {
|
|
_connection = spd_open("ScummVM", "main", NULL, SPD_MODE_THREADED);
|
|
if (_connection == 0) {
|
|
_speechState = BROKEN;
|
|
warning("Couldn't initialize text to speech through speech-dispatcher");
|
|
return;
|
|
}
|
|
|
|
_connection->callback_begin = speech_begin_callback;
|
|
spd_set_notification_on(_connection, SPD_BEGIN);
|
|
_connection->callback_end = speech_end_callback;
|
|
spd_set_notification_on(_connection, SPD_END);
|
|
_connection->callback_cancel = speech_cancel_callback;
|
|
spd_set_notification_on(_connection, SPD_CANCEL);
|
|
_connection->callback_resume = speech_resume_callback;
|
|
spd_set_notification_on(_connection, SPD_RESUME);
|
|
_connection->callback_pause = speech_pause_callback;
|
|
spd_set_notification_on(_connection, SPD_PAUSE);
|
|
|
|
updateVoices();
|
|
_ttsState->_activeVoice = 0;
|
|
#ifdef USE_TRANSLATION
|
|
setLanguage(TransMan.getCurrentLanguage());
|
|
#else
|
|
setLanguage("en");
|
|
#endif
|
|
_speechQueue.clear();
|
|
}
|
|
|
|
SpeechDispatcherManager::~SpeechDispatcherManager() {
|
|
stop();
|
|
|
|
clearState();
|
|
|
|
if (_connection != 0)
|
|
spd_close(_connection);
|
|
if (_threadCreated)
|
|
pthread_join(_thread, NULL);
|
|
pthread_mutex_destroy(&_speechMutex);
|
|
}
|
|
|
|
void SpeechDispatcherManager::updateState(SpeechDispatcherManager::SpeechEvent event) {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
switch(event) {
|
|
case SPEECH_ENDED:
|
|
pthread_mutex_lock(&_speechMutex);
|
|
_speechQueue.pop_front();
|
|
if (_speechQueue.empty())
|
|
_speechState = READY;
|
|
else {
|
|
// reinitialize if needed
|
|
if (!_connection)
|
|
init();
|
|
if (_speechState != BROKEN) {
|
|
if (_threadCreated)
|
|
pthread_join(_thread, NULL);
|
|
_threadCreated = true;
|
|
if (pthread_create(&_thread, NULL, startSpeech, &_params)) {
|
|
_threadCreated = false;
|
|
warning("TTS: Cannot start new speech");
|
|
}
|
|
}
|
|
}
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
break;
|
|
case SPEECH_PAUSED:
|
|
_speechState = PAUSED;
|
|
break;
|
|
case SPEECH_CANCELED:
|
|
if (_speechState != PAUSED) {
|
|
_speechState = READY;
|
|
}
|
|
break;
|
|
case SPEECH_RESUMED:
|
|
break;
|
|
case SPEECH_BEGUN:
|
|
_speechState = SPEAKING;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool SpeechDispatcherManager::say(Common::String str, Action action, Common::String charset) {
|
|
|
|
pthread_mutex_lock(&_speechMutex);
|
|
// reinitialize if needed
|
|
if (!_connection)
|
|
init();
|
|
|
|
if (_speechState == BROKEN) {
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
return true;
|
|
}
|
|
|
|
if (action == DROP && isSpeaking()) {
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
return true;
|
|
}
|
|
|
|
if (charset.empty()) {
|
|
#ifdef USE_TRANSLATION
|
|
charset = TransMan.getCurrentCharset();
|
|
#else
|
|
charset = "ASCII";
|
|
#endif
|
|
}
|
|
|
|
char *tmpStr = Common::Encoding::convert("UTF-8", charset, str.c_str(), str.size());
|
|
if (tmpStr == nullptr) {
|
|
warning("Cannot convert from %s encoding for text to speech", charset.c_str());
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
return true;
|
|
}
|
|
Common::String strUtf8 = tmpStr;
|
|
free(tmpStr);
|
|
|
|
if (!_speechQueue.empty() && action == INTERRUPT_NO_REPEAT &&
|
|
_speechQueue.front() == strUtf8 && isSpeaking()) {
|
|
_speechQueue.clear();
|
|
_speechQueue.push_back(strUtf8);
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
return true;
|
|
}
|
|
|
|
if (!_speechQueue.empty() && action == QUEUE_NO_REPEAT &&
|
|
_speechQueue.back() == strUtf8 && isSpeaking()) {
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
return true;
|
|
}
|
|
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
if (isSpeaking() && (action == INTERRUPT || action == INTERRUPT_NO_REPEAT))
|
|
stop();
|
|
if (!strUtf8.empty()) {
|
|
pthread_mutex_lock(&_speechMutex);
|
|
_speechQueue.push_back(strUtf8);
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
if (isReady()) {
|
|
_speechState = SPEAKING;
|
|
startSpeech((void *)(&_params));
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool SpeechDispatcherManager::stop() {
|
|
if (_speechState == READY || _speechState == BROKEN)
|
|
return true;
|
|
_speechState = READY;
|
|
pthread_mutex_lock(&_speechMutex);
|
|
_speechQueue.clear();
|
|
bool result = spd_cancel(_connection) == -1;
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
return result;
|
|
}
|
|
|
|
bool SpeechDispatcherManager::pause() {
|
|
if (_speechState == READY || _speechState == PAUSED || _speechState == BROKEN)
|
|
return true;
|
|
pthread_mutex_lock(&_speechMutex);
|
|
_speechState = PAUSED;
|
|
bool result = spd_cancel_all(_connection) == -1;
|
|
pthread_mutex_unlock(&_speechMutex);
|
|
if (result)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool SpeechDispatcherManager::resume() {
|
|
if (_speechState == READY || _speechState == SPEAKING || _speechState == BROKEN)
|
|
return true;
|
|
// If there is a thread from before pause() waiting, let it finish (it shouln't
|
|
// do anything). There shouldn't be any other threads getting created,
|
|
// because the speech is paused, so we don't need to synchronize
|
|
if (_threadCreated) {
|
|
pthread_join(_thread, NULL);
|
|
_threadCreated = false;
|
|
}
|
|
_speechState = PAUSED;
|
|
if (!_speechQueue.empty()) {
|
|
_speechState = SPEAKING;
|
|
startSpeech((void *) &_params);
|
|
}
|
|
else
|
|
_speechState = READY;
|
|
return false;
|
|
}
|
|
|
|
bool SpeechDispatcherManager::isSpeaking() {
|
|
return _speechState == SPEAKING;
|
|
}
|
|
|
|
bool SpeechDispatcherManager::isPaused() {
|
|
return _speechState == PAUSED;
|
|
}
|
|
|
|
bool SpeechDispatcherManager::isReady() {
|
|
return _speechState == READY;
|
|
}
|
|
|
|
void SpeechDispatcherManager::setVoice(unsigned index) {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
assert(index < _ttsState->_availableVoices.size());
|
|
Common::TTSVoice voice = _ttsState->_availableVoices[index];
|
|
spd_set_voice_type(_connection, *(SPDVoiceType *)(voice.getData()));
|
|
_ttsState->_activeVoice = index;
|
|
}
|
|
|
|
void SpeechDispatcherManager::setRate(int rate) {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
assert(rate >= -100 && rate <= 100);
|
|
spd_set_voice_rate(_connection, rate);
|
|
_ttsState->_rate = rate;
|
|
}
|
|
|
|
void SpeechDispatcherManager::setPitch(int pitch) {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
assert(pitch >= -100 && pitch <= 100);
|
|
spd_set_voice_pitch(_connection, pitch);
|
|
_ttsState->_pitch = pitch;
|
|
}
|
|
|
|
void SpeechDispatcherManager::setVolume(unsigned volume) {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
assert(volume <= 100);
|
|
spd_set_volume(_connection, (volume - 50) * 2);
|
|
_ttsState->_volume = volume;
|
|
}
|
|
|
|
void SpeechDispatcherManager::setLanguage(Common::String language) {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
Common::TextToSpeechManager::setLanguage(language);
|
|
spd_set_language(_connection, _ttsState->_language.c_str());
|
|
setVoice(_ttsState->_activeVoice);
|
|
}
|
|
|
|
void SpeechDispatcherManager::createVoice(int typeNumber, Common::TTSVoice::Gender gender, Common::TTSVoice::Age age, char *description) {
|
|
// This pointer will point to data needed for voice switching. It is stored
|
|
// in the Common::TTSVoice and it is freed by freeVoiceData() once it
|
|
// is not needed.
|
|
SPDVoiceType *type = (SPDVoiceType *) malloc(sizeof(SPDVoiceType));
|
|
*type = static_cast<SPDVoiceType>(typeNumber);
|
|
Common::TTSVoice voice(gender, age, (void *) type, description);
|
|
_ttsState->_availableVoices.push_back(voice);
|
|
}
|
|
|
|
void SpeechDispatcherManager::updateVoices() {
|
|
if (_speechState == BROKEN)
|
|
return;
|
|
/* just use these voices:
|
|
SPD_MALE1, SPD_MALE2, SPD_MALE3,
|
|
SPD_FEMALE1, SPD_FEMALE2, SPD_FEMALE3,
|
|
SPD_CHILD_MALE, SPD_CHILD_FEMALE
|
|
|
|
it depends on the user to map them to the right voices in speech-dispatcher
|
|
configuration
|
|
*/
|
|
_ttsState->_availableVoices.clear();
|
|
|
|
char **voiceInfo = spd_list_voices(_connection);
|
|
|
|
createVoice(SPD_MALE1, Common::TTSVoice::MALE, Common::TTSVoice::ADULT, voiceInfo[0]);
|
|
createVoice(SPD_MALE2, Common::TTSVoice::MALE, Common::TTSVoice::ADULT, voiceInfo[1]);
|
|
createVoice(SPD_MALE3, Common::TTSVoice::MALE, Common::TTSVoice::ADULT, voiceInfo[2]);
|
|
createVoice(SPD_FEMALE1, Common::TTSVoice::FEMALE, Common::TTSVoice::ADULT, voiceInfo[3]);
|
|
createVoice(SPD_FEMALE2, Common::TTSVoice::FEMALE, Common::TTSVoice::ADULT, voiceInfo[4]);
|
|
createVoice(SPD_FEMALE3, Common::TTSVoice::FEMALE, Common::TTSVoice::ADULT, voiceInfo[5]);
|
|
createVoice(SPD_CHILD_MALE, Common::TTSVoice::MALE, Common::TTSVoice::CHILD, voiceInfo[6]);
|
|
createVoice(SPD_CHILD_FEMALE, Common::TTSVoice::FEMALE, Common::TTSVoice::CHILD, voiceInfo[7]);
|
|
|
|
for (int i = 0; i < 8; i++)
|
|
free(voiceInfo[i]);
|
|
|
|
free(voiceInfo);
|
|
}
|
|
|
|
void SpeechDispatcherManager::freeVoiceData(void *data) {
|
|
free(data);
|
|
}
|
|
|
|
|
|
#endif
|