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(Qt6 REQUIRED COMPONENTS
|
||||
Core Core5Compat Widgets Svg Network LinguistTools SvgWidgets
|
||||
OPTIONAL_COMPONENTS Multimedia Test)
|
||||
OPTIONAL_COMPONENTS Multimedia Test TextToSpeech)
|
||||
## note Core5Compat is only needed for QuaZip
|
||||
get_target_property(_moc_executable Qt${QT_VERSION_MAJOR}::moc IMPORTED_LOCATION)
|
||||
get_filename_component(QT_BINDIR "${_moc_executable}" DIRECTORY)
|
||||
|
|
@ -58,6 +58,11 @@ if (TARGET Qt${QT_VERSION_MAJOR}::Multimedia)
|
|||
else()
|
||||
message("-- QT::Multimedia not found, TTS preview not available")
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -242,13 +242,22 @@ add_library(rbbase
|
|||
${CMAKE_CURRENT_LIST_DIR}/base/ziputil.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/base/ziputil.h
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
target_sources(rbbase PRIVATE
|
||||
${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/base/ttscarbon.h)
|
||||
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
|
||||
cutelogger ${QUAZIP_LIBRARY} mspack bspatch rbspeex
|
||||
voicefont sansapatcher ipodpatcher chinachippatcher
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@
|
|||
#if defined(Q_OS_MACOS)
|
||||
#include "ttscarbon.h"
|
||||
#endif
|
||||
#if defined(QT_TEXTTOSPEECH_LIB) && defined(QT_MULTIMEDIA_LIB)
|
||||
#include "ttsqt.h"
|
||||
#endif
|
||||
|
||||
// list of tts names and identifiers
|
||||
QMap<QString,QString> TTSBase::ttsList;
|
||||
|
|
@ -59,6 +62,9 @@ void TTSBase::initTTSList()
|
|||
#if defined(Q_OS_MACOS)
|
||||
ttsList["carbon"] = tr("OS X System Engine");
|
||||
#endif
|
||||
#if defined(QT_TEXTTOSPEECH_LIB) && defined(QT_MULTIMEDIA_LIB)
|
||||
ttsList["qt"] = tr("Qt TextToSpeech Engine");
|
||||
#endif
|
||||
}
|
||||
|
||||
// function to get a specific encoder
|
||||
|
|
@ -80,6 +86,11 @@ TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName)
|
|||
if(ttsName == "carbon")
|
||||
tts = new TTSCarbon(parent);
|
||||
else
|
||||
#endif
|
||||
#if defined(QT_TEXTTOSPEECH_LIB) && defined(QT_MULTIMEDIA_LIB)
|
||||
if(ttsName == "qt")
|
||||
tts = new TTSQt(parent);
|
||||
else
|
||||
#endif
|
||||
if(ttsName == "espeak")
|
||||
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