rbutilqt: Add the Qt TextToSpeech module as a new optional TTS engine

Co-authored-by: Qwen3.7-Plus
Change-Id: Ib6ddce52eee30139952e6597504ab96517dc15bd
This commit is contained in:
Vencislav Atanasov 2026-07-08 01:36:19 +03:00 committed by Solomon Peachy
parent 390d17893f
commit acba3fcb07
5 changed files with 295 additions and 2 deletions

View file

@ -48,7 +48,7 @@ enable_testing()
find_package(QT NAMES Qt6 REQUIRED) find_package(QT NAMES Qt6 REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS find_package(Qt6 REQUIRED COMPONENTS
Core Core5Compat Widgets Svg Network LinguistTools SvgWidgets Core Core5Compat Widgets Svg Network LinguistTools SvgWidgets
OPTIONAL_COMPONENTS Multimedia Test) OPTIONAL_COMPONENTS Multimedia Test TextToSpeech)
## note Core5Compat is only needed for QuaZip ## note Core5Compat is only needed for QuaZip
get_target_property(_moc_executable Qt${QT_VERSION_MAJOR}::moc IMPORTED_LOCATION) get_target_property(_moc_executable Qt${QT_VERSION_MAJOR}::moc IMPORTED_LOCATION)
get_filename_component(QT_BINDIR "${_moc_executable}" DIRECTORY) get_filename_component(QT_BINDIR "${_moc_executable}" DIRECTORY)
@ -58,6 +58,11 @@ if (TARGET Qt${QT_VERSION_MAJOR}::Multimedia)
else() else()
message("-- QT::Multimedia not found, TTS preview not available") message("-- QT::Multimedia not found, TTS preview not available")
endif() endif()
if (TARGET Qt${QT_VERSION_MAJOR}::TextToSpeech)
message("-- Found Qt::TextToSpeech")
else()
message("-- QT::TextToSpeech not found, Qt TTS not available")
endif()
add_compile_definitions(QT_NO_USE_NODISCARD_FILE_OPEN) add_compile_definitions(QT_NO_USE_NODISCARD_FILE_OPEN)

View file

