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:data-parser, execute-external-code
#include "qtexttospeech_flite_processor.h"
#include "qtexttospeech_flite_plugin.h"
#include <QtCore/qcoreapplication.h>
#include <QtCore/qlocale.h>
#include <QtCore/qmap.h>
#include <QtCore/qpointer.h>
#include <QtCore/qprocessordetection.h>
#include <QtCore/qspan.h>
#include <QtCore/qstring.h>
#include <QtCore/qthreadpool.h>
#include <QtConcurrent/qtconcurrentrun.h>
#include <QtMultimedia/private/qaudiohelpers_p.h>
#include <QtMultimedia/private/qaudiosystem_p.h>
#include <flite/flite.h>
#include <deque>
#include <utility>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using namespace std::chrono_literals;
namespace {
void setRateForVoice(cst_voice *voice, float rate)
{
float stretch = 1.0;
Q_ASSERT(rate >= -1.0 && rate <= 1.0);
// Stretch multipliers taken from Speech Dispatcher
if (rate < 0)
stretch -= rate * 2;
if (rate > 0)
stretch -= rate * (100.0f / 175.0f);
feat_set_float(voice->features, "duration_stretch", stretch);
}
void setPitchForVoice(cst_voice *voice, float pitch)
{
float f0;
Q_ASSERT(pitch >= -1.0 && pitch <= 1.0);
// Conversion taken from Speech Dispatcher
f0 = (pitch * 80) + 100;
feat_set_float(voice->features, "int_f0_target_mean", f0);
}
// Read available flite voices
QStringList fliteAvailableVoices(const QString &libPrefix, const QString &langCode)
{
// Read statically linked voices
QStringList voices;
for (const cst_val *v = flite_voice_list; v; v = val_cdr(v)) {
cst_voice *voice = val_voice(val_car(v));
voices.append(voice->name);
}
// Read available libraries
static const QStringList ldPaths = [] {
const QProcessEnvironment pe;
QStringList ldPaths = pe.value(u"LD_LIBRARY_PATH"_s).split(u":"_s, Qt::SkipEmptyParts);
if (ldPaths.isEmpty()) {
ldPaths = QStringList{
// Fedora-style lib64 library paths
u"/usr/lib64"_s,
u"/usr/local/lib64"_s,
u"/lib64"_s,
// Debian-style multi-arch library paths
#if defined(Q_PROCESSOR_ARM_V8)
# if defined(__MUSL__)
u"/usr/lib/aarch64-linux-musl"_s,
# else
u"/usr/lib/aarch64-linux-gnu"_s,
# endif
#elif defined(Q_PROCESSOR_ARM_V7)
# if defined(__MUSL__)
u"/usr/lib/arm-linux-musleabihf"_s,
# else
# if defined(__ARM_PCS_VFP)
u"/usr/lib/arm-linux-gnueabihf"_s,
# else
u"/usr/lib/arm-linux-gnueabi"_s,
# endif
# endif
#elif defined(Q_PROCESSOR_X86_64)
u"/usr/lib/x86_64-linux-gnu"_s,
#elif defined(Q_PROCESSOR_X86)
u"/usr/lib/i686-linux-gnu"_s,
u"/usr/lib/i386-linux-gnu"_s,
#endif
// generic paths
u"/usr/lib"_s,
u"/usr/local/lib"_s,
u"/lib"_s,
};
} else {
ldPaths.removeDuplicates();
}
ldPaths.removeIf([](const QString &path) {
QDir dir(path);
return !dir.isReadable() || dir.isEmpty();
});
qCDebug(lcSpeechTtsFlite) << "QTextToSpeechProcessorFlite: initialized voice paths to"
<< ldPaths;
return ldPaths;
}();
const QString libPattern = QString(u"lib"_s + libPrefix).arg(langCode).arg("*"_L1);
for (const auto &path : ldPaths) {
QDir dir(path);
dir.setNameFilters({ libPattern });
dir.setFilter(QDir::Files);
const QFileInfoList fileList = dir.entryInfoList();
for (const auto &file : fileList) {
QString vox = file.fileName().mid(16, file.fileName().indexOf(u'.') - 16);
voices.append(std::move(vox));
}
}
voices.removeDuplicates();
return voices;
}
QAudioFormat getAudioFormat(const cst_wave &w)
{
QAudioFormat fmt;
fmt.setSampleFormat(QAudioFormat::Int16);
fmt.setSampleRate(w.sample_rate);
fmt.setChannelCount(w.num_channels);
fmt.setChannelConfig(QAudioFormat::defaultChannelConfigForChannelCount(w.num_channels));
return fmt;
}
// we use a dedicated thread pool for flite synthesis:
// * it has a higher priority than the system thread pool
// * synthesizing multiple voices in parallel does not really make sense, so we limit it to 2
// threads (it will typically only be one)
std::shared_ptr<QThreadPool> getFliteThreadPool()
{
static std::weak_ptr<QThreadPool> singleton;
static QMutex mutex;
std::lock_guard guard{ mutex };
std::shared_ptr<QThreadPool> pool = singleton.lock();
if (pool)
return pool;
pool = std::make_shared<QThreadPool>();
pool->setMaxThreadCount(2);
pool->setThreadPriority(QThread::HighPriority);
pool->setObjectName(u"QFliteThreadPool"_s);
singleton = pool;
return pool;
}
} // namespace
///////////////////////////////////////////////////////////////////////////////////////////////////
class QFliteSynthesisProcess final : public QIODevice
{
struct TokenInformation
{
QString word;
std::chrono::milliseconds startTime;
};
using BoundaryHint = QTextToSpeech::BoundaryHint;
public:
QFliteSynthesisProcess(cst_voice *voice, QTextToSpeechProcessorFlite *parent, QString text,
float pitch, float rate);
~QFliteSynthesisProcess();
void pause(QTextToSpeech::BoundaryHint boundaryHint);
void stop(QTextToSpeech::BoundaryHint boundaryHint);
void resume();
private:
template <typename Closure>
void invokeOnParent(Closure c);
// flite synthesis thread
void runFliteSynthesis();
int outputCallback(const cst_wave *w, int start, int size, int last,
struct cst_audio_streaming_info_struct *asi);
static std::optional<TokenInformation>
detectNewToken(const cst_wave *w, int start, int size,
struct cst_audio_streaming_info_struct *asi);
// QIODevice interface
qint64 readData(char *data, qint64 maxlen) override;
qint64 writeData(const char *, qint64) override { return -1; }
qint64 bytesAvailable() const override;
// immutable state
QTextToSpeechProcessorFlite *const m_parent;
cst_voice *const m_voice; // borrowed
const QString m_text;
// thread
std::shared_ptr<QThreadPool> m_threadPool = getFliteThreadPool();
QFuture<void> m_task;
// state
QAudioFormat m_format;
std::deque<char> m_audioBuffer;
std::deque<TokenInformation> m_tokens;
qsizetype m_currentBytePosition{}; // Position of m_audioBuffer.begin()
qsizetype m_currentTokenIndex{};
bool m_lastChunkReceived{};
// pause/stop handling
bool m_paused{};
// NOTE: at the moment only BoundaryHint::Word is supported
std::optional<QTextToSpeech::BoundaryHint> m_pauseRequest;
std::optional<QTextToSpeech::BoundaryHint> m_stopRequest;
std::optional<qint64> bytesToNextWord() const;
};
QFliteSynthesisProcess::QFliteSynthesisProcess(cst_voice *voice,
QTextToSpeechProcessorFlite *parent, QString text,
float pitch, float rate)
: m_parent(parent), m_voice(voice), m_text(std::move(text))
{
Q_ASSERT(m_voice);
Q_ASSERT(m_parent);
// prepare voice
setRateForVoice(m_voice, rate);
setPitchForVoice(m_voice, pitch);
m_task = QtConcurrent::run(m_threadPool.get(), [this] {
runFliteSynthesis();
});
open(ReadOnly | Unbuffered);
}
QFliteSynthesisProcess::~QFliteSynthesisProcess()
{
m_task.cancel();
m_task.waitForFinished();
}
void QFliteSynthesisProcess::pause(QTextToSpeech::BoundaryHint boundaryHint)
{
if (m_paused)
return;
switch (boundaryHint) {
case BoundaryHint::Default:
case BoundaryHint::Immediate:
Q_UNREACHABLE_RETURN();
break;
default:
m_pauseRequest = boundaryHint;
}
}
void QFliteSynthesisProcess::stop(QTextToSpeech::BoundaryHint boundaryHint)
{
switch (boundaryHint) {
case BoundaryHint::Default:
case BoundaryHint::Immediate:
Q_UNREACHABLE_RETURN();
break;
default:
m_stopRequest = boundaryHint;
}
}
void QFliteSynthesisProcess::resume()
{
m_paused = false;
m_pauseRequest = std::nullopt;
}
template <typename Closure>
void QFliteSynthesisProcess::invokeOnParent(Closure c)
{
QMetaObject::invokeMethod(
m_parent,
[parent = m_parent, self = QPointer{ this }, closure = std::move(c)]() mutable {
if (!parent->m_synthesisProcess || (parent->m_synthesisProcess.get() != self))
return; // Another synthesis process has started
closure(parent);
}, Qt::QueuedConnection);
}
void QFliteSynthesisProcess::runFliteSynthesis()
{
qCDebug(lcSpeechTtsFlite) << "QFliteSynthesisProcess() begin";
cst_audio_streaming_info *asi = new_audio_streaming_info();
asi->asc = [](const cst_wave *w, int start, int size, int last,
struct cst_audio_streaming_info_struct *asi) {
auto *self = static_cast<QFliteSynthesisProcess *>(asi->userdata);
return self->outputCallback(w, start, size, last, asi);
};
asi->userdata = (void *)this;
feat_set(m_voice->features, "streaming_info", audio_streaming_info_val(asi));
float secsToSpeak = flite_text_to_speech(m_text.toUtf8().constData(), m_voice, "none");
if (secsToSpeak <= 0) {
invokeOnParent([](QTextToSpeechProcessorFlite *parent) {
parent->setError(
QTextToSpeech::ErrorReason::Input,
QCoreApplication::translate("QTextToSpeech", "Speech synthesizing failure."));
});
return;
};
qCDebug(lcSpeechTtsFlite) << "QFliteSynthesisProcess() end" << secsToSpeak << "Seconds";
}
int QFliteSynthesisProcess::outputCallback(const cst_wave *w, int start, int size, int last,
cst_audio_streaming_info_struct *asi)
{
Q_ASSERT(w);
if (start == 0) {
invokeOnParent([this, format = getAudioFormat(*w)](QTextToSpeechProcessorFlite *parent) {
m_format = format;
parent->prepareAudioSink(format);
});
}
QSpan fliteStream{
w->samples + start,
size,
};
QByteArray chunk{
reinterpret_cast<const char *>(fliteStream.data()),
fliteStream.size_bytes(),
};
std::optional<TokenInformation> token = detectNewToken(w, start, size, asi);
invokeOnParent([this, chunk = std::move(chunk), token = std::move(token),
last](QTextToSpeechProcessorFlite *) mutable {
m_audioBuffer.insert(m_audioBuffer.end(), chunk.begin(), chunk.end());
if (token)
m_tokens.push_back(std::move(*token));
if (last)
m_lastChunkReceived = true;
emit QIODevice::bytesAvailable();
});
if (m_task.isCanceled())
return CST_AUDIO_STREAM_STOP;
return CST_AUDIO_STREAM_CONT;
}
qint64 QFliteSynthesisProcess::readData(char *data, qint64 maxlen)
{
if (m_paused)
Q_ASSERT(m_pauseRequest || m_stopRequest);
const qint64 bytesAvailable = this->bytesAvailable();
const qint64 bytesRequested = std::min(bytesAvailable, maxlen);
qint64 bytesToRead = bytesRequested;
bool atWordBoundary = false;
if (!m_paused && (m_pauseRequest || m_stopRequest)) {
std::optional<qsizetype> bytesToNextWord = this->bytesToNextWord();
if (bytesToNextWord && bytesToNextWord < bytesRequested) {
// We are at a word boundary, so we only read up to the next word.
bytesToRead = bytesToNextWord.value();
atWordBoundary = true;
}
}
if (m_paused) {
// feed null to sink during async operation
std::fill_n(data, bytesToRead, 0);
} else {
std::copy_n(m_audioBuffer.begin(), bytesToRead, data);
std::fill_n(data, bytesRequested - bytesToRead, 0);
m_audioBuffer.erase(m_audioBuffer.begin(), m_audioBuffer.begin() + bytesToRead);
m_currentBytePosition += bytesToRead;
const std::chrono::microseconds currentTimeStamp{
m_format.durationForBytes(m_currentBytePosition),
};
while (!m_tokens.empty() && m_tokens.front().startTime <= currentTimeStamp) {
const TokenInformation &token = m_tokens.front();
m_currentTokenIndex = m_text.indexOf(token.word, m_currentTokenIndex);
emit m_parent->sayingWord(token.word, m_currentTokenIndex, token.word.length());
m_tokens.pop_front();
}
}
const bool stopSynthesisProcess = [&] {
if (m_lastChunkReceived && m_audioBuffer.empty())
return true; // end of file reached
if (atWordBoundary && m_stopRequest == BoundaryHint::Word)
return true; // stop at word boundary
return false;
}();
if (stopSynthesisProcess) {
m_paused = true; // we feed silence to the audio sink until the stop is processed
invokeOnParent([](QTextToSpeechProcessorFlite *parent) {
parent->stop(QTextToSpeech::BoundaryHint::Immediate);
});
} else if (atWordBoundary && m_pauseRequest == BoundaryHint::Word) {
m_paused = true;
invokeOnParent([](QTextToSpeechProcessorFlite *parent) {
parent->pause(QTextToSpeech::BoundaryHint::Immediate);
});
}
return bytesToRead;
}
qint64 QFliteSynthesisProcess::bytesAvailable() const
{
return qint64(m_audioBuffer.size());
}
std::optional<qint64> QFliteSynthesisProcess::bytesToNextWord() const
{
if (m_tokens.empty())
return std::nullopt;
using namespace std::chrono;
const microseconds currentTimeStamp{
m_format.durationForBytes(m_currentBytePosition),
};
const microseconds nextTokenStart{
m_tokens.front().startTime,
};
return m_format.bytesForDuration((nextTokenStart - currentTimeStamp).count());
}
std::optional<QFliteSynthesisProcess::TokenInformation>
QFliteSynthesisProcess::detectNewToken(const cst_wave *w, int start, int size,
cst_audio_streaming_info_struct *asi)
{
if (!asi->item)
asi->item = relation_head(utt_relation(asi->utt, "Token"));
const float tokenStartTime = flite_ffeature_float(
asi->item, "R:Token.daughter1.R:SylStructure.daughter1.daughter1.R:Segment.p.end");
const int tokenStartSample = int(tokenStartTime * float(w->sample_rate));
if ((tokenStartSample >= start) && (tokenStartSample < start + size)) {
const char *token = flite_ffeature_string(asi->item, "name");
if (!token) {
Q_UNLIKELY_BRANCH;
qCWarning(lcSpeechTtsFlite) << "No token found, skipping";
return std::nullopt;
}
auto normalizeFeatureString = [&](const char *feature) -> const char * {
const char *featureString = flite_ffeature_string(asi->item, feature);
if (cst_streq("0", featureString))
return "";
return featureString;
};
auto tokenStartTimestamp = std::chrono::milliseconds(std::lround(tokenStartTime * 1'000));
qCDebug(lcSpeechTtsFlite).nospace()
<< "Processing token start_time: " << tokenStartTimestamp << " content: \""
<< flite_ffeature_string(asi->item, "whitespace")
<< normalizeFeatureString("prepunctuation") << "'" << token << "'"
<< normalizeFeatureString("punc") << "\"";
asi->item = item_next(asi->item);
return TokenInformation{
QString::fromUtf8(token),
tokenStartTimestamp,
};
}
return std::nullopt;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
QTextToSpeechProcessorFlite::QTextToSpeechProcessorFlite(QAudioDevice audioDevice)
: m_audioDevice(std::move(audioDevice))
{
init();
}
QTextToSpeechProcessorFlite::~QTextToSpeechProcessorFlite()
{
m_synthesisProcess.reset();
for (const VoiceInfo &voice : std::as_const(m_voices))
voice.unregister_func(voice.vox);
}
const QList<QTextToSpeechProcessorFlite::VoiceInfo> &QTextToSpeechProcessorFlite::voices() const
{
return m_voices;
}
typedef cst_voice*(*registerFnType)();
typedef void(*unregisterFnType)(cst_voice *);
bool QTextToSpeechProcessorFlite::init()
{
flite_init();
const QLocale locale(QLocale::English, QLocale::UnitedStates);
// ### FIXME: hardcode for now, the only voice files we know about are for en_US
// We could source the language and perhaps the list of voices we want to load
// (hardcoded below) from an environment variable.
const QString langCode(u"us"_s);
const QString libPrefix(u"flite_cmu_%1_%2.so.1"_s);
const QString registerPrefix(u"register_cmu_%1_%2"_s);
const QString unregisterPrefix(u"unregister_cmu_%1_%2"_s);
for (const auto &voice : fliteAvailableVoices(libPrefix, langCode)) {
QLibrary library(libPrefix.arg(langCode, voice));
if (!library.load()) {
qWarning("Voice library could not be loaded: %s", qPrintable(library.fileName()));
continue;
}
auto registerFn = reinterpret_cast<registerFnType>(library.resolve(
registerPrefix.arg(langCode, voice).toLatin1().constData()));
auto unregisterFn = reinterpret_cast<unregisterFnType>(library.resolve(
unregisterPrefix.arg(langCode, voice).toLatin1().constData()));
if (registerFn && unregisterFn) {
const int id = int(m_voices.count());
m_voices.append(VoiceInfo{
id,
registerFn(),
unregisterFn,
voice,
locale.name(),
QVoice::Male,
QVoice::Adult
});
} else {
library.unload();
}
}
return !m_voices.isEmpty();
}
void QTextToSpeechProcessorFlite::prepareAudioSink(QAudioFormat format)
{
qCDebug(lcSpeechTtsFlite) << "QTextToSpeechProcessorFlite::prepareAudioSink" << format;
m_audioSink = std::make_unique<QAudioSink>(m_audioDevice, format);
m_audioSink->setVolume(m_volume);
m_audioSink->setBufferSize(format.bytesForDuration(std::chrono::microseconds(100ms).count()));
// LATER: use public API (compare QTBUG-138378)
QPlatformAudioSink *platformAudioSink = QPlatformAudioSink::get(*m_audioSink);
if (platformAudioSink)
platformAudioSink->setRole(QPlatformAudioSink::AudioEndpointRole::Accessibility);
QObject::connect(m_audioSink.get(), &QAudioSink::stateChanged, m_audioSink.get(),
[&](QAudio::State state) {
if (state == QAudio::StoppedState && m_audioSink->error() != QAudio::NoError) {
setError(QTextToSpeech::ErrorReason::Playback,
QCoreApplication::translate("QTextToSpeech", "Audio IO."));
}
});
m_audioSink->start(m_synthesisProcess.get());
if (m_audioSink->error() != QAudio::NoError) {
setError(QTextToSpeech::ErrorReason::Playback,
QCoreApplication::translate("QTextToSpeech", "Audio Open error: %1")
.arg(m_audioSink->error()));
return;
}
}
void QTextToSpeechProcessorFlite::setError(QTextToSpeech::ErrorReason err, const QString &errorString)
{
if (err == QTextToSpeech::ErrorReason::NoError)
return;
m_audioSink.reset();
if (m_synthesisProcess)
m_synthesisProcess.reset();
qCDebug(lcSpeechTtsFlite) << "Error" << err << errorString;
updateState(QTextToSpeech::Error);
emit errorOccurred(err, errorString);
}
void QTextToSpeechProcessorFlite::updateState(QTextToSpeech::State state)
{
if (state == m_state)
return;
m_state = state;
qCDebug(lcSpeechTtsFlite) << "State changed to" << state;
emit stateChanged(state);
}
// Check voice validity
bool QTextToSpeechProcessorFlite::checkVoice(int voiceId)
{
if (voiceId >= 0 && voiceId < m_voices.size())
return true;
setError(QTextToSpeech::ErrorReason::Configuration,
QCoreApplication::translate("QTextToSpeech", "Invalid voiceId %1.").arg(voiceId));
return false;
}
// Stop current and cancel subsequent utterances
void QTextToSpeechProcessorFlite::stop(QTextToSpeech::BoundaryHint boundaryHint)
{
using BoundaryHint = QTextToSpeech::BoundaryHint;
switch (m_state) {
case QTextToSpeech::Speaking:
case QTextToSpeech::Paused: {
switch (boundaryHint) {
case BoundaryHint::Sentence: {
qCDebug(lcSpeechTtsFlite)
<< "Stopping after sentence not implemented. Stopping after next word";
return stop(BoundaryHint::Word);
}
case BoundaryHint::Utterance:
Q_UNREACHABLE_RETURN(); // handled by QTextToSpeech
case BoundaryHint::Word: {
m_synthesisProcess->stop(BoundaryHint::Word);
return;
}
default: {
if (m_audioSink) {
m_audioSink->reset();
m_audioSink.reset();
}
m_synthesisProcess.reset();
updateState(QTextToSpeech::Ready);
break;
}
}
}
case QTextToSpeech::Synthesizing:
return; // we cannot stop a synthesis process, it will stop automatically
case QTextToSpeech::Error: {
m_synthesisProcess.reset();
updateState(QTextToSpeech::Ready);
return;
}
case QTextToSpeech::Ready:
break;
default:
Q_UNREACHABLE();
}
}
void QTextToSpeechProcessorFlite::pause(QTextToSpeech::BoundaryHint boundaryHint)
{
using BoundaryHint = QTextToSpeech::BoundaryHint;
if (m_state == QTextToSpeech::Speaking) {
switch (boundaryHint) {
case BoundaryHint::Sentence: {
qCDebug(lcSpeechTtsFlite)
<< "Pausing after sentence not implemented. Pausing after next word";
return pause(BoundaryHint::Word);
}
case BoundaryHint::Utterance:
Q_UNREACHABLE_RETURN(); // handled by QTextToSpeech
case BoundaryHint::Word: {
m_synthesisProcess->pause(BoundaryHint::Word);
return;
}
default:
if (m_audioSink)
m_audioSink->suspend();
updateState(QTextToSpeech::Paused);
}
}
}
void QTextToSpeechProcessorFlite::resume()
{
if (m_synthesisProcess)
m_synthesisProcess->resume();
if (m_state == QTextToSpeech::Paused) {
if (m_audioSink && m_synthesisProcess)
m_audioSink->resume();
updateState(QTextToSpeech::Speaking);
}
}
void QTextToSpeechProcessorFlite::say(const QString &text, int voiceId, double pitch, double rate, double volume)
{
if (text.isEmpty())
return;
if (!checkVoice(voiceId))
return;
switch (m_state) {
case QTextToSpeech::Speaking:
case QTextToSpeech::Paused:
stop(QTextToSpeech::BoundaryHint::Immediate);
break;
case QTextToSpeech::Synthesizing:
return; // we cannot synthesize and speak at the same time
case QTextToSpeech::Ready:
case QTextToSpeech::Error:
break;
default:
Q_UNREACHABLE();
}
const VoiceInfo &voiceInfo = m_voices.at(voiceId);
m_volume = float(volume);
updateState(QTextToSpeech::Speaking);
m_synthesisProcess = std::make_unique<QFliteSynthesisProcess>(voiceInfo.vox, this, text,
float(pitch), float(rate));
}
void QTextToSpeechProcessorFlite::synthesize(const QString &text, int voiceId, double pitch, double rate, double volume)
{
if (text.isEmpty())
return;
if (!checkVoice(voiceId))
return;
switch (m_state) {
case QTextToSpeech::Speaking:
case QTextToSpeech::Paused:
return; // we cannot synthesize and speak at the same time
case QTextToSpeech::Synthesizing:
case QTextToSpeech::Ready:
case QTextToSpeech::Error:
break;
default:
Q_UNREACHABLE();
}
m_synthesisFormat = std::nullopt;
m_volume = float(volume);
qCDebug(lcSpeechTtsFlite) << "processText() begin";
const VoiceInfo &voiceInfo = m_voices.at(voiceId);
cst_voice *voice = voiceInfo.vox;
cst_audio_streaming_info *asi = new_audio_streaming_info();
asi->asc = [](const cst_wave *w, int start, int size, int /*last*/,
cst_audio_streaming_info *asi) -> int {
auto *self = static_cast<QTextToSpeechProcessorFlite *>(asi->userdata);
if (!self->m_synthesisFormat) {
QAudioFormat format = getAudioFormat(*w);
if (!format.isValid())
return CST_AUDIO_STREAM_STOP;
self->m_synthesisFormat = format;
}
const qsizetype bytesToWrite = size * self->m_synthesisFormat->bytesPerSample();
QByteArray chunk(reinterpret_cast<const char *>(w->samples + start), bytesToWrite);
QAudioHelperInternal::applyVolume(self->m_volume, *self->m_synthesisFormat,
as_bytes(QSpan{ chunk }),
as_writable_bytes(QSpan{ chunk }));
emit self->synthesized(*self->m_synthesisFormat, chunk);
return CST_AUDIO_STREAM_CONT;
};
asi->userdata = (void *)this;
feat_set(voice->features, "streaming_info", audio_streaming_info_val(asi));
setRateForVoice(voice, float(rate));
setPitchForVoice(voice, float(pitch));
updateState(QTextToSpeech::Synthesizing);
float secsToSpeak = flite_text_to_speech(text.toUtf8().constData(), voice, "none");
if (secsToSpeak <= 0) {
setError(QTextToSpeech::ErrorReason::Input,
QCoreApplication::translate("QTextToSpeech", "Speech synthesizing failure."));
return;
}
qCDebug(lcSpeechTtsFlite) << "processText() end" << secsToSpeak << "Seconds";
m_synthesisFormat = std::nullopt;
updateState(QTextToSpeech::Ready);
}
QT_END_NAMESPACE