Files
EJM_Display/PublicFunctions/TTSManager.cpp

295 lines
9.3 KiB
C++
Raw Permalink Normal View History

2025-09-15 22:28:43 +08:00
#include "TTSManager.h"
#include <QDebug>
#include <QDir>
#include <algorithm>
TTSManager::TTSManager(QObject *parent)
: QObject(parent),
m_currentState(PlayState::Stopped)
{
// 初始化进程(用于执行 edge-tts + ffplay 命令)
m_voiceProcess = new QProcess(this);
connect(m_voiceProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &TTSManager::onPlayFinished);
// 初始化任务调度定时器(单触发模式,避免任务重叠)
m_taskTimer = new QTimer(this);
m_taskTimer->setSingleShot(true);
m_taskTimer->setInterval(200); // 任务切换间隔(避免进程占用)
connect(m_taskTimer, &QTimer::timeout, this, &TTSManager::processNextTask);
// 初始化临时音频路径(用户目录下,避免权限问题)
m_tempAudioPath = QDir::homePath() + "/tts_temp_audio.wav";
// 检查环境是否就绪
if (!checkEnvironment()) {
qCritical() << "[TTS] 环境检查失败!请按提示安装依赖";
m_currentState = PlayState::Failed;
emit stateChanged(m_currentState);
}
}
TTSManager::~TTSManager()
{
// 析构时停止所有任务并清理
stopAll();
delete m_voiceProcess;
delete m_taskTimer;
// 删除残留的临时音频文件
if (QFile::exists(m_tempAudioPath)) {
QFile::remove(m_tempAudioPath);
}
}
// 对外播放接口
PlayState TTSManager::speak(const QString &text, int repeatCount, int priority)
{
// 环境未就绪,直接返回失败
if (m_currentState == PlayState::Failed || !checkEnvironment()) {
qWarning() << "[TTS] 播放失败:环境未就绪";
return PlayState::Failed;
}
// 文本为空,返回失败
QString trimmedText = text.trimmed();
if (trimmedText.isEmpty()) {
qWarning() << "[TTS] 播放失败:文本为空";
return PlayState::Failed;
}
// 处理参数合法性重复次数≥1优先级0-3
int actualRepeat = qMax(1, repeatCount);
int actualPriority = qBound(0, priority, 3);
SpeechTask newTask = {trimmedText, actualRepeat, actualPriority, 0};
// 检查是否需要打断当前低优先级任务
if (handleHighPriorityTask(newTask)) {
return m_currentState;
}
// 任务入队,若当前无播放则触发调度
enqueueTask(newTask);
if (m_currentState == PlayState::Stopped || m_currentState == PlayState::Completed) {
m_taskTimer->start();
}
return m_currentState;
}
// 停止所有播放任务
void TTSManager::stopAll()
{
// 停止当前进程
if (m_voiceProcess->state() == QProcess::Running) {
m_voiceProcess->kill();
m_voiceProcess->waitForFinished(500); // 等待进程退出
}
// 清空任务队列和状态
m_taskQueue.clear();
m_currentTask = SpeechTask();
m_currentState = PlayState::Stopped;
m_taskTimer->stop();
// 删除临时音频文件
if (QFile::exists(m_tempAudioPath)) {
QFile::remove(m_tempAudioPath);
}
// 发送状态变化信号
emit stateChanged(m_currentState);
qDebug() << "[TTS] 所有任务已停止";
}
// 补充修改 onPlayFinished 函数,确保抢占后状态正确
void TTSManager::onPlayFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
// 检查是否是被高优先级任务中断的情况
if (exitCode == 1 && m_currentState == PlayState::Playing) {
return;
}
// 原有逻辑:清理临时文件 + 处理重复播放 + 调度下一个任务
if (QFile::exists(m_tempAudioPath)) {
QFile::remove(m_tempAudioPath);
}
if (m_currentTask.currentRepeat < m_currentTask.repeatCount - 1) {
m_currentTask.currentRepeat++;
if (executeVoiceTask(m_currentTask.text)) {
m_currentState = PlayState::Playing;
emit stateChanged(m_currentState);
return;
}
}
m_currentState = PlayState::Completed;
emit stateChanged(m_currentState);
m_currentTask = SpeechTask();
m_taskTimer->start();
}
// 处理下一个任务(从队列中取优先级最高的)
void TTSManager::processNextTask()
{
// 队列空则切换到停止状态
if (m_taskQueue.isEmpty()) {
m_currentState = PlayState::Stopped;
emit stateChanged(m_currentState);
return;
}
// 取出队列头部任务(已按优先级排序,头部优先级最高)
m_currentTask = m_taskQueue.takeFirst();
m_currentTask.currentRepeat = 0; // 重置重复计数
// 执行任务,更新状态
if (executeVoiceTask(m_currentTask.text)) {
m_currentState = PlayState::Playing;
} else {
m_currentState = PlayState::Failed;
// 任务失败,调度下一个
m_taskTimer->start();
}
emit stateChanged(m_currentState);
emit currentTaskChanged(m_currentTask);
}
// 核心:执行 edge-tts 生成音频 + ffplay 播放
bool TTSManager::executeVoiceTask(const QString &text)
{
// 进程忙则返回失败
if (m_voiceProcess->state() == QProcess::Running) {
qWarning() << "[TTS] 进程忙,无法执行新任务";
return false;
}
// 步骤1处理文本和路径的双引号转义先复制原字符串再修改临时变量
QString escapedText = text; // 先复制原始文本
escapedText.replace("\"", "\\\""); // 仅修改临时变量
QString escapedAudioPath = m_tempAudioPath; // 先复制原始路径
escapedAudioPath.replace("\"", "\\\""); // 仅修改临时变量
// 步骤2拼接命令使用处理后的临时变量
QString cmd = QString(
"source ~/tts_venv/bin/activate && "
"edge-tts "
"--voice zh-CN-XiaoxiaoNeural "
"--text \"%1\" "
"--write-media %2 && "
"ffplay -autoexit -nodisp %2"
).arg(escapedText, escapedAudioPath);
// 执行命令(通过 bash 解释器,支持 source 命令)
m_voiceProcess->start("bash", QStringList() << "-c" << cmd);
// 等待进程启动超时1秒
if (!m_voiceProcess->waitForStarted(1000)) {
qCritical() << "[TTS] 进程启动失败!请检查环境";
m_voiceProcess->kill();
return false;
}
return true;
}
bool TTSManager::checkEnvironment()
{
bool envReady = true;
// 1. 修复 edge-tts 检测(不用 --version改用 --help 验证是否存在)
QProcess ttsCheck;
QString ttsCheckCmd = QString(
"source /home/zmj/tts_venv/bin/activate && edge-tts --help" // 用 --help 替代 --version
);
ttsCheck.start("bash", QStringList() << "-c" << ttsCheckCmd);
ttsCheck.waitForFinished(3000);
// 检查退出码0 表示存在非0表示不存在
if (ttsCheck.exitCode() != 0) {
qWarning() << "[TTS] 未检测到 edge-tts请执行source /home/zmj/tts_venv/bin/activate && pip3 install edge-tts";
envReady = false;
}
// 2. 修复 ffplay 检测(用 -version 单横线参数)
QProcess ffplayCheck;
ffplayCheck.start("/usr/bin/ffplay -version"); // 单横线 -version
ffplayCheck.waitForFinished(2000);
if (ffplayCheck.exitCode() != 0) {
qWarning() << "[TTS] 未检测到 ffplay请执行sudo apt-get install ffmpeg";
envReady = false;
}
return envReady;
}
// 入队任务(按优先级降序排序,优先级高的在前)
void TTSManager::enqueueTask(const SpeechTask &task)
{
// 插入队列并按优先级排序3>2>1>0
m_taskQueue.append(task);
std::sort(m_taskQueue.begin(), m_taskQueue.end(),
[](const SpeechTask &a, const SpeechTask &b) {
return a.priority > b.priority;
});
}
// 修改 handleHighPriorityTask 函数,强化抢占逻辑
bool TTSManager::handleHighPriorityTask(const SpeechTask &newTask)
{
// 1. 当前无任务或新任务优先级不高于当前,无需抢占
if (m_currentState != PlayState::Playing || newTask.priority <= m_currentTask.priority) {
return false;
}
// 2. 高优先级任务需要抢占,记录当前任务状态以便后续恢复
SpeechTask interruptedTask = m_currentTask;
// 计算剩余重复次数(当前已播次数 + 剩余次数)
interruptedTask.currentRepeat = m_currentTask.currentRepeat;
// 3. 强制停止当前播放进程(立即中断低优先级语音)
if (m_voiceProcess->state() == QProcess::Running) {
m_voiceProcess->kill(); // 强制终止进程
m_voiceProcess->waitForFinished(500); // 等待进程退出
}
// 4. 被中断的任务重新入队(继续完成剩余播放次数)
if (interruptedTask.currentRepeat < interruptedTask.repeatCount - 1) {
// 更新剩余重复次数减去已完成的1次
interruptedTask.repeatCount -= (interruptedTask.currentRepeat + 1);
interruptedTask.currentRepeat = 0;
enqueueTask(interruptedTask);
}
// 5. 立即执行高优先级任务
m_currentTask = newTask;
m_currentTask.currentRepeat = 0; // 重置重复计数
if (executeVoiceTask(m_currentTask.text)) {
m_currentState = PlayState::Playing;
emit stateChanged(m_currentState);
emit currentTaskChanged(m_currentTask);
} else {
m_currentState = PlayState::Failed;
m_taskTimer->start(); // 若启动失败,调度下一个任务
}
return true;
}