From 31e71edac072fe92f5810f782bdcf5801e331cc6 Mon Sep 17 00:00:00 2001 From: Misaki Date: Fri, 23 Jan 2026 23:09:30 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BC=98=E5=8C=96=E4=BA=86=E4=B8=8E?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=95=B0=E6=8D=AE=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E6=9E=B6=E6=9E=84=E8=AE=BE=E8=AE=A1=202.=20?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E4=BA=86=E4=B8=80=E4=BA=9B=E6=9D=82=E9=A1=B9?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + CMakeLists.txt | 2 +- README.md | 16 ++++ src/Core/Inc/AppContext.h | 7 +- src/Core/Src/GLCore.cpp | 1 - src/DAO/Inc/AudioDataTransferObject.h | 68 ++++++++++++++++ src/DAO/Inc/DataTransferObjectBase.h | 21 +++++ src/DAO/Inc/NetWorkDO.h | 40 +-------- src/DAO/Src/AudioDataTransferObject.cpp | 104 ++++++++++++++++++++++++ src/DAO/Src/DataTransferObjectBase.cpp | 4 + src/DAO/Src/NetWorkDO.cpp | 64 +++------------ src/Setting/Src/NetworkPage.cpp | 43 +++++----- 12 files changed, 254 insertions(+), 117 deletions(-) create mode 100644 src/DAO/Inc/AudioDataTransferObject.h create mode 100644 src/DAO/Inc/DataTransferObjectBase.h create mode 100644 src/DAO/Src/AudioDataTransferObject.cpp create mode 100644 src/DAO/Src/DataTransferObjectBase.cpp diff --git a/.gitignore b/.gitignore index 4f0236e..fec1618 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ cmake-build-debug +cmake-build-release build # clion diff --git a/CMakeLists.txt b/CMakeLists.txt index 3488d16..0e0a237 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.30) project(Yosuga VERSION 1.0) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 20) # C++20 set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) diff --git a/README.md b/README.md index edeb7bc..f1b29a1 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,24 @@ ![GitHub issues](https://img.shields.io/github/issues/Misakityan/Yosuga) ![GitHub stars](https://img.shields.io/github/stars/Misakityan/Yosuga?style=social) +欢迎访问本项目。 + +首先向你介绍一下Yosuga这个项目: + +本项目的作者是Misakiotoha(みさきおとは[見崎音羽])。[call me "Misaki" でいいよ] + +之所以叫Yosuga,这个词来源日语当中的单词"縁"的发音,其意思是"缘分,关系"。 + +本项目分为三个部分: +1. Yosuga:这是项目的前端部分,是Yosuga与用户交互的一层,采用C++20 + Qt6.6.3编写,使用到的核心外部库为Live2D For C++ SDK。 +2. Yosuga_server:这是项目的后端部分,是Yosuga的核心,采用python3.11编写,使用到的外部库较多,负责联系项目的各个部分。 +3. Yosuga_embedded:这是项目的拓展部分,使得Yosuga对嵌入式设备拥有几乎完全的自定义控制能力,采用C语言编写,只使用到了cJSON库,平台无关,增强了Yosuga与外界的交互能力。 + +_**本项目为Yosuga.**_ + 本项目使用CMake构建,基于C++Qt6.6.3以及Live2D官方SDK(CubismSdkForNative-5-r.4.1)实现Live2D桌面宠物 (本项目由Yosuga[Qt5] 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台) + 环境为: - cmake version 3.5 diff --git a/src/Core/Inc/AppContext.h b/src/Core/Inc/AppContext.h index 8b2343b..f9493bf 100644 --- a/src/Core/Inc/AppContext.h +++ b/src/Core/Inc/AppContext.h @@ -7,10 +7,7 @@ * 虽然变得方便了,但也带来了危险,如果你肆意通过中介指针去调用GLCore的成员函数 * 可能会导致渲染问题等 */ - -#ifndef YOSUGA_APPCONTEXT_H -#define YOSUGA_APPCONTEXT_H - +#pragma once #include "GLCore.h" class AppContext { @@ -24,5 +21,3 @@ public: private: static inline GLCore* s_glCore = nullptr; // C++17 内联静态成员 }; - -#endif //YOSUGA_APPCONTEXT_H diff --git a/src/Core/Src/GLCore.cpp b/src/Core/Src/GLCore.cpp index 76c848f..46b4c4b 100644 --- a/src/Core/Src/GLCore.cpp +++ b/src/Core/Src/GLCore.cpp @@ -65,7 +65,6 @@ GLCore::GLCore(const int width, const int height, QWidget *parent) this->setWindowFlag(Qt::Tool); // 隐藏应用程序图标 this->setAttribute(Qt::WA_TranslucentBackground); // 设置窗口背景透明 - // 帧率控制初始化 frameTimer = new QTimer(this); connect(frameTimer, &QTimer::timeout, [&]() { diff --git a/src/DAO/Inc/AudioDataTransferObject.h b/src/DAO/Inc/AudioDataTransferObject.h new file mode 100644 index 0000000..78de932 --- /dev/null +++ b/src/DAO/Inc/AudioDataTransferObject.h @@ -0,0 +1,68 @@ +// +// Created by misaki on 2026/1/13. +// + +/** + * 数据传输对象 (DTO) 定义 + * AudioDataTransferObject + * 与Yosuga_server对等 + */ + +#pragma once +#include +#include +#include +#include +#include "DataTransferObjectBase.h" +// 前向声明,减少依赖 +class QJsonObject; +class AudioDataTransferObject final : public DataTransferObjectBase{ +public: + // 构造函数(带默认值) + explicit AudioDataTransferObject(const QString& owner = "client", + bool isStream = false, + bool isStart = false, + bool isEnd = false, + int sequence = 0, + const QByteArray& data = {}, + int sampleRate = 16000, + int channelCount = 1, + int bitDepth = 16, + double duration = 0.0, + const QString& text = ""); + // 静态工厂方法 + static AudioDataTransferObject fromJson(const QJsonObject& json); + + [[nodiscard]] QString type() const override { return "audio_data"; } + + // 序列化 + [[nodiscard]] QJsonObject toJson() const override; // 通过多态即可统一调用方式 + + // 链式调用设置 + AudioDataTransferObject& setData(const QString& key, const QJsonValue& value) override; + + [[nodiscard]] QString owner() const { return m_owner; } + [[nodiscard]] bool isStream() const { return m_isStream; } + [[nodiscard]] bool isStart() const { return m_isStart; } + [[nodiscard]] bool isEnd() const { return m_isEnd; } + [[nodiscard]] int sequence() const { return m_sequence; } + [[nodiscard]] QByteArray audioData() const { return m_data; } + [[nodiscard]] int sampleRate() const { return m_sampleRate; } + [[nodiscard]] int channelCount() const { return m_channelCount; } + [[nodiscard]] int bitDepth() const { return m_bitDepth; } + [[nodiscard]] double duration() const { return m_duration; } + [[nodiscard]] QString text() const { return m_text; } + +private: + QString m_owner; /// 音频数据的拥有者(server or client) + bool m_isStream; /// 音频数据是否为流式数据 + bool m_isStart; /// 音频数据是否开始(流式时有效) + bool m_isEnd; /// 音频数据是否结束(流式时有效) + int m_sequence; /// 音频数据块序列号(流式时有效) + QByteArray m_data; /// 音频数据,流式时为分块数据,base64编码 + int m_sampleRate; /// 音频采样率 + int m_channelCount; /// 音频通道数 + int m_bitDepth; /// 音频采样位数 + double m_duration; /// 音频时长 + QString m_text; /// 音频对应的文本 +}; diff --git a/src/DAO/Inc/DataTransferObjectBase.h b/src/DAO/Inc/DataTransferObjectBase.h new file mode 100644 index 0000000..9949431 --- /dev/null +++ b/src/DAO/Inc/DataTransferObjectBase.h @@ -0,0 +1,21 @@ +// +// Created by misaki on 2026/1/13. +// +#pragma once + +#include + +class DataTransferObjectBase +{ +public: + virtual ~DataTransferObjectBase() = default; + + // 获取类型,用于区分不同的DTO子类对象 + [[nodiscard]] virtual QString type() const = 0; + + // 序列化 + [[nodiscard]] virtual QJsonObject toJson() const = 0; + + // 链式调用设置 + virtual DataTransferObjectBase& setData(const QString& key, const QJsonValue& value) = 0; +}; diff --git a/src/DAO/Inc/NetWorkDO.h b/src/DAO/Inc/NetWorkDO.h index 4dfbcbb..2356127 100644 --- a/src/DAO/Inc/NetWorkDO.h +++ b/src/DAO/Inc/NetWorkDO.h @@ -25,34 +25,8 @@ #include #include -/** - * 数据传输对象 (DTO) 定义 - */ -// 音频文本捆绑数据结构 -struct AudioDataPacket { - QString text; // 文本内容 - QByteArray audioData; // 音频原始数据 (二进制) - int sampleRate; // 采样率 - int channels; // 通道数 - qint64 duration; // 时长 (ms) - - AudioDataPacket() : sampleRate(16000), channels(1), duration(0) {} -}; - -// 控制指令数据结构 (预留) -enum class ControlType { - Click, - Input, - Scroll -}; - -struct ControlDataPacket { - ControlType action; - int x; - int y; - QString extraData; -}; - +#include "DataTransferObjectBase.h" +#include "AudioDataTransferObject.h" /** * NetworkDO */ @@ -75,13 +49,11 @@ public: void registerSender(SenderFunc sender); // 业务发送函数 - void sendAudioPacket(const AudioDataPacket& packet); - void sendControlPacket(const ControlDataPacket& packet); + void sendPacket(const DataTransferObjectBase& packet); signals: // 业务接收信号 - void audioPacketReceived(const AudioDataPacket& packet); // 音频数据准备完成信号 - void controlPacketReceived(const ControlDataPacket& packet); // 控制数据准备完成信号 + void audioPacketReceived(const AudioDataTransferObject& packet); // 音频数据准备完成信号 void errorOccurred(const QString& errorMsg); // 错误信号 public slots: @@ -92,10 +64,6 @@ public: private: // 构造/析构函数私有化 explicit NetworkDO(QObject *parent = nullptr); - - // 内部处理逻辑 - void handleAudioMessage(const QJsonObject& data); - static QScopedPointer m_instance; static QMutex m_mutex; diff --git a/src/DAO/Src/AudioDataTransferObject.cpp b/src/DAO/Src/AudioDataTransferObject.cpp new file mode 100644 index 0000000..b0b3b15 --- /dev/null +++ b/src/DAO/Src/AudioDataTransferObject.cpp @@ -0,0 +1,104 @@ +// +// Created by misaki on 2026/1/13. +// +#include "AudioDataTransferObject.h" +#include +// 构造函数实现(初始化列表) +AudioDataTransferObject::AudioDataTransferObject(const QString& owner, + bool isStream, + bool isStart, + bool isEnd, + int sequence, + const QByteArray& data, + int sampleRate, + int channelCount, + int bitDepth, + double duration, + const QString& text) + : m_owner(owner) + , m_isStream(isStream) + , m_isStart(isStart) + , m_isEnd(isEnd) + , m_sequence(sequence) + , m_data(data) + , m_sampleRate(sampleRate) + , m_channelCount(channelCount) + , m_bitDepth(bitDepth) + , m_duration(duration) + , m_text(text) { +} + +// 静态工厂方法:从 JSON 反序列化 +AudioDataTransferObject AudioDataTransferObject::fromJson(const QJsonObject& json) { + // 逐个字段读取,不存在则用默认值 + QString owner = json.value("Owner").toString("server"); + bool isStream = json.value("isStream").toBool(false); + bool isStart = json.value("isStart").toBool(false); + bool isEnd = json.value("isEnd").toBool(false); + int sequence = json.value("sequence").toInt(0); + // 处理 base64 编码的 data 字段 + QByteArray data; + if (json.contains("data")) { + const QString base64Str = json.value("data").toString(); + data = QByteArray::fromBase64(base64Str.toUtf8()); + } + int sampleRate = json.value("sampleRate").toInt(16000); + int channelCount = json.value("channelCount").toInt(1); + int bitDepth = json.value("bitDepth").toInt(16); + double duration = json.value("duration").toDouble(0.0); + QString text = json.value("text").toString(); + + // 调用构造函数创建对象 + return AudioDataTransferObject(owner, isStream, isStart, isEnd, + sequence, data, sampleRate, channelCount, + bitDepth, duration, text); +} + +// 序列化为 JSON +QJsonObject AudioDataTransferObject::toJson() const { + QJsonObject json; + json["Owner"] = m_owner; + json["isStream"] = m_isStream; + json["isStart"] = m_isStart; + json["isEnd"] = m_isEnd; + json["sequence"] = m_sequence; + // data 字段 base64 编码 + json["data"] = QString(m_data.toBase64()); + json["sampleRate"] = m_sampleRate; + json["channelCount"] = m_channelCount; + json["bitDepth"] = m_bitDepth; + json["duration"] = m_duration; + json["text"] = m_text; + return json; +} + +// 链式设置 +AudioDataTransferObject& AudioDataTransferObject::setData(const QString& key, + const QJsonValue& value) { + if (key == "Owner") { + m_owner = value.toString(); + } else if (key == "isStream") { + m_isStream = value.toBool(); + } else if (key == "isStart") { + m_isStart = value.toBool(); + } else if (key == "isEnd") { + m_isEnd = value.toBool(); + } else if (key == "sequence") { + m_sequence = value.toInt(); + } else if (key == "data") { + // 这里要求已是 base64 字符串 + m_data = QByteArray::fromBase64(value.toString().toUtf8()); + } else if (key == "sampleRate") { + m_sampleRate = value.toInt(); + } else if (key == "channelCount") { + m_channelCount = value.toInt(); + } else if (key == "bitDepth") { + m_bitDepth = value.toInt(); + } else if (key == "duration") { + m_duration = value.toDouble(); + } else if (key == "text") { + m_text = value.toString(); + } + // 如果 key 不存在,默认忽略 + return *this; // 返回自身引用,支持链式调用 +} \ No newline at end of file diff --git a/src/DAO/Src/DataTransferObjectBase.cpp b/src/DAO/Src/DataTransferObjectBase.cpp new file mode 100644 index 0000000..b76a225 --- /dev/null +++ b/src/DAO/Src/DataTransferObjectBase.cpp @@ -0,0 +1,4 @@ +// +// Created by misaki on 2026/1/13. +// +#include "DataTransferObjectBase.h" \ No newline at end of file diff --git a/src/DAO/Src/NetWorkDO.cpp b/src/DAO/Src/NetWorkDO.cpp index b168e73..0136469 100644 --- a/src/DAO/Src/NetWorkDO.cpp +++ b/src/DAO/Src/NetWorkDO.cpp @@ -48,71 +48,31 @@ void NetworkDO::registerSender(SenderFunc sender) m_sender = std::move(sender); } -void NetworkDO::sendAudioPacket(const AudioDataPacket& packet) +void NetworkDO::sendPacket(const DataTransferObjectBase &packet) { // 检查发送器是否已注入 if (!m_sender) { emit errorOccurred("Sender not registered! Call registerSender() first."); return; } - - // 封装数据 (DTO -> JSON) - QJsonObject dataObj; - dataObj["text"] = packet.text; - // 音频转 Base64 字符串传输 - dataObj["audio"] = QString::fromLatin1(packet.audioData.toBase64()); - dataObj["sampleRate"] = packet.sampleRate; - dataObj["channels"] = packet.channels; - dataObj["duration"] = packet.duration; - - // 调用底层发送 (解耦) - // "textAudio" 是与后端约定的协议类型 - m_sender("textAudio", dataObj); -} - -void NetworkDO::sendControlPacket(const ControlDataPacket& packet) -{ - if (!m_sender) return; - - QJsonObject dataObj; - dataObj["action"] = static_cast(packet.action); - dataObj["x"] = packet.x; - dataObj["y"] = packet.y; - - m_sender("control", dataObj); + // 依赖注入 + 多态实现完美解耦 + m_sender(packet.type(), packet.toJson()); } +// 接受并没有完全解耦 void NetworkDO::onDataReceived(const QString& type, const QJsonObject& data) { - // 根据类型分发 - if (type == "textAudio") { - handleAudioMessage(data); + // 根据类型分发数据包 + // 为什么分发做在这里,而不是统一数据再去分发,如果不在这里做分发通知,分开发信号,而使用统一的信号 + // 如果有多个观察者,让观察者自动识别数据包,这会导致信号广播,容易引起性能问题(因为这里依赖的是Qt的信号与槽机制) + // TODO: 在此处使用工厂模式,根据type内容快速创建对应的对象 + if (type == "audio_data") { + emit audioPacketReceived(AudioDataTransferObject::fromJson(data)); // 构造并发送音频对象 } - else if (type == "control") { - // handleControlMessage(data); + else if (type == "control_data") { + } else { qWarning() << "[NetworkDO] Received unknown type:" << type; } } - -void NetworkDO::handleAudioMessage(const QJsonObject& data) -{ - AudioDataPacket packet; - - // 解析基础字段 (JSON -> DTO) - packet.text = data.value("text").toString(); - packet.sampleRate = data.value("sampleRate").toInt(16000); - packet.channels = data.value("channels").toInt(1); - // 注意类型转换,确保 long long 精度 - packet.duration = static_cast(data.value("duration").toDouble()); - - // 解析音频 (Base64 -> Binary) - QString base64Audio = data.value("audio").toString(); - if (!base64Audio.isEmpty()) { - packet.audioData = QByteArray::fromBase64(base64Audio.toLatin1()); - } - - // 通知上层业务 - emit audioPacketReceived(packet); -} \ No newline at end of file diff --git a/src/Setting/Src/NetworkPage.cpp b/src/Setting/Src/NetworkPage.cpp index 297a410..c7bff73 100644 --- a/src/Setting/Src/NetworkPage.cpp +++ b/src/Setting/Src/NetworkPage.cpp @@ -117,13 +117,13 @@ void NetWorkPage::initWebSocketClient() { client->connectToServer(); // 连接 // 使用定时器延迟检查连接状态 QTimer::singleShot(1000, this, [this, client]() { - if (client->isConnected()) { - ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", - "连通测试成功", 800.0, this); - } else { + if (!client->isConnected()) { ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连通测试", "无法连接到服务器,请检查地址和服务器状态", 1500.0, this); + return; } + ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", + "连通测试成功", 800.0, this); }); }); connect(connectPushButton, &ElaPushButton::clicked, this, [this, client]() { @@ -146,28 +146,29 @@ void NetWorkPage::initWebSocketClient() { "正在连接服务器...", 800.0, this); }); connect(disconnectPushButton, &ElaPushButton::clicked, this, [this, client]() { - if (client->isConnected()) { - client->disconnectFromServer(); - ElaMessageBar::success(ElaMessageBarType::TopRight, "断开连接", - "已断开连接", 800.0, this); - } else { + if (!client->isConnected()) { ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接", "当前未连接", 800.0, this); + return; } + client->disconnectFromServer(); + ElaMessageBar::success(ElaMessageBarType::TopRight, "断开连接", + "已断开连接", 800.0, this); }); - connect(sendTestPushButton, &ElaPushButton::clicked, this, [this, netDO]() { + connect(sendTestPushButton, &ElaPushButton::clicked, this, [this, netDO, client]() { + if (!client->isConnected()) { + ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接", + "当前未连接", 800.0, this); + return; + } // 创建数据包 - AudioDataPacket packet; - packet.text = "Hello, World!"; - // 填入元数据 - packet.sampleRate = 16000; - packet.channels = 1; - packet.duration = 2500; // 2.5秒 - // 填入音频数据 (模拟从文件或录音设备读取的二进制数据) - QByteArray rawAudioData; - rawAudioData.append("...这里是真实的PCM或WAV二进制数据..."); - packet.audioData = rawAudioData; - netDO->sendAudioPacket(packet); + AudioDataTransferObject packet; + packet.setData("Owner", "client") + .setData("isStream", true) + .setData("sequence", 42) + .setData("text", "Hello World") + .setData("data", "SGVsbG8gV29ybGQ="); // 填入音频数据 (Hello World的base64) + netDO->sendPacket(packet); ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试", "已成功发送数据包", 1000.0, this); });