1. 稍微重构了一下项目代码结构,使其更加合理
2. 重构了音频播放类,简化其接口,并支持流式wav音频播放 3. 增加了对流式音频数据的处理类 4. 增加了对GUI自动化操作的处理类
This commit is contained in:
+19
-15
@@ -23,18 +23,20 @@ file(GLOB_RECURSE LAppLive2D
|
||||
)
|
||||
file(GLOB_RECURSE YosugaSrc
|
||||
CONFIGURE_DEPENDS
|
||||
"src/AudioHandle/Src/*.cpp"
|
||||
"src/AudioHandle/Inc/*.h"
|
||||
"src/Menu/Src/*.cpp"
|
||||
"src/Menu/Inc/*.h"
|
||||
"src/Handle/AudioHandle/Src/*.cpp"
|
||||
"src/Handle/AudioHandle/Inc/*.h"
|
||||
"src/Handle/NetWorkHandle/Src/*.cpp"
|
||||
"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/Src/*.cpp"
|
||||
"src/NetWorkHandle/Src/*.cpp"
|
||||
"src/NetWorkHandle/Inc/*.h"
|
||||
"src/Setting/Src/*.cpp"
|
||||
"src/Setting/Inc/*.h"
|
||||
"src/Render/TextRender/Src/*.cpp"
|
||||
"src/Render/TextRender/Inc/*.h"
|
||||
"src/UI/Setting/Src/*.cpp"
|
||||
"src/UI/Setting/Inc/*.h"
|
||||
"src/UI/Render/TextRender/Src/*.cpp"
|
||||
"src/UI/Render/TextRender/Inc/*.h"
|
||||
"src/Core/Src/*.cpp"
|
||||
"src/Core/Inc/*.h"
|
||||
"src/Utils/Inc/*.hpp"
|
||||
@@ -192,11 +194,13 @@ target_include_directories(${PROJECT_NAME}
|
||||
3rdparty/Live2D/Src/Core/include
|
||||
3rdparty/Live2D/Src/stb
|
||||
3rdparty/Live2D/Src/LAppLive2D/Inc
|
||||
src/AudioHandle/Inc
|
||||
src/Menu/Inc
|
||||
src/NetWorkHandle/Inc
|
||||
src/Setting/Inc
|
||||
src/Render/TextRender/Inc
|
||||
3rdparty/autogui-cpp/src
|
||||
src/Handle/AudioHandle/Inc
|
||||
src/Handle/NetWorkHandle/Inc
|
||||
src/Handle/DataObjectHandle/Inc
|
||||
src/UI/Menu/Inc
|
||||
src/UI/Setting/Inc
|
||||
src/UI/Render/TextRender/Inc
|
||||
src/Core/Inc
|
||||
src/DAO/Inc
|
||||
src/Utils/Inc
|
||||
|
||||
@@ -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; /// <!存储内存数据
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -19,17 +19,17 @@ class QJsonObject;
|
||||
class AudioDataTransferObject final : public DataTransferObjectBase{
|
||||
public:
|
||||
// 构造函数(带默认值)
|
||||
explicit AudioDataTransferObject(const QString& owner = "client",
|
||||
explicit AudioDataTransferObject(QString owner = "client",
|
||||
bool isStream = false,
|
||||
bool isStart = false,
|
||||
bool isEnd = false,
|
||||
int sequence = 0,
|
||||
const QByteArray& data = {},
|
||||
QByteArray data = {},
|
||||
int sampleRate = 16000,
|
||||
int channelCount = 1,
|
||||
int bitDepth = 16,
|
||||
double duration = 0.0,
|
||||
const QString& text = "");
|
||||
QString text = "");
|
||||
// 静态工厂方法
|
||||
static AudioDataTransferObject fromJson(const QJsonObject& json);
|
||||
|
||||
|
||||
@@ -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; /// 滚动方向
|
||||
};
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include "DataTransferObjectBase.h"
|
||||
#include "AudioDataTransferObject.h"
|
||||
#include "AutoAgentDataObject.h"
|
||||
/**
|
||||
* NetworkDO
|
||||
*/
|
||||
@@ -54,6 +55,8 @@ public:
|
||||
signals:
|
||||
// 业务接收信号
|
||||
void audioPacketReceived(const AudioDataTransferObject& packet); // 音频数据准备完成信号
|
||||
void autoAgentPacketReceived(const AutoAgentDataObject& packet); // 自动代理数据包接收信号
|
||||
|
||||
void errorOccurred(const QString& errorMsg); // 错误信号
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -3,29 +3,30 @@
|
||||
//
|
||||
#include "AudioDataTransferObject.h"
|
||||
#include <QJsonValue>
|
||||
#include <utility>
|
||||
// 构造函数实现(初始化列表)
|
||||
AudioDataTransferObject::AudioDataTransferObject(const QString& owner,
|
||||
AudioDataTransferObject::AudioDataTransferObject(QString owner,
|
||||
bool isStream,
|
||||
bool isStart,
|
||||
bool isEnd,
|
||||
int sequence,
|
||||
const QByteArray& data,
|
||||
QByteArray data,
|
||||
int sampleRate,
|
||||
int channelCount,
|
||||
int bitDepth,
|
||||
double duration,
|
||||
const QString& text)
|
||||
: m_owner(owner)
|
||||
QString text)
|
||||
: m_owner(std::move(owner))
|
||||
, m_isStream(isStream)
|
||||
, m_isStart(isStart)
|
||||
, m_isEnd(isEnd)
|
||||
, m_sequence(sequence)
|
||||
, m_data(data)
|
||||
, m_data(std::move(data))
|
||||
, m_sampleRate(sampleRate)
|
||||
, m_channelCount(channelCount)
|
||||
, m_bitDepth(bitDepth)
|
||||
, m_duration(duration)
|
||||
, m_text(text) {
|
||||
, m_text(std::move(text)) {
|
||||
}
|
||||
|
||||
// 静态工厂方法:从 JSON 反序列化
|
||||
@@ -86,7 +87,7 @@ AudioDataTransferObject& AudioDataTransferObject::setData(const QString& key,
|
||||
} else if (key == "sequence") {
|
||||
m_sequence = value.toInt();
|
||||
} else if (key == "data") {
|
||||
// 这里要求已是 base64 字符串
|
||||
// 这里要求传入的是 base64 字符串
|
||||
m_data = QByteArray::fromBase64(value.toString().toUtf8());
|
||||
} else if (key == "sampleRate") {
|
||||
m_sampleRate = value.toInt();
|
||||
@@ -98,7 +99,9 @@ AudioDataTransferObject& AudioDataTransferObject::setData(const QString& key,
|
||||
m_duration = value.toDouble();
|
||||
} else if (key == "text") {
|
||||
m_text = value.toString();
|
||||
} else {
|
||||
qWarning() << "Unknown key or invalid value type:" << key << value;
|
||||
}
|
||||
// 如果 key 不存在,默认忽略
|
||||
|
||||
return *this; // 返回自身引用,支持链式调用
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -69,8 +69,8 @@ void NetworkDO::onDataReceived(const QString& type, const QJsonObject& data)
|
||||
if (type == "audio_data") {
|
||||
emit audioPacketReceived(AudioDataTransferObject::fromJson(data)); // 构造并发送音频对象
|
||||
}
|
||||
else if (type == "control_data") {
|
||||
|
||||
else if (type == "auto_agent") {
|
||||
emit autoAgentPacketReceived(AutoAgentDataObject::fromJson(data));
|
||||
}
|
||||
else {
|
||||
qWarning() << "[NetworkDO] Received unknown type:" << type;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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: 快捷键,输入文本,滚动待实现
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
### 本模块负责将服务端返回的数据对象做解析并执行相应的动作
|
||||
+2
@@ -20,6 +20,8 @@ WebSocketManager::WebSocketManager(QObject *parent)
|
||||
{
|
||||
// 配置 WebSocket
|
||||
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,
|
||||
this, &WebSocketManager::onConnected);
|
||||
@@ -110,7 +110,7 @@ AudioPage::AudioPage(QWidget* parent)
|
||||
constexpr float duration = 6.0f; // 音频时长
|
||||
TextRenderer::getInstance()->addText(text, 40.0f, QColor("#FF69B4"), duration);
|
||||
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 "websocketmanager.h"
|
||||
#include "NetWorkDO.h"
|
||||
#include <QFile>
|
||||
#include "AudioDataHandle.h"
|
||||
|
||||
NetWorkPage::NetWorkPage(QWidget* parent)
|
||||
: BasePage(parent)
|
||||
@@ -71,6 +73,7 @@ void NetWorkPage::initUI() {
|
||||
void NetWorkPage::initWebSocketClient() {
|
||||
auto* client = WebSocketClient::getInstance(); // 获取单例实例(设置一个默认地址)
|
||||
auto* netDO = NetworkDO::getInstance();
|
||||
AudioDataHandle::getInstance(); // 初始化音频处理模块
|
||||
// 注入:将底层发送能力赋予 NetworkDO
|
||||
netDO->registerSender([client](const QString& type, const QJsonObject& data){
|
||||
client->sendJson(type, data);
|
||||
@@ -78,7 +81,6 @@ void NetWorkPage::initWebSocketClient() {
|
||||
// 监听:底层收到数据 -> NetworkDO 解析
|
||||
connect(client, &WebSocketClient::jsonReceived,
|
||||
netDO, &NetworkDO::onDataReceived);
|
||||
|
||||
// 连接成功的处理
|
||||
connect(client, &WebSocketClient::connected, this, [this]() {
|
||||
ElaMessageBar::success(ElaMessageBarType::TopRight, "WebSocket", "连接成功", 800.0, this);
|
||||
@@ -162,12 +164,21 @@ void NetWorkPage::initWebSocketClient() {
|
||||
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;
|
||||
packet.setData("Owner", "client")
|
||||
.setData("isStream", true)
|
||||
.setData("sequence", 42)
|
||||
.setData("text", "Hello World")
|
||||
.setData("data", "SGVsbG8gV29ybGQ="); // 填入音频数据 (Hello World的base64)
|
||||
.setData("data", base64Str); // 填入测试音频数据
|
||||
netDO->sendPacket(packet);
|
||||
ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试",
|
||||
"已成功发送数据包", 1000.0, this);
|
||||
@@ -14,6 +14,8 @@
|
||||
* 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。
|
||||
*
|
||||
* 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。
|
||||
*
|
||||
* C++20
|
||||
*/
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
Reference in New Issue
Block a user