@ -242,13 +242,22 @@ add_library(rbbase
${CMAKE_CURRENT_LIST_DIR}/base/ziputil.cpp ${CMAKE_CURRENT_LIST_DIR}/base/ziputil.cpp
${CMAKE_CURRENT_LIST_DIR}/base/ziputil.h ${CMAKE_CURRENT_LIST_DIR}/base/ziputil.h
) )
if(APPLE) if(APPLE)
target_sources(rbbase PRIVATE target_sources(rbbase PRIVATE
${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.cpp ${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.cpp
${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.h) ${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.h)
endif() endif()
if (TARGET Qt${QT_VERSION_MAJOR}::TextToSpeech AND TARGET Qt${QT_VERSION_MAJOR}::Multimedia)
target_sources(rbbase PRIVATE
${CMAKE_CURRENT_LIST_DIR}/base/ttsqt.cpp
${CMAKE_CURRENT_LIST_DIR}/base/ttsqt.h)
target_link_libraries(rbbase
Qt${QT_VERSION_MAJOR}::TextToSpeech)
target_link_libraries(rbbase
Qt${QT_VERSION_MAJOR}::Multimedia)
endif()
target_link_libraries(rbbase target_link_libraries(rbbase
cutelogger ${QUAZIP_LIBRARY} mspack bspatch rbspeex cutelogger ${QUAZIP_LIBRARY} mspack bspatch rbspeex
voicefont sansapatcher ipodpatcher chinachippatcher voicefont sansapatcher ipodpatcher chinachippatcher

View file

@ -31,6 +31,9 @@
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
#include "ttscarbon.h" #include "ttscarbon.h"
#endif #endif
#if defined(QT_TEXTTOSPEECH_LIB) && defined(QT_MULTIMEDIA_LIB)
#include "ttsqt.h"
#endif
// list of tts names and identifiers // list of tts names and identifiers
QMap<QString,QString> TTSBase::ttsList; QMap<QString,QString> TTSBase::ttsList;
@ -59,6 +62,9 @@ void TTSBase::initTTSList()
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
ttsList["carbon"] = tr("OS X System Engine"); ttsList["carbon"] = tr("OS X System Engine");
#endif #endif
#if defined(QT_TEXTTOSPEECH_LIB) && defined(QT_MULTIMEDIA_LIB)
ttsList["qt"] = tr("Qt TextToSpeech Engine");
#endif
} }
// function to get a specific encoder // function to get a specific encoder
@ -80,6 +86,11 @@ TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName)
if(ttsName == "carbon") if(ttsName == "carbon")
tts = new TTSCarbon(parent); tts = new TTSCarbon(parent);
else else
#endif
#if defined(QT_TEXTTOSPEECH_LIB) && defined(QT_MULTIMEDIA_LIB)
if(ttsName == "qt")
tts = new TTSQt(parent);
else
#endif #endif
if(ttsName == "espeak") if(ttsName == "espeak")
tts = new TTSEspeak(parent); tts = new TTSEspeak(parent);

View file

@ -0,0 +1,214 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
*
* Copyright (C) 2026 by Vencislav Atanasov
*
* All files in this archive are subject to the GNU General Public License.
* See the file COPYING in the source tree root for full license agreement.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include <QtCore>
#include "ttsbase.h"
#include "ttsqt.h"
#include "encttssettings.h"
#include "rbsettings.h"
#include <QEventLoop>
#include <QFile>
#include <QDataStream>
#include <QIODevice>
#include <QtTextToSpeech/QTextToSpeech>
#include <QtMultimedia/QAudioFormat>
TTSQt::TTSQt(QObject *parent) : TTSBase(parent)
{
m_tts = new QTextToSpeech(this);
}
TTSQt::~TTSQt()
{
}
TTSStatus TTSQt::voice(const QString& text, const QString& wavfile, QString* errStr)
{
if (!m_tts) {
if (errStr) *errStr = "TTS engine not initialized";
return FatalError;
}
if (text.isEmpty()) {
if (errStr) *errStr = "Input text is empty";
return Warning;
}
// Check if the current engine supports synthesis (requires Qt 6.6+)
if (!wavfile.isEmpty() && !(m_tts->engineCapabilities() & QTextToSpeech::Capability::Synthesize)) {
if (errStr) *errStr = "Current TTS engine does not support synthesis to file";
return FatalError;
}
QEventLoop loop;
bool success = true;
QString errorMsg;
bool hasStarted = false;
// Connect to state changes to know when the TTS engine finishes its task
QMetaObject::Connection conn = connect(m_tts, &QTextToSpeech::stateChanged, &loop, [&](QTextToSpeech::State state) {
if (state == QTextToSpeech::Error) {
success = false;
errorMsg = m_tts->errorString();
loop.quit();
} else if (state != QTextToSpeech::Ready) {
// Any state other than Ready (e.g., Speaking, Synthesizing) means it has started
hasStarted = true;
} else if (state == QTextToSpeech::Ready && hasStarted) {
// It has finished and returned to Ready state
loop.quit();
}
});
if (wavfile.isEmpty()) {
m_tts->say(text);
}
else {
m_audioData.clear();
m_format = QAudioFormat();
// Start synthesis. The functor is called with chunks of audio data.
m_tts->synthesize(text, [this](const QAudioFormat &format, const QByteArray &bytes) {
if (!m_format.isValid()) {
m_format = format; // Store the format from the first chunk
}
m_audioData.append(bytes);
});
}
// Wait for the asynchronous operation to finish.
// We check if it finished synchronously (edge case) to avoid deadlocking the QEventLoop.
if (m_tts->state() == QTextToSpeech::Ready && !hasStarted) {
// Already finished or didn't start
} else {
loop.exec();
}
disconnect(conn);
if (!success) {
if (errStr) *errStr = errorMsg;
return FatalError;
}
if (!wavfile.isEmpty()) {
if (m_audioData.isEmpty() || !m_format.isValid()) {
if (errStr) *errStr = "No audio data generated or invalid format";
return Warning;
}
if (!writeWavFile(wavfile, m_audioData, m_format)) {
if (errStr) *errStr = "Failed to write WAV file";
return FatalError;
}
}
return NoError;
}
bool TTSQt::start(QString *errStr)
{
if (!m_tts) {
m_tts = new QTextToSpeech(this);
}
if (m_tts->availableEngines().isEmpty()) {
if (errStr) *errStr = "No TTS engines available on this system";
return false;
}
return true;
}
bool TTSQt::stop()
{
if (m_tts) {
m_tts->stop();
}
return true;
}
QString TTSQt::voiceVendor(void)
{
return "Qt TextToSpeech";
}
bool TTSQt::configOk()
{
return m_tts != nullptr && m_tts->state() != QTextToSpeech::Error;
}
void TTSQt::generateSettings()
{
// TODO
}
void TTSQt::saveSettings()
{
// TODO
}
TTSBase::Capabilities TTSQt::capabilities()
{
return TTSBase::CanSpeak | TTSBase::RunInParallel;
}
bool TTSQt::writeWavFile(const QString &filePath, const QByteArray &audioData, const QAudioFormat &format)
{
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
quint32 dataSize = audioData.size();
quint32 fileSize = 36 + dataSize;
quint32 fmtChunkSize = 16;
// 3 for IEEE Float, 1 for standard PCM
quint16 audioFormat = (format.sampleFormat() == QAudioFormat::Float) ? 3 : 1;
quint16 channels = format.channelCount();
quint32 sampleRate = format.sampleRate();
quint16 bitsPerSample = format.bytesPerSample() * 8;
quint16 blockAlign = channels * format.bytesPerSample();
quint32 byteRate = sampleRate * blockAlign;
QDataStream stream(&file);
stream.setByteOrder(QDataStream::LittleEndian);
// RIFF header
stream.writeRawData("RIFF", 4);
stream << fileSize;
stream.writeRawData("WAVE", 4);
// fmt chunk
stream.writeRawData("fmt ", 4);
stream << fmtChunkSize;
stream << audioFormat;
stream << channels;
stream << sampleRate;
stream << byteRate;
stream << blockAlign;
stream << bitsPerSample;
// data chunk
stream.writeRawData("data", 4);
stream << dataSize;
stream.writeRawData(audioData.constData(), dataSize);
file.close();
return true;
}

View file

@ -0,0 +1,54 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
*
* Copyright (C) 2026 by Vencislav Atanasov
*
* All files in this archive are subject to the GNU General Public License.
* See the file COPYING in the source tree root for full license agreement.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#ifndef TTSQT_H
#define TTSQT_H
#include <QtCore>
#include "ttsbase.h"
#include <QtTextToSpeech/QTextToSpeech>
#include <QtMultimedia/QAudioFormat>
class TTSQt : public TTSBase
{
Q_OBJECT
public:
explicit TTSQt(QObject *parent = nullptr);
~TTSQt() override;
TTSStatus voice(const QString& text, const QString& wavfile, QString* errStr) override;
bool start(QString *errStr) override;
bool stop() override;
QString voiceVendor(void) override;
bool configOk() override;
void generateSettings() override;
void saveSettings() override;
Capabilities capabilities() override;
private:
QTextToSpeech *m_tts = nullptr;
QByteArray m_audioData;
QAudioFormat m_format;
// Helper to write raw PCM data to a WAV file
bool writeWavFile(const QString &filePath, const QByteArray &audioData, const QAudioFormat &format);
};
#endif // TTSQT_H