C++音频数据缓冲区正确操作及语音识别断词问题排查
问题描述
已基于C++实现Windows平台下麦克风音频录制、回放功能,后续需要对接语音识别业务做音频数据处理。为避免识别时出现词语截断问题,原本计划将采集到的小块音频数据拼接写入大容量连续缓冲区,当前使用memcpy实现小块采集缓冲区到大容量缓冲区的拷贝拼接,但程序运行后仅能识别到部分词语,功能不符合预期。需要排查当前缓冲区操作的错误点,同时寻找更便捷合理的音频数据缓冲区操作方案。
现有实现代码
#include <stdio.h> #include <Windows.h> #include <mmsystem.h> #include <iostream> #include <fstream> using namespace std; #pragma comment(lib, "winmm.lib") #define Samples 16000 #define NUM_FRAMES Samples*2 #define Channels 1 const int NUM_BUF = 4; int main() { HWAVEIN inStream; HWAVEOUT outStream; WAVEFORMATEX waveFormat; WAVEHDR buffer[NUM_BUF]; waveFormat.cbSize = 0; waveFormat.wFormatTag = WAVE_FORMAT_PCM; waveFormat.nChannels = Channels; waveFormat.nSamplesPerSec = Samples; waveFormat.wBitsPerSample = 16; waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; waveFormat.nAvgBytesPerSec = waveFormat.nBlockAlign * waveFormat.nSamplesPerSec; HANDLE event = CreateEventA(NULL, TRUE, FALSE, "waveout event"); MMRESULT res = MMSYSERR_NOERROR; res = waveInOpen(&inStream, WAVE_MAPPER, &waveFormat, (unsigned long)event, 0, CALLBACK_EVENT); if (res != MMSYSERR_NOERROR) { printf("error in waveInOpen\n"); return -1; } res = waveOutOpen(&outStream, WAVE_MAPPER, &waveFormat, (unsigned long)event, 0, CALLBACK_EVENT); if (res != MMSYSERR_NOERROR) { printf("error in waveOutOpen\n"); return -2; } short int *_pBuf; size_t bpbuff = 16000*2; _pBuf = new short int [bpbuff * NUM_BUF]; for ( int i = 0; i < NUM_BUF; i++ ) { buffer[i].lpData = (LPSTR)&_pBuf [i * bpbuff]; buffer[i].dwBufferLength = bpbuff*sizeof(*_pBuf); buffer[i].dwFlags = 0L; buffer[i].dwLoops = 0L; waveInPrepareHeader(inStream, & buffer[i], sizeof(WAVEHDR)); } ResetEvent(event); for (int index = 0; index < NUM_BUF; index++) // queue all buffers for input waveInAddBuffer(inStream, &buffer[index], sizeof(WAVEHDR)); waveInStart(inStream); int len_buff = buffer[0].dwBufferLength*6 + 1; int limit_buff = buffer[0].dwBufferLength*5 + 1; int size = buffer[0].dwBufferLength; int rl = 0; int flagg = 0; char * buff1 = new char[len_buff]; char * buff2 = new char[len_buff]; int flag_buf = 0; int flag1 = 0, flag2 = 0; int i = 0; int inIndex = 0, outIndex = 0; // the next input and output to watch while (true) { if (buffer[inIndex].dwFlags & WHDR_DONE & flagg!=1) { flagg = 1; waveInAddBuffer(inStream, &buffer[inIndex], sizeof(WAVEHDR)); inIndex = (inIndex + 1) % NUM_BUF; } if (buffer[outIndex].dwFlags & WHDR_DONE & flagg!=0) { flagg = 0; if (flag_buf == 0) { if (rl<limit_buff) { cout << rl << endl; if (flag1 == 0) { //strcpy(buff1, buffer[outIndex].lpData); memcpy(buff1, buffer[outIndex].lpData, size); flag1 = 1; rl = size + 1; } else { //strcat(buff1, buffer[outIndex].lpData); memcpy(buff1 + rl, buffer[outIndex].lpData, size); rl = rl + size; } } else { //recognize buff1 flag_buf = 1; flag1 = 0; rl = 0; } } else { if (rl<limit_buff) { if (flag2 == 0) { memcpy(buff2, buffer[outIndex].lpData, size); flag2 = 1; rl = size + 1; } else { memcpy(buff2 + rl, buffer[outIndex].lpData, size); rl = rl + size; } } else { //recognize buff2 flag_buf = 0; flag2 = 0; rl = 0; } } waveOutWrite(outStream, &buffer[outIndex], sizeof(WAVEHDR)); outIndex = (outIndex + 1) % NUM_BUF; printf("N_buff_%i %i\n",outIndex , i); i++; } } for (int index = 0; index < 4; index++) waveInUnprepareHeader(inStream, &buffer[inIndex], sizeof(WAVEHDR)); free(buffer); }
现有实现的错误点
- 缓冲区写入偏移计算错误:首次写入大容量缓冲区后,
rl被设置为size + 1,多偏移了1字节,后续每次memcpy的目标地址都会错位1字节,直接导致PCM音频数据解析错乱,语音识别只能识别到零散词语。 - 位运算逻辑优先级错误:判断缓冲区状态的条件
buffer[inIndex].dwFlags & WHDR_DONE & flagg!=1存在运算优先级问题,!=优先级高于&,实际执行逻辑和预期完全不符,会导致采集缓冲区队列提交不及时、已填充数据的缓冲区被重复覆盖,出现音频丢块。 - 双缓冲切换丢数据:当累计数据长度达到
limit_buff触发识别时,直接将rl重置为0、切换到另一块缓冲区,当前已经填充到缓冲区里、不足一块的剩余音频数据被直接丢弃,必然出现词语截断。 - 资源释放逻辑错误:
buffer是栈上数组,末尾调用free(buffer)属于非法内存释放;waveInUnprepareHeader循环固定使用inIndex对应的缓冲区,没有遍历所有已提交的采集缓冲区做资源回收,会触发内存泄漏和API调用异常。 - 事件回调使用错误:
waveInOpen和waveOutOpen绑定了同一个事件句柄,音频采集完成、播放完成都会触发同一个事件,代码没有分别判断是采集还是播放触发的事件,会出现采集、播放流程互相干扰的问题。
更合理的音频缓冲区操作方案
- 采用**环形缓冲区(循环队列)**作为音频采集的统一大容量缓存,不需要做双缓冲硬切换:维护读指针、写指针两个位置标记,采集到新音频块时直接从写指针位置写入、移动写指针;语音识别模块从读指针位置读取固定长度(满足识别输入要求的帧长,比如16k采样率下10ms对应160采样点、320字节)的数据、移动读指针,读写位置通过取模实现环形复用,从根源上避免数据截断和拷贝错位。
- 偏移计算严格按照字节数对齐:PCM数据是16位单声道格式,所有长度、偏移计算统一以字节为单位,不要额外加无意义的+1偏移,拷贝前校验目标缓冲区剩余空间,避免越界。
- 分离采集、播放的事件通知:给音频输入、输出分别创建独立的事件句柄,事件触发后分别判断对应设备的缓冲区状态,避免流程串扰。
- 采集缓冲区回收逻辑和数据拷贝逻辑解耦:采集到满块数据后,第一时间将该块重新提交给采集队列,避免采集丢帧,数据拷贝到环形缓冲区的操作单独处理,不要阻塞采集缓冲区的重新入队。
- 语音识别取数采用滑动窗口逻辑:环形缓冲区中累计的数据长度满足识别要求的最小帧长时,就可以取固定长度的数据送入识别模块,不需要等攒满固定的大块再处理,同时保留前后帧的重叠部分(比如每次取30ms数据,步进10ms),进一步降低词语截断的概率。
内容的提问来源于stack exchange,提问作者Saivaks
相关产品推荐
相关产品推荐

