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 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | typedef QList<QTextToSpeechEngineSpeechd*> QTextToSpeechSpeechDispatcherBackendList; |
19 | Q_GLOBAL_STATIC(QTextToSpeechSpeechDispatcherBackendList, backends) |
20 | |
21 | void speech_finished_callback(size_t msg_id, size_t client_id, SPDNotificationType state); |
22 | |
23 | QLocale 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 | |
33 | QTextToSpeechEngineSpeechd::QTextToSpeechEngineSpeechd(const QVariantMap &, QObject *) |
34 | : speechDispatcher(nullptr) |
35 | { |
36 | backends->append(t: this); |
37 | connectToSpeechDispatcher(); |
38 | } |
39 | |
40 | QTextToSpeechEngineSpeechd::~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 | |
50 | bool 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 |
109 | void 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 | |
125 | void 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 | |
141 | void 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 | |
154 | void QTextToSpeechEngineSpeechd::synthesize(const QString &) |
155 | { |
156 | setError(reason: QTextToSpeech::ErrorReason::Configuration, errorString: tr(s: "Synthesize not supported" )); |
157 | } |
158 | |
159 | void 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 | |
170 | void 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 | |
181 | void QTextToSpeechEngineSpeechd::resume() |
182 | { |
183 | if (!connectToSpeechDispatcher()) |
184 | return; |
185 | |
186 | if (m_state == QTextToSpeech::Paused) { |
187 | spd_resume_all(connection: speechDispatcher); |
188 | } |
189 | } |
190 | |
191 | bool 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 | |
202 | double 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 | |
214 | bool 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 | |
223 | double 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 | |
235 | bool 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 | |
245 | double 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 | |
258 | bool 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 | |
281 | QLocale QTextToSpeechEngineSpeechd::locale() const |
282 | { |
283 | return m_currentVoice.locale(); |
284 | } |
285 | |
286 | bool 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 | |
311 | QVoice QTextToSpeechEngineSpeechd::voice() const |
312 | { |
313 | return m_currentVoice; |
314 | } |
315 | |
316 | QTextToSpeech::State QTextToSpeechEngineSpeechd::state() const |
317 | { |
318 | return m_state; |
319 | } |
320 | |
321 | QTextToSpeech::ErrorReason QTextToSpeechEngineSpeechd::errorReason() const |
322 | { |
323 | return m_errorReason; |
324 | } |
325 | QString QTextToSpeechEngineSpeechd::errorString() const |
326 | { |
327 | return m_errorString; |
328 | } |
329 | |
330 | void 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 | |
372 | QList<QLocale> QTextToSpeechEngineSpeechd::availableLocales() const |
373 | { |
374 | return m_voices.uniqueKeys(); |
375 | } |
376 | |
377 | QList<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) |
386 | void 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 | |
393 | QT_END_NAMESPACE |
394 | |