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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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