Files
EJM_Display/PublicFunctions/TTSManager.cpp
2025-09-15 22:28:43 +08:00

295 lines
9.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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;
}