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 OR GPL-3.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 = [this, &voiceData](const QString &name, QVoice::Gender gender,
75 QVoice::Age age, const char *suffix) {
76 return createVoice(name, locale: m_locale, gender, age,
77 data: QVariant::fromValue<QString>(value: voiceData + suffix));
78 };
79 switch (m_locale.language()) {
80 case QLocale::English: {
81 if (m_locale.territory() == QLocale::UnitedKingdom) {
82 voices << newVoice("Bob", QVoice::Male, QVoice::Adult, "-1")
83 << newVoice("Anne", QVoice::Female, QVoice::Adult, "-2");
84 } else {
85 voices << newVoice("Charly", QVoice::Male, QVoice::Senior, "-1")
86 << newVoice("Mary", QVoice::Female, QVoice::Teenager, "-2");
87 }
88 break;
89 }
90 case QLocale::NorwegianBokmal:
91 voices << newVoice("Eivind", QVoice::Male, QVoice::Adult, "-1")
92 << newVoice("Kjersti", QVoice::Female, QVoice::Adult, "-2");
93 break;
94 case QLocale::NorwegianNynorsk:
95 voices << newVoice("Anders", QVoice::Male, QVoice::Teenager, "-1")
96 << newVoice("Ingvild", QVoice::Female, QVoice::Child, "-2");
97 break;
98 case QLocale::Finnish:
99 voices << newVoice("Kari", QVoice::Male, QVoice::Adult, "-1")
100 << newVoice("Anneli", QVoice::Female, QVoice::Adult, "-2");
101 break;
102 default:
103 Q_ASSERT_X(false, "availableVoices", "Unsupported locale!");
104 break;
105 }
106 }
107 return voices;
108}
109
110void QTextToSpeechEngineMock::say(const QString &text)
111{
112 m_text = text;
113 m_currentIndex = 0;
114 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
115 m_state = QTextToSpeech::Speaking;
116 emit stateChanged(state: m_state);
117}
118
119void QTextToSpeechEngineMock::synthesize(const QString &text)
120{
121 m_text = text;
122 m_currentIndex = 0;
123 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
124 m_state = QTextToSpeech::Synthesizing;
125 emit stateChanged(state: m_state);
126
127 m_format.setSampleRate(22050);
128 m_format.setChannelConfig(QAudioFormat::ChannelConfigMono);
129 m_format.setSampleFormat(QAudioFormat::Int16);
130}
131
132void QTextToSpeechEngineMock::stop(QTextToSpeech::BoundaryHint boundaryHint)
133{
134 Q_UNUSED(boundaryHint);
135 if (m_state == QTextToSpeech::Ready || m_state == QTextToSpeech::Error)
136 return;
137
138 Q_ASSERT(m_state == QTextToSpeech::Paused || m_timer.isActive());
139 // finish immediately
140 m_text.clear();
141 m_currentIndex = -1;
142 m_timer.stop();
143
144 m_state = QTextToSpeech::Ready;
145 emit stateChanged(state: m_state);
146}
147
148void QTextToSpeechEngineMock::pause(QTextToSpeech::BoundaryHint boundaryHint)
149{
150 Q_UNUSED(boundaryHint);
151 if (m_state != QTextToSpeech::Speaking)
152 return;
153
154 // implement "pause after word end"
155 m_pauseRequested = true;
156}
157
158void QTextToSpeechEngineMock::resume()
159{
160 if (m_state != QTextToSpeech::Paused)
161 return;
162
163 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
164 m_state = QTextToSpeech::Speaking;
165 emit stateChanged(state: m_state);
166}
167
168void QTextToSpeechEngineMock::timerEvent(QTimerEvent *e)
169{
170 if (e->timerId() != m_timer.timerId()) {
171 QTextToSpeechEngine::timerEvent(event: e);
172 return;
173 }
174
175 Q_ASSERT(m_state == QTextToSpeech::Speaking || m_state == QTextToSpeech::Synthesizing);
176 Q_ASSERT(m_text.length());
177
178 // Find start of next word, skipping punctuations. This is good enough for testing.
179 QRegularExpressionMatch match;
180 qsizetype nextSpace = m_text.indexOf(re: QRegularExpression(u"\\W+"_s), from: m_currentIndex, rmatch: &match);
181 if (nextSpace == -1)
182 nextSpace = m_text.length();
183 const QString word = m_text.sliced(pos: m_currentIndex, n: nextSpace - m_currentIndex);
184 sayingWord(word, start: m_currentIndex, length: nextSpace - m_currentIndex);
185 m_currentIndex = nextSpace + match.captured().length();
186
187 emit synthesized(format: m_format, data: QByteArray(m_format.bytesForDuration(microseconds: wordTime() * 1000), 0));
188
189 if (m_currentIndex >= m_text.length()) {
190 // done speaking all words
191 m_timer.stop();
192 m_state = QTextToSpeech::Ready;
193 m_currentIndex = -1;
194 emit stateChanged(state: m_state);
195 } else if (m_pauseRequested) {
196 m_timer.stop();
197 m_state = QTextToSpeech::Paused;
198 emit stateChanged(state: m_state);
199 }
200 m_pauseRequested = false;
201}
202
203double QTextToSpeechEngineMock::rate() const
204{
205 return m_rate;
206}
207
208bool QTextToSpeechEngineMock::setRate(double rate)
209{
210 m_rate = rate;
211 if (m_timer.isActive()) {
212 m_timer.stop();
213 m_timer.start(msec: wordTime(), t: Qt::PreciseTimer, obj: this);
214 }
215 return true;
216}
217
218double QTextToSpeechEngineMock::pitch() const
219{
220 return m_pitch;
221}
222
223bool QTextToSpeechEngineMock::setPitch(double pitch)
224{
225 m_pitch = pitch;
226 return true;
227}
228
229QLocale QTextToSpeechEngineMock::locale() const
230{
231 return m_locale;
232}
233
234bool QTextToSpeechEngineMock::setLocale(const QLocale &locale)
235{
236 if (!availableLocales().contains(t: locale))
237 return false;
238 m_locale = locale;
239 const auto voices = availableVoices();
240 if (!voices.contains(t: m_voice))
241 m_voice = voices.isEmpty() ? QVoice() : voices.first();
242 return true;
243}
244
245double QTextToSpeechEngineMock::volume() const
246{
247 return m_volume;
248}
249
250bool QTextToSpeechEngineMock::setVolume(double volume)
251{
252 if (volume < 0.0 || volume > 1.0)
253 return false;
254
255 m_volume = volume;
256 return true;
257}
258
259QVoice QTextToSpeechEngineMock::voice() const
260{
261 return m_voice;
262}
263
264bool QTextToSpeechEngineMock::setVoice(const QVoice &voice)
265{
266 const QString voiceId = voiceData(voice).toString();
267 const QLocale voiceLocale = QLocale(voiceId.left(n: voiceId.lastIndexOf(s: "-")));
268 if (!availableLocales().contains(t: voiceLocale)) {
269 qWarning(msg: "Engine does not support voice's locale %s",
270 qPrintable(voiceLocale.bcp47Name()));
271 return false;
272 }
273 m_locale = voiceLocale;
274 if (!availableVoices().contains(t: voice)) {
275 qWarning(msg: "Engine does not support voice %s in the locale %s",
276 qPrintable(voice.name()), qPrintable(voiceLocale.bcp47Name()));
277 return false;
278 }
279 m_voice = voice;
280 return true;
281}
282
283QTextToSpeech::State QTextToSpeechEngineMock::state() const
284{
285 return m_state;
286}
287
288QTextToSpeech::ErrorReason QTextToSpeechEngineMock::errorReason() const
289{
290 return m_errorReason;
291}
292
293QString QTextToSpeechEngineMock::errorString() const
294{
295 return m_errorString;
296}
297
298QT_END_NAMESPACE
299

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