以下代码全都开源在github: ai_client
概览
原定目标是创建一个智能录音模块,它能够自动检测静音并在没有实际音频输入时暂停录制,从而避免不必要的资源消耗。整个系统围绕Proto框架构建,包括了音频流管理、静音识别逻辑及数据处理等核心部分。最终成为AI助手的后端。
静音检测
原理
- 能量分析: 通过计算每帧音频样本的能量总和,即其绝对值的平均,来评估当前的音量水平。
- 阈值判定:设定一个能量阈值
m_flitterSize
,当连续几帧能量均值超过此阈值时,认为有声音,反之则视为静音。 - 滤波处理:引入
flitterFunc
函数,基于能量序列判断是否连续超过静音阈值,考虑了容错率,防止因瞬间噪声触发录音。
int OnStreamCallBack(const void *inputBuffer,
void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *sysData)
获取数据
// framesPerBuffer是本次采样数据地址,channelCount是通道数,双通道就是左右声道交错排列
auto fNum = framesPerBuffer * _this->m_params->channelCount;
均化振幅
// 最终的v也就是本次的振幅了
float total = 0;
float v = 0;
for (size_t i = 0; i < fNum; ++i) {
auto tt = rprt[i];
if (rprt[i] > middle) {
v = static_cast<float>(rprt[i]) / static_cast<float>(maxPcm);
} else {
v = (static_cast<float>(fabs(rprt[i])) / static_cast<float>(fabs(minPcm)));
}
total += v;
}
v = total / static_cast<float>(fNum);
处理波动
// 本次均化之后的数据加入时间轮
_this->m_energyBuffer.push(v);
if (_this->m_energyBuffer.isFull()) {
if (_this->m_records) {
// 正在录音,滤波不通过取消录音
if (!flitterFunc(_this->m_energyBuffer.getOrdered(),
_this->m_flitterSize,
_this->m_flitterFiltration)) {
auto now = Timer::GetCurrentTimeMillis();
if (now - _this->m_lastTime > _this->m_flitterTime) {
bool r = true;
if (_this->m_muteCallback) {
r = _this->m_muteCallback(_this, _user);
}
_this->m_lastTime = now;
// 防止爆闪
_this->m_records = false;
_this->m_insert = false;
}
}
} else {
// 没开始录音,超过阈值
if (v > _this->m_flitterSize) {
// 滤波,防止瞬时噪音
if (flitterFunc(_this->m_energyBuffer.getOrdered(),
_this->m_flitterSize,
0.5)) {
bool r = true;
if (_this->m_StartCallback) {
r = _this->m_StartCallback(_this, _user);
}
if (r) {
_this->m_records = true;
_this->m_insert = true;
}
}
}else {
// 因为过滤了短暂的爆音,但是这种过滤会导致丢掉说话开始时的一秒左右数据,所以要一个预存
// 保留前reNum次采样的数据
uint32_t reNum = 3;
// 最大预存尺寸
uint32_t maxIndex = reNum * fNum;
if (_this->m_scanIndex >= maxIndex) {
std::vector<short> tempV(maxIndex);
tempV.clear();
tempV.insert(tempV.end(), _this->m_pcmData.begin() + fNum, _this->m_pcmData.end());
_this->m_scanIndex = tempV.size();
_this->m_pcmData.clear();
_this->m_pcmData.insert(_this->m_pcmData.end(), tempV.begin(), tempV.end());
_this->m_insert = true;
} else {
_this->m_insert = true;
}
}
}
}
插入录制的数据
if (_this->m_insert || _this->m_records) {
_this->m_pcmData.insert(_this->m_pcmData.end(), rprt, rprt + framesPerBuffer * _this->m_params->channelCount);
if (_this->m_pcmData.size() >= 10000) {
_this->m_scanIndex += static_cast<int32_t>(framesPerBuffer * _this->m_params->channelCount);
if (_this->m_pcmData.size() >= _this->m_maxSize) {
_this->m_scanIndex = 0;
_this->m_pcmData.clear();
}
}
}
使用
#include "audio/AudioClient.h"
#include "utils/extend/Logger.h"
#include <iostream>
#include <thread>
int main() {
auto cli = AC::AudioClient::GetInstance();
auto dev = AC::AudioClient::GetDefaultInputDevice();
PrintDebug("default input device: {}", dev.Name.c_str());
auto s = cli->OpenStream(dev);
s.SetMuteCallback([](AC::AudioStream *stream, void *userData)->bool {
stream->SaveData("re.wav", true);
PrintInfo("saved");
return true;
});
s.SetStartCallback([](AC::AudioStream *stream, void *userData)->bool {
PrintInfo("start");
return true;
});
std::thread a([&]() { s.Start(0.02,0.05,3000); });
std::this_thread::sleep_for(std::chrono::seconds(5));
while (true) {
std::string cmd;
std::cin >> cmd;
if (cmd == "exit") {
s.Stop();
a.join();
break;
}
}
return 0;
}
评论 (1)