From c32f085732abf014e575e98b21b470281b7c4822 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 31 Jan 2026 23:03:22 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E7=A8=8D=E5=BE=AE=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BA=86=E4=B8=80=E4=B8=8B=E9=A1=B9=E7=9B=AE=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E4=BD=BF=E5=85=B6=E6=9B=B4=E5=8A=A0?= =?UTF-8?q?=E5=90=88=E7=90=86=202.=20=E9=87=8D=E6=9E=84=E4=BA=86=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E6=92=AD=E6=94=BE=E7=B1=BB=EF=BC=8C=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E5=85=B6=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=B5=81=E5=BC=8Fwav=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE=203.?= =?UTF-8?q?=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=AF=B9=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E6=95=B0=E6=8D=AE=E7=9A=84=E5=A4=84=E7=90=86?= =?UTF-8?q?=E7=B1=BB=204.=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=AF=B9GUI?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E6=93=8D=E4=BD=9C=E7=9A=84=E5=A4=84?= =?UTF-8?q?=E7=90=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 34 +- src/AudioHandle/Inc/AudioOutput.h | 156 --------- src/AudioHandle/Src/AudioOutput.cpp | 300 ------------------ src/DAO/Inc/AudioDataTransferObject.h | 6 +- src/DAO/Inc/AutoAgentDataObject.h | 56 ++++ src/DAO/Inc/NetWorkDO.h | 3 + src/DAO/Src/AudioDataTransferObject.cpp | 19 +- src/DAO/Src/AutoAgentDataObject.cpp | 72 +++++ src/DAO/Src/NetWorkDO.cpp | 4 +- src/{ => Handle}/AudioHandle/Inc/AudioInput.h | 0 src/Handle/AudioHandle/Inc/AudioOutput.h | 121 +++++++ .../AudioHandle/Src/AudioInput.cpp | 0 src/Handle/AudioHandle/Src/AudioOutput.cpp | 227 +++++++++++++ src/{ => Handle}/AudioHandle/readme.md | 0 .../DataObjectHandle/Inc/AudioDataHandle.h | 35 ++ .../DataObjectHandle/Inc/AutoAgentHandle.h | 40 +++ .../DataObjectHandle/Src/AudioDataHandle.cpp | 56 ++++ .../DataObjectHandle/Src/AutoAgentHandle.cpp | 65 ++++ src/Handle/DataObjectHandle/readme.md | 1 + .../NetWorkHandle/Inc/networkmanager.h | 0 .../NetWorkHandle/Inc/serialportmanager.h | 0 .../NetWorkHandle/Inc/socketmanager.h | 0 .../NetWorkHandle/Inc/websocketmanager.h | 0 .../NetWorkHandle/Src/networkmanager.cpp | 0 .../NetWorkHandle/Src/serialportmanager.cpp | 0 .../NetWorkHandle/Src/socketmanager.cpp | 0 .../NetWorkHandle/Src/websocketmanager.cpp | 2 + src/{ => Handle}/NetWorkHandle/readme.md | 0 src/{ => UI}/Menu/Inc/menu.h | 0 src/{ => UI}/Menu/Src/menu.cpp | 0 src/{ => UI}/Menu/readme.md | 0 .../Render/TextRender/Inc/TextRenderer.h | 0 .../Render/TextRender/Src/TextRenderer.cpp | 0 src/{ => UI}/Setting/Inc/AudioPage.h | 0 src/{ => UI}/Setting/Inc/BasePage.h | 0 src/{ => UI}/Setting/Inc/HomePage.h | 0 src/{ => UI}/Setting/Inc/ModelPage.h | 0 src/{ => UI}/Setting/Inc/NetworkPage.h | 0 src/{ => UI}/Setting/Inc/RenderPage.h | 0 src/{ => UI}/Setting/Inc/Setting.h | 0 src/{ => UI}/Setting/Inc/UISetting.h | 0 src/{ => UI}/Setting/Src/AudioPage.cpp | 2 +- src/{ => UI}/Setting/Src/BasePage.cpp | 0 src/{ => UI}/Setting/Src/HomePage.cpp | 0 src/{ => UI}/Setting/Src/ModelPage.cpp | 0 src/{ => UI}/Setting/Src/NetworkPage.cpp | 15 +- src/{ => UI}/Setting/Src/RenderPage.cpp | 0 src/{ => UI}/Setting/Src/Setting.cpp | 0 src/{ => UI}/Setting/Src/UISetting.cpp | 0 src/{ => UI}/Setting/readme.md | 0 src/Utils/Inc/cobs.hpp | 2 + 51 files changed, 729 insertions(+), 487 deletions(-) delete mode 100644 src/AudioHandle/Inc/AudioOutput.h delete mode 100644 src/AudioHandle/Src/AudioOutput.cpp create mode 100644 src/DAO/Inc/AutoAgentDataObject.h create mode 100644 src/DAO/Src/AutoAgentDataObject.cpp rename src/{ => Handle}/AudioHandle/Inc/AudioInput.h (100%) create mode 100644 src/Handle/AudioHandle/Inc/AudioOutput.h rename src/{ => Handle}/AudioHandle/Src/AudioInput.cpp (100%) create mode 100644 src/Handle/AudioHandle/Src/AudioOutput.cpp rename src/{ => Handle}/AudioHandle/readme.md (100%) create mode 100644 src/Handle/DataObjectHandle/Inc/AudioDataHandle.h create mode 100644 src/Handle/DataObjectHandle/Inc/AutoAgentHandle.h create mode 100644 src/Handle/DataObjectHandle/Src/AudioDataHandle.cpp create mode 100644 src/Handle/DataObjectHandle/Src/AutoAgentHandle.cpp create mode 100644 src/Handle/DataObjectHandle/readme.md rename src/{ => Handle}/NetWorkHandle/Inc/networkmanager.h (100%) rename src/{ => Handle}/NetWorkHandle/Inc/serialportmanager.h (100%) rename src/{ => Handle}/NetWorkHandle/Inc/socketmanager.h (100%) rename src/{ => Handle}/NetWorkHandle/Inc/websocketmanager.h (100%) rename src/{ => Handle}/NetWorkHandle/Src/networkmanager.cpp (100%) rename src/{ => Handle}/NetWorkHandle/Src/serialportmanager.cpp (100%) rename src/{ => Handle}/NetWorkHandle/Src/socketmanager.cpp (100%) rename src/{ => Handle}/NetWorkHandle/Src/websocketmanager.cpp (98%) rename src/{ => Handle}/NetWorkHandle/readme.md (100%) rename src/{ => UI}/Menu/Inc/menu.h (100%) rename src/{ => UI}/Menu/Src/menu.cpp (100%) rename src/{ => UI}/Menu/readme.md (100%) rename src/{ => UI}/Render/TextRender/Inc/TextRenderer.h (100%) rename src/{ => UI}/Render/TextRender/Src/TextRenderer.cpp (100%) rename src/{ => UI}/Setting/Inc/AudioPage.h (100%) rename src/{ => UI}/Setting/Inc/BasePage.h (100%) rename src/{ => UI}/Setting/Inc/HomePage.h (100%) rename src/{ => UI}/Setting/Inc/ModelPage.h (100%) rename src/{ => UI}/Setting/Inc/NetworkPage.h (100%) rename src/{ => UI}/Setting/Inc/RenderPage.h (100%) rename src/{ => UI}/Setting/Inc/Setting.h (100%) rename src/{ => UI}/Setting/Inc/UISetting.h (100%) rename src/{ => UI}/Setting/Src/AudioPage.cpp (98%) rename src/{ => UI}/Setting/Src/BasePage.cpp (100%) rename src/{ => UI}/Setting/Src/HomePage.cpp (100%) rename src/{ => UI}/Setting/Src/ModelPage.cpp (100%) rename src/{ => UI}/Setting/Src/NetworkPage.cpp (92%) rename src/{ => UI}/Setting/Src/RenderPage.cpp (100%) rename src/{ => UI}/Setting/Src/Setting.cpp (100%) rename src/{ => UI}/Setting/Src/UISetting.cpp (100%) rename src/{ => UI}/Setting/readme.md (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index ec6098c..0dd85ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/AudioHandle/Inc/AudioOutput.h b/src/AudioHandle/Inc/AudioOutput.h deleted file mode 100644 index 1997bd3..0000000 --- a/src/AudioHandle/Inc/AudioOutput.h +++ /dev/null @@ -1,156 +0,0 @@ -// -// Created by Administrator on 2025/1/17. -// -#pragma once - -#include -#include // 音频播放模块 -#include // QMediaPlayer 的音量控制组件 -#include // 音频输出组件, 用于原始数据播放 -#include -#include -#include -#include - -/** - * @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 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; /// -#include - -QScopedPointer 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(volume) / 100.0; - audioOutput->setVolume(static_cast(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(this->getPlayPosition()) / static_cast(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(); -} \ No newline at end of file diff --git a/src/DAO/Inc/AudioDataTransferObject.h b/src/DAO/Inc/AudioDataTransferObject.h index 78de932..d49ba68 100644 --- a/src/DAO/Inc/AudioDataTransferObject.h +++ b/src/DAO/Inc/AudioDataTransferObject.h @@ -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); diff --git a/src/DAO/Inc/AutoAgentDataObject.h b/src/DAO/Inc/AutoAgentDataObject.h new file mode 100644 index 0000000..8cb7967 --- /dev/null +++ b/src/DAO/Inc/AutoAgentDataObject.h @@ -0,0 +1,56 @@ +// +// Created by misaki on 2026/1/30. +// + +/** + * 自动代理数据对象 + * 非对等传输对象,只被用于将服务端返回的auto_agent json转换为对象 + */ + +#pragma once +#include +#include +#include +#include +#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; /// 滚动方向 +}; \ No newline at end of file diff --git a/src/DAO/Inc/NetWorkDO.h b/src/DAO/Inc/NetWorkDO.h index 2356127..56fd64c 100644 --- a/src/DAO/Inc/NetWorkDO.h +++ b/src/DAO/Inc/NetWorkDO.h @@ -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: diff --git a/src/DAO/Src/AudioDataTransferObject.cpp b/src/DAO/Src/AudioDataTransferObject.cpp index b0b3b15..b8f56f4 100644 --- a/src/DAO/Src/AudioDataTransferObject.cpp +++ b/src/DAO/Src/AudioDataTransferObject.cpp @@ -3,29 +3,30 @@ // #include "AudioDataTransferObject.h" #include +#include // 构造函数实现(初始化列表) -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; // 返回自身引用,支持链式调用 } \ No newline at end of file diff --git a/src/DAO/Src/AutoAgentDataObject.cpp b/src/DAO/Src/AutoAgentDataObject.cpp new file mode 100644 index 0000000..1d25e8a --- /dev/null +++ b/src/DAO/Src/AutoAgentDataObject.cpp @@ -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; +} \ No newline at end of file diff --git a/src/DAO/Src/NetWorkDO.cpp b/src/DAO/Src/NetWorkDO.cpp index 1df373a..9846a0c 100644 --- a/src/DAO/Src/NetWorkDO.cpp +++ b/src/DAO/Src/NetWorkDO.cpp @@ -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; diff --git a/src/AudioHandle/Inc/AudioInput.h b/src/Handle/AudioHandle/Inc/AudioInput.h similarity index 100% rename from src/AudioHandle/Inc/AudioInput.h rename to src/Handle/AudioHandle/Inc/AudioInput.h diff --git a/src/Handle/AudioHandle/Inc/AudioOutput.h b/src/Handle/AudioHandle/Inc/AudioOutput.h new file mode 100644 index 0000000..63908fa --- /dev/null +++ b/src/Handle/AudioHandle/Inc/AudioOutput.h @@ -0,0 +1,121 @@ +// +// Created by Administrator on 2025/1/17. +// +#pragma once + +#include +#include // 音频播放模块 +#include // QMediaPlayer 的音量控制组件 +#include // 音频输出组件, 用于原始数据播放 +#include +#include +#include +#include +#include +#include + +/** + * @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 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 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; +}; \ No newline at end of file diff --git a/src/AudioHandle/Src/AudioInput.cpp b/src/Handle/AudioHandle/Src/AudioInput.cpp similarity index 100% rename from src/AudioHandle/Src/AudioInput.cpp rename to src/Handle/AudioHandle/Src/AudioInput.cpp diff --git a/src/Handle/AudioHandle/Src/AudioOutput.cpp b/src/Handle/AudioHandle/Src/AudioOutput.cpp new file mode 100644 index 0000000..99b0126 --- /dev/null +++ b/src/Handle/AudioHandle/Src/AudioOutput.cpp @@ -0,0 +1,227 @@ +// +// Created by Administrator on 2025/1/17. +// + +#include "AudioOutput.h" +#include +#include +#include +#include + +// 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::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(); +} \ No newline at end of file diff --git a/src/AudioHandle/readme.md b/src/Handle/AudioHandle/readme.md similarity index 100% rename from src/AudioHandle/readme.md rename to src/Handle/AudioHandle/readme.md diff --git a/src/Handle/DataObjectHandle/Inc/AudioDataHandle.h b/src/Handle/DataObjectHandle/Inc/AudioDataHandle.h new file mode 100644 index 0000000..d2be0c9 --- /dev/null +++ b/src/Handle/DataObjectHandle/Inc/AudioDataHandle.h @@ -0,0 +1,35 @@ +// +// Created by misaki on 2026/1/30. +// + +#pragma once + +#include +#include + +#include "AudioDataTransferObject.h" + +class AudioDataHandle final : public QObject +{ +Q_OBJECT +Q_DISABLE_COPY(AudioDataHandle) // 禁用拷贝 + private: + /** + * 构造函数私有化 + * @param parent + */ + explicit AudioDataHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理 + + static QScopedPointer m_instance; // 单例类 + static QMutex m_mutex; +private slots: + // 业务接收槽函数,当获取到音频数据包时,进行解析并播放 + void onAudioPacketReceived(const AudioDataTransferObject& packet); +public: + // 单例访问点 + static AudioDataHandle *getInstance(); + // 显式销毁 + static void destroy(); + + ~AudioDataHandle() override; +}; diff --git a/src/Handle/DataObjectHandle/Inc/AutoAgentHandle.h b/src/Handle/DataObjectHandle/Inc/AutoAgentHandle.h new file mode 100644 index 0000000..ed6a8ef --- /dev/null +++ b/src/Handle/DataObjectHandle/Inc/AutoAgentHandle.h @@ -0,0 +1,40 @@ +// +// Created by misaki on 2026/1/30. +// + +/** + * 本模块通过解析AutoAgentDataObject的内容并调用 AutoGUI 模块 + * 来完成自动化GUI操作 + * 对于GUI自动化执行器而言,运行时只需要有一个实例即可,因此采用单例模式,并在AppCore当中进行创建 + */ + +#pragma once +#include +#include +#include "AutoAgentDataObject.h" + +class AutoAgentHandle final : public QObject +{ +Q_OBJECT +Q_DISABLE_COPY(AutoAgentHandle) // 禁用拷贝 +private: + /** + * 构造函数私有化 + * @param parent + */ + explicit AutoAgentHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理 + + static QScopedPointer m_instance; // 单例类 + static QMutex m_mutex; +private slots: + // 业务接收槽函数,当获取到自动化agent数据包时,进行解析并调用 AutoGUI 模块 + void onAutoAgentPacketReceived(const AutoAgentDataObject& packet); +public: + // 单例访问点 + static AutoAgentHandle *getInstance(); + // 显式销毁 + static void destroy(); + + ~AutoAgentHandle() override; +}; + diff --git a/src/Handle/DataObjectHandle/Src/AudioDataHandle.cpp b/src/Handle/DataObjectHandle/Src/AudioDataHandle.cpp new file mode 100644 index 0000000..feeb3c0 --- /dev/null +++ b/src/Handle/DataObjectHandle/Src/AudioDataHandle.cpp @@ -0,0 +1,56 @@ +// +// Created by misaki on 2026/1/30. +// +#include "AudioDataHandle.h" +#include "NetWorkDO.h" +#include "AudioOutput.h" +// 初始化静态成员 +QScopedPointer 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()); +} + diff --git a/src/Handle/DataObjectHandle/Src/AutoAgentHandle.cpp b/src/Handle/DataObjectHandle/Src/AutoAgentHandle.cpp new file mode 100644 index 0000000..ea2b1a7 --- /dev/null +++ b/src/Handle/DataObjectHandle/Src/AutoAgentHandle.cpp @@ -0,0 +1,65 @@ +// +// Created by misaki on 2026/1/30. +// + +#include "AutoAgentHandle.h" +#include "NetWorkDO.h" +#include // 引入 AutoGUI 头文件 +// 初始化静态成员 +QScopedPointer 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: 快捷键,输入文本,滚动待实现 +} diff --git a/src/Handle/DataObjectHandle/readme.md b/src/Handle/DataObjectHandle/readme.md new file mode 100644 index 0000000..00b2d2e --- /dev/null +++ b/src/Handle/DataObjectHandle/readme.md @@ -0,0 +1 @@ +### 本模块负责将服务端返回的数据对象做解析并执行相应的动作 \ No newline at end of file diff --git a/src/NetWorkHandle/Inc/networkmanager.h b/src/Handle/NetWorkHandle/Inc/networkmanager.h similarity index 100% rename from src/NetWorkHandle/Inc/networkmanager.h rename to src/Handle/NetWorkHandle/Inc/networkmanager.h diff --git a/src/NetWorkHandle/Inc/serialportmanager.h b/src/Handle/NetWorkHandle/Inc/serialportmanager.h similarity index 100% rename from src/NetWorkHandle/Inc/serialportmanager.h rename to src/Handle/NetWorkHandle/Inc/serialportmanager.h diff --git a/src/NetWorkHandle/Inc/socketmanager.h b/src/Handle/NetWorkHandle/Inc/socketmanager.h similarity index 100% rename from src/NetWorkHandle/Inc/socketmanager.h rename to src/Handle/NetWorkHandle/Inc/socketmanager.h diff --git a/src/NetWorkHandle/Inc/websocketmanager.h b/src/Handle/NetWorkHandle/Inc/websocketmanager.h similarity index 100% rename from src/NetWorkHandle/Inc/websocketmanager.h rename to src/Handle/NetWorkHandle/Inc/websocketmanager.h diff --git a/src/NetWorkHandle/Src/networkmanager.cpp b/src/Handle/NetWorkHandle/Src/networkmanager.cpp similarity index 100% rename from src/NetWorkHandle/Src/networkmanager.cpp rename to src/Handle/NetWorkHandle/Src/networkmanager.cpp diff --git a/src/NetWorkHandle/Src/serialportmanager.cpp b/src/Handle/NetWorkHandle/Src/serialportmanager.cpp similarity index 100% rename from src/NetWorkHandle/Src/serialportmanager.cpp rename to src/Handle/NetWorkHandle/Src/serialportmanager.cpp diff --git a/src/NetWorkHandle/Src/socketmanager.cpp b/src/Handle/NetWorkHandle/Src/socketmanager.cpp similarity index 100% rename from src/NetWorkHandle/Src/socketmanager.cpp rename to src/Handle/NetWorkHandle/Src/socketmanager.cpp diff --git a/src/NetWorkHandle/Src/websocketmanager.cpp b/src/Handle/NetWorkHandle/Src/websocketmanager.cpp similarity index 98% rename from src/NetWorkHandle/Src/websocketmanager.cpp rename to src/Handle/NetWorkHandle/Src/websocketmanager.cpp index 0b4c616..c21a274 100644 --- a/src/NetWorkHandle/Src/websocketmanager.cpp +++ b/src/Handle/NetWorkHandle/Src/websocketmanager.cpp @@ -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); diff --git a/src/NetWorkHandle/readme.md b/src/Handle/NetWorkHandle/readme.md similarity index 100% rename from src/NetWorkHandle/readme.md rename to src/Handle/NetWorkHandle/readme.md diff --git a/src/Menu/Inc/menu.h b/src/UI/Menu/Inc/menu.h similarity index 100% rename from src/Menu/Inc/menu.h rename to src/UI/Menu/Inc/menu.h diff --git a/src/Menu/Src/menu.cpp b/src/UI/Menu/Src/menu.cpp similarity index 100% rename from src/Menu/Src/menu.cpp rename to src/UI/Menu/Src/menu.cpp diff --git a/src/Menu/readme.md b/src/UI/Menu/readme.md similarity index 100% rename from src/Menu/readme.md rename to src/UI/Menu/readme.md diff --git a/src/Render/TextRender/Inc/TextRenderer.h b/src/UI/Render/TextRender/Inc/TextRenderer.h similarity index 100% rename from src/Render/TextRender/Inc/TextRenderer.h rename to src/UI/Render/TextRender/Inc/TextRenderer.h diff --git a/src/Render/TextRender/Src/TextRenderer.cpp b/src/UI/Render/TextRender/Src/TextRenderer.cpp similarity index 100% rename from src/Render/TextRender/Src/TextRenderer.cpp rename to src/UI/Render/TextRender/Src/TextRenderer.cpp diff --git a/src/Setting/Inc/AudioPage.h b/src/UI/Setting/Inc/AudioPage.h similarity index 100% rename from src/Setting/Inc/AudioPage.h rename to src/UI/Setting/Inc/AudioPage.h diff --git a/src/Setting/Inc/BasePage.h b/src/UI/Setting/Inc/BasePage.h similarity index 100% rename from src/Setting/Inc/BasePage.h rename to src/UI/Setting/Inc/BasePage.h diff --git a/src/Setting/Inc/HomePage.h b/src/UI/Setting/Inc/HomePage.h similarity index 100% rename from src/Setting/Inc/HomePage.h rename to src/UI/Setting/Inc/HomePage.h diff --git a/src/Setting/Inc/ModelPage.h b/src/UI/Setting/Inc/ModelPage.h similarity index 100% rename from src/Setting/Inc/ModelPage.h rename to src/UI/Setting/Inc/ModelPage.h diff --git a/src/Setting/Inc/NetworkPage.h b/src/UI/Setting/Inc/NetworkPage.h similarity index 100% rename from src/Setting/Inc/NetworkPage.h rename to src/UI/Setting/Inc/NetworkPage.h diff --git a/src/Setting/Inc/RenderPage.h b/src/UI/Setting/Inc/RenderPage.h similarity index 100% rename from src/Setting/Inc/RenderPage.h rename to src/UI/Setting/Inc/RenderPage.h diff --git a/src/Setting/Inc/Setting.h b/src/UI/Setting/Inc/Setting.h similarity index 100% rename from src/Setting/Inc/Setting.h rename to src/UI/Setting/Inc/Setting.h diff --git a/src/Setting/Inc/UISetting.h b/src/UI/Setting/Inc/UISetting.h similarity index 100% rename from src/Setting/Inc/UISetting.h rename to src/UI/Setting/Inc/UISetting.h diff --git a/src/Setting/Src/AudioPage.cpp b/src/UI/Setting/Src/AudioPage.cpp similarity index 98% rename from src/Setting/Src/AudioPage.cpp rename to src/UI/Setting/Src/AudioPage.cpp index 5328b25..85f64f1 100644 --- a/src/Setting/Src/AudioPage.cpp +++ b/src/UI/Setting/Src/AudioPage.cpp @@ -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")); }); diff --git a/src/Setting/Src/BasePage.cpp b/src/UI/Setting/Src/BasePage.cpp similarity index 100% rename from src/Setting/Src/BasePage.cpp rename to src/UI/Setting/Src/BasePage.cpp diff --git a/src/Setting/Src/HomePage.cpp b/src/UI/Setting/Src/HomePage.cpp similarity index 100% rename from src/Setting/Src/HomePage.cpp rename to src/UI/Setting/Src/HomePage.cpp diff --git a/src/Setting/Src/ModelPage.cpp b/src/UI/Setting/Src/ModelPage.cpp similarity index 100% rename from src/Setting/Src/ModelPage.cpp rename to src/UI/Setting/Src/ModelPage.cpp diff --git a/src/Setting/Src/NetworkPage.cpp b/src/UI/Setting/Src/NetworkPage.cpp similarity index 92% rename from src/Setting/Src/NetworkPage.cpp rename to src/UI/Setting/Src/NetworkPage.cpp index c7bff73..fe1953b 100644 --- a/src/Setting/Src/NetworkPage.cpp +++ b/src/UI/Setting/Src/NetworkPage.cpp @@ -9,6 +9,8 @@ #include "ElaMessageBar.h" #include "websocketmanager.h" #include "NetWorkDO.h" +#include +#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); diff --git a/src/Setting/Src/RenderPage.cpp b/src/UI/Setting/Src/RenderPage.cpp similarity index 100% rename from src/Setting/Src/RenderPage.cpp rename to src/UI/Setting/Src/RenderPage.cpp diff --git a/src/Setting/Src/Setting.cpp b/src/UI/Setting/Src/Setting.cpp similarity index 100% rename from src/Setting/Src/Setting.cpp rename to src/UI/Setting/Src/Setting.cpp diff --git a/src/Setting/Src/UISetting.cpp b/src/UI/Setting/Src/UISetting.cpp similarity index 100% rename from src/Setting/Src/UISetting.cpp rename to src/UI/Setting/Src/UISetting.cpp diff --git a/src/Setting/readme.md b/src/UI/Setting/readme.md similarity index 100% rename from src/Setting/readme.md rename to src/UI/Setting/readme.md diff --git a/src/Utils/Inc/cobs.hpp b/src/Utils/Inc/cobs.hpp index 5a7cbf1..0d01d26 100644 --- a/src/Utils/Inc/cobs.hpp +++ b/src/Utils/Inc/cobs.hpp @@ -14,6 +14,8 @@ * 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。 * * 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。 + * + * C++20 */ #pragma once #include