1// Copyright (C) 2015 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only
3
4
5#include "qtexttospeech_speechd.h"
6
7#include <QtCore/QDebug>
8#include <QtCore/QCoreApplication>
9
10#include <libspeechd.h>
11
12#if LIBSPEECHD_MAJOR_VERSION > 0 || LIBSPEECHD_MINOR_VERSION >= 9
13 #define HAVE_SPD_090
14#endif
15
16QT_BEGIN_NAMESPACE
17
18typedef QList<QTextToSpeechEngineSpeechd*> QTextToSpeechSpeechDispatcherBackendList;
19Q_GLOBAL_STATIC(QTextToSpeechSpeechDispatcherBackendList, backends)
20
21void speech_finished_callback(size_t msg_id, size_t client_id, SPDNotificationType state);
22
23QLocale QTextToSpeechEngineSpeechd::localeForVoice(SPDVoice *voice) const
24{
25 QString lang_var = QString::fromLatin1(ba: voice->language);
26 if (qstrcmp(str1: voice->variant, str2: "none") != 0) {
27 QString var = QString::fromLatin1(ba: voice->variant);
28 lang_var += QLatin1Char('_') + var;
29 }
30 return QLocale(lang_var);
31}
32
33QTextToSpeechEngineSpeechd::QTextToSpeechEngineSpeechd(const QVariantMap &, QObject *)
34 : speechDispatcher(nullptr)
35{
36 backends->append(t: this);
37 connectToSpeechDispatcher();
38}
39
40QTextToSpeechEngineSpeechd::~QTextToSpeechEngineSpeechd()
41{
42 if (speechDispatcher) {
43 if ((m_state != QTextToSpeech::Error) && (m_state != QTextToSpeech::Ready))
44 spd_cancel_all(connection: speechDispatcher);
45 spd_close(connection: speechDispatcher);
46 }
47 backends->removeAll(t: this);
48}
49
50bool QTextToSpeechEngineSpeechd::connectToSpeechDispatcher()
51{
52 if (speechDispatcher)
53 return true;
54
55 speechDispatcher = spd_open(client_name: "QTextToSpeech", connection_name: "main", user_name: nullptr, mode: SPD_MODE_THREADED);
56 if (!speechDispatcher) {
57 setError(reason: QTextToSpeech::ErrorReason::Initialization,
58 errorString: QCoreApplication::translate(context: "QTextToSpeech", key: "Connection to speech-dispatcher failed"));
59 return false;
60 }
61
62 speechDispatcher->callback_begin = speech_finished_callback;
63 spd_set_notification_on(connection: speechDispatcher, notification: SPD_BEGIN);
64 speechDispatcher->callback_end = speech_finished_callback;
65 spd_set_notification_on(connection: speechDispatcher, notification: SPD_END);
66 speechDispatcher->callback_cancel = speech_finished_callback;
67 spd_set_notification_on(connection: speechDispatcher, notification: SPD_CANCEL);
68 speechDispatcher->callback_resume = speech_finished_callback;
69 spd_set_notification_on(connection: speechDispatcher, notification: SPD_RESUME);
70 speechDispatcher->callback_pause = speech_finished_callback;
71 spd_set_notification_on(connection: speechDispatcher, notification: SPD_PAUSE);
72
73 QStringList availableModules;
74 char **modules = spd_list_modules(connection: speechDispatcher);
75 int i = 0;
76 while (modules && modules[i]) {
77 availableModules.append(t: QString::fromUtf8(utf8: modules[i]));
78 ++i;
79 }
80
81 if (availableModules.length() == 0) {
82 setError(reason: QTextToSpeech::ErrorReason::Configuration,
83 errorString: QCoreApplication::translate(context: "QTextToSpeech",
84 key: "Found no modules in speech-dispatcher."));
85 return false;
86 }
87
88 updateVoices();
89 if (m_currentVoice == QVoice()) {
90 // Set the default locale (which is usually the system locale), and fall back
91 // to a locale that has the same language if that fails. That might then still fail,
92 // in which case there won't be a valid voice.
93 if (!setLocale(QLocale()) && !setLocale(QLocale().language())) {
94 setError(reason: QTextToSpeech::ErrorReason::Configuration,
95 errorString: QCoreApplication::translate(context: "QTextToSpeech",
96 key: "Failed to initialize default locale and voice."));
97 return false;
98 }
99 }
100
101 m_state = QTextToSpeech::Ready;
102 m_errorReason = QTextToSpeech::ErrorReason::NoError;
103 m_errorString.clear();
104
105 return true;
106}
107
108// hack to get state notifications
109void QTextToSpeechEngineSpeechd::spdStateChanged(SPDNotificationType state)
110{
111 QTextToSpeech::State s = QTextToSpeech::Error;
112 if (state == SPD_EVENT_PAUSE)
113 s = QTextToSpeech::Paused;
114 else if ((state == SPD_EVENT_BEGIN) || (state == SPD_EVENT_RESUME))
115 s = QTextToSpeech::Speaking;
116 else if ((state == SPD_EVENT_CANCEL) || (state == SPD_EVENT_END))
117 s = QTextToSpeech::Ready;
118
119 if (m_state != s) {
120 m_state = s;
121 emit stateChanged(state: m_state);
122 }
123}
124
125void QTextToSpeechEngineSpeechd::setError(QTextToSpeech::ErrorReason reason, const QString &errorString)
126{
127 m_errorReason = reason;
128 m_errorString = errorString;
129 if (reason == QTextToSpeech::ErrorReason::NoError) {
130 m_errorString.clear();
131 return;
132 }
133
134 if (m_state != QTextToSpeech::Error) {
135 m_state = QTextToSpeech::Error;
136 emit stateChanged(state: m_state);
137 }
138 emit errorOccurred(error: m_errorReason, errorString: m_errorString);
139}
140
141void QTextToSpeechEngineSpeechd::say(const QString &text)
142{
143 if (text.isEmpty() || !connectToSpeechDispatcher())
144 return;
145
146 if (m_state != QTextToSpeech::Ready)
147 stop(boundaryHint: QTextToSpeech::BoundaryHint::Default);
148
149 if (spd_say(connection: speechDispatcher, priority: SPD_MESSAGE, text: text.toUtf8().constData()) < 0)
150 setError(reason: QTextToSpeech::ErrorReason::Input,
151 errorString: QCoreApplication::translate(context: "QTextToSpeech", key: "Text synthesizing failure."));
152}
153
154void QTextToSpeechEngineSpeechd::synthesize(const QString &)
155{
156 setError(reason: QTextToSpeech::ErrorReason::Configuration, errorString: tr(s: "Synthesize not supported"));
157}
158
159void QTextToSpeechEngineSpeechd::stop(QTextToSpeech::BoundaryHint boundaryHint)
160{
161 Q_UNUSED(boundaryHint);
162 if (!connectToSpeechDispatcher())
163 return;
164
165 if (m_state == QTextToSpeech::Paused)
166 spd_resume_all(connection: speechDispatcher);
167 spd_cancel_all(connection: speechDispatcher);
168}
169
170void QTextToSpeechEngineSpeechd::pause(QTextToSpeech::BoundaryHint boundaryHint)
171{
172 Q_UNUSED(boundaryHint);
173 if (!connectToSpeechDispatcher())
174 return;
175
176 if (m_state == QTextToSpeech::Speaking) {
177 spd_pause_all(connection: speechDispatcher);
178 }
179}
180
181void QTextToSpeechEngineSpeechd::resume()
182{
183 if (!connectToSpeechDispatcher())
184 return;
185
186 if (m_state == QTextToSpeech::Paused) {
187 spd_resume_all(connection: speechDispatcher);
188 }
189}
190
191bool QTextToSpeechEngineSpeechd::setPitch(double pitch)
192{
193 if (!connectToSpeechDispatcher())
194 return false;
195
196 int result = spd_set_voice_pitch(connection: speechDispatcher, pitch: static_cast<int>(pitch * 100));
197 if (result == 0)
198 return true;
199 return false;
200}
201
202double QTextToSpeechEngineSpeechd::pitch() const
203{
204 double pitch = 0.0;
205#ifdef HAVE_SPD_090
206 if (speechDispatcher != 0) {
207 int result = spd_get_voice_pitch(connection: speechDispatcher);
208 pitch = result / 100.0;
209 }
210#endif
211 return pitch;
212}
213
214bool QTextToSpeechEngineSpeechd::setRate(double rate)
215{
216 if (!connectToSpeechDispatcher())
217 return false;
218
219 int result = spd_set_voice_rate(connection: speechDispatcher, rate: static_cast<int>(rate * 100));
220 return result == 0;
221}
222
223double QTextToSpeechEngineSpeechd::rate() const
224{
225 double rate = 0.0;
226#ifdef HAVE_SPD_090
227 if (speechDispatcher != 0) {
228 int result = spd_get_voice_rate(connection: speechDispatcher);
229 rate = result / 100.0;
230 }
231#endif
232 return rate;
233}
234
235bool QTextToSpeechEngineSpeechd::setVolume(double volume)
236{
237 if (!connectToSpeechDispatcher())
238 return false;
239
240 // convert from 0.0..1.0 to -100..100
241 int result = spd_set_volume(connection: speechDispatcher, volume: (volume - 0.5) * 200);
242 return result == 0;
243}
244
245double QTextToSpeechEngineSpeechd::volume() const
246{
247 double volume = 0.0;
248#ifdef HAVE_SPD_090
249 if (speechDispatcher != 0) {
250 int result = spd_get_volume(connection: speechDispatcher);
251 // -100..100 to 0.0..1.0
252 volume = (result + 100) / 200.0;
253 }
254#endif
255 return volume;
256}
257
258bool QTextToSpeechEngineSpeechd::setLocale(const QLocale &locale)
259{
260 if (!connectToSpeechDispatcher())
261 return false;
262
263 const int result = spd_set_language(connection: speechDispatcher, language: locale.uiLanguages().at(i: 0).toUtf8().data());
264 if (result == 0) {
265 const QVoice previousVoice = m_currentVoice;
266
267 const QList<QVoice> voices = m_voices.values(key: locale);
268 // QMultiHash returns the values in the reverse order
269 if (voices.size() > 0 && setVoice(voices.last()))
270 return true;
271
272 // try to go back to the previous locale/voice
273 setVoice(previousVoice);
274 }
275 setError(reason: QTextToSpeech::ErrorReason::Configuration,
276 errorString: QCoreApplication::translate(context: "QTextToSpeech", key: "Locale not available: %1")
277 .arg(a: locale.name()));
278 return false;
279}
280
281QLocale QTextToSpeechEngineSpeechd::locale() const
282{
283 return m_currentVoice.locale();
284}
285
286bool QTextToSpeechEngineSpeechd::setVoice(const QVoice &voice)
287{
288 if (!connectToSpeechDispatcher())
289 return false;
290
291 const QByteArray moduleName = voiceData(voice).value<QByteArray>();
292 const int result = spd_set_output_module(connection: speechDispatcher, output_module: moduleName);
293 if (result != 0) {
294 setError(reason: QTextToSpeech::ErrorReason::Configuration,
295 errorString: QCoreApplication::translate(context: "QTextToSpeech",
296 key: "Output module %1, associated with voice %2 not available")
297 .arg(a: moduleName).arg(a: voice.name()));
298 return false;
299 }
300 const int result2 = spd_set_synthesis_voice(speechDispatcher, voice_name: voice.name().toUtf8().data());
301 if (result2 == 0) {
302 m_currentVoice = voice;
303 return true;
304 }
305 setError(reason: QTextToSpeech::ErrorReason::Configuration,
306 errorString: QCoreApplication::translate(context: "QTextToSpeech", key: "Invalid voice: %1")
307 .arg(a: voice.name()));
308 return false;
309}
310
311QVoice QTextToSpeechEngineSpeechd::voice() const
312{
313 return m_currentVoice;
314}
315
316QTextToSpeech::State QTextToSpeechEngineSpeechd::state() const
317{
318 return m_state;
319}
320
321QTextToSpeech::ErrorReason QTextToSpeechEngineSpeechd::errorReason() const
322{
323 return m_errorReason;
324}
325QString QTextToSpeechEngineSpeechd::errorString() const
326{
327 return m_errorString;
328}
329
330void QTextToSpeechEngineSpeechd::updateVoices()
331{
332 char **modules = spd_list_modules(connection: speechDispatcher);
333#ifdef HAVE_SPD_090
334 char *original_module = spd_get_output_module(connection: speechDispatcher);
335#else
336 char *original_module = modules[0];
337#endif
338 char **module = modules;
339 while (module != nullptr && module[0] != nullptr) {
340 spd_set_output_module(connection: speechDispatcher, output_module: module[0]);
341
342 SPDVoice **voices = spd_list_synthesis_voices(connection: speechDispatcher);
343 int i = 0;
344 while (voices != nullptr && voices[i] != nullptr) {
345 const QLocale locale = localeForVoice(voice: voices[i]);
346 const QVariant data = QVariant::fromValue<QByteArray>(value: module[0]);
347 // speechd declares enums and APIs for gender and age, but the SPDVoice struct
348 // carries no relevant information.
349 const QVoice voice = createVoice(name: QString::fromUtf8(utf8: voices[i]->name), locale,
350 gender: QVoice::Unknown, age: QVoice::Other, data);
351 m_voices.insert(key: locale, value: voice);
352 ++i;
353 }
354 // free voices.
355#ifdef HAVE_SPD_090
356 free_spd_voices(voices);
357#endif
358 ++module;
359 }
360
361#ifdef HAVE_SPD_090
362 // Also free modules.
363 free_spd_modules(modules);
364#endif
365 // Set the output module back to what it was.
366 spd_set_output_module(connection: speechDispatcher, output_module: original_module);
367#ifdef HAVE_SPD_090
368 free(ptr: original_module);
369#endif
370}
371
372QList<QLocale> QTextToSpeechEngineSpeechd::availableLocales() const
373{
374 return m_voices.uniqueKeys();
375}
376
377QList<QVoice> QTextToSpeechEngineSpeechd::availableVoices() const
378{
379 QList<QVoice> resultList = m_voices.values(key: m_currentVoice.locale());
380 std::reverse(first: resultList.begin(), last: resultList.end());
381 return resultList;
382}
383
384// We have no way of knowing our own client_id since speech-dispatcher seems to be incomplete
385// (history functions are just stubs)
386void speech_finished_callback(size_t msg_id, size_t client_id, SPDNotificationType state)
387{
388 qDebug() << "Message from speech dispatcher" << msg_id << client_id;
389 for (QTextToSpeechEngineSpeechd *backend : std::as_const(t&: *backends))
390 backend->spdStateChanged(state);
391}
392
393QT_END_NAMESPACE
394

source code of qtspeech/src/plugins/tts/speechdispatcher/qtexttospeech_speechd.cpp