Source code

Revision control

Copy as Markdown

// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qtexttospeech_mock.h"
#include <QtCore/QTimerEvent>
#include <QtCore/QTimer>
#include <QtCore/qregularexpression.h>
#include <QtCore/qset.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
QTextToSpeechEngineMock::QTextToSpeechEngineMock(const QVariantMap &parameters, QObject *parent)
: QTextToSpeechEngine(parent), m_parameters(parameters)
{
m_locale = availableLocales().first();
m_voice = availableVoices().first();
if (m_parameters[u"delayedInitialization"_s].toBool()) {
QTimer::singleShot(50, this, [this]{
m_state = QTextToSpeech::Ready;
emit stateChanged(m_state);
});
} else {
m_state = QTextToSpeech::Ready;
}
m_errorReason = QTextToSpeech::ErrorReason::NoError;
}
QTextToSpeechEngineMock::~QTextToSpeechEngineMock()
{
}
QList<QLocale> QTextToSpeechEngineMock::availableLocales() const
{
QList<QLocale> locales;
if (const auto it = m_parameters.find("voices"); it != m_parameters.constEnd()) {
using VoiceData = QList<std::tuple<QString, QLocale, QVoice::Gender, QVoice::Age>>;
const auto voicesData = it->value<VoiceData>();
QSet<QLocale> localeSet;
for (const auto &voiceData : voicesData)
localeSet.insert(std::get<1>(voiceData));
locales = localeSet.values();
} else {
locales << QLocale(QLocale::English, QLocale::UnitedKingdom)
<< QLocale(QLocale::English, QLocale::UnitedStates)
<< QLocale(QLocale::NorwegianBokmal, QLocale::Norway)
<< QLocale(QLocale::NorwegianNynorsk, QLocale::Norway)
<< QLocale(QLocale::Finnish, QLocale::Finland);
}
return locales;
}
QList<QVoice> QTextToSpeechEngineMock::availableVoices() const
{
QList<QVoice> voices;
if (const auto it = m_parameters.find("voices"); it != m_parameters.constEnd()) {
const auto voicesData = it->value<QList<std::tuple<QString, QLocale, QVoice::Gender, QVoice::Age>>>();
for (const auto &voiceData : voicesData) {
const QLocale &voiceLocale = std::get<1>(voiceData);
if (voiceLocale == m_locale) {
voices << createVoice(std::get<0>(voiceData),
voiceLocale,
std::get<2>(voiceData),
std::get<3>(voiceData),
u"%1-%2"_s.arg(m_locale.bcp47Name()).arg(voices.count() + 1));
}
}
} else {
const QString voiceData = m_locale.bcp47Name();
const auto newVoice = [this, &voiceData](const QString &name, QVoice::Gender gender,
QVoice::Age age, const char *suffix) {
return createVoice(name, m_locale, gender, age,
QVariant::fromValue<QString>(voiceData + suffix));
};
switch (m_locale.language()) {
case QLocale::English: {
if (m_locale.territory() == QLocale::UnitedKingdom) {
voices << newVoice("Bob", QVoice::Male, QVoice::Adult, "-1")
<< newVoice("Anne", QVoice::Female, QVoice::Adult, "-2");
} else {
voices << newVoice("Charly", QVoice::Male, QVoice::Senior, "-1")
<< newVoice("Mary", QVoice::Female, QVoice::Teenager, "-2");
}
break;
}
case QLocale::NorwegianBokmal:
voices << newVoice("Eivind", QVoice::Male, QVoice::Adult, "-1")
<< newVoice("Kjersti", QVoice::Female, QVoice::Adult, "-2");
break;
case QLocale::NorwegianNynorsk:
voices << newVoice("Anders", QVoice::Male, QVoice::Teenager, "-1")
<< newVoice("Ingvild", QVoice::Female, QVoice::Child, "-2");
break;
case QLocale::Finnish:
voices << newVoice("Kari", QVoice::Male, QVoice::Adult, "-1")
<< newVoice("Anneli", QVoice::Female, QVoice::Adult, "-2");
break;
default:
Q_ASSERT_X(false, "availableVoices", "Unsupported locale!");
break;
}
}
return voices;
}
void QTextToSpeechEngineMock::say(const QString &text)
{
m_text = text;
m_currentIndex = 0;
m_timer.start(wordTime(), Qt::PreciseTimer, this);
m_state = QTextToSpeech::Speaking;
emit stateChanged(m_state);
}
void QTextToSpeechEngineMock::synthesize(const QString &text)
{
m_text = text;
m_currentIndex = 0;
m_timer.start(wordTime(), Qt::PreciseTimer, this);
m_state = QTextToSpeech::Synthesizing;
emit stateChanged(m_state);
m_format.setSampleRate(22050);
m_format.setChannelConfig(QAudioFormat::ChannelConfigMono);
m_format.setSampleFormat(QAudioFormat::Int16);
}
void QTextToSpeechEngineMock::stop(QTextToSpeech::BoundaryHint boundaryHint)
{
Q_UNUSED(boundaryHint);
if (m_state == QTextToSpeech::Ready || m_state == QTextToSpeech::Error)
return;
Q_ASSERT(m_state == QTextToSpeech::Paused || m_timer.isActive());
// finish immediately
m_text.clear();
m_currentIndex = -1;
m_timer.stop();
m_state = QTextToSpeech::Ready;
emit stateChanged(m_state);
}
void QTextToSpeechEngineMock::pause(QTextToSpeech::BoundaryHint boundaryHint)
{
Q_UNUSED(boundaryHint);
if (m_state != QTextToSpeech::Speaking)
return;
// implement "pause after word end"
m_pauseRequested = true;
}
void QTextToSpeechEngineMock::resume()
{
if (m_state != QTextToSpeech::Paused)
return;
m_timer.start(wordTime(), Qt::PreciseTimer, this);
m_state = QTextToSpeech::Speaking;
emit stateChanged(m_state);
}
void QTextToSpeechEngineMock::timerEvent(QTimerEvent *e)
{
if (e->timerId() != m_timer.timerId()) {
QTextToSpeechEngine::timerEvent(e);
return;
}
Q_ASSERT(m_state == QTextToSpeech::Speaking || m_state == QTextToSpeech::Synthesizing);
Q_ASSERT(m_text.length());
// Find start of next word, skipping punctuations. This is good enough for testing.
QRegularExpressionMatch match;
qsizetype nextSpace = m_text.indexOf(QRegularExpression(u"\\W+"_s), m_currentIndex, &match);
if (nextSpace == -1)
nextSpace = m_text.length();
const QString word = m_text.sliced(m_currentIndex, nextSpace - m_currentIndex);
sayingWord(word, m_currentIndex, nextSpace - m_currentIndex);
m_currentIndex = nextSpace + match.captured().length();
emit synthesized(m_format, QByteArray(m_format.bytesForDuration(wordTime() * 1000), 0));
if (m_currentIndex >= m_text.length()) {
// done speaking all words
m_timer.stop();
m_state = QTextToSpeech::Ready;
m_currentIndex = -1;
emit stateChanged(m_state);
} else if (m_pauseRequested) {
m_timer.stop();
m_state = QTextToSpeech::Paused;
emit stateChanged(m_state);
}
m_pauseRequested = false;
}
double QTextToSpeechEngineMock::rate() const
{
return m_rate;
}
bool QTextToSpeechEngineMock::setRate(double rate)
{
m_rate = rate;
if (m_timer.isActive()) {
m_timer.stop();
m_timer.start(wordTime(), Qt::PreciseTimer, this);
}
return true;
}
double QTextToSpeechEngineMock::pitch() const
{
return m_pitch;
}
bool QTextToSpeechEngineMock::setPitch(double pitch)
{
m_pitch = pitch;
return true;
}
QLocale QTextToSpeechEngineMock::locale() const
{
return m_locale;
}
bool QTextToSpeechEngineMock::setLocale(const QLocale &locale)
{
if (!availableLocales().contains(locale))
return false;
m_locale = locale;
const auto voices = availableVoices();
if (!voices.contains(m_voice))
m_voice = voices.isEmpty() ? QVoice() : voices.first();
return true;
}
double QTextToSpeechEngineMock::volume() const
{
return m_volume;
}
bool QTextToSpeechEngineMock::setVolume(double volume)
{
if (volume < 0.0 || volume > 1.0)
return false;
m_volume = volume;
return true;
}
QVoice QTextToSpeechEngineMock::voice() const
{
return m_voice;
}
bool QTextToSpeechEngineMock::setVoice(const QVoice &voice)
{
const QString voiceId = voiceData(voice).toString();
const QLocale voiceLocale = QLocale(voiceId.left(voiceId.lastIndexOf("-")));
if (!availableLocales().contains(voiceLocale)) {
qWarning("Engine does not support voice's locale %s",
qPrintable(voiceLocale.bcp47Name()));
return false;
}
m_locale = voiceLocale;
if (!availableVoices().contains(voice)) {
qWarning("Engine does not support voice %s in the locale %s",
qPrintable(voice.name()), qPrintable(voiceLocale.bcp47Name()));
return false;
}
m_voice = voice;
return true;
}
QTextToSpeech::State QTextToSpeechEngineMock::state() const
{
return m_state;
}
QTextToSpeech::ErrorReason QTextToSpeechEngineMock::errorReason() const
{
return m_errorReason;
}
QString QTextToSpeechEngineMock::errorString() const
{
return m_errorString;
}
QT_END_NAMESPACE