C++实现高性能流式语音识别系统:基于环形缓冲区的实时ASR架构
前言
在智能语音交互领域,实时流式语音识别(Streaming ASR)是一项关键技术。与传统的离线语音识别不同,流式ASR能够在用户说话的同时进行识别,大大提升了交互体验。本文将详细介绍如何使用C++实现一个高性能的流式语音识别系统,该系统采用环形缓冲区、多线程处理池等技术,实现了低延迟、高并发的实时语音识别。
项目地址:e2e_voice
本文流 ASR 实现请看仓库src目录下的 main_streaming_asr.cpp
系统架构概览
流式ASR系统主要包含以下核心组件:
- StreamingAudioRecorder - 流式音频录制器,负责音频采集和缓冲管理
- 环形缓冲区(Ring Buffer) - 高效的音频数据存储结构
- VAD状态机 - 语音活动检测,自动分割语音段
- ASRThreadPool - 多线程ASR处理池,并行处理音频片段
- 有序结果输出 - 确保识别结果按时间顺序输出
系统工作流程
麦克风输入 → 音频采集 → 环形缓冲区 → VAD检测 → 语音段提取 → ASR线程池 → 有序输出
↓ ↓ ↓ ↓ ↓ ↓ ↓
实时录音 重采样 循环存储 状态机 分段处理 并行识别 结果排序
核心技术实现
1. 环形缓冲区设计
环形缓冲区是流式处理的核心数据结构,它允许我们高效地存储和访问音频数据流:
class StreamingAudioRecorder {
private:
// 环形缓冲区
std::vector<float> ringBuffer;
size_t ringBufferSize;
std::atomic<size_t> writePos{0}; // 写入位置
std::atomic<size_t> readPos{0}; // 读取位置
std::mutex bufferMutex;
// 添加音频数据到环形缓冲区
void addToRingBuffer(const float* samples, size_t numSamples) {
std::lock_guard<std::mutex> lock(bufferMutex);
size_t currentWritePos = writePos.load();
for (size_t i = 0; i < numSamples; ++i) {
ringBuffer[currentWritePos] = samples[i];
currentWritePos = (currentWritePos + 1) % ringBufferSize;
}
writePos.store(currentWritePos);
}
// 从环形缓冲区提取音频段
std::vector<float> extractFromRingBuffer(size_t startPos, size_t numSamples) {
std::lock_guard<std::mutex> lock(bufferMutex);
std::vector<float> segment;
segment.reserve(numSamples);
size_t pos = startPos;
for (size_t i = 0; i < numSamples; ++i) {
segment.push_back(ringBuffer[pos]);
pos = (pos + 1) % ringBufferSize;
}
return segment;
}
};
环形缓冲区的优势:
- 内存效率:固定大小,避免频繁的内存分配
- 时间复杂度:O(1)的读写操作
- 线程安全:使用原子变量和互斥锁保护
2. VAD状态机实现
语音活动检测(VAD)使用状态机模式,能够准确地分割连续语音流:
enum class VADState {
WAITING_FOR_SPEECH, // 等待语音开始
RECORDING_SPEECH, // 正在录制语音
SILENCE_DETECTED // 检测到静音
};
void processAudioFrame(const float* samples, size_t numSamples) {
// 检测当前帧是否包含语音
bool isSpeech = detectSpeech(samples, numSamples);
switch (vadState) {
case VADState::WAITING_FOR_SPEECH:
if (isSpeech) {
vadState = VADState::RECORDING_SPEECH;
// 包含预语音缓冲区
size_t preSpeechSamples = preSpeechBufferSec * sampleRate;
speechStartPos = getBufferPosition(preSpeechSamples);
std::cout << "[Speech detected]" << std::flush;
}
break;
case VADState::RECORDING_SPEECH:
if (!isSpeech) {
vadState = VADState::SILENCE_DETECTED;
silenceFrames = 1;
}
break;
case VADState::SILENCE_DETECTED:
if (!isSpeech) {
silenceFrames++;
if (silenceFrames >= silenceThresholdFrames) {
// 静音持续时间超过阈值,创建语音段
createAndQueueSegment();
vadState = VADState::WAITING_FOR_SPEECH;
}
} else {
// 语音恢复,继续录制
vadState = VADState::RECORDING_SPEECH;
silenceFrames = 0;
}
break;
}
}
3. 多线程ASR处理池
为了充分利用多核CPU,采用一个专门的ASR处理线程池:
class ASRThreadPool {
private:
std::vector<std::thread> workers;
std::queue<AudioSegment> taskQueue;
std::mutex queueMutex;
std::condition_variable cv;
std::atomic<bool> stop{false};
public:
void initialize(std::shared_ptr<ASRModel> model) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this, model]() {
while (!stop) {
AudioSegment segment;
{
std::unique_lock<std::mutex> lock(queueMutex);
cv.wait(lock, [this] { return stop || !taskQueue.empty(); });
if (stop && taskQueue.empty()) return;
segment = std::move(taskQueue.front());
taskQueue.pop();
}
// 处理音频段
auto result = model->recognize(segment.samples);
// 回调结果
if (resultCallback) {
ASRResult asrResult{
.text = result,
.segment_id = segment.segment_id,
.timestamp_start = segment.timestamp_start,
.timestamp_end = segment.timestamp_end
};
resultCallback(asrResult);
}
}
});
}
}
void processSegment(const AudioSegment& segment) {
{
std::lock_guard<std::mutex> lock(queueMutex);
taskQueue.push(segment);
}
cv.notify_one();
}
};
4. 音频重采样
由于ASR模型通常需要特定的采样率(如16kHz),采用简单的重采样算法,当然,还可以进行算法优化:
void resampleAudio(const float* input, size_t inputSamples,
std::vector<float>& output) {
// 简单线性插值重采样
double ratio = static_cast<double>(target_sample_rate) / sample_rate;
size_t outputSamples = static_cast<size_t>(inputSamples * ratio);
output.resize(outputSamples);
for (size_t i = 0; i < outputSamples; ++i) {
double srcIndex = i / ratio;
size_t srcIndexInt = static_cast<size_t>(srcIndex);
double fraction = srcIndex - srcIndexInt;
if (srcIndexInt + 1 < inputSamples) {
// 线性插值
output[i] = input[srcIndexInt] * (1.0 - fraction) +
input[srcIndexInt + 1] * fraction;
} else {
output[i] = input[inputSamples - 1];
}
}
}
5. Silero VAD集成
除了能量VAD,系统还支持更精确的Silero VAD模型:
bool detectSpeechSilero(const float* samples, size_t numSamples) {
const size_t windowSize = 512; // Silero需要512样本窗口
static std::vector<float> accumBuffer;
// 累积样本
accumBuffer.insert(accumBuffer.end(), samples, samples + numSamples);
bool isSpeech = false;
// 处理完整窗口
while (accumBuffer.size() >= windowSize) {
std::vector<float> window(accumBuffer.begin(),
accumBuffer.begin() + windowSize);
float prob = sileroVAD->detectVAD(window);
// 应用触发/停止阈值
if (vadState == VADState::WAITING_FOR_SPEECH) {
isSpeech = prob > vadTriggerThreshold;
} else {
isSpeech = prob > vadStopThreshold;
}
// 移除已处理窗口
accumBuffer.erase(accumBuffer.begin(),
accumBuffer.begin() + windowSize);
}
return isSpeech;
}
性能优化技巧
1. 内存管理优化
- 预分配内存:避免运行时频繁的内存分配
- 对象池:复用AudioSegment对象
- 移动语义:使用std::move减少拷贝
// 使用对象池管理AudioSegment
class SegmentPool {
std::queue<std::unique_ptr<AudioSegment>> pool;
std::mutex poolMutex;
public:
std::unique_ptr<AudioSegment> acquire() {
std::lock_guard<std::mutex> lock(poolMutex);
if (pool.empty()) {
return std::make_unique<AudioSegment>();
}
auto segment = std::move(pool.front());
pool.pop();
return segment;
}
void release(std::unique_ptr<AudioSegment> segment) {
segment->samples.clear();
std::lock_guard<std::mutex> lock(poolMutex);
pool.push(std::move(segment));
}
};
2. 并发优化
- 无锁编程:使用原子变量减少锁竞争
- 批处理:累积多个小任务后批量处理
- 线程亲和性:绑定线程到特定CPU核心
3. 实时性保证
- 优先级设置:音频线程使用实时优先级
- 缓冲区大小:平衡延迟和稳定性
- 预测性处理:提前准备可能需要的资源
使用示例
int main() {
// 初始化流式录音器
StreamingAudioRecorder recorder(device_index, sample_rate);
// 配置VAD
recorder.setVADType("silero");
recorder.setVADTriggerThreshold(0.5f);
recorder.setVADStopThreshold(0.35f);
// 初始化ASR线程池
ASRThreadPool asrPool(4); // 4个工作线程
asrPool.initialize(asrModel);
// 设置结果回调
asrPool.setResultCallback([](const ASRResult& result) {
std::cout << "[" << result.timestamp_start << "s - "
<< result.timestamp_end << "s] "
<< result.text << std::endl;
});
// 开始流式识别
recorder.startStreamingSession(
[&](const AudioSegment& segment) {
asrPool.processSegment(segment);
},
300.0, // 最大时长5分钟
0.5, // 静音阈值0.5秒
0.25 // 预语音缓冲0.25秒
);
// 等待完成
recorder.waitForCompletion();
return 0;
}
性能测试结果
在测试中,该流式ASR系统表现出色(m1max测试):
- 延迟:从语音结束到识别结果输出 < 10ms
- 并发:单机支持 10+ 路并发流(取决于CPU)
- 准确率:与离线识别相当(> 95%)
- 内存占用:每路流约 300MB
应用场景
- 实时字幕:会议、直播实时字幕生成
- 语音助手:智能音箱、车载系统
- 客服系统:电话客服实时转写
- 教育场景:在线教育实时识别
- 医疗记录:医生口述病历实时记录
总结
本文介绍的流式ASR系统通过环形缓冲区、VAD状态机、多线程处理池等技术,实现了高性能的实时语音识别。系统具有以下特点:
- 低延迟:流式处理,边说边识别
- 高并发:多线程架构,充分利用多核
- 内存高效:环形缓冲区,固定内存占用
- 易扩展:模块化设计,方便集成新功能
完整的源代码已开源在GitHub,欢迎大家使用和贡献代码。未来我将添加更多功能,如说话人分离、情感识别等。
参考资料
- SenseVoice: https://github.com/FunAudioLLM/SenseVoice
- Silero VAD: https://github.com/snakers4/silero-vad
- PortAudio: http://www.portaudio.com/
- ONNX Runtime: https://onnxruntime.ai/
如果这篇文章对你有帮助,欢迎点赞、收藏和关注!有任何问题也欢迎在评论区交流讨论。