first
This commit is contained in:
@@ -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; /// 静音持续时间
|
||||
};
|
||||
|
||||
@@ -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; /// <!存储内存数据
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
实现了录音和播放的功能
|
||||
@@ -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
|
||||
@@ -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; /// 当前鼠标位置
|
||||
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -0,0 +1 @@
|
||||
本目录下为右键菜单的相关代码
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
设置界面,UI使用Ela UI
|
||||
Reference in New Issue
Block a user