1. 优化了音频输出输出模块的内容
2. 新增了对模型动作组管理的接口,方便后续模型的动作管理
This commit is contained in:
+23
-4
@@ -14,7 +14,9 @@
|
|||||||
#include <Rendering/OpenGL/CubismOffscreenSurface_OpenGLES2.hpp>
|
#include <Rendering/OpenGL/CubismOffscreenSurface_OpenGLES2.hpp>
|
||||||
|
|
||||||
#include "LAppWavFileHandler.hpp"
|
#include "LAppWavFileHandler.hpp"
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
/**
|
/**
|
||||||
* @brief ユーザーが実際に使用するモデルの実装クラス<br>
|
* @brief ユーザーが実際に使用するモデルの実装クラス<br>
|
||||||
* モデル生成、機能コンポーネント生成、更新処理とレンダリングの呼び出しを行う。
|
* モデル生成、機能コンポーネント生成、更新処理とレンダリングの呼び出しを行う。
|
||||||
@@ -34,19 +36,35 @@ public:
|
|||||||
*/
|
*/
|
||||||
virtual ~LAppModel();
|
virtual ~LAppModel();
|
||||||
|
|
||||||
|
struct MotionInfo {
|
||||||
|
std::string FileName; // 动作的文件名
|
||||||
|
int SequenceId; // 组内的播放序列号
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按组分类的动作映射表
|
||||||
|
* Key: 组名 (如 "Idle"), Value: 动作信息列表
|
||||||
|
*/
|
||||||
|
std::map<std::string, std::vector<MotionInfo>> GetMotionMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在控制台打印当前模型所有的动作组和序列信息
|
||||||
|
*/
|
||||||
|
void DumpMotionMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获得Idle动画总数量
|
* @brief 获得Idle动画总数量
|
||||||
* @author Misaki
|
* @author Misaki
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
int getIdleMotionCount();
|
[[nodiscard]] int getIdleMotionCount() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获得TapBody动画总数量
|
* @brief 获得TapBody动画总数量
|
||||||
* @author Misaki
|
* @author Misaki
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
int getTapBodyMotionCount();
|
[[nodiscard]] int getTapBodyMotionCount() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取 Live2D 模型的 Canvas 宽度像素 (在 Live2D 坐标系下)
|
* @brief 获取 Live2D 模型的 Canvas 宽度像素 (在 Live2D 坐标系下)
|
||||||
@@ -198,6 +216,8 @@ private:
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] bool IsPointOnDrawable(Csm::csmFloat32 x, Csm::csmFloat32 y);
|
[[nodiscard]] bool IsPointOnDrawable(Csm::csmFloat32 x, Csm::csmFloat32 y);
|
||||||
|
|
||||||
|
// 辅助函数: 从完整路径提取纯文件名
|
||||||
|
std::string ExtractFileName(const std::string& fullPath);
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief model3.jsonからモデルを生成する。<br>
|
* @brief model3.jsonからモデルを生成する。<br>
|
||||||
@@ -266,6 +286,5 @@ private:
|
|||||||
Live2D::Cubism::Framework::csmFloat32 alpha = 0.8f; // 滤波系数,范围在0到1之间,值越小,平滑效果越强
|
Live2D::Cubism::Framework::csmFloat32 alpha = 0.8f; // 滤波系数,范围在0到1之间,值越小,平滑效果越强
|
||||||
Live2D::Cubism::Framework::csmFloat32 filteredValue = 0.0f; // 滤波后的值
|
Live2D::Cubism::Framework::csmFloat32 filteredValue = 0.0f; // 滤波后的值
|
||||||
|
|
||||||
|
|
||||||
Csm::Rendering::CubismOffscreenSurface_OpenGLES2 _renderBuffer; ///< フレームバッファ以外の描画先
|
Csm::Rendering::CubismOffscreenSurface_OpenGLES2 _renderBuffer; ///< フレームバッファ以外の描画先
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ namespace LAppDefine {
|
|||||||
// const csmChar* PowerImageName = "close.png";
|
// const csmChar* PowerImageName = "close.png";
|
||||||
|
|
||||||
// モデル定義------------------------------------------
|
// モデル定義------------------------------------------
|
||||||
// 外部定義ファイル(json)と合わせる
|
// 外部定義ファイル(json)と合わせる [要注意:部分模型可能缺失下面的某个字段或者全部缺失]
|
||||||
const csmChar* MotionGroupIdle = "Idle"; // アイドリング
|
const csmChar* MotionGroupIdle = "Idle"; // アイドリング
|
||||||
const csmChar* MotionGroupTapBody = "TapBody"; // 体をタップしたとき
|
const csmChar* MotionGroupTapBody = "TapBody"; // 体をタップしたとき
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ LAppLive2DManager::LAppLive2DManager()
|
|||||||
// Resources/Haru/ Haru.model3.json
|
// Resources/Haru/ Haru.model3.json
|
||||||
LoadModelFromPath("Resources/Live2DModels/KITU17/", "KITU17.model3.json"); // 默认加载的模型
|
LoadModelFromPath("Resources/Live2DModels/KITU17/", "KITU17.model3.json"); // 默认加载的模型
|
||||||
//ChangeScene(_sceneIndex);
|
//ChangeScene(_sceneIndex);
|
||||||
|
if (DebugLogEnable) {
|
||||||
|
_models[0]->DumpMotionMap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LAppLive2DManager::~LAppLive2DManager()
|
LAppLive2DManager::~LAppLive2DManager()
|
||||||
@@ -217,11 +220,10 @@ void LAppLive2DManager::OnTap(csmFloat32 x, csmFloat32 y)
|
|||||||
|
|
||||||
void LAppLive2DManager::OnUpdate() const
|
void LAppLive2DManager::OnUpdate() const
|
||||||
{
|
{
|
||||||
int width, height;
|
|
||||||
//glfwGetWindowSize(LAppDelegate::GetInstance()->GetWindow(), &width, &height);
|
//glfwGetWindowSize(LAppDelegate::GetInstance()->GetWindow(), &width, &height);
|
||||||
|
|
||||||
width = LAppDelegate::GetInstance()->GetWindow()->width();
|
int width = LAppDelegate::GetInstance()->GetWindow()->width();
|
||||||
height = LAppDelegate::GetInstance()->GetWindow()->height();
|
int height = LAppDelegate::GetInstance()->GetWindow()->height();
|
||||||
|
|
||||||
csmUint32 modelCount = _models.GetSize();
|
csmUint32 modelCount = _models.GetSize();
|
||||||
for (csmUint32 i = 0; i < modelCount; ++i)
|
for (csmUint32 i = 0; i < modelCount; ++i)
|
||||||
@@ -263,7 +265,6 @@ void LAppLive2DManager::OnUpdate() const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#include <AppContext.h>
|
#include <AppContext.h>
|
||||||
|
|
||||||
void LAppLive2DManager::ModelSizeChange(const int Sacle = 15)
|
void LAppLive2DManager::ModelSizeChange(const int Sacle = 15)
|
||||||
{
|
{
|
||||||
// 加载完后根据模型大小来重新设置当前窗口大小
|
// 加载完后根据模型大小来重新设置当前窗口大小
|
||||||
@@ -346,6 +347,10 @@ void LAppLive2DManager::MountLoadedModel(LAppModel* model)
|
|||||||
// 加入新模型
|
// 加入新模型
|
||||||
_models.PushBack(model);
|
_models.PushBack(model);
|
||||||
|
|
||||||
|
if (DebugLogEnable) {
|
||||||
|
_models[0]->DumpMotionMap(); // 打印模型动作列表
|
||||||
|
}
|
||||||
|
|
||||||
// 加载完后根据模型大小来重新设置当前窗口大小
|
// 加载完后根据模型大小来重新设置当前窗口大小
|
||||||
const int width = static_cast<int>(_models[0]->GetModel()->GetCanvasWidthPixel() / 15.0);
|
const int width = static_cast<int>(_models[0]->GetModel()->GetCanvasWidthPixel() / 15.0);
|
||||||
const int height = static_cast<int>(_models[0]->GetModel()->GetCanvasHeightPixel() / 15.0);
|
const int height = static_cast<int>(_models[0]->GetModel()->GetCanvasHeightPixel() / 15.0);
|
||||||
|
|||||||
+98
-4
@@ -660,6 +660,102 @@ CubismMotionQueueEntryHandle LAppModel::StartRandomMotion(const csmChar* group,
|
|||||||
return StartMotion(group, no, priority, onFinishedMotionHandler);
|
return StartMotion(group, no, priority, onFinishedMotionHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::map<std::string, std::vector<LAppModel::MotionInfo>> LAppModel::GetMotionMap()
|
||||||
|
{
|
||||||
|
std::map<std::string, std::vector<MotionInfo>> motionMap;
|
||||||
|
if (_modelSetting == nullptr)
|
||||||
|
{
|
||||||
|
return motionMap;
|
||||||
|
}
|
||||||
|
// 获取组总数
|
||||||
|
const int groupCount = _modelSetting->GetMotionGroupCount();
|
||||||
|
for (int i = 0; i < groupCount; i++)
|
||||||
|
{
|
||||||
|
// 获取组名 (从 const char* 转 std::string)
|
||||||
|
const char* groupNameChar = _modelSetting->GetMotionGroupName(i);
|
||||||
|
std::string groupName(groupNameChar);
|
||||||
|
// 获取该组动作数
|
||||||
|
const int motionCount = _modelSetting->GetMotionCount(groupNameChar);
|
||||||
|
|
||||||
|
std::vector<MotionInfo> motionList;
|
||||||
|
for (int j = 0; j < motionCount; j++)
|
||||||
|
{
|
||||||
|
MotionInfo info;
|
||||||
|
info.SequenceId = j;
|
||||||
|
// 获取路径并处理
|
||||||
|
const char* filePath = _modelSetting->GetMotionFileName(groupNameChar, j);
|
||||||
|
if (filePath)
|
||||||
|
{
|
||||||
|
info.FileName = ExtractFileName(std::string(filePath));
|
||||||
|
}
|
||||||
|
motionList.push_back(info);
|
||||||
|
}
|
||||||
|
motionMap[groupName] = motionList;
|
||||||
|
}
|
||||||
|
return motionMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LAppModel::DumpMotionMap()
|
||||||
|
{
|
||||||
|
if (_modelSetting == nullptr)
|
||||||
|
{
|
||||||
|
LAppPal::PrintLogLn("[Live2D Debug] Cannot dump MotionMap: Model assets not loaded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取映射表
|
||||||
|
const std::map<std::string, std::vector<MotionInfo>> motionMap = GetMotionMap();
|
||||||
|
|
||||||
|
if (motionMap.empty())
|
||||||
|
{
|
||||||
|
// 模型动作栏目为空,大概率是模型本身没带动作
|
||||||
|
printf("[Live2D Debug] MotionMap is empty. Make sure the model is loaded correctly.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n================ [Live2D Motion Dump] =================\n");
|
||||||
|
|
||||||
|
// 遍历 Map (组)
|
||||||
|
for (auto & it : motionMap)
|
||||||
|
{
|
||||||
|
const std::string& groupName = it.first;
|
||||||
|
const std::vector<MotionInfo>& motions = it.second;
|
||||||
|
|
||||||
|
printf("Group: [%s] (%zu motions)\n", groupName.c_str(), motions.size());
|
||||||
|
|
||||||
|
// 遍历 Vector (组内动作)
|
||||||
|
for (const auto& info : motions)
|
||||||
|
{
|
||||||
|
// 打印 序列号 和 处理后的文件名
|
||||||
|
printf(" ├── ID: %d | Name: %s\n",
|
||||||
|
info.SequenceId,
|
||||||
|
info.FileName.c_str());
|
||||||
|
}
|
||||||
|
printf(" └── (End of %s)\n", groupName.c_str());
|
||||||
|
}
|
||||||
|
printf("=======================================================\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:处理 csmString 路径截取
|
||||||
|
std::string LAppModel::ExtractFileName(const std::string& fullPath)
|
||||||
|
{
|
||||||
|
// 提取带后缀的文件名 (例如从 "motions/special_03.motion3.json" 变为 "special_03.motion3.json")
|
||||||
|
const size_t lastSlash = fullPath.find_last_of("/\\");
|
||||||
|
std::string fileName = (lastSlash == std::string::npos) ? fullPath : fullPath.substr(lastSlash + 1);
|
||||||
|
// 循环去掉后缀,直到名字中不再包含 ".json" 或 ".motion3"
|
||||||
|
// Live2D 动作文件通常以 .motion3.json 结尾
|
||||||
|
const std::string extensions[] = { ".json", ".motion3" };
|
||||||
|
|
||||||
|
for (const std::string& ext : extensions)
|
||||||
|
{
|
||||||
|
const size_t pos = fileName.find(ext);
|
||||||
|
if (pos != std::string::npos)
|
||||||
|
{
|
||||||
|
fileName = fileName.substr(0, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 讲一下两种动画的不同
|
/** 讲一下两种动画的不同
|
||||||
* MotionGroupIdle:
|
* MotionGroupIdle:
|
||||||
@@ -678,13 +774,11 @@ CubismMotionQueueEntryHandle LAppModel::StartRandomMotion(const csmChar* group,
|
|||||||
* 下面的两个函数分别就是获取这两种动作的数量的,不同的动作对应着一个序号,播放的序号不可以超过下面函数返回的最大值
|
* 下面的两个函数分别就是获取这两种动作的数量的,不同的动作对应着一个序号,播放的序号不可以超过下面函数返回的最大值
|
||||||
* 要注意的是部分模型可能没有动画
|
* 要注意的是部分模型可能没有动画
|
||||||
*/
|
*/
|
||||||
int LAppModel::getIdleMotionCount()
|
int LAppModel::getIdleMotionCount() const {
|
||||||
{
|
|
||||||
return _modelSetting->GetMotionCount(MotionGroupIdle);
|
return _modelSetting->GetMotionCount(MotionGroupIdle);
|
||||||
}
|
}
|
||||||
|
|
||||||
int LAppModel::getTapBodyMotionCount()
|
int LAppModel::getTapBodyMotionCount() const {
|
||||||
{
|
|
||||||
return _modelSetting->GetMotionCount(MotionGroupTapBody);
|
return _modelSetting->GetMotionCount(MotionGroupTapBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ make
|
|||||||
关于授权 <br>
|
关于授权 <br>
|
||||||
本项目采用多重授权结构:
|
本项目采用多重授权结构:
|
||||||
|
|
||||||
1. 原创代码部分:MIT License
|
1. 原创代码部分:MIT License <br>
|
||||||
src/*.*
|
src/*.*
|
||||||
|
|
||||||
2. 依赖库:
|
2. 依赖库:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QMutex>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 录音模块
|
* @brief 录音模块
|
||||||
@@ -21,15 +23,15 @@
|
|||||||
class AudioInput : public QObject
|
class AudioInput : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(AudioInput) // 禁用拷贝
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief 构造函数
|
* @brief 构造函数
|
||||||
* @param parent
|
* @param parent
|
||||||
*/
|
*/
|
||||||
explicit AudioInput(QObject *parent = nullptr);
|
explicit AudioInput(QObject *parent = nullptr);
|
||||||
static AudioInput* instance;
|
static QScopedPointer<AudioInput> instance;
|
||||||
|
static QMutex mutex;
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief 获取实例
|
* @brief 获取实例
|
||||||
@@ -145,5 +147,6 @@ private:
|
|||||||
std::vector<qreal> m_rmsValues; /// RMS值vector
|
std::vector<qreal> m_rmsValues; /// RMS值vector
|
||||||
qreal m_silenceThreshold = 1200; /// 静音阈值
|
qreal m_silenceThreshold = 1200; /// 静音阈值
|
||||||
int m_silenceDuration = 1500; /// 静音持续时间
|
int m_silenceDuration = 1500; /// 静音持续时间
|
||||||
|
qreal m_smoothRms = 0.0; /// 平滑RMS值(用于防止低频杂波突然打断静音检测)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <QAudioSink> // 音频输出组件, 用于原始数据播放
|
#include <QAudioSink> // 音频输出组件, 用于原始数据播放
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QBuffer>
|
#include <QBuffer>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QMutex>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 音频播放模块
|
* @brief 音频播放模块
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
class AudioOutput : public QObject
|
class AudioOutput : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY(AudioOutput) // 禁用拷贝
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* 构造函数私有化
|
* 构造函数私有化
|
||||||
@@ -30,7 +33,8 @@ private:
|
|||||||
*/
|
*/
|
||||||
explicit AudioOutput(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
|
explicit AudioOutput(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
|
||||||
|
|
||||||
static AudioOutput *instance; // 单例类
|
static QScopedPointer<AudioOutput> instance; // 单例类
|
||||||
|
static QMutex mutex;
|
||||||
public:
|
public:
|
||||||
static AudioOutput *getInstance();
|
static AudioOutput *getInstance();
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,18 @@
|
|||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <QtEndian> // 用于处理字节序
|
#include <QtEndian> // 用于处理字节序
|
||||||
|
|
||||||
AudioInput *AudioInput::instance = nullptr;
|
QScopedPointer<AudioInput> AudioInput::instance;
|
||||||
|
QMutex AudioInput::mutex;
|
||||||
AudioInput *AudioInput::getInstance()
|
AudioInput *AudioInput::getInstance()
|
||||||
{
|
{
|
||||||
// 懒汉式 依旧单线程无需加锁
|
// 懒汉式 依旧单线程无需加锁
|
||||||
if (instance == nullptr) {
|
if (instance.isNull()) {
|
||||||
instance = new AudioInput();
|
QMutexLocker locker(&mutex);
|
||||||
|
if (instance.isNull()) {
|
||||||
|
instance.reset(new AudioInput);
|
||||||
}
|
}
|
||||||
return instance;
|
}
|
||||||
|
return instance.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioInput::AudioInput(QObject *parent) : QObject(parent)
|
AudioInput::AudioInput(QObject *parent) : QObject(parent)
|
||||||
@@ -79,7 +83,7 @@ void AudioInput::startAudio()
|
|||||||
// 调大缓冲区以避免溢出
|
// 调大缓冲区以避免溢出
|
||||||
m_audioSource->setBufferSize(128000);
|
m_audioSource->setBufferSize(128000);
|
||||||
|
|
||||||
// start() 返回一个 QIODevice,我们可以从中读取数据
|
// start() 返回一个 QIODevice,可以从中读取数据
|
||||||
m_ioDevice = m_audioSource->start();
|
m_ioDevice = m_audioSource->start();
|
||||||
|
|
||||||
if (m_ioDevice) {
|
if (m_ioDevice) {
|
||||||
@@ -107,8 +111,8 @@ void AudioInput::stopAudio()
|
|||||||
if (!m_rawPCMData.isEmpty()) {
|
if (!m_rawPCMData.isEmpty()) {
|
||||||
wavData = generateWavHeader(m_rawPCMData.size());
|
wavData = generateWavHeader(m_rawPCMData.size());
|
||||||
wavData.append(m_rawPCMData);
|
wavData.append(m_rawPCMData);
|
||||||
|
#ifdef QT_DEBUG
|
||||||
// 如果需要保存文件
|
// 如果需要保存文件(Debug下启用)
|
||||||
if (!m_outputFilePath.isEmpty()) {
|
if (!m_outputFilePath.isEmpty()) {
|
||||||
QFile file(m_outputFilePath);
|
QFile file(m_outputFilePath);
|
||||||
if (file.open(QIODevice::WriteOnly)) {
|
if (file.open(QIODevice::WriteOnly)) {
|
||||||
@@ -117,10 +121,9 @@ void AudioInput::stopAudio()
|
|||||||
qDebug() << "Saved WAV to:" << m_outputFilePath;
|
qDebug() << "Saved WAV to:" << m_outputFilePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
m_rawPCMData.clear();
|
m_rawPCMData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
isAutoRecording = false;
|
isAutoRecording = false;
|
||||||
isAutoThreshold = false;
|
isAutoThreshold = false;
|
||||||
|
|
||||||
@@ -138,18 +141,28 @@ void AudioInput::onReadyRead()
|
|||||||
QByteArray data = m_ioDevice->readAll();
|
QByteArray data = m_ioDevice->readAll();
|
||||||
if (data.isEmpty()) return;
|
if (data.isEmpty()) return;
|
||||||
|
|
||||||
// 1. 保存原始 PCM 数据
|
// 保存原始 PCM 数据
|
||||||
m_rawPCMData.append(data);
|
m_rawPCMData.append(data);
|
||||||
|
|
||||||
// 2. 计算 RMS (仅用于分析,取最后一小段或者整体计算,这里计算当前块的RMS)
|
// 计算 RMS (仅用于分析,计算当前块的RMS)
|
||||||
m_rmsValue = calculateRMS(data);
|
const qreal currentRms = calculateRMS(data);
|
||||||
|
m_rmsValue = currentRms;
|
||||||
|
// 计算平滑RMS (用于防止低频杂波突然打断静音检测)
|
||||||
|
constexpr qreal alpha = 0.3; // 70% 历史权重, 30% 当前权重
|
||||||
|
if (qFuzzyIsNull(m_smoothRms)) {
|
||||||
|
// 如果是第一帧数据,直接赋值,避免从0开始慢慢爬升
|
||||||
|
m_smoothRms = currentRms;
|
||||||
|
} else {
|
||||||
|
// 新值 = (旧值 * (1 - alpha)) + (当前值 * alpha)
|
||||||
|
m_smoothRms = (m_smoothRms * (1.0 - alpha)) + (currentRms * alpha);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 自动停止逻辑 (VAD)
|
// 自动停止逻辑 (VAD)
|
||||||
if (isAutoRecording) {
|
if (isAutoRecording) {
|
||||||
// 输出 RMS 用于调试
|
// 输出 RMS 用于调试
|
||||||
// qDebug() << "RMS:" << m_rmsValue;
|
qDebug() << "Raw:" << currentRms << " Smooth:" << m_smoothRms;
|
||||||
|
|
||||||
if (m_rmsValue < m_silenceThreshold) {
|
if (m_smoothRms < m_silenceThreshold) {
|
||||||
// 静音状态
|
// 静音状态
|
||||||
if (!m_silenceTimer->isActive()) {
|
if (!m_silenceTimer->isActive()) {
|
||||||
m_silenceTimer->start(m_silenceDuration);
|
m_silenceTimer->start(m_silenceDuration);
|
||||||
@@ -161,10 +174,10 @@ void AudioInput::onReadyRead()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 自动阈值计算逻辑
|
// 自动阈值计算逻辑
|
||||||
if (isAutoThreshold) {
|
if (isAutoThreshold) {
|
||||||
m_rmsValues.push_back(m_rmsValue);
|
m_rmsValues.push_back(m_smoothRms);
|
||||||
emit rmsRealValue(m_rmsValue);
|
emit rmsRealValue(m_smoothRms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +185,7 @@ qreal AudioInput::calculateRMS(const QByteArray& buffer)
|
|||||||
{
|
{
|
||||||
if (buffer.isEmpty()) return 0;
|
if (buffer.isEmpty()) return 0;
|
||||||
|
|
||||||
// 假设是 Int16 格式 (16位深)
|
// 设定为 Int16 格式 (16位深)
|
||||||
// 如果是 Stereo,数据排列是 L R L R...
|
// 如果是 Stereo,数据排列是 L R L R...
|
||||||
// 简单的 RMS 计算可以将所有通道数据视为一个长序列
|
// 简单的 RMS 计算可以将所有通道数据视为一个长序列
|
||||||
|
|
||||||
@@ -242,7 +255,7 @@ void AudioInput::startAutoStopAudio(const qreal silenceThreshold, const int sile
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动阈值计算
|
// 启动阈值计算
|
||||||
void AudioInput::startAutoThresholdClu(int Duration)
|
void AudioInput::startAutoThresholdClu(const int Duration)
|
||||||
{
|
{
|
||||||
isAutoThreshold = true;
|
isAutoThreshold = true;
|
||||||
m_rmsValues.clear();
|
m_rmsValues.clear();
|
||||||
@@ -250,19 +263,36 @@ void AudioInput::startAutoThresholdClu(int Duration)
|
|||||||
m_thresholdTimer->start(Duration);
|
m_thresholdTimer->start(Duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2025.12.30重构 Misaki
|
||||||
|
* 从均值阈值计算的基础上增加了N倍标准差
|
||||||
|
* 即阈值 = 均值 + N * 标准差(N取3)
|
||||||
|
*/
|
||||||
void AudioInput::thresholdTimeout()
|
void AudioInput::thresholdTimeout()
|
||||||
{
|
{
|
||||||
isAutoThreshold = false;
|
isAutoThreshold = false;
|
||||||
stopAudio(); // 内部会处理 stop
|
stopAudio(); // 内部会处理 stop
|
||||||
|
|
||||||
if (!m_rmsValues.empty()) {
|
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);
|
emit thresholdCalculated(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// 计算均值
|
||||||
|
const double mean = std::accumulate(m_rmsValues.begin(), m_rmsValues.end(), 0.0) / m_rmsValues.size();
|
||||||
|
// 计算标准差
|
||||||
|
const double sq_sum = std::inner_product(m_rmsValues.begin(), m_rmsValues.end(), m_rmsValues.begin(), 0.0);
|
||||||
|
double variance = (sq_sum / m_rmsValues.size()) - (mean * mean);
|
||||||
|
// 防止浮点误差导致负数
|
||||||
|
if (variance < 0) variance = 0;
|
||||||
|
const double stdDev = std::sqrt(variance);
|
||||||
|
// 阈值 = 均值 + 2 * 标准差
|
||||||
|
const double bestThreshold = mean + 3 * stdDev;
|
||||||
|
m_silenceThreshold = std::max(bestThreshold, 150.0);
|
||||||
|
m_silenceThreshold = std::min(m_silenceThreshold, 30000.0);
|
||||||
|
qDebug() << "Auto Threshold Calc -> Mean:" << mean
|
||||||
|
<< " StdDev:" << stdDev
|
||||||
|
<< " Result:" << m_silenceThreshold;
|
||||||
|
emit thresholdCalculated(m_silenceThreshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray AudioInput::generateWavHeader(const quint32 dataSize) const {
|
QByteArray AudioInput::generateWavHeader(const quint32 dataSize) const {
|
||||||
@@ -285,7 +315,7 @@ QByteArray AudioInput::generateWavHeader(const quint32 dataSize) const {
|
|||||||
|
|
||||||
header.numChannels = static_cast<quint16>(m_format.channelCount());
|
header.numChannels = static_cast<quint16>(m_format.channelCount());
|
||||||
header.sampleRate = static_cast<quint32>(m_format.sampleRate());
|
header.sampleRate = static_cast<quint32>(m_format.sampleRate());
|
||||||
header.bitsPerSample = 16; // 我们强制使用了 Int16
|
header.bitsPerSample = 16; // 强制使用了 Int16
|
||||||
|
|
||||||
header.byteRate = header.sampleRate * header.numChannels * (header.bitsPerSample / 8);
|
header.byteRate = header.sampleRate * header.numChannels * (header.bitsPerSample / 8);
|
||||||
header.blockAlign = header.numChannels * (header.bitsPerSample / 8);
|
header.blockAlign = header.numChannels * (header.bitsPerSample / 8);
|
||||||
|
|||||||
@@ -6,15 +6,20 @@
|
|||||||
#include <QMediaDevices>
|
#include <QMediaDevices>
|
||||||
#include <QDataStream>
|
#include <QDataStream>
|
||||||
|
|
||||||
AudioOutput *AudioOutput::instance = nullptr;
|
QScopedPointer<AudioOutput> AudioOutput::instance; // 使用QScopedPointer去管理单例,自动析构
|
||||||
|
QMutex AudioOutput::mutex;
|
||||||
|
|
||||||
AudioOutput *AudioOutput::getInstance()
|
AudioOutput *AudioOutput::getInstance()
|
||||||
{
|
{
|
||||||
// 懒汉式(单线程播放,无需考虑加锁)
|
// 懒汉式(单线程播放,无需考虑加锁)
|
||||||
if (instance == nullptr) {
|
if (instance.isNull()) { // 若未访问
|
||||||
instance = new AudioOutput();
|
QMutexLocker locker(&mutex);
|
||||||
|
if (instance.isNull()) {
|
||||||
|
// 使用reset初始化
|
||||||
|
instance.reset(new AudioOutput());
|
||||||
}
|
}
|
||||||
return instance;
|
}
|
||||||
|
return instance.data(); // 返回单例实例
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioOutput::AudioOutput(QObject *parent) : QObject(parent), mediaPlayer(nullptr), audioOutput(nullptr), audioSink(nullptr), audioBuffer(nullptr)
|
AudioOutput::AudioOutput(QObject *parent) : QObject(parent), mediaPlayer(nullptr), audioOutput(nullptr), audioSink(nullptr), audioBuffer(nullptr)
|
||||||
@@ -39,7 +44,6 @@ AudioOutput::AudioOutput(QObject *parent) : QObject(parent), mediaPlayer(nullptr
|
|||||||
format.setSampleFormat(QAudioFormat::Int16); // 采样格式
|
format.setSampleFormat(QAudioFormat::Int16); // 采样格式
|
||||||
audioSink = new QAudioSink(QMediaDevices::defaultAudioOutput(), format, this);
|
audioSink = new QAudioSink(QMediaDevices::defaultAudioOutput(), format, this);
|
||||||
audioBuffer = new QBuffer(this);
|
audioBuffer = new QBuffer(this);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ public:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
// 业务接收信号
|
// 业务接收信号
|
||||||
void audioPacketReceived(const AudioDataPacket& packet);
|
void audioPacketReceived(const AudioDataPacket& packet); // 音频数据准备完成信号
|
||||||
void controlPacketReceived(const ControlDataPacket& packet);
|
void controlPacketReceived(const ControlDataPacket& packet); // 控制数据准备完成信号
|
||||||
void errorOccurred(const QString& errorMsg);
|
void errorOccurred(const QString& errorMsg); // 错误信号
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
// 接收底层 JSON 数据
|
// 接收底层 JSON 数据
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ public:
|
|||||||
void setGlobalFont(const QFont &newFont);
|
void setGlobalFont(const QFont &newFont);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参数建议值:
|
* 参数建议值:<br>
|
||||||
效果类型 gravity dampFactor holdDuration
|
效果类型 gravity dampFactor holdDuration <br>
|
||||||
柔和下落 600.0f 0.85f 1.0f
|
柔和下落 600.0f 0.85f 1.0f <br>
|
||||||
快速坠落 1200.0f 0.6f 0.3f
|
快速坠落 1200.0f 0.6f 0.3f <br>
|
||||||
弹性效果 900.0f 0.75f 0.8f
|
弹性效果 900.0f 0.75f 0.8f <br>
|
||||||
真实物理模拟 980.0f 0.82f 0.5f
|
真实物理模拟 980.0f 0.82f 0.5f
|
||||||
*/
|
*/
|
||||||
void setHoldDuration(const float seconds) { defaultHoldDuration = seconds; }
|
void setHoldDuration(const float seconds) { defaultHoldDuration = seconds; }
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ AudioPage::AudioPage(QWidget* parent)
|
|||||||
|
|
||||||
ElaScrollPageArea* audioInputProgressBarArea = new ElaScrollPageArea(this);
|
ElaScrollPageArea* audioInputProgressBarArea = new ElaScrollPageArea(this);
|
||||||
QHBoxLayout* audioInputProgressBarLayout = new QHBoxLayout(audioInputProgressBarArea);
|
QHBoxLayout* audioInputProgressBarLayout = new QHBoxLayout(audioInputProgressBarArea);
|
||||||
ElaText* audioInputProgressBarText = new ElaText("录音阈值", this);
|
ElaText* audioInputProgressBarText = new ElaText("静音检测阈值", this);
|
||||||
|
audioInputProgressBarText->setToolTip("测试当前环境的静音阈值,用于对话中的静音检测");
|
||||||
audioInputProgressBarText->setTextPixelSize(15);
|
audioInputProgressBarText->setTextPixelSize(15);
|
||||||
audioInputProgressBarLayout->addWidget(audioInputProgressBarText);
|
audioInputProgressBarLayout->addWidget(audioInputProgressBarText);
|
||||||
audioInputProgressBarLayout->addWidget(audioInputProgressBar, 1);
|
audioInputProgressBarLayout->addWidget(audioInputProgressBar, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user