Revision control
Copy as Markdown
// Copyright (C) 2015 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qtexttospeech_speechd.h"
#include <QtCore/QDebug>
#include <QtCore/QCoreApplication>
#include <QtCore/QLoggingCategory>
#include <libspeechd.h>
#if LIBSPEECHD_MAJOR_VERSION > 0 || LIBSPEECHD_MINOR_VERSION >= 9
#define HAVE_SPD_090
#endif
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcSpeechTtsSpeechd, "qt.speech.tts.speechd")
typedef QList<QTextToSpeechEngineSpeechd*> QTextToSpeechSpeechDispatcherBackendList;
Q_GLOBAL_STATIC(QTextToSpeechSpeechDispatcherBackendList, backends)
void speech_finished_callback(size_t msg_id, size_t client_id, SPDNotificationType state);
QLocale QTextToSpeechEngineSpeechd::localeForVoice(SPDVoice *voice) const
{
QString lang_var = QString::fromLatin1(voice->language);
if (qstrcmp(voice->variant, "none") != 0) {
QString var = QString::fromLatin1(voice->variant);
lang_var += QLatin1Char('_') + var;
}
return QLocale(lang_var);
}
QTextToSpeechEngineSpeechd::QTextToSpeechEngineSpeechd(const QVariantMap &, QObject *)
: speechDispatcher(nullptr)
{
backends->append(this);
connectToSpeechDispatcher();
}
QTextToSpeechEngineSpeechd::~QTextToSpeechEngineSpeechd()
{
if (speechDispatcher) {
if ((m_state != QTextToSpeech::Error) && (m_state != QTextToSpeech::Ready))
spd_cancel_all(speechDispatcher);
spd_close(speechDispatcher);
}
backends->removeAll(this);
}
bool QTextToSpeechEngineSpeechd::connectToSpeechDispatcher()
{
if (speechDispatcher)
return true;
speechDispatcher = spd_open("QTextToSpeech", "main", nullptr, SPD_MODE_THREADED);
if (!speechDispatcher) {
setError(QTextToSpeech::ErrorReason::Initialization,
QCoreApplication::translate("QTextToSpeech", "Connection to speech-dispatcher failed"));
return false;
}
speechDispatcher->callback_begin = speech_finished_callback;
spd_set_notification_on(speechDispatcher, SPD_BEGIN);
speechDispatcher->callback_end = speech_finished_callback;
spd_set_notification_on(speechDispatcher, SPD_END);
speechDispatcher->callback_cancel = speech_finished_callback;
spd_set_notification_on(speechDispatcher, SPD_CANCEL);
speechDispatcher->callback_resume = speech_finished_callback;
spd_set_notification_on(speechDispatcher, SPD_RESUME);
speechDispatcher->callback_pause = speech_finished_callback;
spd_set_notification_on(speechDispatcher, SPD_PAUSE);
QStringList availableModules;
char **modules = spd_list_modules(speechDispatcher);
int i = 0;
while (modules && modules[i]) {
availableModules.append(QString::fromUtf8(modules[i]));
++i;
}
if (availableModules.length() == 0) {
setError(QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech",
"Found no modules in speech-dispatcher."));
return false;
}
updateVoices();
if (m_currentVoice == QVoice()) {
// Set the default locale (which is usually the system locale), and fall back
// to a locale that has the same language if that fails. That might then still fail,
// in which case there won't be a valid voice.
if (!setLocale(QLocale()) && !setLocale(QLocale().language())) {
setError(QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech",
"Failed to initialize default locale and voice."));
return false;
}
}
m_state = QTextToSpeech::Ready;
m_errorReason = QTextToSpeech::ErrorReason::NoError;
m_errorString.clear();
return true;
}
// hack to get state notifications
void QTextToSpeechEngineSpeechd::spdStateChanged(SPDNotificationType state)
{
QTextToSpeech::State s = QTextToSpeech::Error;
if (state == SPD_EVENT_PAUSE)
s = QTextToSpeech::Paused;
else if ((state == SPD_EVENT_BEGIN) || (state == SPD_EVENT_RESUME))
s = QTextToSpeech::Speaking;
else if ((state == SPD_EVENT_CANCEL) || (state == SPD_EVENT_END))
s = QTextToSpeech::Ready;
if (m_state != s) {
m_state = s;
emit stateChanged(m_state);
}
}
void QTextToSpeechEngineSpeechd::setError(QTextToSpeech::ErrorReason reason, const QString &errorString)
{
m_errorReason = reason;
m_errorString = errorString;
if (reason == QTextToSpeech::ErrorReason::NoError) {
m_errorString.clear();
return;
}
if (m_state != QTextToSpeech::Error) {
m_state = QTextToSpeech::Error;
emit stateChanged(m_state);
}
emit errorOccurred(m_errorReason, m_errorString);
}
void QTextToSpeechEngineSpeechd::say(const QString &text)
{
if (text.isEmpty() || !connectToSpeechDispatcher())
return;
if (m_state != QTextToSpeech::Ready)
stop(QTextToSpeech::BoundaryHint::Default);
if (spd_say(speechDispatcher, SPD_MESSAGE, text.toUtf8().constData()) < 0)
setError(QTextToSpeech::ErrorReason::Input,
QCoreApplication::translate("QTextToSpeech", "Text synthesizing failure."));
}
void QTextToSpeechEngineSpeechd::synthesize(const QString &)
{
setError(QTextToSpeech::ErrorReason::Configuration, tr("Synthesize not supported"));
}
void QTextToSpeechEngineSpeechd::stop(QTextToSpeech::BoundaryHint boundaryHint)
{
Q_UNUSED(boundaryHint);
if (!connectToSpeechDispatcher())
return;
if (m_state == QTextToSpeech::Paused)
spd_resume_all(speechDispatcher);
spd_cancel_all(speechDispatcher);
}
void QTextToSpeechEngineSpeechd::pause(QTextToSpeech::BoundaryHint boundaryHint)
{
Q_UNUSED(boundaryHint);
if (!connectToSpeechDispatcher())
return;
if (m_state == QTextToSpeech::Speaking) {
spd_pause_all(speechDispatcher);
}
}
void QTextToSpeechEngineSpeechd::resume()
{
if (!connectToSpeechDispatcher())
return;
if (m_state == QTextToSpeech::Paused) {
spd_resume_all(speechDispatcher);
}
}
bool QTextToSpeechEngineSpeechd::setPitch(double pitch)
{
if (!connectToSpeechDispatcher())
return false;
int result = spd_set_voice_pitch(speechDispatcher, static_cast<int>(pitch * 100));
if (result == 0)
return true;
return false;
}
double QTextToSpeechEngineSpeechd::pitch() const
{
double pitch = 0.0;
#ifdef HAVE_SPD_090
if (speechDispatcher != 0) {
int result = spd_get_voice_pitch(speechDispatcher);
pitch = result / 100.0;
}
#endif
return pitch;
}
bool QTextToSpeechEngineSpeechd::setRate(double rate)
{
if (!connectToSpeechDispatcher())
return false;
int result = spd_set_voice_rate(speechDispatcher, static_cast<int>(rate * 100));
return result == 0;
}
double QTextToSpeechEngineSpeechd::rate() const
{
double rate = 0.0;
#ifdef HAVE_SPD_090
if (speechDispatcher != 0) {
int result = spd_get_voice_rate(speechDispatcher);
rate = result / 100.0;
}
#endif
return rate;
}
bool QTextToSpeechEngineSpeechd::setVolume(double volume)
{
if (!connectToSpeechDispatcher())
return false;
// convert from 0.0..1.0 to -100..100
int result = spd_set_volume(speechDispatcher, (volume - 0.5) * 200);
return result == 0;
}
double QTextToSpeechEngineSpeechd::volume() const
{
double volume = 0.0;
#ifdef HAVE_SPD_090
if (speechDispatcher != 0) {
int result = spd_get_volume(speechDispatcher);
// -100..100 to 0.0..1.0
volume = (result + 100) / 200.0;
}
#endif
return volume;
}
bool QTextToSpeechEngineSpeechd::setLocale(const QLocale &locale)
{
if (!connectToSpeechDispatcher())
return false;
const int result = spd_set_language(speechDispatcher, locale.uiLanguages().at(0).toUtf8().data());
if (result == 0) {
const QVoice previousVoice = m_currentVoice;
const QList<QVoice> voices = m_voices.values(locale);
// QMultiHash returns the values in the reverse order
if (voices.size() > 0 && setVoice(voices.last()))
return true;
// try to go back to the previous locale/voice
setVoice(previousVoice);
}
setError(QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech", "Locale not available: %1")
.arg(locale.name()));
return false;
}
QLocale QTextToSpeechEngineSpeechd::locale() const
{
return m_currentVoice.locale();
}
bool QTextToSpeechEngineSpeechd::setVoice(const QVoice &voice)
{
if (!connectToSpeechDispatcher())
return false;
const QByteArray moduleName = voiceData(voice).value<QByteArray>();
const int result = spd_set_output_module(speechDispatcher, moduleName.data());
if (result != 0) {
setError(QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech",
"Output module %1, associated with voice %2 not available")
.arg(moduleName).arg(voice.name()));
return false;
}
const int result2 = spd_set_synthesis_voice(speechDispatcher, voice.name().toUtf8().data());
if (result2 == 0) {
m_currentVoice = voice;
return true;
}
setError(QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech", "Invalid voice: %1")
.arg(voice.name()));
return false;
}
QVoice QTextToSpeechEngineSpeechd::voice() const
{
return m_currentVoice;
}
QTextToSpeech::State QTextToSpeechEngineSpeechd::state() const
{
return m_state;
}
QTextToSpeech::ErrorReason QTextToSpeechEngineSpeechd::errorReason() const
{
return m_errorReason;
}
QString QTextToSpeechEngineSpeechd::errorString() const
{
return m_errorString;
}
void QTextToSpeechEngineSpeechd::updateVoices()
{
char **modules = spd_list_modules(speechDispatcher);
#ifdef HAVE_SPD_090
char *original_module = spd_get_output_module(speechDispatcher);
#else
char *original_module = modules[0];
#endif
char **module = modules;
while (module != nullptr && module[0] != nullptr) {
spd_set_output_module(speechDispatcher, module[0]);
SPDVoice **voices = spd_list_synthesis_voices(speechDispatcher);
int i = 0;
while (voices != nullptr && voices[i] != nullptr) {
const QLocale locale = localeForVoice(voices[i]);
const QVariant data = QVariant::fromValue<QByteArray>(module[0]);
// speechd declares enums and APIs for gender and age, but the SPDVoice struct
// carries no relevant information.
const QVoice voice = createVoice(QString::fromUtf8(voices[i]->name), locale,
QVoice::Unknown, QVoice::Other, data);
m_voices.insert(locale, voice);
++i;
}
// free voices.
#ifdef HAVE_SPD_090
free_spd_voices(voices);
#endif
++module;
}
#ifdef HAVE_SPD_090
// Also free modules.
free_spd_modules(modules);
#endif
// Set the output module back to what it was.
spd_set_output_module(speechDispatcher, original_module);
#ifdef HAVE_SPD_090
free(original_module);
#endif
}
QList<QLocale> QTextToSpeechEngineSpeechd::availableLocales() const
{
return m_voices.uniqueKeys();
}
QList<QVoice> QTextToSpeechEngineSpeechd::availableVoices() const
{
QList<QVoice> resultList = m_voices.values(m_currentVoice.locale());
std::reverse(resultList.begin(), resultList.end());
return resultList;
}
// We have no way of knowing our own client_id since speech-dispatcher seems to be incomplete
// (history functions are just stubs)
void speech_finished_callback(size_t msg_id, size_t client_id, SPDNotificationType state)
{
qCDebug(lcSpeechTtsSpeechd) << "Message from speech dispatcher" << msg_id << client_id;
for (QTextToSpeechEngineSpeechd *backend : std::as_const(*backends))
backend->spdStateChanged(state);
}
QT_END_NAMESPACE