scummvm/backends/text-to-speech/linux/linux-text-to-speech.cpp
Vladimir Serbinenko 68a9136e4d
COMMON: Rewrite Encoder and drop dependency on iconv (#2586)
Different platforms have different levels of support of encodings and
often have slight variations. We already have tables for most encoding
with only CJK missing. Full transcoding inclusion allows us to get reliable
encoding results independently of platform. The biggest con is the need for
external tables encoding.dat.

It removes a duplicate table for korean in graphics/korfont.cpp
2020-11-15 16:20:35 +01:00

369 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"
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(const Common::U32String &str, Action action) {
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;
}
Common::String strUtf8 = str.encode();
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