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 | |
17 | QT_BEGIN_NAMESPACE |
18 | |
19 | Q_LOGGING_CATEGORY(lcSpeechTtsSpeechd, "qt.speech.tts.speechd") |
20 | |
21 | typedef QList<QTextToSpeechEngineSpeechd*> QTextToSpeechSpeechDispatcherBackendList; |
22 | Q_GLOBAL_STATIC(QTextToSpeechSpeechDispatcherBackendList, backends) |
23 | |
24 | void speech_finished_callback(size_t msg_id, size_t client_id, SPDNotificationType state); |
25 | |
26 | QLocale 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 | |
36 | QTextToSpeechEngineSpeechd::QTextToSpeechEngineSpeechd(const QVariantMap &, QObject *) |
37 | : speechDispatcher(nullptr) |
38 | { |
39 | backends->append(t: this); |
40 | connectToSpeechDispatcher(); |
41 | } |
42 | |
43 | QTextToSpeechEngineSpeechd::~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 | |
53 | bool 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 |
112 | void 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 | |
128 | void 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 | |
144 | void 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 | |
157 | void QTextToSpeechEngineSpeechd::synthesize(const QString &) |
158 | { |
159 | setError(reason: QTextToSpeech::ErrorReason::Configuration, errorString: tr(s: "Synthesize not supported")); |
160 | } |
161 | |
162 | void 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 | |
173 | void 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 | |
184 | void QTextToSpeechEngineSpeechd::resume() |
185 | { |
186 | if (!connectToSpeechDispatcher()) |
187 | return; |
188 | |
189 | if (m_state == QTextToSpeech::Paused) { |
190 | spd_resume_all(connection: speechDispatcher); |
191 | } |
192 | } |
193 | |
194 | bool 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 | |
205 | double 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 | |
217 | bool 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 | |
226 | double 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 | |
238 | bool 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 | |
248 | double 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 | |
261 | bool 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 | |
284 | QLocale QTextToSpeechEngineSpeechd::locale() const |
285 | { |
286 | return m_currentVoice.locale(); |
287 | } |
288 | |
289 | bool 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 | |
314 | QVoice QTextToSpeechEngineSpeechd::voice() const |
315 | { |
316 | return m_currentVoice; |
317 | } |
318 | |
319 | QTextToSpeech::State QTextToSpeechEngineSpeechd::state() const |
320 | { |
321 | return m_state; |
322 | } |
323 | |
324 | QTextToSpeech::ErrorReason QTextToSpeechEngineSpeechd::errorReason() const |
325 | { |
326 | return m_errorReason; |
327 | } |
328 | QString QTextToSpeechEngineSpeechd::errorString() const |
329 | { |
330 | return m_errorString; |
331 | } |
332 | |
333 | void 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 | |
375 | QList<QLocale> QTextToSpeechEngineSpeechd::availableLocales() const |
376 | { |
377 | return m_voices.uniqueKeys(); |
378 | } |
379 | |
380 | QList<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) |
389 | void 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 | |
396 | QT_END_NAMESPACE |
397 |
Definitions
- lcSpeechTtsSpeechd
- backends
- localeForVoice
- QTextToSpeechEngineSpeechd
- ~QTextToSpeechEngineSpeechd
- connectToSpeechDispatcher
- spdStateChanged
- setError
- say
- synthesize
- stop
- pause
- resume
- setPitch
- pitch
- setRate
- rate
- setVolume
- volume
- setLocale
- locale
- setVoice
- voice
- state
- errorReason
- errorString
- updateVoices
- availableLocales
- availableVoices
Learn to use CMake with our Intro Training
Find out more