mirror of
https://github.com/Rockbox/rockbox.git
synced 2026-07-10 13:29:52 -04:00
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:
parent
390d17893f
commit
acba3fcb07
5 changed files with 295 additions and 2 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
214
utils/rbutilqt/base/ttsqt.cpp
Normal file
214
utils/rbutilqt/base/ttsqt.cpp
Normal 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;
|
||||||
|
}
|
||||||
54
utils/rbutilqt/base/ttsqt.h
Normal file
54
utils/rbutilqt/base/ttsqt.h
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue