1. 稍微重构了一下项目代码结构,使其更加合理

2. 重构了音频播放类,简化其接口,并支持流式wav音频播放
3. 增加了对流式音频数据的处理类
4. 增加了对GUI自动化操作的处理类
This commit is contained in:
Misaki
2026-01-31 23:03:22 +08:00
parent 96b5ed59b7
commit c32f085732
51 changed files with 729 additions and 487 deletions
+19 -15
View File
@@ -23,18 +23,20 @@ file(GLOB_RECURSE LAppLive2D
) )
file(GLOB_RECURSE YosugaSrc file(GLOB_RECURSE YosugaSrc
CONFIGURE_DEPENDS CONFIGURE_DEPENDS
"src/AudioHandle/Src/*.cpp" "src/Handle/AudioHandle/Src/*.cpp"
"src/AudioHandle/Inc/*.h" "src/Handle/AudioHandle/Inc/*.h"
"src/Menu/Src/*.cpp" "src/Handle/NetWorkHandle/Src/*.cpp"
"src/Menu/Inc/*.h" "src/Handle/NetWorkHandle/Inc/*.h"
"src/Handle/DataObjectHandle/Src/*.cpp"
"src/Handle/DataObjectHandle/Inc/*.h"
"src/UI/Menu/Src/*.cpp"
"src/UI/Menu/Inc/*.h"
"src/DAO/Inc/*.h" "src/DAO/Inc/*.h"
"src/DAO/Src/*.cpp" "src/DAO/Src/*.cpp"
"src/NetWorkHandle/Src/*.cpp" "src/UI/Setting/Src/*.cpp"
"src/NetWorkHandle/Inc/*.h" "src/UI/Setting/Inc/*.h"
"src/Setting/Src/*.cpp" "src/UI/Render/TextRender/Src/*.cpp"
"src/Setting/Inc/*.h" "src/UI/Render/TextRender/Inc/*.h"
"src/Render/TextRender/Src/*.cpp"
"src/Render/TextRender/Inc/*.h"
"src/Core/Src/*.cpp" "src/Core/Src/*.cpp"
"src/Core/Inc/*.h" "src/Core/Inc/*.h"
"src/Utils/Inc/*.hpp" "src/Utils/Inc/*.hpp"
@@ -192,11 +194,13 @@ target_include_directories(${PROJECT_NAME}
3rdparty/Live2D/Src/Core/include 3rdparty/Live2D/Src/Core/include
3rdparty/Live2D/Src/stb 3rdparty/Live2D/Src/stb
3rdparty/Live2D/Src/LAppLive2D/Inc 3rdparty/Live2D/Src/LAppLive2D/Inc
src/AudioHandle/Inc 3rdparty/autogui-cpp/src
src/Menu/Inc src/Handle/AudioHandle/Inc
src/NetWorkHandle/Inc src/Handle/NetWorkHandle/Inc
src/Setting/Inc src/Handle/DataObjectHandle/Inc
src/Render/TextRender/Inc src/UI/Menu/Inc
src/UI/Setting/Inc
src/UI/Render/TextRender/Inc
src/Core/Inc src/Core/Inc
src/DAO/Inc src/DAO/Inc
src/Utils/Inc src/Utils/Inc
-156
View File
@@ -1,156 +0,0 @@
//
// Created by Administrator on 2025/1/17.
//
#pragma once
#include <QObject>
#include <QMediaPlayer> // 音频播放模块
#include <QAudioOutput> // QMediaPlayer 的音量控制组件
#include <QAudioSink> // 音频输出组件, 用于原始数据播放
#include <QUrl>
#include <QBuffer>
#include <QScopedPointer>
#include <QMutex>
/**
* @brief 音频播放模块
* @author Misaki
* 单例类
* 本模块重新基于Qt6重构
* 实现的功能
* 1. 设定传入的音频文件路径
* 2. 根据音频文件路径播放音频
* 将上面的两个函数封装成一个槽函数,以及设定一个对应的信号
*/
class AudioOutput : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AudioOutput) // 禁用拷贝
private:
/**
* 构造函数私有化
* @param parent
*/
explicit AudioOutput(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static QScopedPointer<AudioOutput> instance; // 单例类
static QMutex mutex;
public:
static AudioOutput *getInstance();
/**
* 析构函数
*/
~AudioOutput() override;
/**
* 播放来自文件的音频
* @param url
*/
void playAudio(const QUrl& url) const;
/**
* 播放内存中的WAV字节流数据
* @param wavData 完整的WAV格式字节流
*/
void playFromByteArray(const QByteArray &wavData);
/**
* 获取WAV音频格式
* @param wavData
* @return
*/
QAudioFormat getWavFormat(const QByteArray &wavData) const;
/**
* 暂停播放音频
*/
void pauseAudio() const;
/**
* 停止播放音频
*/
void stopAudio() const;
/**
* 设置播放速度
* @param speed
*/
void setPlaySpeed(double speed) const;
/**
* 获取播放速度
* @return double
*/
[[nodiscard]] double getPlaySpeed() const;
/**
* 设置播放音量
* @param volume
*/
void setPlayVolume(int volume) const;
/**
* 获取播放音量
* @return int
*/
[[nodiscard]] int getPlayVolume() const;
/**
* 获取当前播放位置(毫秒)
* @return qint64
*/
[[nodiscard]] qint64 getPlayPosition() const;
/**
* 获取媒体总时长(毫秒)
* @return qint64
*/
[[nodiscard]] qint64 getMediaDuration() const;
/**
* 根据当前播放位置,返回播放进度百分比
* @return double
*/
[[nodiscard]] double getPlayProgress() const;
void setAudioFormat(int sampleRate, int channels, QAudioFormat::SampleFormat sampleType);
void setAudioFormat(const QAudioFormat &format_);
[[nodiscard]] QAudioFormat getAudioFormat() const;
/**
* 获取播放状态
* QMediaPlayer::StoppedState、QMediaPlayer::PlayingState、QMediaPlayer::PausedState
* @return QMediaPlayer::State
*/
[[nodiscard]] QMediaPlayer::MediaStatus getState() const;
/**
* 获取错误类型
* NoError,
ResourceError,
FormatError,
NetworkError,
AccessDeniedError
* @return QMediaPlayer::Error
*/
[[nodiscard]] QMediaPlayer::Error getError() const;
/**
* 获取错误描述
* @return QString
*/
[[nodiscard]] QString getErrorString() const;
signals:
void playbackFinished(); // 播放完成信号
private:
QMediaPlayer *mediaPlayer; /// <!音频播放核心组件
QAudioOutput *audioOutput; /// <!音量和设备控制
QAudioSink *audioSink; /// <!字节流播放组件
QAudioFormat format; /// <!字节流播放时所用音频格式
QBuffer *audioBuffer; /// <!存储内存数据
};
-300
View File
@@ -1,300 +0,0 @@
//
// Created by Administrator on 2025/1/17.
//
#include "AudioOutput.h"
#include <QMediaDevices>
#include <QDataStream>
QScopedPointer<AudioOutput> AudioOutput::instance; // 使用QScopedPointer去管理单例,自动析构
QMutex AudioOutput::mutex;
AudioOutput *AudioOutput::getInstance()
{
// 懒汉式(单线程播放,无需考虑加锁)
if (instance.isNull()) { // 若未访问
QMutexLocker locker(&mutex);
if (instance.isNull()) {
// 使用reset初始化
instance.reset(new AudioOutput());
}
}
return instance.data(); // 返回单例实例
}
AudioOutput::AudioOutput(QObject *parent) : QObject(parent), mediaPlayer(nullptr), audioOutput(nullptr), audioSink(nullptr), audioBuffer(nullptr)
{
audioBuffer = new QBuffer(this); // 初始化缓冲区
// 初始化 QMediaPlayer 用于文件播放
mediaPlayer = new QMediaPlayer(this);
// 初始化QAudioOutput 用于控制 QMediaPlayer 的音量与设备
audioOutput = new QAudioOutput(QMediaDevices::defaultAudioOutput(), this);
mediaPlayer->setAudioOutput(audioOutput); // 将 QMediaPlayer 与 QAudioOutput 关联起来
// 监听 QMediaPlayer 状态变化
connect(mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus state) {
if (state == QMediaPlayer::EndOfMedia) {
emit playbackFinished(); // 触发播放完成信号
}
});
// 初始化 默认的QAudioSink 和 QBuffer 用于字节流播放
format.setSampleRate(44100); // 采样率
format.setChannelCount(2); // 播放通道数
format.setSampleFormat(QAudioFormat::Int16); // 采样格式
audioSink = new QAudioSink(QMediaDevices::defaultAudioOutput(), format, this);
audioBuffer = new QBuffer(this);
}
AudioOutput::~AudioOutput()
{
if (mediaPlayer->playbackState() != QMediaPlayer::StoppedState) {
mediaPlayer->stop();
}
if (audioSink->state() != QAudio::StoppedState) {
audioSink->stop();
}
}
void AudioOutput::playAudio(const QUrl& url) const {
mediaPlayer->setSource(url);
mediaPlayer->play();
}
/**
* 从字节数组中播放音频
* @param wavData
*/
void AudioOutput::playFromByteArray(const QByteArray &wavData) {
// 确保数据有效
if (wavData.isEmpty()) {
qWarning() << "尝试播放空音频数据";
return;
}
// 停止 QMediaPlayer 播放,防止冲突
if (mediaPlayer->playbackState() != QMediaPlayer::StoppedState) {
mediaPlayer->stop();
}
// 停止并释放旧的 QAudioSink 这里因为QAudioSink 不支持运行时修改播放音频格式,因此只能这么做
if (audioSink) {
audioSink->stop();
delete audioSink;
audioSink = nullptr;
}
// 解析新的音频格式
const QAudioFormat newFormat = getWavFormat(wavData);
if (!newFormat.isValid()) {
qWarning() << "音频格式解析失败,停止播放。";
return;
}
// 创建一个新的 QAudioSink 实例
audioSink = new QAudioSink(QMediaDevices::defaultAudioOutput(), newFormat, this);
// 准备 QBuffer:只包含音频原始数据 (跳过 44 字节的 WAV 头部)
const QByteArray rawAudioData = wavData.mid(44);
// 重置缓冲区
audioBuffer->close();
audioBuffer->setData(rawAudioData);
audioBuffer->open(QIODevice::ReadOnly);
// 监听 QAudioSink 状态变化以触发 playbackFinished 信号
connect(audioSink, &QAudioSink::stateChanged, this, [this](const QAudio::State state) {
if (state == QAudio::StoppedState) {
if (audioSink->error() == QAudio::NoError) {
emit playbackFinished();
} else {
qWarning() << "QAudioSink 播放错误:" << audioSink->error();
}
}
});
// 将 QBuffer (QIODevice) 传递给 QAudioSink,并开始播放
audioSink->start(audioBuffer);
}
/**
* @brief 从完整的 WAV 字节流中解析 QAudioFormat
* @param wavData 完整的 WAV 文件字节流
* @return QAudioFormat 如果解析成功,返回正确的格式;否则返回默认格式
*/
QAudioFormat AudioOutput::getWavFormat(const QByteArray &wavData) const {
QAudioFormat format_;
// 完整的 WAV 文件至少有 44 字节的头部
if (wavData.size() < 44) {
qWarning() << "WAV 数据太短,无法解析头部";
return this->format; // 返回一个默认格式
}
// 使用 QDataStream 以小端模式读取二进制数据
QDataStream stream(wavData);
stream.setByteOrder(QDataStream::LittleEndian);
// 跳过 RIFF 和 FORMAT 块 (共 20 字节)
stream.skipRawData(20);
// 读取声道数 (2 bytes)
quint16 channels = 0;
stream >> channels;
format_.setChannelCount(channels);
// 读取采样率 (4 bytes)
quint32 sampleRate = 0;
stream >> sampleRate;
format_.setSampleRate(sampleRate);
// 跳过 ByteRate 和 BlockAlign (共 6 字节)
stream.skipRawData(6);
// 读取位深 (2 bytes)
quint16 bitsPerSample = 0;
stream >> bitsPerSample;
// 根据位深设置 QAudioFormat 的 SampleFormat
if (bitsPerSample == 8) {
format_.setSampleFormat(QAudioFormat::UInt8);
} else if (bitsPerSample == 16) {
format_.setSampleFormat(QAudioFormat::Int16);
} else if (bitsPerSample == 32) {
format_.setSampleFormat(QAudioFormat::Float); // 32位通常是浮点数
} else {
qWarning() << "不支持的位深:" << bitsPerSample;
return this->format; // 返回一个默认格式
}
qDebug() << "WAV Format - Rate:" << sampleRate << ", Channels:" << channels << ", Bits:" << bitsPerSample;
return format_;
}
/**
* 暂停播放
*/
void AudioOutput::pauseAudio() const {
mediaPlayer->pause();
}
/**
* 停止播放
*/
void AudioOutput::stopAudio() const {
mediaPlayer->stop();
}
/**
* 设置播放速度
* @param speed
*/
void AudioOutput::setPlaySpeed(const double speed) const {
mediaPlayer->setPlaybackRate(speed);
}
/**
* 获取播放速度
* @return double
*/
double AudioOutput::getPlaySpeed() const {
return mediaPlayer->playbackRate();
}
/**
* 设置播放音量
* @param volume
*/
void AudioOutput::setPlayVolume(const int volume) const {
// 将 0-100 转换为 0.0 - 1.0
const qreal newVolume = static_cast<qreal>(volume) / 100.0;
audioOutput->setVolume(static_cast<float>(newVolume));
}
/**
* 获取播放音量
* @return int
*/
int AudioOutput::getPlayVolume() const {
// 将 0.0 - 1.0 转换为 0-100
return qRound(audioOutput->volume() * 100);
}
/**
* 获取当前播放位置(毫秒)
* @return qint64
*/
qint64 AudioOutput::getPlayPosition() const {
// 检查 QMediaPlayer 是否处于播放状态,且当前是否有源在播放
if (mediaPlayer->playbackState() == QMediaPlayer::PlayingState) {
return mediaPlayer->position();
}
// 否则返回 0
return 0;
}
/**
* 获取音频时长(毫秒)
* @return qint64
*/
qint64 AudioOutput::getMediaDuration() const {
if(mediaPlayer->playbackState() == QMediaPlayer::PlayingState){
return mediaPlayer->duration();
}
return 0;
}
/**
* 根据当前播放位置,返回播放进度百分比
* @return double
*/
double AudioOutput::getPlayProgress() const {
if(mediaPlayer->playbackState() != QMediaPlayer::PlayingState){
return 0;
}
return static_cast<double>(this->getPlayPosition()) / static_cast<double>(this->getMediaDuration());
}
void AudioOutput::setAudioFormat(const int sampleRate, const int channels, const QAudioFormat::SampleFormat sampleType) {
this->format.setSampleRate(sampleRate);
this->format.setChannelCount(channels);
this->format.setSampleFormat(sampleType);
}
void AudioOutput::setAudioFormat(const QAudioFormat &format_) {
this->format = format_;
}
QAudioFormat AudioOutput::getAudioFormat() const {
return this->format;
}
/**
* 获取播放状态
* QMediaPlayer::StoppedState、QMediaPlayer::PlayingState、QMediaPlayer::PausedState
* @return QMediaPlayer::State
*/
QMediaPlayer::MediaStatus AudioOutput::getState() const {
return mediaPlayer->mediaStatus();
}
/**
* 获取错误类型
* NoError,
ResourceError,
FormatError,
NetworkError,
AccessDeniedError,
ServiceMissingError,
MediaIsPlaylist
* @return QMediaPlayer::Error
*/
QMediaPlayer::Error AudioOutput::getError() const {
return mediaPlayer->error();
}
/**
* 获取错误描述
* @return QString
*/
QString AudioOutput::getErrorString() const {
return mediaPlayer->errorString();
}
+3 -3
View File
@@ -19,17 +19,17 @@ class QJsonObject;
class AudioDataTransferObject final : public DataTransferObjectBase{ class AudioDataTransferObject final : public DataTransferObjectBase{
public: public:
// 构造函数(带默认值) // 构造函数(带默认值)
explicit AudioDataTransferObject(const QString& owner = "client", explicit AudioDataTransferObject(QString owner = "client",
bool isStream = false, bool isStream = false,
bool isStart = false, bool isStart = false,
bool isEnd = false, bool isEnd = false,
int sequence = 0, int sequence = 0,
const QByteArray& data = {}, QByteArray data = {},
int sampleRate = 16000, int sampleRate = 16000,
int channelCount = 1, int channelCount = 1,
int bitDepth = 16, int bitDepth = 16,
double duration = 0.0, double duration = 0.0,
const QString& text = ""); QString text = "");
// 静态工厂方法 // 静态工厂方法
static AudioDataTransferObject fromJson(const QJsonObject& json); static AudioDataTransferObject fromJson(const QJsonObject& json);
+56
View File
@@ -0,0 +1,56 @@
//
// Created by misaki on 2026/1/30.
//
/**
* 自动代理数据对象
* 非对等传输对象,只被用于将服务端返回的auto_agent json转换为对象
*/
#pragma once
#include <QObject>
#include <QByteArray>
#include <QJsonObject>
#include <QJsonValue>
#include "DataTransferObjectBase.h"
class QJsonObject;
class AutoAgentDataObject final : public DataTransferObjectBase {
public:
// 构造函数(带默认值)
explicit AutoAgentDataObject(const QString& Action,
int X1,
int Y1,
int X2,
int Y2,
const QString& Key,
const QString& Content,
const QString& Direction);
// 静态工厂方法
static AutoAgentDataObject fromJson(const QJsonObject& json);
[[nodiscard]] QString type() const override { return "auto_agent"; }
[[nodiscard]] QJsonObject toJson() const override; // 通过多态即可统一调用方式
// 链式调用设置
AutoAgentDataObject& setData(const QString& key, const QJsonValue& value) override;
[[nodiscard]] QString getAction() const { return m_action; }
[[nodiscard]] int getX1() const { return m_x1; }
[[nodiscard]] int getY1() const { return m_y1; }
[[nodiscard]] int getX2() const { return m_x2; }
[[nodiscard]] int getY2() const { return m_y2; }
[[nodiscard]] QString getKey() const { return m_key; }
[[nodiscard]] QString getContent() const { return m_content; }
[[nodiscard]] QString getDirection() const { return m_direction; }
private:
QString m_action; /// 自动化动作名称
int m_x1; /// 鼠标起始位置x1
int m_y1; /// 鼠标起始位置y1
int m_x2; /// 鼠标结束位置x2
int m_y2; /// 鼠标结束位置y2
QString m_key; /// 快捷键
QString m_content; /// 输入文本内容
QString m_direction; /// 滚动方向
};
+3
View File
@@ -27,6 +27,7 @@
#include "DataTransferObjectBase.h" #include "DataTransferObjectBase.h"
#include "AudioDataTransferObject.h" #include "AudioDataTransferObject.h"
#include "AutoAgentDataObject.h"
/** /**
* NetworkDO * NetworkDO
*/ */
@@ -54,6 +55,8 @@ public:
signals: signals:
// 业务接收信号 // 业务接收信号
void audioPacketReceived(const AudioDataTransferObject& packet); // 音频数据准备完成信号 void audioPacketReceived(const AudioDataTransferObject& packet); // 音频数据准备完成信号
void autoAgentPacketReceived(const AutoAgentDataObject& packet); // 自动代理数据包接收信号
void errorOccurred(const QString& errorMsg); // 错误信号 void errorOccurred(const QString& errorMsg); // 错误信号
public slots: public slots:
+11 -8
View File
@@ -3,29 +3,30 @@
// //
#include "AudioDataTransferObject.h" #include "AudioDataTransferObject.h"
#include <QJsonValue> #include <QJsonValue>
#include <utility>
// 构造函数实现(初始化列表) // 构造函数实现(初始化列表)
AudioDataTransferObject::AudioDataTransferObject(const QString& owner, AudioDataTransferObject::AudioDataTransferObject(QString owner,
bool isStream, bool isStream,
bool isStart, bool isStart,
bool isEnd, bool isEnd,
int sequence, int sequence,
const QByteArray& data, QByteArray data,
int sampleRate, int sampleRate,
int channelCount, int channelCount,
int bitDepth, int bitDepth,
double duration, double duration,
const QString& text) QString text)
: m_owner(owner) : m_owner(std::move(owner))
, m_isStream(isStream) , m_isStream(isStream)
, m_isStart(isStart) , m_isStart(isStart)
, m_isEnd(isEnd) , m_isEnd(isEnd)
, m_sequence(sequence) , m_sequence(sequence)
, m_data(data) , m_data(std::move(data))
, m_sampleRate(sampleRate) , m_sampleRate(sampleRate)
, m_channelCount(channelCount) , m_channelCount(channelCount)
, m_bitDepth(bitDepth) , m_bitDepth(bitDepth)
, m_duration(duration) , m_duration(duration)
, m_text(text) { , m_text(std::move(text)) {
} }
// 静态工厂方法:从 JSON 反序列化 // 静态工厂方法:从 JSON 反序列化
@@ -86,7 +87,7 @@ AudioDataTransferObject& AudioDataTransferObject::setData(const QString& key,
} else if (key == "sequence") { } else if (key == "sequence") {
m_sequence = value.toInt(); m_sequence = value.toInt();
} else if (key == "data") { } else if (key == "data") {
// 这里要求是 base64 字符串 // 这里要求传入的是 base64 字符串
m_data = QByteArray::fromBase64(value.toString().toUtf8()); m_data = QByteArray::fromBase64(value.toString().toUtf8());
} else if (key == "sampleRate") { } else if (key == "sampleRate") {
m_sampleRate = value.toInt(); m_sampleRate = value.toInt();
@@ -98,7 +99,9 @@ AudioDataTransferObject& AudioDataTransferObject::setData(const QString& key,
m_duration = value.toDouble(); m_duration = value.toDouble();
} else if (key == "text") { } else if (key == "text") {
m_text = value.toString(); m_text = value.toString();
} else {
qWarning() << "Unknown key or invalid value type:" << key << value;
} }
// 如果 key 不存在,默认忽略
return *this; // 返回自身引用,支持链式调用 return *this; // 返回自身引用,支持链式调用
} }
+72
View File
@@ -0,0 +1,72 @@
//
// Created by misaki on 2026/1/30.
//
#include "AutoAgentDataObject.h"
AutoAgentDataObject::AutoAgentDataObject(const QString& Action,
const int X1,
const int Y1,
const int X2,
const int Y2,
const QString& Key,
const QString& Content,
const QString& Direction)
: m_action(Action),
m_x1(X1),
m_y1(Y1),
m_x2(X2),
m_y2(Y2),
m_key(Key),
m_content(Content),
m_direction(Direction) {}
AutoAgentDataObject AutoAgentDataObject::fromJson(const QJsonObject& json) {
// 从JSON对象中提取数据,如果不存在则使用默认值
const QString action = json.value("Action").toString("");
const int x1 = json.value("x1").toInt(-1);
const int y1 = json.value("y1").toInt(-1);
const int x2 = json.value("x2").toInt(-1);
const int y2 = json.value("y2").toInt(-1);
const QString key = json.value("key").toString("");
const QString content = json.value("content").toString("");
const QString direction = json.value("direction").toString("");
return AutoAgentDataObject(action, x1, y1, x2, y2, key, content, direction);
}
QJsonObject AutoAgentDataObject::toJson() const {
QJsonObject json;
json["Action"] = m_action;
json["x1"] = m_x1;
json["y1"] = m_y1;
json["x2"] = m_x2;
json["y2"] = m_y2;
json["key"] = m_key;
json["content"] = m_content;
json["direction"] = m_direction;
return json;
}
AutoAgentDataObject& AutoAgentDataObject::setData(const QString& key, const QJsonValue& value) {
// 根据键名设置对应的成员变量
if (key == "Action" && value.isString()) {
m_action = value.toString();
} else if (key == "x1" && (value.isDouble() || value.isString())) {
m_x1 = value.toInt();
} else if (key == "y1" && (value.isDouble() || value.isString())) {
m_y1 = value.toInt();
} else if (key == "x2" && (value.isDouble() || value.isString())) {
m_x2 = value.toInt();
} else if (key == "y2" && (value.isDouble() || value.isString())) {
m_y2 = value.toInt();
} else if (key == "key" && value.isString()) {
m_key = value.toString();
} else if (key == "content" && value.isString()) {
m_content = value.toString();
} else if (key == "direction" && value.isString()) {
m_direction = value.toString();
} else {
qWarning() << "Unknown key or invalid value type:" << key << value;
}
return *this;
}
+2 -2
View File
@@ -69,8 +69,8 @@ void NetworkDO::onDataReceived(const QString& type, const QJsonObject& data)
if (type == "audio_data") { if (type == "audio_data") {
emit audioPacketReceived(AudioDataTransferObject::fromJson(data)); // 构造并发送音频对象 emit audioPacketReceived(AudioDataTransferObject::fromJson(data)); // 构造并发送音频对象
} }
else if (type == "control_data") { else if (type == "auto_agent") {
emit autoAgentPacketReceived(AutoAgentDataObject::fromJson(data));
} }
else { else {
qWarning() << "[NetworkDO] Received unknown type:" << type; qWarning() << "[NetworkDO] Received unknown type:" << type;
+121
View File
@@ -0,0 +1,121 @@
//
// Created by Administrator on 2025/1/17.
//
#pragma once
#include <QObject>
#include <QMediaPlayer> // 音频播放模块
#include <QAudioOutput> // QMediaPlayer 的音量控制组件
#include <QAudioSink> // 音频输出组件, 用于原始数据播放
#include <QThread>
#include <QMutex>
#include <QQueue>
#include <QUrl>
#include <QBuffer>
#include <QAudioFormat>
/**
* @brief 音频播放模块
* @author Misaki
* 单例类
* 本模块重新基于Qt6重构 2026.1.31第三次重构
* 实现的功能
* 1. 流式wav音频播放
* 2. 根据音频文件路径播放音频
*/
// Worker 类定义 (负责流式音频的底层处理) 注意:此类实例将完全运行在子线程中
class StreamAudioWorker : public QObject {
Q_OBJECT
public:
explicit StreamAudioWorker(QObject* parent = nullptr) : QObject(parent) {}
~StreamAudioWorker() override;
public slots:
// 初始化并启动音频设备
void start(int sampleRate, int channelCount, int bitDepth);
// 处理接收到的音频数据块
void processChunk(const QByteArray& chunk);
// 停止播放并清理资源
void stop();
signals:
void errorOccurred(const QString& msg);
void playbackFinished(); // 流播放结束(通常指队列空了)
private:
QScopedPointer<QAudioSink> m_sink;
QIODevice* m_ioDevice = nullptr; // 由 m_sink->start() 返回,不需要且不能手动 delete
bool m_firstChunk = true; // 标记是否是第一块数据(用于剥离WAV头)
};
// AudioOutput 主类 (单例,线程安全)
class AudioOutput : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AudioOutput)
private:
explicit AudioOutput(QObject *parent = nullptr);
static QScopedPointer<AudioOutput> m_instance;
static QMutex m_mutex;
public:
static AudioOutput *getInstance();
static void destroy(); // 显式销毁
~AudioOutput() override;
// 通用控制接口
// 停止所有播放 (文件和流)
void stopPlayback();
// 设置音量 (0-100)
void setVolume(int volume);
// 获取当前状态
bool isPlaying() const;
// 文件/URL 播放接口 (基于 QMediaPlayer)
void playUrl(const QUrl& url);
void playData(const QByteArray& data); // 播放完整的内存文件
// 流式播放接口 (基于 QAudioSink + Worker Thread)
/**
* @brief 开启流式播放会话
* @param sampleRate 采样率 (默认 32000)
* @param channelCount 通道数 (默认 1)
* @param bitDepth 位深 (默认 16)
*/
void startStream(int sampleRate = 32000, int channelCount = 1, int bitDepth = 16);
/**
* @brief 写入流数据
* @param chunk 音频数据块
*/
void pushStreamData(const QByteArray& chunk);
/**
* @brief 结束流 (停止接收新数据,播放完当前缓冲后停止)
*/
void stopStream();
signals:
// 内部转发给 Worker 的信号
void sigOperateStreamStart(int sampleRate, int channelCount, int bitDepth);
void sigOperateStreamChunk(const QByteArray& chunk);
void sigOperateStreamStop();
// 对外通知信号
void playbackFinished();
void errorOccurred(const QString& error);
private:
// 文件播放组件
QMediaPlayer* m_player = nullptr;
QAudioOutput* m_audioOutput = nullptr;
// 流式播放组件
QThread* m_workerThread = nullptr;
StreamAudioWorker* m_streamWorker = nullptr;
// 状态管理
bool m_isStreaming = false;
};
+227
View File
@@ -0,0 +1,227 @@
//
// Created by Administrator on 2025/1/17.
//
#include "AudioOutput.h"
#include <QMediaDevices>
#include <QDebug>
#include <QCoreApplication>
#include <QDataStream>
// Static Helpers (WAV Header Parser)
static bool hasWavHeader(const QByteArray& data) {
if (data.size() < 44) return false;
return data.startsWith("RIFF") && data.mid(8, 4) == "WAVE";
}
// StreamAudioWorker 实现
StreamAudioWorker::~StreamAudioWorker() {
// 确保析构时资源释放
stop();
}
void StreamAudioWorker::start(int sampleRate, int channelCount, int bitDepth) {
if (m_sink) {
m_sink->stop();
m_sink.reset();
}
m_firstChunk = true;
// 配置音频格式
QAudioFormat format;
format.setSampleRate(sampleRate);
format.setChannelCount(channelCount);
if (bitDepth == 8) format.setSampleFormat(QAudioFormat::UInt8);
else if (bitDepth == 16) format.setSampleFormat(QAudioFormat::Int16);
else if (bitDepth == 32) format.setSampleFormat(QAudioFormat::Float); // 32位通常为Float
else format.setSampleFormat(QAudioFormat::Int16); // 默认回退
// 检查设备是否支持
auto device = QMediaDevices::defaultAudioOutput();
if (!device.isFormatSupported(format)) {
qWarning() << "[Worker] Device does not support format, using preferred format.";
format = device.preferredFormat();
}
// 创建 Sink (必须在 Worker 线程中创建)
m_sink.reset(new QAudioSink(device, format));
// 监听状态
connect(m_sink.data(), &QAudioSink::stateChanged, this, [this](QAudio::State state){
if (state == QAudio::IdleState) {
emit playbackFinished();
}
else if (state == QAudio::StoppedState) {
if (m_sink->error() != QAudio::NoError) {
emit errorOccurred("Audio Sink Error: " + QString::number(m_sink->error()));
}
}
});
// 启动,获取 IO 设备
m_ioDevice = m_sink->start();
if (!m_ioDevice) {
emit errorOccurred("Failed to start audio device");
} else {
qDebug() << "[Worker] Stream started:" << sampleRate << "Hz";
}
}
void StreamAudioWorker::processChunk(const QByteArray& chunk) {
if (chunk.isEmpty() || !m_ioDevice || !m_sink) return;
QByteArray dataToWrite = chunk;
// 智能处理 WAV 头
if (m_firstChunk) {
if (hasWavHeader(chunk)) {
qDebug() << "[Worker] Detected WAV header, stripping 44 bytes.";
dataToWrite = chunk.mid(44);
}
m_firstChunk = false;
}
// 写入音频设备 (QAudioSink 内部有缓冲区,这里直接 write 即可)
// 如果数据量过大,write 可能会阻塞,但在独立线程中这是可以接受的
qint64 written = m_ioDevice->write(dataToWrite);
if (written != dataToWrite.size()) {
qWarning() << "[Worker] Incomplete write:" << written << "/" << dataToWrite.size();
}
}
void StreamAudioWorker::stop() {
if (m_sink) {
m_sink->stop();
m_sink.reset(); // 删除对象
}
m_ioDevice = nullptr;
qDebug() << "[Worker] Stream stopped";
}
// AudioOutput 主类实现
QScopedPointer<AudioOutput> AudioOutput::m_instance;
QMutex AudioOutput::m_mutex;
AudioOutput* AudioOutput::getInstance() {
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
m_instance.reset(new AudioOutput());
}
}
return m_instance.data();
}
void AudioOutput::destroy() {
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset();
}
}
AudioOutput::AudioOutput(QObject *parent) : QObject(parent) {
// 初始化文件播放器
m_player = new QMediaPlayer(this);
m_audioOutput = new QAudioOutput(this);
m_player->setAudioOutput(m_audioOutput);
connect(m_player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status){
if (status == QMediaPlayer::EndOfMedia) emit playbackFinished();
});
// 预先初始化流式 Worker 线程
// 保持一个常驻线程Worker,通过信号控制
m_streamWorker = new StreamAudioWorker(); // 不能指定 parent,因为要 moveToThread
m_workerThread = new QThread(this);
m_streamWorker->moveToThread(m_workerThread);
// 连接信号槽
// 主线程 -> Worker
connect(this, &AudioOutput::sigOperateStreamStart, m_streamWorker, &StreamAudioWorker::start);
connect(this, &AudioOutput::sigOperateStreamChunk, m_streamWorker, &StreamAudioWorker::processChunk);
connect(this, &AudioOutput::sigOperateStreamStop, m_streamWorker, &StreamAudioWorker::stop);
// Worker -> 主线程
connect(m_streamWorker, &StreamAudioWorker::errorOccurred, this, &AudioOutput::errorOccurred);
// 线程启动
m_workerThread->start();
}
AudioOutput::~AudioOutput() {
stopPlayback();
// 清理线程
if (m_workerThread) {
m_workerThread->quit();
m_workerThread->wait(3000); // 等待退出
delete m_streamWorker;
}
}
// 对外接口
void AudioOutput::stopPlayback() {
// 停止文件播放
if (m_player->playbackState() != QMediaPlayer::StoppedState) {
m_player->stop();
}
// 停止流播放
if (m_isStreaming) {
stopStream();
}
}
void AudioOutput::setVolume(int volume) {
if (m_audioOutput) m_audioOutput->setVolume(volume / 100.0);
// 注意:流式播放的音量控制需要在 Worker 内单独实现
}
bool AudioOutput::isPlaying() const {
return (m_player->playbackState() == QMediaPlayer::PlayingState) || m_isStreaming;
}
// 文件播放
void AudioOutput::playUrl(const QUrl& url) {
stopPlayback(); // 互斥,播放新文件前停止旧的
m_player->setSource(url);
m_player->play();
}
void AudioOutput::playData(const QByteArray& data) {
// 这个方法对于 QMediaPlayer 比较麻烦,需要自定义 QIODevice
// 建议直接走 stream 接口,或者使用 QBuffer + StreamWorker 的一次性模式
// 为了简单,这里将 buffer 视为 stream 播放
stopPlayback();
startStream(44100, 2, 16); // 假设默认 wav 格式,Worker 会自动解析头
pushStreamData(data);
// 不需要显式 stopStream,让它播完
}
// 流式播放
void AudioOutput::startStream(int sampleRate, int channelCount, int bitDepth) {
stopPlayback(); // 确保干净的状态
m_isStreaming = true;
// 通过信号跨线程调用 Worker 的 start
emit sigOperateStreamStart(sampleRate, channelCount, bitDepth);
}
void AudioOutput::pushStreamData(const QByteArray& chunk) {
if (!m_isStreaming) return;
// 直接发射信号,Qt 会把 chunk copy 到子线程事件队列
emit sigOperateStreamChunk(chunk);
}
void AudioOutput::stopStream() {
if (!m_isStreaming) return;
m_isStreaming = false;
emit sigOperateStreamStop();
}
@@ -0,0 +1,35 @@
//
// Created by misaki on 2026/1/30.
//
#pragma once
#include <QObject>
#include <QMutex>
#include "AudioDataTransferObject.h"
class AudioDataHandle final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AudioDataHandle) // 禁用拷贝
private:
/**
* 构造函数私有化
* @param parent
*/
explicit AudioDataHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static QScopedPointer<AudioDataHandle> m_instance; // 单例类
static QMutex m_mutex;
private slots:
// 业务接收槽函数,当获取到音频数据包时,进行解析并播放
void onAudioPacketReceived(const AudioDataTransferObject& packet);
public:
// 单例访问点
static AudioDataHandle *getInstance();
// 显式销毁
static void destroy();
~AudioDataHandle() override;
};
@@ -0,0 +1,40 @@
//
// Created by misaki on 2026/1/30.
//
/**
* 本模块通过解析AutoAgentDataObject的内容并调用 AutoGUI 模块
* 来完成自动化GUI操作
* 对于GUI自动化执行器而言,运行时只需要有一个实例即可,因此采用单例模式,并在AppCore当中进行创建
*/
#pragma once
#include <QObject>
#include <QMutex>
#include "AutoAgentDataObject.h"
class AutoAgentHandle final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AutoAgentHandle) // 禁用拷贝
private:
/**
* 构造函数私有化
* @param parent
*/
explicit AutoAgentHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static QScopedPointer<AutoAgentHandle> m_instance; // 单例类
static QMutex m_mutex;
private slots:
// 业务接收槽函数,当获取到自动化agent数据包时,进行解析并调用 AutoGUI 模块
void onAutoAgentPacketReceived(const AutoAgentDataObject& packet);
public:
// 单例访问点
static AutoAgentHandle *getInstance();
// 显式销毁
static void destroy();
~AutoAgentHandle() override;
};
@@ -0,0 +1,56 @@
//
// Created by misaki on 2026/1/30.
//
#include "AudioDataHandle.h"
#include "NetWorkDO.h"
#include "AudioOutput.h"
// 初始化静态成员
QScopedPointer<AudioDataHandle> AudioDataHandle::m_instance;
QMutex AudioDataHandle::m_mutex;
// 单例实现 (QScopedPointer + Mutex)
AudioDataHandle* AudioDataHandle::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new AudioDataHandle());
}
}
return m_instance.data();
}
void AudioDataHandle::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
AudioDataHandle::AudioDataHandle(QObject *parent) : QObject(parent)
{
connect(NetworkDO::getInstance(), &NetworkDO::audioPacketReceived, this, &AudioDataHandle::onAudioPacketReceived);
}
AudioDataHandle::~AudioDataHandle()
{
qDebug() << "AutoAgentHandle destroyed";
}
void AudioDataHandle::onAudioPacketReceived(const AudioDataTransferObject &packet) {
// 管理并调用AudioOutput播放流式wav音频
if (packet.isEnd()) { // 如果是结束包
AudioOutput::getInstance()->stopStream(); // 停止播放
return;
}
if (packet.isStart()) { // 如果是开始包
AudioOutput::getInstance()->startStream(packet.sampleRate(), packet.channelCount(), packet.bitDepth());; // 播放开始
return;
}
// 否则播放即可
AudioOutput::getInstance()->pushStreamData(packet.audioData());
}
@@ -0,0 +1,65 @@
//
// Created by misaki on 2026/1/30.
//
#include "AutoAgentHandle.h"
#include "NetWorkDO.h"
#include <SimpleAutoGUI.h> // 引入 AutoGUI 头文件
// 初始化静态成员
QScopedPointer<AutoAgentHandle> AutoAgentHandle::m_instance;
QMutex AutoAgentHandle::m_mutex;
// 单例实现 (QScopedPointer + Mutex)
AutoAgentHandle* AutoAgentHandle::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new AutoAgentHandle());
}
}
return m_instance.data();
}
void AutoAgentHandle::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
AutoAgentHandle::AutoAgentHandle(QObject *parent) : QObject(parent)
{
connect(NetworkDO::getInstance(), &NetworkDO::autoAgentPacketReceived, this, &AutoAgentHandle::onAutoAgentPacketReceived);
}
AutoAgentHandle::~AutoAgentHandle()
{
qDebug() << "AutoAgentHandle destroyed";
}
void AutoAgentHandle::onAutoAgentPacketReceived(const AutoAgentDataObject &packet) {
if (packet.getAction() == "click") { // 单击
qDebug() << "Click: " << packet.getX1() << ", " << packet.getY1();
AutoGUI::moveTo(packet.getX1(), packet.getY1(), 0.6);
AutoGUI::click(packet.getX1(), packet.getY1());
}
if (packet.getAction() == "left_double") { // 双击
qDebug() << "Double click: " << packet.getX1() << ", " << packet.getY1();
AutoGUI::moveTo(packet.getX1(), packet.getY1(), 0.6);
AutoGUI::leftDouble(packet.getX1(), packet.getY1());
}
if (packet.getAction() == "right_single") { // 右键单击
qDebug() << "Right click: " << packet.getX1() << ", " << packet.getY1();
AutoGUI::moveTo(packet.getX1(), packet.getY1(), 0.6);
AutoGUI::rightSingle(packet.getX1(), packet.getY1());
}
if (packet.getAction() == "drag") { // 拖拽
qDebug() << "Drag: " << packet.getX1() << ", " << packet.getY1() << " to " << packet.getX2() << ", " << packet.getY2();
AutoGUI::drag(packet.getX1(), packet.getY1(), packet.getX2(), packet.getY2(), 0.8);
}
// TODO: 快捷键,输入文本,滚动待实现
}
+1
View File
@@ -0,0 +1 @@
### 本模块负责将服务端返回的数据对象做解析并执行相应的动作
@@ -20,6 +20,8 @@ WebSocketManager::WebSocketManager(QObject *parent)
{ {
// 配置 WebSocket // 配置 WebSocket
m_socket->setParent(this); // 确保 socket 也在工作线程 m_socket->setParent(this); // 确保 socket 也在工作线程
m_socket->setMaxAllowedIncomingFrameSize(50 * 1024 * 1024); // 单帧最大 50MB // 防止发送大数据包导致websocket断开
m_socket->setMaxAllowedIncomingMessageSize(50 * 1024 * 1024); // 完整消息最大 50MB
// 连接信号 // 连接信号
connect(m_socket, &QWebSocket::connected, connect(m_socket, &QWebSocket::connected,
this, &WebSocketManager::onConnected); this, &WebSocketManager::onConnected);
@@ -110,7 +110,7 @@ AudioPage::AudioPage(QWidget* parent)
constexpr float duration = 6.0f; // 音频时长 constexpr float duration = 6.0f; // 音频时长
TextRenderer::getInstance()->addText(text, 40.0f, QColor("#FF69B4"), duration); TextRenderer::getInstance()->addText(text, 40.0f, QColor("#FF69B4"), duration);
LAppLive2DManager::GetInstance()->StartLipSync("Resources/TestFiles/test.wav"); LAppLive2DManager::GetInstance()->StartLipSync("Resources/TestFiles/test.wav");
AudioOutput::getInstance()->playAudio(QUrl("Resources/TestFiles/test.wav")); AudioOutput::getInstance()->playUrl(QUrl("Resources/TestFiles/test.wav"));
}); });
@@ -9,6 +9,8 @@
#include "ElaMessageBar.h" #include "ElaMessageBar.h"
#include "websocketmanager.h" #include "websocketmanager.h"
#include "NetWorkDO.h" #include "NetWorkDO.h"
#include <QFile>
#include "AudioDataHandle.h"
NetWorkPage::NetWorkPage(QWidget* parent) NetWorkPage::NetWorkPage(QWidget* parent)
: BasePage(parent) : BasePage(parent)
@@ -71,6 +73,7 @@ void NetWorkPage::initUI() {
void NetWorkPage::initWebSocketClient() { void NetWorkPage::initWebSocketClient() {
auto* client = WebSocketClient::getInstance(); // 获取单例实例(设置一个默认地址) auto* client = WebSocketClient::getInstance(); // 获取单例实例(设置一个默认地址)
auto* netDO = NetworkDO::getInstance(); auto* netDO = NetworkDO::getInstance();
AudioDataHandle::getInstance(); // 初始化音频处理模块
// 注入:将底层发送能力赋予 NetworkDO // 注入:将底层发送能力赋予 NetworkDO
netDO->registerSender([client](const QString& type, const QJsonObject& data){ netDO->registerSender([client](const QString& type, const QJsonObject& data){
client->sendJson(type, data); client->sendJson(type, data);
@@ -78,7 +81,6 @@ void NetWorkPage::initWebSocketClient() {
// 监听:底层收到数据 -> NetworkDO 解析 // 监听:底层收到数据 -> NetworkDO 解析
connect(client, &WebSocketClient::jsonReceived, connect(client, &WebSocketClient::jsonReceived,
netDO, &NetworkDO::onDataReceived); netDO, &NetworkDO::onDataReceived);
// 连接成功的处理 // 连接成功的处理
connect(client, &WebSocketClient::connected, this, [this]() { connect(client, &WebSocketClient::connected, this, [this]() {
ElaMessageBar::success(ElaMessageBarType::TopRight, "WebSocket", "连接成功", 800.0, this); ElaMessageBar::success(ElaMessageBarType::TopRight, "WebSocket", "连接成功", 800.0, this);
@@ -162,12 +164,21 @@ void NetWorkPage::initWebSocketClient() {
return; return;
} }
// 创建数据包 // 创建数据包
QFile test_wav_file("Resources/TestFiles/test.wav");
if (!test_wav_file.open(QIODevice::ReadOnly)) {
qDebug() << "Failed to open test.wav";
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "发送测试",
"无法打开测试音频文件", 1500.0, this);
return;
}
QByteArray wavData = test_wav_file.readAll();
QString base64Str = QString::fromLatin1(wavData.toBase64());
AudioDataTransferObject packet; AudioDataTransferObject packet;
packet.setData("Owner", "client") packet.setData("Owner", "client")
.setData("isStream", true) .setData("isStream", true)
.setData("sequence", 42) .setData("sequence", 42)
.setData("text", "Hello World") .setData("text", "Hello World")
.setData("data", "SGVsbG8gV29ybGQ="); // 填入音频数据 (Hello World的base64) .setData("data", base64Str); // 填入测试音频数据
netDO->sendPacket(packet); netDO->sendPacket(packet);
ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试", ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试",
"已成功发送数据包", 1000.0, this); "已成功发送数据包", 1000.0, this);
+2
View File
@@ -14,6 +14,8 @@
* 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。 * 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。
* *
* 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。 * 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。
*
* C++20
*/ */
#pragma once #pragma once
#include <cstdint> #include <cstdint>