This commit is contained in:
Misaki
2025-12-04 19:11:29 +08:00
commit bb600bbbc4
2741 changed files with 364700 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
//
// Created by Administrator on 2025/1/17.
//
#pragma once
#include <QAudioSource>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include <QTimer>
#include <QDir>
#include <vector>
/**
* @brief 录音模块
* @author Misaki
* @date 2025/1/17(first) 2025/11/30(update)
* 单例类
* 使用 QAudioSource 获取原始 PCM 数据,实现 RMS 计算和 WAV 保存
*/
class AudioInput : public QObject
{
Q_OBJECT
private:
/**
* @brief 构造函数
* @param parent
*/
explicit AudioInput(QObject *parent = nullptr);
static AudioInput* instance;
public:
/**
* @brief 获取实例
* @return AudioInput*
*/
static AudioInput* getInstance();
/**
* @brief 析构函数
*/
~AudioInput() override;
/**
* @brief 配置音频参数 (Qt6 中推荐使用 float 或 int16)
*/
void setAudioSettings(int rate = 44100, int channels = 2);
/**
* @brief 设置录音文件输出路径与文件名
* @param path 输出路径
* @param fileName 文件名
*/
void setAudioPath(const QString &path, const QString &fileName);
/**
* @brief 开始录音
*/
void startAudio();
/**
* @brief 停止录音
*/
void stopAudio();
/**
* @brief 设置录音时间并开始录音
* @param duration 录音时长,单位为秒
*/
void startAudioWithDuration(int duration);
/**
* @brief 开始自动录音,根据声音判断是否停止
* @param silenceThreshold 静音阈值,低于该值则认为没有声音
* @param silenceDuration 静音持续时间,单位为毫秒
*/
void startAutoStopAudio(qreal silenceThreshold = 1200, int silenceDuration = 1500);
/**
* @brief 开始最佳阈值计算
* @param Duration 持续时间,单位为毫秒
*/
void startAutoThresholdClu(int Duration = 5000);
/**
* @brief 获取当前系统所有的音频输入设备
* @return 音频输入设备名称列表
*/
static QList<QString> getAvailableAudioInputDevices();
/**
* @brief 设置当前录音设备
* @param deviceName 设备名称
*/
void setAudioInputDevice(const QString &deviceName);
/**
* @brief 设置静音阈值
* @param silenceThreshold 阈值
*/
void setSilenceThreshold(qreal silenceThreshold);
[[nodiscard]] qreal getSilenceThreshold() const;
private:
// WAV头生成工具函数
[[nodiscard]] QByteArray generateWavHeader(quint32 dataSize) const;
// 计算RMS值工具函数
static qreal calculateRMS(const QByteArray& buffer);
signals:
// 录音完成信号
void recordingFinished();
void recordingFinished_Byte(const QByteArray &wavData); // 携带音频数据
// 实时RMS值信号
void rmsRealValue(qreal value);
// 阈值计算完成信号
void thresholdCalculated(qreal bestThreshold);
private slots:
void onTimeout(); // 定时器超时槽函数
void thresholdTimeout(); // 阈值超时槽函数
// void processBuffer(const QAudioBuffer& buffer); // 处理缓冲区数据
void onReadyRead(); // 替代原先的 processBuffer,当有音频数据来时触发
private:
QAudioSource *m_audioSource = nullptr; /// Qt6 核心录音对象
QIODevice *m_ioDevice = nullptr; /// 用于读取数据的 IO 设备
QAudioFormat m_format; /// 音频格式
QAudioDevice m_currentDevice; /// 当前选中的输入设备
// 数据缓存
QByteArray m_rawPCMData; /// 存储原始PCM数据
QString m_outputFilePath;
// 逻辑控制变量
bool isAutoRecording = false; /// 是否自动录音状态
bool isAutoThreshold = false; /// 是否自动计算阈值
qreal m_rmsValue = 0.0; /// 实时RMS值
// 定时器
QTimer *m_timer; /// 总时长定时器
QTimer *m_silenceTimer; /// 静音检测定时器
QTimer *m_thresholdTimer; /// 阈值计算定时器
// 阈值算法相关
std::vector<qreal> m_rmsValues; /// RMS值vector
qreal m_silenceThreshold = 1200; /// 静音阈值
int m_silenceDuration = 1500; /// 静音持续时间
};
+152
View File
@@ -0,0 +1,152 @@
//
// Created by Administrator on 2025/1/17.
//
#pragma once
#include <QObject>
#include <QMediaPlayer> // 音频播放模块
#include <QAudioOutput> // QMediaPlayer 的音量控制组件
#include <QAudioSink> // 音频输出组件, 用于原始数据播放
#include <QUrl>
#include <QBuffer>
/**
* @brief 音频播放模块
* @author Misaki
* 单例类
* 本模块重新基于Qt6重构
* 实现的功能
* 1. 设定传入的音频文件路径
* 2. 根据音频文件路径播放音频
* 将上面的两个函数封装成一个槽函数,以及设定一个对应的信号
*/
class AudioOutput : public QObject
{
Q_OBJECT
private:
/**
* 构造函数私有化
* @param parent
*/
explicit AudioOutput(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static AudioOutput *instance; // 单例类
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; /// <!存储内存数据
};
+306
View File
@@ -0,0 +1,306 @@
//
// Created by Administrator on 2025/1/17.
//
#include "AudioInput.h"
#include <QDebug>
#include <QtMath>
#include <QtEndian> // 用于处理字节序
AudioInput *AudioInput::instance = nullptr;
AudioInput *AudioInput::getInstance()
{
// 懒汉式 依旧单线程无需加锁
if (instance == nullptr) {
instance = new AudioInput();
}
return instance;
}
AudioInput::AudioInput(QObject *parent) : QObject(parent)
{
// new一些必要的对象
// 初始化定时器
m_timer = new QTimer(this);
m_silenceTimer = new QTimer(this);
m_thresholdTimer = new QTimer(this);
m_thresholdTimer->setSingleShot(true);
// 连接定时器信号
connect(m_timer, &QTimer::timeout, this, &AudioInput::onTimeout); // 录音超时槽函数
connect(m_silenceTimer, &QTimer::timeout, this, &AudioInput::stopAudio); // 录音超时槽函数
connect(m_thresholdTimer, &QTimer::timeout, this, &AudioInput::thresholdTimeout); // 阈值检测超时槽函数
// 初始化默认设备和格式
m_currentDevice = QMediaDevices::defaultAudioInput();
setAudioSettings(); // 使用默认参数
}
AudioInput::~AudioInput()
{
stopAudio(); // 停止录音
if (m_audioSource) {
delete m_audioSource;
}
}
void AudioInput::setAudioSettings(const int rate, const int channels)
{
m_format.setSampleRate(rate);
m_format.setChannelCount(channels);
// 重要:Qt6 默认可能是 float,为了生成标准 WAV 且方便计算 RMS,强制设为 Int16
m_format.setSampleFormat(QAudioFormat::Int16);
// 检查设备是否支持该格式,不支持则使用最接近的
if (!m_currentDevice.isFormatSupported(m_format)) {
qWarning() << "Requested format not supported, using preferred format.";
m_format = m_currentDevice.preferredFormat();
}
}
void AudioInput::setAudioPath(const QString &path, const QString &fileName)
{
this->m_outputFilePath = path + fileName;
}
void AudioInput::startAudio()
{
// 每次开始前重新创建 QAudioSource,确保状态重置
if (m_audioSource) {
delete m_audioSource;
m_audioSource = nullptr;
}
m_audioSource = new QAudioSource(m_currentDevice, m_format, this);
// 调大缓冲区以避免溢出
m_audioSource->setBufferSize(128000);
// start() 返回一个 QIODevice,我们可以从中读取数据
m_ioDevice = m_audioSource->start();
if (m_ioDevice) {
connect(m_ioDevice, &QIODevice::readyRead, this, &AudioInput::onReadyRead);
qDebug() << "Started recording with device:" << m_currentDevice.description();
} else {
qCritical() << "Failed to start audio recording.";
}
}
void AudioInput::stopAudio()
{
if (m_audioSource) {
m_audioSource->stop();
// 注意:不要立即 delete m_audioSource,某些情况下可能导致 crash,停止即可
}
// 停止所有定时器
m_timer->stop();
m_silenceTimer->stop();
m_thresholdTimer->stop();
// 生成 WAV 数据
QByteArray wavData;
if (!m_rawPCMData.isEmpty()) {
wavData = generateWavHeader(m_rawPCMData.size());
wavData.append(m_rawPCMData);
// 如果需要保存文件
if (!m_outputFilePath.isEmpty()) {
QFile file(m_outputFilePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(wavData);
file.close();
qDebug() << "Saved WAV to:" << m_outputFilePath;
}
}
m_rawPCMData.clear();
}
isAutoRecording = false;
isAutoThreshold = false;
emit recordingFinished();
emit recordingFinished_Byte(wavData);
qDebug() << "Recording stopped.";
}
// 阈值检测超时槽函数
void AudioInput::onReadyRead()
{
if (!m_ioDevice) return;
// 读取当前所有可用的音频数据
QByteArray data = m_ioDevice->readAll();
if (data.isEmpty()) return;
// 1. 保存原始 PCM 数据
m_rawPCMData.append(data);
// 2. 计算 RMS (仅用于分析,取最后一小段或者整体计算,这里计算当前块的RMS)
m_rmsValue = calculateRMS(data);
// 3. 自动停止逻辑 (VAD)
if (isAutoRecording) {
// 输出 RMS 用于调试
// qDebug() << "RMS:" << m_rmsValue;
if (m_rmsValue < m_silenceThreshold) {
// 静音状态
if (!m_silenceTimer->isActive()) {
m_silenceTimer->start(m_silenceDuration);
}
} else {
// 有声音,重置定时器
m_silenceTimer->stop();
m_silenceTimer->start(m_silenceDuration);
}
}
// 4. 自动阈值计算逻辑
if (isAutoThreshold) {
m_rmsValues.push_back(m_rmsValue);
emit rmsRealValue(m_rmsValue);
}
}
qreal AudioInput::calculateRMS(const QByteArray& buffer)
{
if (buffer.isEmpty()) return 0;
// 假设是 Int16 格式 (16位深)
// 如果是 Stereo,数据排列是 L R L R...
// 简单的 RMS 计算可以将所有通道数据视为一个长序列
const qint16 *data = reinterpret_cast<const qint16*>(buffer.constData());
const int sampleCount = buffer.size() / sizeof(qint16); // 样本数量
if (sampleCount == 0) return 0;
qreal sumSquared = 0;
for (int i = 0; i < sampleCount; ++i) {
const qreal sample = static_cast<qreal>(data[i]);
sumSquared += sample * sample;
}
return qSqrt(sumSquared / sampleCount);
}
// 启动带时长的录音
void AudioInput::startAudioWithDuration(int duration)
{
startAudio();
m_timer->start(duration * 1000);
}
void AudioInput::onTimeout()
{
stopAudio();
qDebug() << "Recording stopped by duration timeout.";
}
// 获取所有音频输入设备
QList<QString> AudioInput::getAvailableAudioInputDevices()
{
QList<QString> list;
const auto devices = QMediaDevices::audioInputs();
for (const auto &device : devices) {
list.append(device.description());
}
return list;
}
// 设置当前录音设备
void AudioInput::setAudioInputDevice(const QString &deviceName)
{
const auto devices = QMediaDevices::audioInputs();
for (const auto &device : devices) {
if (device.description() == deviceName) {
m_currentDevice = device;
break;
}
}
}
// 启动自动录音 (VAD)
void AudioInput::startAutoStopAudio(const qreal silenceThreshold, const int silenceDuration)
{
isAutoRecording = true;
m_silenceThreshold = silenceThreshold;
m_silenceDuration = silenceDuration;
startAudio();
// 延迟启动静音检测,给一点缓冲时间
QTimer::singleShot(200, this, [this](){
m_silenceTimer->start(m_silenceDuration);
});
}
// 启动阈值计算
void AudioInput::startAutoThresholdClu(int Duration)
{
isAutoThreshold = true;
m_rmsValues.clear();
startAudio();
m_thresholdTimer->start(Duration);
}
void AudioInput::thresholdTimeout()
{
isAutoThreshold = false;
stopAudio(); // 内部会处理 stop
if (!m_rmsValues.empty()) {
const double sum = std::accumulate(m_rmsValues.begin(), m_rmsValues.end(), 0.0);
const double avg = sum / m_rmsValues.size();
m_silenceThreshold = avg + 500.0; // 这里的 500 是经验值,可以根据需要调整
emit thresholdCalculated(m_silenceThreshold);
} else {
emit thresholdCalculated(0);
}
}
QByteArray AudioInput::generateWavHeader(const quint32 dataSize) const {
// WAV头结构定义
struct WavHeader {
char riff[4] = {'R','I','F','F'};
quint32 chunkSize;
char wave[4] = {'W','A','V','E'};
char fmt[4] = {'f','m','t',' '};
quint32 fmtSize = 16;
quint16 audioFormat = 1; // PCM
quint16 numChannels;
quint32 sampleRate;
quint32 byteRate;
quint16 blockAlign;
quint16 bitsPerSample;
char data[4] = {'d','a','t','a'};
quint32 dataSize;
} header;
header.numChannels = static_cast<quint16>(m_format.channelCount());
header.sampleRate = static_cast<quint32>(m_format.sampleRate());
header.bitsPerSample = 16; // 我们强制使用了 Int16
header.byteRate = header.sampleRate * header.numChannels * (header.bitsPerSample / 8);
header.blockAlign = header.numChannels * (header.bitsPerSample / 8);
header.dataSize = dataSize;
header.chunkSize = 36 + dataSize;
return QByteArray(reinterpret_cast<const char*>(&header), sizeof(WavHeader));
}
void AudioInput::setSilenceThreshold(const qreal silenceThreshold)
{
this->m_silenceThreshold = silenceThreshold;
}
qreal AudioInput::getSilenceThreshold() const
{
return this->m_silenceThreshold;
}
+296
View File
@@ -0,0 +1,296 @@
//
// Created by Administrator on 2025/1/17.
//
#include "AudioOutput.h"
#include <QMediaDevices>
#include <QDataStream>
AudioOutput *AudioOutput::instance = nullptr;
AudioOutput *AudioOutput::getInstance()
{
// 懒汉式(单线程播放,无需考虑加锁)
if (instance == nullptr) {
instance = new AudioOutput();
}
return instance;
}
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();
}
+1
View File
@@ -0,0 +1 @@
实现了录音和播放的功能
+28
View File
@@ -0,0 +1,28 @@
//
// Created by Administrator on 2025/3/30.
//
/**
* @brief 中介类,避免直接让GLCore成为单例类
* 虽然变得方便了,但也带来了危险,如果你肆意通过中介指针去调用GLCore的成员函数
* 可能会导致渲染问题等
*/
#ifndef YOSUGA_APPCONTEXT_H
#define YOSUGA_APPCONTEXT_H
#include "GLCore.h"
class AppContext {
public:
// 注册GLCore
static void RegisterGLCore(GLCore* core) { s_glCore = core; }
// 注销GLCore
static void UnregisterGLCore() { s_glCore = nullptr; }
static GLCore* GetGLCore() { return s_glCore; }
private:
static inline GLCore* s_glCore = nullptr; // C++17 内联静态成员
};
#endif //YOSUGA_APPCONTEXT_H
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include <QtWidgets/QWidget>
#include <QOpenGLWidget>
#include "menu.h"
class GLCore : public QOpenGLWidget
{
Q_OBJECT
public:
GLCore(int width, int height, QWidget* parent = nullptr);
// 删除拷贝构造函数
GLCore(const GLCore&) = delete;
// 删除拷贝运算符
GLCore& operator=(const GLCore&) = delete;
// 删除移动构造函数
GLCore(GLCore&&) = delete;
// 删除移动运算符
GLCore& operator=(GLCore&&) = delete;
~GLCore();
// 帧率控制
void setFrameRate(double fps);
double getFrameRate();
// 帧率表
static QMap<QString, double> getFrameRateMap();
static QStringList getFrameRateList();
void mouseMoveEvent(QMouseEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
// 重写函数
void initializeGL() override;
void paintGL() override;
void resizeGL(int w, int h) override;
void closeEvent(QCloseEvent* event) override;
private:
void closeGL(); // 关闭当前窗口
private:
/*
* 在这个地方我遇到一个很抽象的问题,就是私有成员变量顺序的问题
* 出问题的顺序是:
* bool isLeftPressed; /// 鼠标左键是否按下
bool isRightPressed; /// 鼠标右键是否按下
QPoint currentPos; /// 当前鼠标位置
Menu *contextMenu; /// 使用 Menu 类
AudioInput *audioInput; /// 音频录制类
AudioOutput *audioOutput; /// 音频播放类
这样的顺序导致了我的鼠标一放在窗口上,窗口就往右下瞬移
改成现在下面的顺序就正常了
我一开始以为是我音频录制类里面多线程导致的
但想了想我都没new这个对象,哪来的多线程
后面问了问AI,它的解释是:
可能与C++中类成员的初始化顺序有关。
在C++中,类成员变量按照它们在类中声明的顺序进行初始化,
而不是根据它们在构造函数初始化列表中的顺序。
如果某些成员变量的初始化依赖于其他成员变量的状态
,而它们的实际初始化顺序与预期不符,可能会导致未定义行为或其他意外问题。
我感觉这不一定是根本原因,谁能告诉我到底发生了啥???
2025.3.30(Misaki): 上述问题已经解决,原因是isLeftPressed与isRightPressed
这两个成员变量没有初始化,导致其值是随机的,进而产生bug
*/
double frameRate = 60.0; /// 帧率
static QMap<QString, double> frameRateMap; /// 帧率映射表
QTimer* frameTimer; /// 帧控制定时器
Menu *contextMenu; /// 使用 Menu 类
bool isLeftPressed; /// 鼠标左键是否按下
bool isRightPressed; /// 鼠标右键是否按下
QPoint currentPos; /// 当前鼠标位置
};
+233
View File
@@ -0,0 +1,233 @@
#include "LAppDelegate.hpp" // 必须要放在第一个,否则会出现头文件顺序错误
#include "LAppView.hpp"
#include "LAppPal.hpp"
#include "LAppLive2DManager.hpp"
#include "LAppDefine.hpp"
#include "GLCore.h"
#include <QTimer>
#include <QMouseEvent>
#include <QDebug>
#include <QFont>
#include <QApplication>
#include <QFontDatabase>
#include <algorithm>
#include "TextRenderer.h"
// #include "AudioOutput.h"
#include "AppContext.h"
QMap<QString, double> GLCore::frameRateMap = {
{"30", 30.0},
{"60", 60.0},
{"120", 120.0},
{"144", 144.0},
{"165", 165.0},
{"240", 240.0}
};
GLCore::GLCore(int w, int h, QWidget *parent)
: QOpenGLWidget(parent),
isLeftPressed(false), // 显式初始化
isRightPressed(false) // 显式初始化
{
// 启用高分辨率位图(High DPI Pixmaps)支持
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#else
//根据实际屏幕缩放比例更改
qputenv("QT_SCALE_FACTOR", "1.5");
#endif
#endif
// 不为窗口创建额外的兄弟窗口,从而简化窗口管理并可能提高性能
QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
// 设置字体
QFontDatabase::addApplicationFont("Resources/Font/ElaAwesome.ttf");
QApplication::setFont(QFont("Microsoft YaHei", 13));
// new一些必要的对象
contextMenu = new Menu(this);
// 设置窗口大小
setFixedSize(w, h);
// 设置文本渲染器窗口大小
TextRenderer::getInstance()->setWindowSize(w, h);
TextRenderer::getInstance()->setGlobalFont(QFont("Microsoft YaHei", 14, QFont::Bold));
TextRenderer::getInstance()->setHoldDuration(1.0f); // 停留1.2秒
TextRenderer::getInstance()->setGravity(600.0f); // 更快的下坠速度
TextRenderer::getInstance()->setDampFactor(0.85f); // 更强的弹性效果
this->setWindowFlag(Qt::FramelessWindowHint); // 设置无边框窗口
this->setWindowFlag(Qt::WindowStaysOnTopHint); // 设置窗口始终在顶部
this->setWindowFlag(Qt::Tool); // 隐藏应用程序图标
this->setAttribute(Qt::WA_TranslucentBackground); // 设置窗口背景透明
// 帧率控制初始化
frameTimer = new QTimer(this);
connect(frameTimer, &QTimer::timeout, [&]() {
update();
});
frameTimer->start(static_cast<int>((1.0 / frameRate) * 1000)); // 使用成员变量计算间隔
// 启用鼠标跟踪,不启用鼠标按下才会回调mouseMoveEvent函数
this->setMouseTracking(true);
// 连接一些必要的信号与槽
connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL);
// 注册当前实例到中介类
AppContext::RegisterGLCore(this);
}
GLCore::~GLCore()
{
// 注销实例
AppContext::UnregisterGLCore();
// 释放TextRender单例
TextRenderer::releaseInstance();
// 释放Live2D 单例
LAppDelegate::ReleaseInstance();
}
// 帧率设置
void GLCore::setFrameRate(double fps)
{
if (qFuzzyCompare(fps, frameRate)) // 避免无意义更新
return;
if (fps <= 0.0) {
qWarning() << "Invalid frame rate:" << fps << "using default 60.0";
fps = 60.0;
}
frameRate = fps;
frameTimer->setInterval(static_cast<int>((1.0 / frameRate) * 1000));
}
// 获取当前帧率
double GLCore::getFrameRate()
{
return frameRate;
}
QMap<QString, double> GLCore::getFrameRateMap()
{
return frameRateMap;
}
QStringList GLCore::getFrameRateList()
{
// 将 frameRateMap中的String部分转换为 QStringList
QStringList frameRateList;
for (auto it = frameRateMap.begin(); it != frameRateMap.end(); ++it) {
frameRateList.append(it.key());
}
// 将frameRateList的数字字符从小到大排序
std::sort(frameRateList.begin(), frameRateList.end(), [](const QString& a, const QString& b) {
return a.toDouble() < b.toDouble();
});
// 将60放在第一个位置
std::swap(frameRateList[0], frameRateList[frameRateList.indexOf("60")]);
return frameRateList;
}
/**
* 关闭窗口
*/
void GLCore::closeGL()
{
this->close();
}
/**
* 主要是为setWindowFlag(Qt::Tool)这段代码擦屁股。
* 在 Qt 中,程序的退出通常依赖于主事件循环(QApplication的事件循环)的退出。当主窗口关闭时,通常会触发QApplication的lastWindowClosed信号,从而退出事件循环,导致程序退出。
然而,当你将窗口设置为工具窗口(Qt::Tool)时,这个窗口可能不会被视为应用程序的“主窗口”,因此关闭它可能不会触发lastWindowClosed信号,导致程序不会正常退出。
* @param event
*/
void GLCore::closeEvent(QCloseEvent* event)
{
QApplication::quit(); // 显式退出事件循环
event->accept(); // 确保关闭事件被接受
}
void GLCore::mouseMoveEvent(QMouseEvent* event)
{
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(
event->position().x(),
event->position().y()
);
if (isLeftPressed) {
const QPoint newPos = event->globalPos() - currentPos;
this->move(newPos);
}
}
void GLCore::mousePressEvent(QMouseEvent* event)
{
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(
event->position().x(),
event->position().y()
);
if (event->button() == Qt::LeftButton) {
this->isLeftPressed = true;
this->currentPos = event->globalPos() - this->frameGeometry().topLeft();
}
// TODO: 右键菜单等
if (event->button() == Qt::RightButton) {
// 在鼠标右键点击的位置创建菜单,显示自定义右键菜单
contextMenu->showMenu(event->globalPos());
this->isRightPressed = true;
}
}
void GLCore::mouseReleaseEvent(QMouseEvent* event)
{
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(
event->position().x(),
event->position().y()
);
if (event->button() == Qt::LeftButton) {
isLeftPressed = false;
}
if (event->button() == Qt::RightButton) {
isRightPressed = false;
}
}
void GLCore::initializeGL()
{
LAppDelegate::GetInstance()->Initialize(this);
}
void GLCore::paintGL()
{
LAppDelegate::GetInstance()->update();
// 渲染文本
TextRenderer::getInstance()->update();
TextRenderer::getInstance()->render();
}
void GLCore::resizeGL(int w, int h)
{
// 设置文本渲染器窗口大小
TextRenderer::getInstance()->setWindowSize(w, h);
LAppDelegate::GetInstance()->resize(w, h);
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
/**
* @brief 菜单
* @author Misaki
*
* 基于Ela UI的菜单控件
*/
#include "ElaMenu.h"
#include <QMenu>
#include <QAction>
#include <QPoint>
#include <QScopedPointer> // 智能指针
#include "Setting.h"
#include "networkmanager.h"
#include "socketmanager.h"
class Menu : public ElaMenu
{
Q_OBJECT
public:
explicit Menu(QWidget *parent = nullptr);
~Menu();
void showMenu(const QPoint &pos);
signals:
void closeMainWindow(); // 自定义关闭主窗口的信号
void startPlay(); // 自定义开始播放的信号
private:
void createMenu();
QAction *toggleThe; /// 切换主题(全局)
QAction *startExchangeAction; /// 开启对话
QAction *settingsAction; /// 设置
QAction *closeAction; /// 关闭
QScopedPointer<Setting> settingWindow; // 使用智能指针管理 Setting 窗口
private slots:
void toggleTheme();
// void startExchange();
};
+161
View File
@@ -0,0 +1,161 @@
#include "menu.h"
#include "ElaTheme.h"
#include "LAppLive2DManager.hpp"
#include <QDebug>
#include <QTimer>
#include "TextRenderer.h"
// #include "AudioInput.h"
// #include "AudioOutput.h"
Menu::Menu(QWidget *parent)
: ElaMenu(parent)
{
// 设置默认主题
eTheme->setThemeMode(ElaThemeType::Dark);
createMenu();
}
Menu::~Menu()
{
}
void Menu::createMenu()
{
toggleThe = addAction("切换主题");
// 连续对话功能按钮
startExchangeAction = addAction("连续对话(测试)");
// 添加设置按钮
settingsAction = addAction("设置");
// 添加关闭按钮
closeAction = addAction("关闭");
// 连接信号与槽
// 切换主题按钮
connect(toggleThe, &QAction::triggered, this, [this]() {
toggleTheme();
});
// TODO 连续对话功能,需要优化实现
connect(startExchangeAction, &QAction::triggered, this, [this]() {
// startExchange();
qDebug() << "Start Exchange triggered";
});
// 设置按钮
connect(settingsAction, &QAction::triggered, this, [this]() {
qDebug() << "Settings triggered";
// 打开设置窗口
// 如果 Setting 窗口已经存在,则不再创建
if (settingWindow) {
settingWindow->show();
return;
}
// 动态创建 Setting 窗口
settingWindow.reset(new Setting()); // 使用智能指针管理
// 显示 Setting 窗口
settingWindow->show();
});
// 关闭
connect(closeAction, &QAction::triggered, this, [this]() {
emit closeMainWindow(); // 发射关闭信号
});
}
void Menu::showMenu(const QPoint &pos)
{
// 在指定位置显示菜单
exec(pos);
}
void Menu::toggleTheme()
{
if (eTheme->getThemeMode() == ElaThemeType::Light) {
eTheme->setThemeMode(ElaThemeType::Dark);
} else {
eTheme->setThemeMode(ElaThemeType::Light);
}
}
/*
void Menu::startExchange()
{
// 列出所有音频输入设备
QList<QString> devices = AudioInput::getAvailableAudioInputDevices();
qDebug() << "可用录音设备:";
for (const QString &device : devices) {
qDebug() << device;
}
// 设置当前录音设备(假设选择第一个设备)
if (!devices.isEmpty()) {
qDebug() << "选择的录音设备是: " << devices.first();
AudioInput::getInstance()->setAudioInputDevice(devices.first());
qDebug() << "当前录音设备: " << AudioInput::getInstance()->audioInput(); // 检查当前录音设备
}
// 第一次需要主动录音来启动 信号与槽状态机(FSM)
qDebug() << "开始录音";
AudioInput::getInstance()->startAutoStopAudio();
// 检查录音状态
if (AudioInput::getInstance()->state() == QMediaRecorder::RecordingState) {
qDebug() << "录音已启动";
} else {
qDebug() << "录音启动失败";
}
// 连接信号和槽
// 播放回答
// 当完整接受wav文件后播放相关的wav文件
connect(SocketManager::getInstance(), &SocketManager::revWavFileFinish, [this](const QString &filePath, const QString &response, const float duration) {
LAppLive2DManager::GetInstance()->StartLipSync(filePath.toUtf8().constData());
AudioOutput::getInstance()->setAudioPath(filePath);
AudioOutput::getInstance()->playAudio();
TextRenderer::getInstance()->addText(response, 40.0f, QColor("#FF69B4"), duration);
});
// 开始录音
// 当播放完成后继续开始录音
connect(AudioOutput::getInstance(), &AudioOutput::playbackFinished, [this]() {
qDebug() << "开始录音";
AudioInput::getInstance()->startAutoStopAudio();
// 检查录音状态
if (AudioInput::getInstance()->state() == QMediaRecorder::RecordingState) {
qDebug() << "录音已启动";
} else {
qDebug() << "录音启动失败";
}
});
// 上传录音
// 当录音完成时,发送wav文件
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte, [this](const QByteArray &wavData) {
qDebug() << "录音完成,开始上传录音文件...";
if (wavData.isEmpty()) {
qWarning() << "录音数据为空!";
return;
}
qDebug() << "准备发送WAV数据,大小:" << wavData.size() << "字节";
SocketManager::getInstance()->sendWavFile(wavData);
});
}
*/
+1
View File
@@ -0,0 +1 @@
本目录下为右键菜单的相关代码
+95
View File
@@ -0,0 +1,95 @@
//
// Created by Administrator on 2025/1/19.
//
/**
* 已废弃
*/
#ifndef AIRI_DESKTOPGRIL_NETWORKMANAGER_H
#define AIRI_DESKTOPGRIL_NETWORKMANAGER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include <QTimer>
#include <QMutex>
#include <QWaitCondition>
class NetWorkManager : public QObject
{
Q_OBJECT
public:
explicit NetWorkManager(QObject *parent = nullptr);
~NetWorkManager();
// GET 请求
void get(const QString &url);
// POST 请求(JSON 数据)
void post(const QString &url, const QJsonObject &json);
// 文件下载
void downloadFile(const QString &url, const QString &savePath);
// 文件上传
void uploadFile(const QString &url, const QString &filePath);
// 设置超时时间(毫秒)
void setTimeout(int timeout);
// 设置请求头
void setHeader(const QString &key, const QString &value);
// 清除请求头
void clearHeaders();
signals:
// 请求完成信号,返回响应数据
void requestFinished(const QByteArray &response);
// 下载进度信号
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
// 上传进度信号
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
// 错误信号
void errorOccurred(const QString &errorString);
// 超时信号
void timeoutOccurred();
private slots:
// 请求完成槽函数
void onReplyFinished(QNetworkReply *reply);
// 下载进度槽函数
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
// 上传进度槽函数
void onUploadProgress(qint64 bytesSent, qint64 bytesTotal);
// 超时槽函数
void onTimeout();
private:
QNetworkAccessManager *manager; /// 网络管理对象
QFile *file; /// 文件对象(用于下载/上传)
QNetworkReply *reply; /// 网络响应对象
QTimer *timer; /// 超时计时器
QMutex mutex; /// 互斥锁
QWaitCondition condition; /// 条件变量
QMap<QString, QString> headers; /// 请求头
int timeout; /// 超时时间
};
#endif //AIRI_DESKTOPGRIL_NETWORKMANAGER_H
+106
View File
@@ -0,0 +1,106 @@
//
// Created by Administrator on 2025/2/5.
//
#ifndef AIRI_DESKTOPGRIL_SOCKETMANAGER_H
#define AIRI_DESKTOPGRIL_SOCKETMANAGER_H
#include <QTcpSocket>
#include <QObject>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QFile>
#include <QUrl>
#include <QDataStream>
#include <QHostAddress>
#include <QDateTime>
#include <QMutex>
/**
* @author Misaki
* @brief SocketManager类
* 本模块基于QTcpSocket进一步拓展,主要针对音频文件的上传与下载
* 基于CS架构与服务端进行通信
* Socket提供长连接支持,异步通信
*/
class SocketManager : public QTcpSocket {
Q_OBJECT
private:
// 构造函数私有化
explicit SocketManager(QObject *parent = nullptr);
public:
// 删除拷贝构造函数和赋值运算符,禁止复制
SocketManager(const SocketManager&) = delete;
SocketManager& operator=(const SocketManager&) = delete;
~SocketManager();
// 获取单例的静态方法
static SocketManager* getInstance();
void connectToServer();
void disconnectFromServer();
/**
* 发送文件(wav) \n
* 使用前需要先确保连接到服务端,不要调用完connectToServer后就直接调用这个函数 \n
* TCP握手是需要一个很短的时间的,等连接稳定了多次调用这个函数就没有问题了 \n
* 注意:调用这个函数请显示传入QString类型,否则编译器会不知道使用哪一个重载 \n
* 原因如下: \n
* 当调用 sendWavFile 时,如果传递的参数类型可能会被这两种函数参数类型接受或隐式转换,编译器就会报 “ambigous” 错误。例如: \n
如果调用 sendWavFile("audio.wav"),因为 "audio.wav" 是一个 C 风格的字符串(const char*), \n
而 QString 和 QByteArray 都可以接受 const char* 的隐式转换: \n
QString 的构造函数可以接受一个 const char*。 \n
QByteArray 的构造函数也可以接受一个 const char*。 \n
因此,编译器无法确定是要调用 sendWavFile(const QString &filePath) 还是 sendWavFile(const QByteArray &wavData),从而导致歧义。\n
* @author Misaki
* @param filePath
*/
void sendWavFile(const QString &filePath);
/**
* 直接发送二进制数据(wav)
* 使用前需要先确保连接到服务端,不要调用完connectToServer后就直接调用这个函数
* TCP握手是需要一个很短的时间的,等连接稳定了多次调用这个函数就没有问题了
* @author Misaki
* @param wavData
*/
void sendWavFile(const QByteArray &wavData);
// 设置目标服务端ip和端口的get&set方法
void setIp(const QString &ip);
QString getIp();
void setPort(qint16 port);
qint16 getPort();
signals:
void revWavFileFinish(const QString &filePath, const QString &response, const float duration);
void revWavDataFinish(const QByteArray &wavData); // 字节流信号
private slots:
/**
* 处理接收到的数据
* @author Misaki
*/
void handleReadyRead();
private:
// 单例实例指针
static SocketManager* m_instance;
static QMutex m_mutex; // 互斥锁确保线程安全
QString ip = "127.0.0.1"; /// 目标ip
qint16 port = 12345; /// 目标端口
QString filePath = "WavFiles\\"; /// 文件路径
QString receiveBuffer; /// 接收缓冲区
const char *endMarker = "<Eden*>"; /// 结束标记
};
#endif //AIRI_DESKTOPGRIL_SOCKETMANAGER_H
+59
View File
@@ -0,0 +1,59 @@
//
// Created by Administrator on 2025/2/4.
//
/**
* 已废弃
*/
#ifndef AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
#define AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
#include <QObject>
#include <QtWebSockets/QWebSocket>
class WebSocketManager : public QObject
{
Q_OBJECT
public:
explicit WebSocketManager(QObject *parent = nullptr);
~WebSocketManager();
// 上传接口(传入本地文件路径)
Q_INVOKABLE void uploadFile(const QString &filePath);
// 连接服务器
void connectToServer();
// 断开服务器
void disconnectFromServer();
// 设置服务器地址get&set方法
void setUrl(const QString &url);
QString getUrl();
signals:
// 上传进度(0-100
void uploadProgressChanged(int percent);
// 下载进度(0-100
void downloadProgressChanged(int percent);
// 文件处理完成(返回保存路径)
void fileProcessed(const QString &filePath);
// 错误通知
void errorOccurred(const QString &message);
private slots:
void onConnected();
void onBinaryMessageReceived(const QByteArray &message);
void onError(QAbstractSocket::SocketError error);
private:
QWebSocket m_socket;
QByteArray m_receivedData;
qint64 m_totalFileSize = 0;
QString url = "ws://localhost:8765";
};
#endif //AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
+216
View File
@@ -0,0 +1,216 @@
//
// Created by Administrator on 2025/1/19.
//
#include "networkmanager.h"
#include <QHttpMultiPart>
#include <QHttpPart>
#include <QFileInfo>
#include <QThread>
NetWorkManager::NetWorkManager(QObject *parent) : QObject(parent)
{
this->manager = new QNetworkAccessManager(this);
this->file = nullptr;
this->reply = nullptr;
this->timer = new QTimer(this);
this->timeout = 30000; // 默认超时时间为 30 秒
// 连接请求完成信号
connect(this->manager, &QNetworkAccessManager::finished, this, &NetWorkManager::onReplyFinished);
// 连接超时信号
connect(this->timer, &QTimer::timeout, this, &NetWorkManager::onTimeout);
}
NetWorkManager::~NetWorkManager()
{
if (this->reply) {
this->reply->abort();
this->reply->deleteLater();
}
if (this->file) {
this->file->close();
delete this->file;
}
}
// GET 请求
void NetWorkManager::get(const QString &url)
{
QNetworkRequest request;
request.setUrl(QUrl(url));
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
this->reply = manager->get(request);
// 启动超时计时器
this->timer->start(timeout);
// 连接下载进度信号
connect(this->reply, &QNetworkReply::downloadProgress, this, &NetWorkManager::onDownloadProgress);
}
// POST 请求(JSON 数据)
void NetWorkManager::post(const QString &url, const QJsonObject &json)
{
QNetworkRequest request;
request.setUrl(QUrl(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
QByteArray data = QJsonDocument(json).toJson();
this->reply = manager->post(request, data);
// 启动超时计时器
this->timer->start(timeout);
// 连接上传进度信号
connect(this->reply, &QNetworkReply::uploadProgress, this, &NetWorkManager::onUploadProgress);
}
// 文件下载
void NetWorkManager::downloadFile(const QString &url, const QString &savePath)
{
QNetworkRequest request;
request.setUrl(QUrl(url));
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
this->reply = manager->get(request);
// 启动超时计时器
this->timer->start(timeout);
// 打开文件
this->file = new QFile(savePath);
if (!this->file->open(QIODevice::WriteOnly)) {
emit errorOccurred("Failed to open file for writing");
delete this->file;
this->file = nullptr;
return;
}
// 连接下载进度信号
connect(this->reply, &QNetworkReply::downloadProgress, this, &NetWorkManager::onDownloadProgress);
// 读取数据并写入文件
connect(reply, &QNetworkReply::readyRead, this, [this]() {
if (this->file) {
this->file->write(reply->readAll());
}
});
}
// 文件上传
void NetWorkManager::uploadFile(const QString &url, const QString &filePath)
{
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
// 创建文件部分
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"" + QFileInfo(filePath).fileName() + "\""));
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
QFile *file = new QFile(filePath);
if (!file->open(QIODevice::ReadOnly)) {
emit errorOccurred("Failed to open file for reading");
delete file;
return;
}
filePart.setBodyDevice(file);
file->setParent(multiPart); // 将文件对象绑定到 multiPart,由 multiPart 负责释放
multiPart->append(filePart);
// 发送请求
QNetworkRequest request;
request.setUrl(QUrl(url));
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
this->reply = manager->post(request, multiPart);
multiPart->setParent(reply); // 将 multiPart 绑定到 reply,由 reply 负责释放
// 启动超时计时器
this->timer->start(timeout);
// 连接上传进度信号
connect(reply, &QNetworkReply::uploadProgress, this, &NetWorkManager::onUploadProgress);
}
// 设置超时时间
void NetWorkManager::setTimeout(int timeout)
{
this->timeout = timeout;
}
// 设置请求头
void NetWorkManager::setHeader(const QString &key, const QString &value)
{
this->headers[key] = value;
}
// 清除请求头
void NetWorkManager::clearHeaders()
{
this->headers.clear();
}
// 请求完成槽函数
void NetWorkManager::onReplyFinished(QNetworkReply *reply)
{
this->timer->stop(); // 停止超时计时器
if (reply->error() == QNetworkReply::NoError) {
QByteArray response = reply->readAll();
emit requestFinished(response);
} else {
emit errorOccurred(reply->errorString());
}
// 关闭并释放文件对象
if (file) {
file->close();
delete file;
file = nullptr;
}
reply->deleteLater();
}
// 下载进度槽函数
void NetWorkManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
emit downloadProgress(bytesReceived, bytesTotal);
}
// 上传进度槽函数
void NetWorkManager::onUploadProgress(qint64 bytesSent, qint64 bytesTotal)
{
emit uploadProgress(bytesSent, bytesTotal);
}
// 超时槽函数
void NetWorkManager::onTimeout()
{
if (reply) {
this->reply->abort();
emit timeoutOccurred();
}
}
+208
View File
@@ -0,0 +1,208 @@
//
// Created by Administrator on 2025/2/5.
//
#include "socketmanager.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
// 初始化静态成员变量
SocketManager* SocketManager::m_instance = nullptr;
QMutex SocketManager::m_mutex;
SocketManager* SocketManager::getInstance()
{
QMutexLocker locker(&m_mutex); // 自动加锁,确保线程安全
if (!m_instance) {
m_instance = new SocketManager(); // 延迟初始化,首次调用时创建实例
}
return m_instance;
}
SocketManager::SocketManager(QObject *parent) : QTcpSocket(parent)
{
connect(this, &QTcpSocket::connected, [=]() {
qDebug() << "SocketManager::connected !";
});
connect(this, &QTcpSocket::disconnected, [=]() {
qDebug() << "SocketManager::disconnected !";
});
// 当有数据到达就调用handleReadyRead进行处理数据
connect(this, &QTcpSocket::readyRead, this, &SocketManager::handleReadyRead);
}
SocketManager::~SocketManager()
{
}
void SocketManager::connectToServer()
{
// 如果未连接,就连接
if(this->state() == QAbstractSocket::UnconnectedState){
this->connectToHost(this->ip, this->port);
return;
}
// 已连接则返回
if(this->state() == QAbstractSocket::ConnectedState){
return;
}
}
void SocketManager::disconnectFromServer()
{
this->disconnectFromHost();
}
/**
* 报文形式:先发送数据长度,再发送数据本身
* 数据长度占4字节,大端
* 数据本身压缩发送
* @param filePath
*/
void SocketManager::sendWavFile(const QString &filePath)
{
if(this->state() == QAbstractSocket::ConnectedState){
QFile file(filePath);
if(file.open(QIODevice::ReadOnly)){
// 压缩数据
QByteArray compressedData = qCompress(file.readAll(), 9);
// 发送数据长度(4字节,大端)
quint32 totalSize = compressedData.size();
QByteArray sizeData;
QDataStream sizeStream(&sizeData, QIODevice::WriteOnly);
sizeStream.setByteOrder(QDataStream::BigEndian);
sizeStream << totalSize;
this->write(sizeData);
this->flush();
// 发送压缩数据
this->write(compressedData);
this->flush();
file.close();
}
} else{
qDebug() << "SocketManager::发送失败! 请检查是否连接到服务端!";
}
}
/**
* 发送二进制WAV数据
* 数据长度占4字节,大端
* 数据本身压缩发送
* @param wavData
*/
void SocketManager::sendWavFile(const QByteArray &wavData)
{
if (this->state() == QAbstractSocket::ConnectedState) {
if (!wavData.isEmpty()) {
// 压缩数据(保持与文件发送相同的压缩方式)
QByteArray compressedData = qCompress(wavData, 9);
// 发送数据长度(4字节大端)
quint32 totalSize = compressedData.size();
QByteArray sizeData;
QDataStream sizeStream(&sizeData, QIODevice::WriteOnly);
sizeStream.setByteOrder(QDataStream::BigEndian);
sizeStream << totalSize;
// 分步发送确保可靠性
this->write(sizeData);
this->flush();
this->write(compressedData);
this->flush();
qDebug() << "二进制WAV数据已发送,原始大小:" << wavData.size()
<< "压缩后大小:" << compressedData.size();
} else {
qWarning() << "尝试发送空的WAV数据";
}
} else {
qDebug() << "SocketManager::发送失败! 未连接到服务端!";
}
}
void SocketManager::handleReadyRead()
{
receiveBuffer.append(this->readAll()); // 累积数据到缓冲区
// 检查是否包含结束标记
int endIndex;
while ((endIndex = receiveBuffer.indexOf(endMarker)) != -1) {
// 提取结束标记前的数据
QString data = receiveBuffer.left(endIndex);
receiveBuffer = receiveBuffer.mid(endIndex + strlen(endMarker)); // 移除已处理数据
// 处理数据
// 使用动态文件名
QFile rev(filePath + "revTest_" + QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss") +".wav");
if (rev.open(QIODevice::WriteOnly)) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(data.toUtf8());
// 提取JSON对象
QJsonObject jsonObj = jsonDoc.object();
QString response;
// 解析response字段
if (jsonObj.contains("response") && jsonObj["response"].isString()) {
response = jsonObj["response"].toString();
qDebug() << "解析到response:" << response;
} else {
qDebug() << "response字段缺失或类型错误";
}
float duration;
if (jsonObj.contains("wav_duration") && jsonObj["wav_duration"].isDouble()) {
duration = jsonObj["wav_duration"].toDouble();
qDebug() << "解析到duration:" << duration;
} else {
qDebug() << "duration字段缺失或类型错误";
}
QByteArray wavData;
// 解析wav_data_base64字段
if (jsonObj.contains("wav_data_base64") && jsonObj["wav_data_base64"].isString()) {
QString base64Data = jsonObj["wav_data_base64"].toString();
wavData = QByteArray::fromBase64(base64Data.toUtf8());
// 获取当前音频时长
qDebug() << "音频数据大小:" << wavData.size() << "字节";
} else {
qDebug() << "wav_data_base64字段缺失或类型错误";
}
rev.write(wavData);
rev.close();
// 清空缓冲区,这一步很重要,不然会导致下次接收数据时,会保存着上次接收的数据
receiveBuffer.clear();
// 发送信号
emit revWavFileFinish(rev.fileName(), response, duration);
emit revWavDataFinish(wavData);
qDebug() << "文件接收完成并保存: " + rev.fileName();
} else {
qDebug() << "无法保存文件";
}
}
}
void SocketManager::setIp(const QString &ip)
{
this->ip = ip;
}
QString SocketManager::getIp()
{
return this->ip;
}
void SocketManager::setPort(qint16 port)
{
this->port = port;
}
qint16 SocketManager::getPort()
{
return this->port;
}
+125
View File
@@ -0,0 +1,125 @@
//
// Created by Administrator on 2025/2/4.
//
#include "websocketmanager.h"
#include <QFile>
#include <QDir>
#include <QDebug>
#include <zlib.h>
WebSocketManager::WebSocketManager(QObject *parent)
: QObject(parent)
{
// 连接信号槽
connect(&m_socket, &QWebSocket::connected,
this, &WebSocketManager::onConnected);
connect(&m_socket, &QWebSocket::binaryMessageReceived,
this, &WebSocketManager::onBinaryMessageReceived);
connect(&m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
this, &WebSocketManager::onError);
}
WebSocketManager::~WebSocketManager()
{
m_socket.close();
}
void WebSocketManager::uploadFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
emit errorOccurred(tr("无法打开文件: %1").arg(filePath));
return;
}
// 读取并压缩数据
QByteArray rawData = file.readAll();
QByteArray compressedData = qCompress(rawData, 9);
// 重置接收缓存
m_receivedData.clear();
m_totalFileSize = compressedData.size();
// 分块发送(每64KB一个块)
const int chunkSize = 64 * 1024;
int totalChunks = (compressedData.size() + chunkSize - 1) / chunkSize;
for (int i = 0; i < totalChunks; ++i) {
QByteArray chunk = compressedData.mid(i * chunkSize, chunkSize);
m_socket.sendBinaryMessage(chunk);
// 计算上传进度
int progress = (i + 1) * 100 / totalChunks;
emit uploadProgressChanged(progress);
}
// 发送结束标记
m_socket.sendBinaryMessage("END");
}
void WebSocketManager::onBinaryMessageReceived(const QByteArray &message)
{
if (message == "END") {
// 解压接收数据
QByteArray decompressedData = qUncompress(m_receivedData);
// 生成保存路径
QString savePath = QDir::tempPath() + "/processed_" +
QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".wav";
// 保存文件
QFile file(savePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(decompressedData);
emit fileProcessed(savePath);
} else {
emit errorOccurred(tr("无法保存文件: %1").arg(savePath));
}
m_receivedData.clear();
} else {
m_receivedData.append(message);
// 计算下载进度
if (m_totalFileSize > 0) {
int progress = m_receivedData.size() * 100 / m_totalFileSize;
emit downloadProgressChanged(progress);
}
}
}
void WebSocketManager::connectToServer()
{
// 检查是否已经连接到服务器
if (m_socket.state() == QAbstractSocket::ConnectedState) {
emit errorOccurred(tr("已经连接到服务器"));
return;
}
// 连接服务器
m_socket.open(QUrl(this->url));
}
void WebSocketManager::setUrl(const QString &url)
{
this->url = url;
}
QString WebSocketManager::getUrl()
{
return this->url;
}
void WebSocketManager::onConnected()
{
qDebug() << "Connected to server";
}
void WebSocketManager::onError(QAbstractSocket::SocketError error)
{
emit errorOccurred(tr("网络错误: %1").arg(m_socket.errorString()));
}
+103
View File
@@ -0,0 +1,103 @@
//
// Created by Administrator on 2025/2/16.
//
#ifndef AIRI_DESKTOPGRIL_TEXTRENDERER_H
#define AIRI_DESKTOPGRIL_TEXTRENDERER_H
#include <QOpenGLWidget>
#include <QVector>
#include <QString>
#include <QColor>
#include <QElapsedTimer>
#include <QVector2D>
#include <QFontMetrics>
#include <QLinearGradient>
class TextRenderer {
public:
// 删除拷贝构造函数和赋值运算符
TextRenderer(const TextRenderer&) = delete;
void operator=(const TextRenderer&) = delete;
// 获取单例实例
static TextRenderer* getInstance()
{
if(instance == nullptr){
instance = new TextRenderer();
}
return instance;
}
struct TextInstance {
QString text; // 文本内容
QVector2D basePosition; // 基础位置(Y轴)
QColor primaryColor; // 主要文字颜色
QColor outlineColor; // 轮廓颜色
float duration; // 显示总时长(秒)
qint64 startTime; // 开始显示时间(毫秒)
bool isDropping; // 是否正在下坠
qint64 dropStartTime; // 下坠开始时间
float dropYVelocity; // Y轴下落速度
float alpha; // 透明度
QList<QPoint> charPositions; // 字符位置
QList<int> charWidths; // 每个字符宽度
int visibleChars; // 可见字符数
bool flowCompleted; // 流式显示是否完成
float holdDuration; // 实际使用的停留时间
qint64 flowEndTime; // 流式完成时间戳
TextInstance() : isDropping(false), dropYVelocity(0.0f),
alpha(1.0f), visibleChars(0),
flowCompleted(false), holdDuration(0.5f),
flowEndTime(0) {}
};
void setWindowSize(int w, int h);
void addText(const QString &text, float yPos,
const QColor &color, float duration);
void update();
void render();
void setGlobalFont(const QFont &newFont);
/**
* 参数建议值:
效果类型 gravity dampFactor holdDuration
柔和下落 600.0f 0.85f 1.0f
快速坠落 1200.0f 0.6f 0.3f
弹性效果 900.0f 0.75f 0.8f
真实物理模拟 980.0f 0.82f 0.5f
*/
void setHoldDuration(const float seconds) { defaultHoldDuration = seconds; }
void setGravity(const float g) { gravity = g; }
void setDampFactor(const float damp) { dampFactor = damp; }
// 释放单例
static void releaseInstance() {
if (instance) {
delete instance;
instance = nullptr;
}
}
private:
explicit TextRenderer(); // 构造函数私有化
void updateFlowPositions(TextInstance &instance);
void updateDropPositions(TextInstance &instance, float deltaTime);
private:
static TextRenderer *instance;
QList<TextInstance> activeTexts; /// 当前显示的文字
QElapsedTimer globalTimer; /// 全局计时器
QFont font; /// 全局字体
int windowWidth; /// 窗口宽度
int windowHeight; /// 窗口高度
qint64 lastFrameTime; /// 上一帧的时间
// 一些自定义参数
float defaultHoldDuration; /// 默认停留时间(秒)
float gravity; /// 重力加速度(像素/秒²)
float dampFactor; /// 碰撞阻尼系数
};
#endif //AIRI_DESKTOPGRIL_TEXTRENDERER_H
+248
View File
@@ -0,0 +1,248 @@
//
// Created by Administrator on 2025/2/16.
//
/**
* 用于渲染文本显示
*/
#include "TextRenderer.h"
#include <QPainter>
#include <QOpenGLPaintDevice>
#include <cmath>
#include <QRandomGenerator>
TextRenderer *TextRenderer::instance = nullptr;
TextRenderer::TextRenderer() : windowWidth(800), // 默认窗口宽
windowHeight(600), // 默认窗口高
lastFrameTime(0),
defaultHoldDuration(0.5f), // 默认停留0.5秒
gravity(980.0f), // 默认重力
dampFactor(0.82f) // 默认阻尼
{
globalTimer.start();
font.setFamily("Microsoft YaHei");
font.setPixelSize(28);
font.setWeight(QFont::Bold);
}
void TextRenderer::setGlobalFont(const QFont &newFont)
{
font = newFont;
font.setWeight(QFont::Bold);
}
void TextRenderer::setWindowSize(int w, int h)
{
windowWidth = w;
windowHeight = h;
}
void TextRenderer::addText(const QString &text, float yPos,
const QColor &color, float duration)
{
TextInstance instance;
instance.text = text;
instance.basePosition = QVector2D(0, yPos);
instance.primaryColor = color;
instance.outlineColor = QColor(0, 0, 0, 180);
instance.duration = duration;
instance.startTime = globalTimer.elapsed();
instance.holdDuration = defaultHoldDuration; // 应用当前全局设置
QFontMetrics metrics(font);
instance.charWidths.clear();
for (const QChar &ch : text) {
instance.charWidths.append(metrics.horizontalAdvance(ch));
}
// 初始位置计算
updateFlowPositions(instance);
activeTexts.append(instance);
}
void TextRenderer::updateFlowPositions(TextInstance &instance)
{
QFontMetrics metrics(font);
const int rightMargin = 20; // 右侧留白
// 计算可见部分总宽度
int visibleWidth = 0;
for (int i = 0; i < instance.visibleChars; ++i) {
visibleWidth += instance.charWidths[i];
}
// 动态计算起始位置
int startX = qMin(
windowWidth - visibleWidth - rightMargin, // 优先保证右侧空间
(windowWidth - visibleWidth) / 2 // 次选居中显示
);
// 边界保护:至少保留20px左侧边距
startX = qMax(20, startX);
// 更新字符位置
int currentX = startX;
instance.charPositions.clear();
for (int i = 0; i < instance.text.size(); ++i) {
if (i < instance.visibleChars) {
instance.charPositions.append(QPoint(currentX, instance.basePosition.y()));
currentX += instance.charWidths[i];
} else {
instance.charPositions.append(QPoint(-10000, -10000));
}
}
// 自动滚动调整:当文字溢出时整体左移
if (currentX > windowWidth - rightMargin) {
int overflow = currentX - (windowWidth - rightMargin);
for (QPoint &pos : instance.charPositions) {
if (pos.x() != -10000) {
pos.rx() -= overflow;
}
}
}
}
void TextRenderer::updateDropPositions(TextInstance &instance, float deltaTime)
{
const float floorY = windowHeight - 30;
instance.dropYVelocity += gravity * deltaTime; // 使用全局重力值
float deltaY = instance.dropYVelocity * deltaTime;
bool hasCollision = false;
for (QPoint &pos : instance.charPositions) {
float newY = pos.y() + deltaY;
if (newY >= floorY) {
newY = floorY;
instance.dropYVelocity = -qAbs(instance.dropYVelocity) * dampFactor; // 使用全局阻尼系数
hasCollision = true;
pos.rx() += QRandomGenerator::global()->bounded(-3, 4);
}
pos.setY(newY);
}
// 碰撞后处理
if (hasCollision) {
// 速度衰减到临界值时开始加速透明
if (qAbs(instance.dropYVelocity) < 100.0f) {
instance.alpha *= 0.92f; // 加快透明度衰减速度
}
// 完全静止后强制移除
if (qAbs(instance.dropYVelocity) < 5.0f) {
instance.alpha = 0.0f;
}
}
// 常规透明度衰减
instance.alpha = qMax(0.0f, instance.alpha * 0.98f);
// 添加随机水平扰动(只在有速度时)
if (qAbs(instance.dropYVelocity) > 10.0f) {
for (QPoint &pos : instance.charPositions) {
pos.rx() += QRandomGenerator::global()->bounded(-1, 2);
}
}
}
void TextRenderer::update()
{
qint64 currentTime = globalTimer.elapsed();
float deltaTime = (currentTime - lastFrameTime) / 1000.0f;
lastFrameTime = currentTime;
auto it = activeTexts.begin();
while (it != activeTexts.end()) {
TextInstance &instance = *it;
if (instance.isDropping) {
updateDropPositions(instance, deltaTime); // 使用统一的deltaTime
// 强化消失条件:Y轴速度接近零 或 透明度低于阈值
if ((qAbs(instance.dropYVelocity) < 5.0f && instance.alpha < 0.3f)
|| instance.alpha < 0.01f) {
it = activeTexts.erase(it);
continue;
}
} else {
// 流式显示更新
float progress = (currentTime - instance.startTime) / 1000.0f / instance.duration;
progress = qMin(progress, 1.0f);
if (!instance.flowCompleted) {
int newVisible = qMin(instance.text.size(),
static_cast<int>(progress * instance.text.size()));
if (newVisible != instance.visibleChars) {
instance.visibleChars = newVisible;
updateFlowPositions(instance);
}
// 流式显示完成检测
if (progress >= 1.0f) {
instance.flowCompleted = true;
instance.flowEndTime = currentTime; // 记录完成时间
}
}
// 已流式完成但未开始下坠
else if (!instance.isDropping) {
// 检查停留时间是否结束
if (currentTime - instance.flowEndTime >= instance.holdDuration * 1000) {
instance.isDropping = true;
instance.dropStartTime = currentTime;
// 最终居中定位
int totalWidth = 0;
for (int w : instance.charWidths) totalWidth += w;
int startX = (windowWidth - totalWidth) / 2;
int currentX = startX;
instance.charPositions.clear();
for (int i = 0; i < instance.text.size(); ++i) {
instance.charPositions.append(QPoint(currentX, instance.basePosition.y()));
currentX += instance.charWidths[i];
}
}
}
}
++it;
}
}
void TextRenderer::render()
{
QOpenGLPaintDevice device(windowWidth, windowHeight);
QPainter painter(&device);
painter.setFont(font);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
for (const auto &instance : activeTexts) {
QColor mainColor = instance.primaryColor;
mainColor.setAlphaF(instance.alpha);
QColor outlineColor = instance.outlineColor;
outlineColor.setAlphaF(instance.alpha * 0.7f);
for (int i = 0; i < instance.charPositions.size(); ++i) {
const QPoint &pos = instance.charPositions[i];
if (pos.x() < -9999) continue; // 跳过隐藏字符
// 绘制轮廓
painter.setPen(outlineColor);
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
if (dx == 0 && dy == 0) continue;
painter.drawText(pos + QPoint(dx, dy), QString(instance.text[i]));
}
}
// 绘制主体
painter.setPen(mainColor);
painter.drawText(pos, QString(instance.text[i]));
}
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// Created by Administrator on 2025/3/4.
//
#ifndef AIRI_DESKTOPGRIL_AUDIOPAGE_H
#define AIRI_DESKTOPGRIL_AUDIOPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
class ElaComboBox;
class ElaSpinBox;
class ElaProgressBar;
class ElaPushButton;
class AudioPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit AudioPage(QWidget* parent = nullptr);
~AudioPage() override;
private:
ElaComboBox* audioInputDeviceComboBox = nullptr;
ElaSpinBox* audioInputSpinBox = nullptr;
ElaProgressBar* audioInputProgressBar = nullptr;
ElaPushButton* audioAutoThresholdStartButton = nullptr;
ElaPushButton* audioManualThresholdStartButton = nullptr;
ElaPushButton* testAudioPlayButton = nullptr;
};
#endif //AIRI_DESKTOPGRIL_AUDIOPAGE_H
+21
View File
@@ -0,0 +1,21 @@
//
// Created by Administrator on 2025/2/27.
//
#ifndef AIRI_DESKTOPGRIL_BASEPAGE_H
#define AIRI_DESKTOPGRIL_BASEPAGE_H
#include <ElaScrollPage.h>
class QVBoxLayout;
class BasePage : public ElaScrollPage
{
Q_OBJECT
public:
Q_INVOKABLE explicit BasePage(QWidget* parent = nullptr);
~BasePage();
protected:
void createCustomWidget(QString desText);
};
#endif //AIRI_DESKTOPGRIL_BASEPAGE_H
+23
View File
@@ -0,0 +1,23 @@
//
// Created by Administrator on 2025/2/28.
//
#ifndef AIRI_DESKTOPGRIL_HOMEPAGE_H
#define AIRI_DESKTOPGRIL_HOMEPAGE_H
#include "BasePage.h"
class ElaMenu;
class HomePage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit HomePage(QWidget* parent = nullptr);
~HomePage();
Q_SIGNALS:
Q_SIGNAL void audioNavigation();
Q_SIGNAL void modelShopNavigation();
};
#endif //AIRI_DESKTOPGRIL_HOMEPAGE_H
+50
View File
@@ -0,0 +1,50 @@
//
// Created by Administrator on 2025/4/1.
//
/**
* @brief 模型页面
* 暂时只做最简单功能切换模型
*/
#ifndef YOSUGA_MODELPAGE_H
#define YOSUGA_MODELPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaComboBox.h"
#include <QUrl>
#include <utility>
class ElaLineEdit;
class ElaPushButton;
class ModelPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit ModelPage(QWidget* parent = nullptr);
std::pair<QString, QString> splitPath(const QString& fullPath);
~ModelPage();
private:
// 设置当前模型
ElaLineEdit* modelUrlEdit = nullptr;
ElaPushButton* modelChoosePushButton = nullptr;
ElaPushButton* modelUsePushButton = nullptr;
QUrl modelFileUrl;
QString modelFilePathFirst;
QString modelFilePathSecond;
};
#endif //YOSUGA_MODELPAGE_H
+42
View File
@@ -0,0 +1,42 @@
//
// Created by Administrator on 2025/3/2.
//
#ifndef AIRI_DESKTOPGRIL_NETWORKPAGE_H
#define AIRI_DESKTOPGRIL_NETWORKPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
class ElaPushButton;
class ElaLineEdit;
class NetWorkPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit NetWorkPage(QWidget* parent = nullptr);
~NetWorkPage();
private:
// IP控件
ElaPushButton* ipPushButton = nullptr;
ElaLineEdit* ipLineEdit = nullptr;
// 端口控件
ElaPushButton* portPushButton = nullptr;
ElaLineEdit* portLineEdit = nullptr;
// 连接测试
ElaPushButton* connectTestPushButton = nullptr;
// 连接
ElaPushButton* connectPushButton = nullptr;
// 断开
ElaPushButton* disconnectPushButton = nullptr;
};
#endif //AIRI_DESKTOPGRIL_NETWORKPAGE_H
+30
View File
@@ -0,0 +1,30 @@
//
// Created by Administrator on 2025/3/30.
//
#ifndef YOSUGA_RENDERPAGE_H
#define YOSUGA_RENDERPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaComboBox.h"
class RenderPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit RenderPage(QWidget* parent = nullptr);
~RenderPage();
private:
// 帧率设置
ElaComboBox* frameRateComboBox = nullptr;
};
#endif //YOSUGA_RENDERPAGE_H
+68
View File
@@ -0,0 +1,68 @@
//
// Created by Administrator on 2025/1/21.
//
#ifndef AIRI_DESKTOPGRIL_SETTING_H
#define AIRI_DESKTOPGRIL_SETTING_H
#include <ElaWidget.h>
#include <ElaWindow.h>
#include <ElaPushButton.h>
#include <ElaScrollPage.h>
#include <QStackedWidget>
#include "HomePage.h"
#include "NetworkPage.h"
#include "UISetting.h"
#include "AudioPage.h"
#include "RenderPage.h"
#include "ModelPage.h"
class Setting : public ElaWindow
{
Q_OBJECT
public:
explicit Setting(QWidget *parent = nullptr);
~Setting();
private:
/**
* 初始化所有页面指针
* @author : Misaki
*/
void initPages();
/**
* 初始化导航栏
* @author : Misaki
*/
void initNavigationBar();
/**
* 初始化上下文切换
* @author : Misaki
*/
void initContent();
private slots:
void toggleTheme();
private:
// 页面指针
HomePage *homePage;
NetWorkPage *networkPage;
UISetting *uiSetting;
AudioPage *audioPage;
RenderPage *renderPage;
ModelPage *modelPage;
// 节点键值
QString basePageKey;
//
ElaPushButton *themeToggleButton;
};
#endif //AIRI_DESKTOPGRIL_SETTING_H
+29
View File
@@ -0,0 +1,29 @@
//
// Created by Administrator on 2025/3/2.
//
#ifndef AIRI_DESKTOPGRIL_UISETTING_H
#define AIRI_DESKTOPGRIL_UISETTING_H
#include "BasePage.h"
class ElaRadioButton;
class ElaToggleSwitch;
class ElaComboBox;
class UISetting : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit UISetting(QWidget* parent = nullptr);
~UISetting();
private:
ElaComboBox* _themeComboBox = nullptr;
ElaToggleSwitch* _micaSwitchButton = nullptr;
ElaToggleSwitch* _logSwitchButton = nullptr;
ElaRadioButton* _minimumButton = nullptr;
ElaRadioButton* _compactButton = nullptr;
ElaRadioButton* _maximumButton = nullptr;
ElaRadioButton* _autoButton = nullptr;
};
#endif //AIRI_DESKTOPGRIL_UISETTING_H
+131
View File
@@ -0,0 +1,131 @@
//
// Created by Administrator on 2025/3/4.
//
#include "AudioPage.h"
#include <QHBoxLayout>
#include "ElaComboBox.h"
#include "ElaPlainTextEdit.h"
#include "ElaProgressBar.h"
#include "ElaScrollPageArea.h"
#include "ElaSlider.h"
#include "ElaSpinBox.h"
#include "ElaText.h"
#include "ElaMessageBar.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "TextRenderer.h"
#include "LAppLive2DManager.hpp"
AudioPage::AudioPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("AudioPage");
audioInputDeviceComboBox = new ElaComboBox(this);
audioInputDeviceComboBox->setToolTip("选择可用的录音设备");
QStringList comboList = AudioInput::getAvailableAudioInputDevices();
audioInputDeviceComboBox->addItems(comboList);
ElaScrollPageArea* comboBoxArea = new ElaScrollPageArea(this);
QHBoxLayout* comboBoxLayout = new QHBoxLayout(comboBoxArea);
ElaText* comboBoxText = new ElaText("录音设备", this);
comboBoxText->setTextPixelSize(15);
comboBoxLayout->addWidget(comboBoxText);
comboBoxLayout->addWidget(audioInputDeviceComboBox);
comboBoxLayout->addStretch();
comboBoxLayout->addSpacing(10);
connect(audioInputDeviceComboBox, &ElaComboBox::currentTextChanged, [this](const QString& text) {
AudioInput::getInstance()->setAudioInputDevice(text);
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "成功设置 " + text + " 为当前录音设备", 800.0, this);
});
audioInputSpinBox = new ElaSpinBox(this); // SpinBox
audioInputSpinBox->setRange(0, 10000);
audioInputProgressBar = new ElaProgressBar(this);
audioInputProgressBar->setRange(0, 10000);
// 关闭ProgressBar的百分比显示
audioInputProgressBar->setTextVisible(false);
// 将SpinBox和ProgressBar的数值相互绑定
connect(audioInputSpinBox, QOverload<int>::of(&ElaSpinBox::valueChanged), [this](int value) {
audioInputProgressBar->setValue(value);
});
connect(audioInputProgressBar, QOverload<int>::of(&ElaProgressBar::valueChanged), [this](int value) {
audioInputSpinBox->setValue(value);
});
// 绑定实时录音阈值到ProgressBar,同时归一到0~1000范围内
connect(AudioInput::getInstance(), &AudioInput::rmsRealValue, [this](const qreal value) {
audioInputProgressBar->setValue(value);
});
// 当计算完成最优阈值
connect(AudioInput::getInstance(), &AudioInput::thresholdCalculated, [this](qreal value) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "自动计算出的最优阈值为:" + QString::number(value), 1000, this);
// AudioInput会自动设置计算出的最优阈值
});
audioAutoThresholdStartButton = new ElaPushButton("自动最优阈值", this);
audioAutoThresholdStartButton->setToolTip("点击后保持当前环境音5秒,自动计算出最合适的静音检测阈值");
connect(audioAutoThresholdStartButton, &ElaPushButton::clicked, [=]() {
AudioInput::getInstance()->startAutoThresholdClu(5000);
qDebug("开始计算最优阈值");
});
audioManualThresholdStartButton = new ElaPushButton("手动设置阈值", this);
audioManualThresholdStartButton->setToolTip("如果你觉得自动计算的不准的话");
connect(audioManualThresholdStartButton, &ElaPushButton::clicked, [this]() {
AudioInput::getInstance()->setSilenceThreshold(audioInputSpinBox->value());
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "手动设置的阈值为:" + QString::number(audioInputSpinBox->value()), 1000, this);
});
ElaScrollPageArea* audioInputProgressBarArea = new ElaScrollPageArea(this);
QHBoxLayout* audioInputProgressBarLayout = new QHBoxLayout(audioInputProgressBarArea);
ElaText* audioInputProgressBarText = new ElaText("录音阈值", this);
audioInputProgressBarText->setTextPixelSize(15);
audioInputProgressBarLayout->addWidget(audioInputProgressBarText);
audioInputProgressBarLayout->addWidget(audioInputProgressBar, 1);
audioInputProgressBarLayout->addWidget(audioInputSpinBox);
audioInputProgressBarLayout->addStretch(); // 添加弹性空间将后续控件推到右侧
audioInputProgressBarLayout->addWidget(audioAutoThresholdStartButton);
audioInputProgressBarLayout->addWidget(audioManualThresholdStartButton);
audioInputProgressBarLayout->addStretch();
audioInputProgressBarLayout->addSpacing(10);
testAudioPlayButton = new ElaPushButton("播放测试", this);
testAudioPlayButton->setToolTip("播放一段测试音频来检测播放功能是否正常,注意观察模型嘴唇以及文字下落动画");
ElaScrollPageArea* testAudioArea = new ElaScrollPageArea(this);
QHBoxLayout* testAudioLayout = new QHBoxLayout(testAudioArea);
ElaText* testAudioText = new ElaText("测试", this);
testAudioText->setTextPixelSize(15);
testAudioLayout->addWidget(testAudioText);
testAudioLayout->addStretch(); // 添加弹性空间将后续控件推到右侧
testAudioLayout->addWidget(testAudioPlayButton);
testAudioLayout->addSpacing(10);
connect(testAudioPlayButton, &ElaPushButton::clicked, [this]() {
const QString text = "あれアイリーじゃないよ!急にいなくなるからどこに行ったのかと思えば~";
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"));
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("音频设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(comboBoxArea);
centerLayout->addWidget(audioInputProgressBarArea);
centerLayout->addWidget(testAudioArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
AudioPage::~AudioPage()
{
}
+89
View File
@@ -0,0 +1,89 @@
//
// Created by Administrator on 2025/2/27.
//
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "BasePage.h"
#include "ElaMenu.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaToolButton.h"
BasePage::BasePage(QWidget* parent)
: ElaScrollPage(parent)
{
connect(eTheme, &ElaTheme::themeModeChanged, this, [=, this]() {
if (!parent)
{
update();
}
});
}
BasePage::~BasePage()
{
}
void BasePage::createCustomWidget(QString desText)
{
// 顶部元素
QWidget* customWidget = new QWidget(this);
ElaText* subTitleText = new ElaText(this);
subTitleText->setText("https://github.com/Liniyous/ElaWidgetTools");
subTitleText->setTextInteractionFlags(Qt::TextSelectableByMouse);
subTitleText->setTextPixelSize(11);
ElaToolButton* documentationButton = new ElaToolButton(this);
documentationButton->setFixedHeight(35);
documentationButton->setIsTransparent(false);
documentationButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
//_toolButton->setPopupMode(QToolButton::MenuButtonPopup);
documentationButton->setText("Documentation");
documentationButton->setElaIcon(ElaIconType::FileDoc);
ElaMenu* documentationMenu = new ElaMenu(this);
documentationMenu->addElaIconAction(ElaIconType::CardsBlank, "CardsBlank");
documentationMenu->addElaIconAction(ElaIconType::EarthAmericas, "EarthAmericas");
documentationButton->setMenu(documentationMenu);
ElaToolButton* sourceButton = new ElaToolButton(this);
sourceButton->setFixedHeight(35);
sourceButton->setIsTransparent(false);
sourceButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
sourceButton->setText("Source");
sourceButton->setElaIcon(ElaIconType::NfcSymbol);
ElaMenu* sourceMenu = new ElaMenu(this);
sourceMenu->addElaIconAction(ElaIconType::FireBurner, "FireBurner");
sourceMenu->addElaIconAction(ElaIconType::Galaxy, "Galaxy~~~~");
sourceButton->setMenu(sourceMenu);
ElaToolButton* themeButton = new ElaToolButton(this);
themeButton->setFixedSize(35, 35);
themeButton->setIsTransparent(false);
themeButton->setElaIcon(ElaIconType::MoonStars);
connect(themeButton, &ElaToolButton::clicked, this, [=]() {
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark : ElaThemeType::Light);
});
QHBoxLayout* buttonLayout = new QHBoxLayout();
buttonLayout->addWidget(documentationButton);
buttonLayout->addSpacing(5);
buttonLayout->addWidget(sourceButton);
buttonLayout->addStretch();
buttonLayout->addWidget(themeButton);
buttonLayout->addSpacing(15);
ElaText* descText = new ElaText(this);
descText->setText(desText);
descText->setTextPixelSize(13);
QVBoxLayout* topLayout = new QVBoxLayout(customWidget);
topLayout->setContentsMargins(0, 0, 0, 0);
topLayout->addWidget(subTitleText);
topLayout->addSpacing(5);
topLayout->addLayout(buttonLayout);
topLayout->addSpacing(5);
topLayout->addWidget(descText);
setCustomWidget(customWidget);
}
+158
View File
@@ -0,0 +1,158 @@
//
// Created by Administrator on 2025/2/28.
//
#include "HomePage.h"
#include <QDebug>
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QVBoxLayout>
#include "ElaAcrylicUrlCard.h"
#include "ElaFlowLayout.h"
#include "ElaImageCard.h"
#include "ElaMenu.h"
#include "ElaMessageBar.h"
#include "ElaNavigationRouter.h"
#include "ElaPopularCard.h"
#include "ElaScrollArea.h"
#include "ElaText.h"
#include "ElaToolTip.h"
HomePage::HomePage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("Home");
setTitleVisible(false);
setContentsMargins(2, 2, 0, 0);
// 标题卡片区域
ElaText* desText = new ElaText("UI By ElaWidgetTools", this);
desText->setTextPixelSize(18);
ElaText* titleText = new ElaText("Yosuga!", this);
titleText->setTextPixelSize(35);
QVBoxLayout* titleLayout = new QVBoxLayout();
titleLayout->setContentsMargins(30, 60, 0, 0);
titleLayout->addWidget(desText);
titleLayout->addWidget(titleText);
ElaImageCard* backgroundCard = new ElaImageCard(this);
backgroundCard->setBorderRadius(10);
backgroundCard->setFixedHeight(400);
backgroundCard->setMaximumAspectRatio(1.7);
backgroundCard->setCardImage(QImage("Resources/Pic/Airi/Airi_Background.png"));
ElaAcrylicUrlCard* urlCard1 = new ElaAcrylicUrlCard(this);
urlCard1->setCardPixmapSize(QSize(62, 62));
urlCard1->setFixedSize(195, 225);
urlCard1->setTitlePixelSize(17);
urlCard1->setTitleSpacing(25);
urlCard1->setSubTitleSpacing(13);
urlCard1->setUrl("https://github.com/Misakiotoha/Yosuga");
urlCard1->setCardPixmap(QPixmap("Resources/Pic/Others/img.png"));
urlCard1->setTitle("Yosuga Github");
urlCard1->setSubTitle("Star++!");
ElaToolTip* urlCard1ToolTip = new ElaToolTip(urlCard1);
urlCard1ToolTip->setToolTip("点击前往本项目GitHub");
ElaAcrylicUrlCard* urlCard2 = new ElaAcrylicUrlCard(this);
urlCard2->setCardPixmapSize(QSize(62, 62));
urlCard2->setFixedSize(195, 225);
urlCard2->setTitlePixelSize(17);
urlCard2->setTitleSpacing(25);
urlCard2->setSubTitleSpacing(13);
urlCard2->setUrl("https://space.bilibili.com/140315806");
urlCard2->setCardPixmap(QPixmap("Resources/Pic/Others/Misaki.jpg"));
urlCard2->setTitle("Misaki");
urlCard2->setSubTitle("1841738040@qq.com");
ElaToolTip* urlCard2ToolTip = new ElaToolTip(urlCard2);
urlCard2ToolTip->setToolTip("点击前往 Misaki 的个人主页");
ElaScrollArea* cardScrollArea = new ElaScrollArea(this);
cardScrollArea->setWidgetResizable(true);
cardScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
cardScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
cardScrollArea->setIsGrabGesture(true, 0);
cardScrollArea->setIsOverShoot(Qt::Horizontal, true);
QWidget* cardScrollAreaWidget = new QWidget(this);
cardScrollAreaWidget->setStyleSheet("background-color:transparent;");
cardScrollArea->setWidget(cardScrollAreaWidget);
QHBoxLayout* urlCardLayout = new QHBoxLayout();
urlCardLayout->setSpacing(15);
urlCardLayout->setContentsMargins(30, 0, 0, 6);
urlCardLayout->addWidget(urlCard1);
urlCardLayout->addWidget(urlCard2);
urlCardLayout->addStretch();
QVBoxLayout* cardScrollAreaWidgetLayout = new QVBoxLayout(cardScrollAreaWidget);
cardScrollAreaWidgetLayout->setContentsMargins(0, 0, 0, 0);
cardScrollAreaWidgetLayout->addStretch();
cardScrollAreaWidgetLayout->addLayout(urlCardLayout);
QVBoxLayout* backgroundLayout = new QVBoxLayout(backgroundCard);
backgroundLayout->setContentsMargins(0, 0, 0, 0);
backgroundLayout->addLayout(titleLayout);
backgroundLayout->addWidget(cardScrollArea);
// 推荐卡片
ElaText* flowText = new ElaText("快速转到", this);
flowText->setTextPixelSize(20);
QHBoxLayout* flowTextLayout = new QHBoxLayout();
flowTextLayout->setContentsMargins(33, 0, 0, 0);
flowTextLayout->addWidget(flowText);
// ElaFlowLayout
// 模型商店卡片
ElaPopularCard* ModeShopCard = new ElaPopularCard(this);
connect(ModeShopCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() {
Q_EMIT modelShopNavigation();
});
ModeShopCard->setCardPixmap(QPixmap("Resources/Pic/Others/Live2D.png"));
ModeShopCard->setTitle("模型商店");
ModeShopCard->setSubTitle("属于你的Live2D模型");
ModeShopCard->setInteractiveTips("By Misaki");
ModeShopCard->setDetailedText("选择你喜欢的Live2D模型,模型来自多个作者,多个平台,有免费也有收费的");
// 音频设置卡片
ElaPopularCard* AudioSettingCard = new ElaPopularCard(this);
connect(AudioSettingCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() {
Q_EMIT audioNavigation();
});
AudioSettingCard->setTitle("音频设置");
AudioSettingCard->setSubTitle("录音与播放的设置");
AudioSettingCard->setCardPixmap(QPixmap("Resources/Pic/control/AutomationProperties.png"));
AudioSettingCard->setInteractiveTips("By Misaki");
AudioSettingCard->setDetailedText("自定义音频与播放的相关设定,打造最舒适的交流环境。");
ElaFlowLayout* flowLayout = new ElaFlowLayout(0, 5, 5);
flowLayout->setContentsMargins(30, 0, 0, 0);
flowLayout->setIsAnimation(true);
flowLayout->addWidget(ModeShopCard);
flowLayout->addWidget(AudioSettingCard);
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("Home");
QVBoxLayout* centerVLayout = new QVBoxLayout(centralWidget);
centerVLayout->setSpacing(0);
centerVLayout->setContentsMargins(0, 0, 0, 0);
centerVLayout->addWidget(backgroundCard);
centerVLayout->addSpacing(20);
centerVLayout->addLayout(flowTextLayout);
centerVLayout->addSpacing(10);
centerVLayout->addLayout(flowLayout);
centerVLayout->addStretch();
addCentralWidget(centralWidget);
// 初始化提示
ElaMessageBar::success(ElaMessageBarType::BottomRight, "Success", "初始化成功!", 2000);
qDebug() << "初始化成功";
}
HomePage::~HomePage()
{
}
+104
View File
@@ -0,0 +1,104 @@
//
// Created by Administrator on 2025/4/1.
//
#include "ModelPage.h"
#include <QFileInfo>
#include <QFileDialog>
#include <QDebug>
#include <QHBoxLayout>
#include <QtWidgets>
#include "ElaComboBox.h"
#include "ElaMessageBar.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "LAppLive2DManager.hpp"
ModelPage::ModelPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("ModelPage");
modelUrlEdit = new ElaLineEdit(this);
modelUrlEdit->setFixedWidth(300);
modelUrlEdit->setPlaceholderText("用于显示当前的模型Url");
modelChoosePushButton = new ElaPushButton("选择模型", this);
modelChoosePushButton->setToolTip("选择.model3.json结尾的文件");
modelUsePushButton = new ElaPushButton("使用模型", this);
modelUsePushButton->setToolTip("使用选择的模型或Url对应的模型");
ElaScrollPageArea* modelSetArea = new ElaScrollPageArea(this);
QHBoxLayout* modelSetLayout = new QHBoxLayout(modelSetArea);
ElaText* modelSetText = new ElaText("模型设置", this);
modelSetText->setTextPixelSize(15);
modelSetLayout->addWidget(modelSetText);
modelSetLayout->addWidget(modelUrlEdit);
modelSetLayout->addStretch();
modelSetLayout->addWidget(modelChoosePushButton);
modelSetLayout->addWidget(modelUsePushButton);
modelSetLayout->addSpacing(10);
connect(modelChoosePushButton, &ElaPushButton::clicked, this, [this]() {
// 获取当前exe所在目录的本地路径
QString exeDir = QCoreApplication::applicationDirPath();
// 转换为QUrl格式(自动处理路径分隔符)
QUrl initialDir = QUrl::fromLocalFile(exeDir);
// 打开文件选择对话框
modelFileUrl = QFileDialog::getOpenFileUrl(
this,
"选择模型文件",
initialDir, // 初始目录为当前目录
"*.model3.json");
// 检查url是否有效
if(!modelFileUrl.isEmpty()){
QString t = modelFileUrl.toLocalFile();
std::pair<QString, QString> path = this->splitPath(t);
this->modelFilePathFirst = path.first;
this->modelFilePathSecond = path.second;
this->modelUrlEdit->setText(t);
}
else{
ElaMessageBar::information(ElaMessageBarType::BottomRight, "模型设置", "似乎并没有选择模型", 800.0, this);
}
});
connect(modelUsePushButton, &ElaPushButton::clicked, this, [this]() {
if(!modelFileUrl.isEmpty()){
LAppLive2DManager::GetInstance()->LoadModelFromPath(this->modelFilePathFirst.toStdString(), this->modelFilePathSecond.toStdString());
}
else{
ElaMessageBar::information(ElaMessageBarType::BottomRight, "模型设置", "似乎并没有选择模型", 800.0, this);
}
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("模型商店");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(modelSetArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
// 返回 pair<目录路径, 文件名>
std::pair<QString, QString> ModelPage::splitPath(const QString& fullPath)
{
QFileInfo fileInfo(fullPath);
// 获取目录部分(自动处理末尾斜杠)
QString dirPath = fileInfo.dir().absolutePath() + "/";
// 获取文件名部分(如果是目录则返回空)
QString fileName = fileInfo.fileName();
return {dirPath, fileName};
}
ModelPage::~ModelPage()
{
}
+157
View File
@@ -0,0 +1,157 @@
//
// Created by Administrator on 2025/3/2.
//
#include "NetworkPage.h"
#include <QHBoxLayout>
#include "ElaComboBox.h"
#include "ElaPlainTextEdit.h"
#include "ElaScrollPageArea.h"
#include "ElaSpinBox.h"
#include "ElaText.h"
#include "socketmanager.h"
#include <QHostAddress>
#include "ElaMessageBar.h"
NetWorkPage::NetWorkPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("NetworkPage");
// ip
ipPushButton = new ElaPushButton("设定",this);
ipLineEdit = new ElaLineEdit(this);
ElaScrollPageArea* ipToggleSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* ipToggleSwitchLayout = new QHBoxLayout(ipToggleSwitchArea);
ElaText* ipToggleSwitchText = new ElaText("服务端IP:", this);
ipToggleSwitchText->setTextPixelSize(15);
ipToggleSwitchLayout->addWidget(ipToggleSwitchText);
ipToggleSwitchLayout->addWidget(ipLineEdit);
ipToggleSwitchLayout->addStretch();
connect(ipPushButton, &ElaPushButton::clicked, this, [=, this]() {
// 我爱lambda函数
QString ip_temp = ipLineEdit->text();
auto f_temp = [=](const QString &ip) -> bool {
QHostAddress addr;
if (addr.setAddress(ip) && addr.protocol() == QAbstractSocket::IPv4Protocol) {
return true;
}
return false;
};
if(!f_temp(ip_temp)){
ElaMessageBar::error(ElaMessageBarType::TopLeft, "连接设置", "服务端IP格式错误", 800.0, this);
}
else{
SocketManager::getInstance()->setIp(ip_temp);
ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置", "服务端IP设置成功", 800.0, this);
}
});
ipToggleSwitchLayout->addWidget(ipPushButton);
ipToggleSwitchLayout->addSpacing(10);
// 端口
portPushButton = new ElaPushButton("设定",this);
portLineEdit = new ElaLineEdit(this);
ElaScrollPageArea* portToggleSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* portToggleSwitchLayout = new QHBoxLayout(portToggleSwitchArea);
ElaText* portToggleSwitchText = new ElaText("服务端端口:", this);
portToggleSwitchText->setTextPixelSize(15);
portToggleSwitchLayout->addWidget(portToggleSwitchText);
portToggleSwitchLayout->addWidget(portLineEdit);
portToggleSwitchLayout->addStretch();
connect(portPushButton, &ElaPushButton::clicked, this, [=, this]() {
QString port_temp = portLineEdit->text();
auto f_temp = [=](const QString &port) -> bool {
return port.toInt() > 0 && port.toInt() < 65535;
};
if(!f_temp(port_temp)){
ElaMessageBar::error(ElaMessageBarType::TopLeft, "连接设置", "服务端端口格式错误", 800.0, this);
}
else{
SocketManager::getInstance()->setPort(port_temp.toInt());
ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置", "服务端端口设置成功", 800.0, this);
}
});
portToggleSwitchLayout->addWidget(portPushButton);
portToggleSwitchLayout->addSpacing(10);
connectTestPushButton = new ElaPushButton("连通测试",this);
connectTestPushButton->setToolTip("测试与服务器连通性(如果成功连通会自动连上服务器)");
connectPushButton = new ElaPushButton("连接",this);
disconnectPushButton = new ElaPushButton("断开",this);
ElaScrollPageArea* connectTestArea = new ElaScrollPageArea(this);
QHBoxLayout* connectTestLayout = new QHBoxLayout(connectTestArea);
connectTestLayout->addWidget(connectTestPushButton);
connectTestLayout->addStretch();
connect(connectTestPushButton, &ElaPushButton::clicked, this, [=, this]() {
if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){
// 如果已连接
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "已连通", 800.0, this);
}
else{
SocketManager::getInstance()->connectToServer();
// TODO:等待很短的时间,等tcp握手完成
if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "已连通", 800.0, this);
}
else{
ElaMessageBar::error(ElaMessageBarType::TopLeft, "连通测试", "未连通,请检查服务器是否开启或IP和端口信息是否正确", 1500.0, this);
}
}
});
connect(connectPushButton, &ElaPushButton::clicked, this, [=, this]() {
if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){
// 如果已连接
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "已连接", 800.0, this);
}
if(SocketManager::getInstance()->state() == QAbstractSocket::UnconnectedState){
// 如果未连接
SocketManager::getInstance()->connectToServer();
if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "连接成功", 800.0, this);
}
else{
ElaMessageBar::error(ElaMessageBarType::TopLeft, "连通测试", "连接失败,请检查服务器是否开启或IP和端口信息是否正确", 1500.0, this);
}
}
});
connect(disconnectPushButton, &ElaPushButton::clicked, this, [=, this]() {
if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){
// 如果已连接,则断开
SocketManager::getInstance()->disconnectFromServer();
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "断开成功", 800.0, this);
}
else{
// 如果未连接,则提示
ElaMessageBar::information(ElaMessageBarType::BottomRight, "连通测试", "似乎并没有连接到服务器", 800.0, this);
}
});
connectTestLayout->addWidget(disconnectPushButton);
connectTestLayout->addWidget(connectPushButton);
connectTestLayout->addSpacing(10);
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("连接设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(ipToggleSwitchArea);
centerLayout->addWidget(portToggleSwitchArea);
centerLayout->addWidget(connectTestArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
NetWorkPage::~NetWorkPage()
{
}
+54
View File
@@ -0,0 +1,54 @@
//
// Created by Administrator on 2025/3/30.
//
#include "RenderPage.h"
#include <QHBoxLayout>
#include <QtWidgets>
#include "ElaComboBox.h"
#include "ElaMessageBar.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "GLCore.h"
#include "AppContext.h"
RenderPage::RenderPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("RenderPage");
frameRateComboBox = new ElaComboBox(this);
QStringList frameRateComboList = GLCore::getFrameRateList();
frameRateComboBox->addItems(frameRateComboList);
ElaScrollPageArea* frameRateComboBoxArea = new ElaScrollPageArea(this);
QHBoxLayout* frameRateComboBoxLayout = new QHBoxLayout(frameRateComboBoxArea);
ElaText* frameRateComboBoxText = new ElaText("帧率设置", this);
frameRateComboBoxText->setTextPixelSize(15);
frameRateComboBoxLayout->addWidget(frameRateComboBoxText);
frameRateComboBoxLayout->addStretch();
frameRateComboBoxLayout->addWidget(frameRateComboBox);
frameRateComboBoxLayout->addSpacing(10);
connect(frameRateComboBox, &ElaComboBox::currentTextChanged, this, [this](const QString& text) {
AppContext::GetGLCore()->setFrameRate(GLCore::getFrameRateMap().value(text));
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("渲染设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(frameRateComboBoxArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
RenderPage::~RenderPage()
{
}
+114
View File
@@ -0,0 +1,114 @@
//
// Created by Administrator on 2025/1/21.
//
#include <ElaTheme.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSvgRenderer>
#include <QPainter>
#include "Setting.h"
#include "socketmanager.h"
Setting::Setting(QWidget *parent)
: ElaWindow(parent)
{
// 设置窗口标题
setWindowTitle("设置");
// 设置窗口图标
setWindowIcon(QIcon("Resources/Pic/Airi/Airi_s.svg"));
// 初始化窗口
resize(1000, 740);
// 禁用窗口缩放按钮
setWindowButtonFlag(ElaAppBarType::MaximizeButtonHint, false); // 隐藏最大化按钮
setWindowButtonFlag(ElaAppBarType::MinimizeButtonHint, false); // 隐藏最小化按钮
// 移动窗口到屏幕中心
this->moveToCenter();
this->setWindowFlag(Qt::Tool); // 隐藏应用程序图标
this->setWindowFlag(Qt::WindowStaysOnTopHint); // 默认设置窗口始终在顶部
// 设置用户信息卡
this->setUserInfoCardTitle("Yosuga");
this->setUserInfoCardSubTitle("联系维度的桥梁!");
// 加载 SVG 图片
QSvgRenderer renderer(QString("Resources/Pic/Airi/Airi.svg"));
// 创建 QPixmap 并绘制 SVG
QPixmap pixmap(64, 64);
pixmap.fill(Qt::transparent); // 设置透明背景
QPainter painter(&pixmap);
renderer.render(&painter);
this->setUserInfoCardPixmap(pixmap);
// 初始化页面
initPages();
// 创建导航栏和内容区域
initNavigationBar();
// 创建上下文
initContent();
// 设置初始主题
eTheme->setThemeMode(ElaThemeType::Dark);
}
Setting::~Setting()
{
}
void Setting::initPages()
{
homePage = new HomePage(this);
networkPage = new NetWorkPage(this);
uiSetting = new UISetting(this);
audioPage = new AudioPage(this);
renderPage = new RenderPage(this);
modelPage = new ModelPage(this);
}
void Setting::initNavigationBar()
{
// 添加主页节点(顶级节点)
addPageNode("主页", homePage, ElaIconType::House);
// 添加模型商店节点
addPageNode("模型商店", modelPage, ElaIconType::Shop);
// 添加网络连接设置节点
addPageNode("连接设置", networkPage, ElaIconType::NetworkWired);
// 添加音频设置节点
addPageNode("音频设置", audioPage, ElaIconType::MusicNote);
// 添加渲染设置节点
addPageNode("渲染设置", renderPage, ElaIconType::ArrowsRotate);
QString uiSettingKey;
addFooterNode("UI设置", uiSetting, uiSettingKey, 0, ElaIconType::GearComplex);
}
void Setting::initContent()
{
connect(homePage, &HomePage::modelShopNavigation, this, [&](){
this->navigation(modelPage->property("ElaPageKey").toString());
});
connect(homePage, &HomePage::audioNavigation, this, [&](){
this->navigation(audioPage->property("ElaPageKey").toString());
});
}
void Setting::toggleTheme()
{
if (eTheme->getThemeMode() == ElaThemeType::Light) {
eTheme->setThemeMode(ElaThemeType::Dark);
} else {
eTheme->setThemeMode(ElaThemeType::Light);
}
}
+166
View File
@@ -0,0 +1,166 @@
//
// Created by Administrator on 2025/3/2.
//
#include "UISetting.h"
#include <QDebug>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "ElaApplication.h"
#include "ElaComboBox.h"
#include "ElaLog.h"
#include "ElaRadioButton.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaToggleSwitch.h"
#include "ElaWindow.h"
UISetting::UISetting(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
ElaWindow* window = dynamic_cast<ElaWindow*>(parent);
setWindowTitle("Setting");
ElaText* themeText = new ElaText("主题设置", this);
themeText->setWordWrap(false);
themeText->setTextPixelSize(18);
_themeComboBox = new ElaComboBox(this);
_themeComboBox->addItem("日间模式");
_themeComboBox->addItem("夜间模式");
ElaScrollPageArea* themeSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* themeSwitchLayout = new QHBoxLayout(themeSwitchArea);
ElaText* themeSwitchText = new ElaText("主题切换", this);
themeSwitchText->setWordWrap(false);
themeSwitchText->setTextPixelSize(15);
themeSwitchLayout->addWidget(themeSwitchText);
themeSwitchLayout->addStretch();
themeSwitchLayout->addWidget(_themeComboBox);
connect(_themeComboBox, QOverload<int>::of(&ElaComboBox::currentIndexChanged), this, [=](int index) {
if (index == 0)
{
eTheme->setThemeMode(ElaThemeType::Light);
}
else
{
eTheme->setThemeMode(ElaThemeType::Dark);
}
});
connect(eTheme, &ElaTheme::themeModeChanged, this, [=, this](ElaThemeType::ThemeMode themeMode) {
_themeComboBox->blockSignals(true);
if (themeMode == ElaThemeType::Light)
{
_themeComboBox->setCurrentIndex(0);
}
else
{
_themeComboBox->setCurrentIndex(1);
}
_themeComboBox->blockSignals(false);
});
ElaText* helperText = new ElaText("应用程序设置", this);
helperText->setWordWrap(false);
helperText->setTextPixelSize(18);
_micaSwitchButton = new ElaToggleSwitch(this);
ElaScrollPageArea* micaSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* micaSwitchLayout = new QHBoxLayout(micaSwitchArea);
ElaText* micaSwitchText = new ElaText("启用云母效果", this);
micaSwitchText->setWordWrap(false);
micaSwitchText->setTextPixelSize(15);
micaSwitchLayout->addWidget(micaSwitchText);
micaSwitchLayout->addStretch();
micaSwitchLayout->addWidget(_micaSwitchButton);
connect(_micaSwitchButton, &ElaToggleSwitch::toggled, this, [=](bool checked) {
eApp->setIsEnableMica(checked);
});
_logSwitchButton = new ElaToggleSwitch(this);
ElaScrollPageArea* logSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* logSwitchLayout = new QHBoxLayout(logSwitchArea);
ElaText* logSwitchText = new ElaText("启用日志功能", this);
logSwitchText->setWordWrap(false);
logSwitchText->setTextPixelSize(15);
logSwitchLayout->addWidget(logSwitchText);
logSwitchLayout->addStretch();
logSwitchLayout->addWidget(_logSwitchButton);
connect(_logSwitchButton, &ElaToggleSwitch::toggled, this, [=](bool checked) {
ElaLog::getInstance()->initMessageLog(checked);
if (checked)
{
qDebug() << "日志已启用!";
}
else
{
qDebug() << "日志已关闭!";
}
});
_minimumButton = new ElaRadioButton("Minimum", this);
_compactButton = new ElaRadioButton("Compact", this);
_maximumButton = new ElaRadioButton("Maximum", this);
_autoButton = new ElaRadioButton("Auto", this);
_autoButton->setChecked(true);
ElaScrollPageArea* displayModeArea = new ElaScrollPageArea(this);
QHBoxLayout* displayModeLayout = new QHBoxLayout(displayModeArea);
ElaText* displayModeText = new ElaText("导航栏模式选择", this);
displayModeText->setWordWrap(false);
displayModeText->setTextPixelSize(15);
displayModeLayout->addWidget(displayModeText);
displayModeLayout->addStretch();
displayModeLayout->addWidget(_minimumButton);
displayModeLayout->addWidget(_compactButton);
displayModeLayout->addWidget(_maximumButton);
displayModeLayout->addWidget(_autoButton);
connect(_minimumButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Minimal);
}
});
connect(_compactButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Compact);
}
});
connect(_maximumButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Maximal);
}
});
connect(_autoButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Auto);
}
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("UI设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addSpacing(30);
centerLayout->addWidget(themeText);
centerLayout->addSpacing(10);
centerLayout->addWidget(themeSwitchArea);
centerLayout->addSpacing(15);
centerLayout->addWidget(helperText);
centerLayout->addSpacing(10);
centerLayout->addWidget(logSwitchArea);
centerLayout->addWidget(micaSwitchArea);
centerLayout->addWidget(displayModeArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
UISetting::~UISetting()
{
}
+1
View File
@@ -0,0 +1 @@
设置界面,UI使用Ela UI