1// Copyright (C) 2022 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_mock.h"
6#include <QtCore/QTimerEvent>
7#include <QtCore/QTimer>
8#include <QtCore/qregularexpression.h>
9
10QT_BEGIN_NAMESPACE
11
12using namespace Qt::StringLiterals;
13
14QTextToSpeechEngineMock::QTextToSpeechEngineMock(const QVariantMap &parameters, QObject *parent)
15 : QTextToSpeechEngine(parent), m_parameters(parameters)
16{
17 m_locale = availableLocales().first();
18 m_voice = availableVoices().first();
19 if (m_parameters[u"delayedInitialization"_s].toBool()) {
20 QTimer::singleShot(interval: 50, receiver: this, slot: [this]{
21 m_state = QTextToSpeech::Ready;
22 emit stateChanged(state: m_state);
23 });
24 } else {
25 m_state = QTextToSpeech::Ready;
26 }
27 m_errorReason = QTextToSpeech::ErrorReason::NoError;
28}
29
30QTextToSpeechEngineMock::~QTextToSpeechEngineMock()
31{
32}
33
34QList<QLocale> QTextToSpeechEngineMock::availableLocales() const
35{
36 QList<QLocale> locales;
37
38 if (const auto it = m_parameters.find(key: "voices"); it != m_parameters.constEnd()) {
39 using VoiceData = QList<std::tuple<QString, QLocale, QVoice::Gender, QVoice::Age>>;
40 const auto voicesData = it->value<VoiceData>();
41 QSet<QLocale> localeSet;
42 for (const auto &voiceData : voicesData)
43 localeSet.insert(value: std::get<1>(t: voiceData));
44 locales = localeSet.values();
45 } else {
46 locales << QLocale(QLocale::English, QLocale::UnitedKingdom)
47 << QLocale(QLocale::English, QLocale::UnitedStates)
48 << QLocale(QLocale::NorwegianBokmal, QLocale::Norway)
49 << QLocale(QLocale::NorwegianNynorsk, QLocale::Norway)
50 << QLocale(QLocale::Finnish, QLocale::Finland);
51 }
52
53 return locales;
54}
55
56QList<QVoice> QTextToSpeechEngineMock::availableVoices() const
57{
58 QList<QVoice> voices;
59
60 if (const auto it = m_parameters.find(key: "voices"); it != m_parameters.constEnd()) {
61 const auto voicesData = it->value<QList<std::tuple<QString, QLocale, QVoice::Gender, QVoice::Age>>>();
62 for (const auto &voiceData : voicesData) {
63 const QLocale &voiceLocale = std::get<1>(t: voiceData);
64 if (voiceLocale == m_locale) {
65 voices << createVoice(name: std::get<0>(t: voiceData),
66 locale: voiceLocale,
67 gender: std::get<2>(t: voiceData),
68 age: std::get<3>(t: voiceData),
69 data: u"%1-%2"_s.arg(a: m_locale.bcp47Name()).arg(a: voices.count() + 1));
70 }
71 }
72 } else {
73 const QString voiceData = m_locale.bcp47Name();
74 const auto newVoice = [=](const QString &name, QVoice::Gender gender,
75 QVoice::Age age, const char *suffix) {
76 return createVoice(name, locale: m_locale, gender, age, data: voiceData + suffix);
77 };
78 switch (m_locale.language()) {
79 case QLocale::English: {
80 if (m_locale.territory() == QLocale::UnitedKingdom) {
81 voices << newVoice("Bob", QVoice::Male, QVoice::Adult, "-1")
82 << newVoice("Anne", QVoice::Female, QVoice::Adult, "-2");
83 } else {
84 voices << newVoice("Charly", QVoice::Male, QVoice::Senior, "-1")
85 << newVoice("Mary", QVoice::Female, QVoice::Teenager, "-2");
86 }
87 break;
88 }
89 case QLocale::NorwegianBokmal:
90 voices << newVoice("Eivind", QVoice::Male, QVoice::Adult, "-1")
91 << newVoice("Kjersti", QVoice::Female, QVoice::Adult, "-2");
92 break;
93 case QLocale::NorwegianNynorsk:
94 voices << newVoice("Anders", QVoice::Male, QVoice::Teenager, "-1")
95 << newVoice("Ingvild", QVoice::Female, QVoice::Child, "-2");
96 break;
97 case QLocale::Finnish:
98 voices << newVoice("Kari", QVoice::Male, QVoice::Adult, "-1")
99 << newVoice("Anneli", QVoice::Female, QVoice::Adult, "-2");
100 break;
101 default:
102 Q_ASSERT_X(false, "availableVoices", "Unsupported locale!");
103 break;
104 }
105 }
106 return voices;
107}
108
109void QTextToSpeechEngineMock::say(const QString &text)
110{
111 m_text = text;
112 m_currentIndex = 0;
113 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
114 m_state = QTextToSpeech::Speaking;
115 emit stateChanged(state: m_state);
116}
117
118void QTextToSpeechEngineMock::synthesize(const QString &text)
119{
120 m_text = text;
121 m_currentIndex = 0;
122 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
123 m_state = QTextToSpeech::Synthesizing;
124 emit stateChanged(state: m_state);
125
126 m_format.setSampleRate(22050);
127 m_format.setChannelConfig(QAudioFormat::ChannelConfigMono);
128 m_format.setSampleFormat(QAudioFormat::Int16);
129}
130
131void QTextToSpeechEngineMock::stop(QTextToSpeech::BoundaryHint boundaryHint)
132{
133 Q_UNUSED(boundaryHint);
134 if (m_state == QTextToSpeech::Ready || m_state == QTextToSpeech::Error)
135 return;
136
137 Q_ASSERT(m_state == QTextToSpeech::Paused || m_timer.isActive());
138 // finish immediately
139 m_text.clear();
140 m_currentIndex = -1;
141 m_timer.stop();
142
143 m_state = QTextToSpeech::Ready;
144 emit stateChanged(state: m_state);
145}
146
147void QTextToSpeechEngineMock::pause(QTextToSpeech::BoundaryHint boundaryHint)
148{
149 Q_UNUSED(boundaryHint);
150 if (m_state != QTextToSpeech::Speaking)
151 return;
152
153 // implement "pause after word end"
154 m_pauseRequested = true;
155}
156
157void QTextToSpeechEngineMock::resume()
158{
159 if (m_state != QTextToSpeech::Paused)
160 return;
161
162 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
163 m_state = QTextToSpeech::Speaking;
164 emit stateChanged(state: m_state);
165}
166
167void QTextToSpeechEngineMock::timerEvent(QTimerEvent *e)
168{
169 if (e->timerId() != m_timer.timerId()) {
170 QTextToSpeechEngine::timerEvent(event: e);
171 return;
172 }
173
174 Q_ASSERT(m_state == QTextToSpeech::Speaking || m_state == QTextToSpeech::Synthesizing);
175 Q_ASSERT(m_text.length());
176
177 // Find start of next word, skipping punctuations. This is good enough for testing.
178 QRegularExpressionMatch match;
179 qsizetype nextSpace = m_text.indexOf(re: QRegularExpression(u"\\W+"_s), from: m_currentIndex, rmatch: &match);
180 if (nextSpace == -1)
181 nextSpace = m_text.length();
182 const QString word = m_text.sliced(pos: m_currentIndex, n: nextSpace - m_currentIndex);
183 sayingWord(word, start: m_currentIndex, length: nextSpace - m_currentIndex);
184 m_currentIndex = nextSpace + match.captured().length();
185
186 emit synthesized(format: m_format, data: QByteArray(m_format.bytesForDuration(microseconds: wordTime() * 1000), 0));
187
188 if (m_currentIndex >= m_text.length()) {
189 // done speaking all words
190 m_timer.stop();
191 m_state = QTextToSpeech::Ready;
192 m_currentIndex = -1;
193 emit stateChanged(state: m_state);
194 } else if (m_pauseRequested) {
195 m_timer.stop();
196 m_state = QTextToSpeech::Paused;
197 emit stateChanged(state: m_state);
198 }
199 m_pauseRequested = false;
200}
201
202double QTextToSpeechEngineMock::rate() const
203{
204 return m_rate;
205}
206
207bool QTextToSpeechEngineMock::setRate(double rate)
208{
209 m_rate = rate;
210 if (m_timer.isActive()) {
211 m_timer.stop();
212 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
213 }
214 return true;
215}
216
217double QTextToSpeechEngineMock::pitch() const
218{
219 return m_pitch;
220}
221
222bool QTextToSpeechEngineMock::setPitch(double pitch)
223{
224 m_pitch = pitch;
225 return true;
226}
227
228QLocale QTextToSpeechEngineMock::locale() const
229{
230 return m_locale;
231}
232
233bool QTextToSpeechEngineMock::setLocale(const QLocale &locale)
234{
235 if (!availableLocales().contains(t: locale))
236 return false;
237 m_locale = locale;
238 const auto voices = availableVoices();
239 if (!voices.contains(t: m_voice))
240 m_voice = voices.isEmpty() ? QVoice() : voices.first();
241 return true;
242}
243
244double QTextToSpeechEngineMock::volume() const
245{
246 return m_volume;
247}
248
249bool QTextToSpeechEngineMock::setVolume(double volume)
250{
251 if (volume < 0.0 || volume > 1.0)
252 return false;
253
254 m_volume = volume;
255 return true;
256}
257
258QVoice QTextToSpeechEngineMock::voice() const
259{
260 return m_voice;
261}
262
263bool QTextToSpeechEngineMock::setVoice(const QVoice &voice)
264{
265 const QString voiceId = voiceData(voice).toString();
266 const QLocale voiceLocale = QLocale(voiceId.left(n: voiceId.lastIndexOf(s: "-")));
267 if (!availableLocales().contains(t: voiceLocale)) {
268 qWarning(msg: "Engine does not support voice's locale %s",
269 qPrintable(voiceLocale.bcp47Name()));
270 return false;
271 }
272 m_locale = voiceLocale;
273 if (!availableVoices().contains(t: voice)) {
274 qWarning(msg: "Engine does not support voice %s in the locale %s",
275 qPrintable(voice.name()), qPrintable(voiceLocale.bcp47Name()));
276 return false;
277 }
278 m_voice = voice;
279 return true;
280}
281
282QTextToSpeech::State QTextToSpeechEngineMock::state() const
283{
284 return m_state;
285}
286
287QTextToSpeech::ErrorReason QTextToSpeechEngineMock::errorReason() const
288{
289 return m_errorReason;
290}
291
292QString QTextToSpeechEngineMock::errorString() const
293{
294 return m_errorString;
295}
296
297QT_END_NAMESPACE
298

source code of qtspeech/src/plugins/tts/mock/qtexttospeech_mock.cpp