高性能 流ASR C++实现

发布于:2025-08-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

C++实现高性能流式语音识别系统:基于环形缓冲区的实时ASR架构

前言

在智能语音交互领域,实时流式语音识别(Streaming ASR)是一项关键技术。与传统的离线语音识别不同,流式ASR能够在用户说话的同时进行识别,大大提升了交互体验。本文将详细介绍如何使用C++实现一个高性能的流式语音识别系统,该系统采用环形缓冲区、多线程处理池等技术,实现了低延迟、高并发的实时语音识别。

项目地址e2e_voice

本文流 ASR 实现请看仓库src目录下的 main_streaming_asr.cpp

系统架构概览

流式ASR系统主要包含以下核心组件:

  1. StreamingAudioRecorder - 流式音频录制器,负责音频采集和缓冲管理
  2. 环形缓冲区(Ring Buffer) - 高效的音频数据存储结构
  3. VAD状态机 - 语音活动检测,自动分割语音段
  4. ASRThreadPool - 多线程ASR处理池,并行处理音频片段
  5. 有序结果输出 - 确保识别结果按时间顺序输出

系统工作流程

麦克风输入 → 音频采集 → 环形缓冲区 → 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

应用场景

  1. 实时字幕:会议、直播实时字幕生成
  2. 语音助手:智能音箱、车载系统
  3. 客服系统:电话客服实时转写
  4. 教育场景:在线教育实时识别
  5. 医疗记录:医生口述病历实时记录

总结

本文介绍的流式ASR系统通过环形缓冲区、VAD状态机、多线程处理池等技术,实现了高性能的实时语音识别。系统具有以下特点:

  1. 低延迟:流式处理,边说边识别
  2. 高并发:多线程架构,充分利用多核
  3. 内存高效:环形缓冲区,固定内存占用
  4. 易扩展:模块化设计,方便集成新功能

完整的源代码已开源在GitHub,欢迎大家使用和贡献代码。未来我将添加更多功能,如说话人分离、情感识别等。

参考资料

  1. SenseVoice: https://github.com/FunAudioLLM/SenseVoice
  2. Silero VAD: https://github.com/snakers4/silero-vad
  3. PortAudio: http://www.portaudio.com/
  4. ONNX Runtime: https://onnxruntime.ai/

如果这篇文章对你有帮助,欢迎点赞、收藏和关注!有任何问题也欢迎在评论区交流讨论。