网络语音实时通信音频下的缓冲区设计

来源:互联网 发布:javascript 清空对象 编辑:程序博客网 时间:2024/04/30 21:10

      这个是根据自己的项目来设计的缓冲区,应用方面可能比较窄,只适用相关操作。

      解决方案是MFC中Timer+缓冲区的方式

      先说一下相关的背景,我这里的结构是这样的:本地一个线程专门负责采集数据recordsound,本地同样有一个线程专门负责播放数据playsound。但是播放线程有个弊端,利用的是windows下最底层的音频捕获和播放函数,waveOutWrite,而且该函数只能是将数据放入然后播放。音频的播放方式目前我了解是有两种,回调函数和线程函数。我目前利用的是线程的播放方式,所以只能用windows下的信号(postmessage)方式让相关播放函数运行。

waveOutOpen( &m_hPlay, WAVE_MAPPER,&m_WaveFormatEx, ::GetCurrentThreadId(), 0, CALLBACK_THREAD);如果在waveOutOpen中将方式改为回调函数方式,则可以通过waveOutWrite播放完成后产生的WM_DONE消息自动调用回调函数来进行播放和设置,回调函数的方式可以见:http://wmnmtm.blog.163.com/blog/static/3824571420111030125597/?suggestedreading&wumii

本文原理和他类似,只是解决的是线程下的问题,如果能用上面博客方法解决线程的问题,欢迎讨论我只是没解决。

介绍下playsound的方法(该方法是参考的一个音视频播放的代码)

OnStartPlaying(WPARAM wParam, LPARAM lParam)     //开始播放OnStopPlaying(WPARAM wParam, LPARAM lParam)//结束播放OnEndPlaySoundData(WPARAM wParam, LPARAM lParam)           //清空播放数据OnWriteSoundData(WPARAM wParam, LPARAM lParam)//播放函数,该函数需要提供数据和数据长度,来一个包则播放,是被动式的OnEndThread(WPARAM wParam, LPARAM lParam)          //结束线程

如果是本地采集和本地播放,没有问题,因为数据来到的顺序和时间基本每延迟,但是如果是网络的话,则会出现一顿一顿的现象,非常严重,因为原理是这样,来一个包,播放一个,而包与包之间是有延迟的,则会出现顿顿的感觉。


缓冲区是一个循环队列,一个写入指针NR,一个播放指针NP,两个指针独立变化,互相不干扰。(图是http://wmnmtm.blog.163.com/blog/static/3824571420111030125597/?suggestedreading&wumii取的,原理类似)


nP 和nR指向的位置一开始都是0。如果同时启动网络接 收和音频播放的话就会造成同一块内存的读写冲突,导致程序崩溃,所以程序中当nR>2时才启动音频播放(造成延迟的主要原因)。

  nR和nP的移动是异步的。当接收到一帧网络音频数据时nR = (nR+1)%MAXBUFFER;而nP的移动需要视网络状态好坏而定。通过协调两者的移动步伐达到网络拥塞控制的目的。具 体如下:

  1)网络正常,nR持续读入数据,nP则持续播放。由于是UDP的方式接收,包顺序不定,包内数据按序列号存入相应位置,nR指向最新数据,但是要保证,nR-nP不能大于缓冲区大小,则nP = (nP+1)%MAXBUFFER;

  2)网络繁忙,即nR指针移动过满,导致nR-nP < 2,则 nP保持不动,下一次播放的数据为空数据;

  3)网络较好,且因某些原因声卡输出慢了,即nR-nP > 缓冲区大小,则 NR停止录音等待NP播放;通过丢弃一些数据帧以保证声音的实时性,否则会造成声音延时过大,而且随着 时间推移会越来越大。

将播放设计为每0.5秒播放一次(或者更小),设计一个Timer,每0.5秒调用postmessage,将信号给播放线程,播放线程进行播放。而网络收到的包则将数据填入缓冲区,播放线程则依次进行播放,这两个过程是分开的。

数据包的接收是UDP的方式,包的顺序可能到达不一,所以需要一个音频的包序列号,根据该序列号将数据填入相应缓冲区。NR是写入指针,NP是播放指针。缓冲区一共有MAXBUFFER个大缓冲区(存入0.5秒的音频数据),而一个包的大小则是这个大缓冲区的1/5,只有0.1秒(0.1秒采集一次,可以更小)

AUDIOBUFFER

AddInBuffer(char * lParam)//发送音频数据到播放线程{//NR永远大于等于NP//将数据存入缓冲区,符合标准的包才会进入缓冲区,其他的丢弃LPMSG_AUDIO_DATA data=(LPMSG_AUDIO_DATA)lParam;         /*
          在缓冲区内的位置|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
*/int buff_p=(data->m_nSeque)%(MAXBUFFER*BUFFERSIZE);//NR,NP缓冲区未满,继续存入数据if((NR+1)%MAXBUFFER!=NP){//NP指针和NP+1的缓冲区处于写保护,无法存入数据,正在播放和即将播放的数据if(!(buff_p >= ((NP+MAXBUFFER-1)%MAXBUFFER) * BUFFERSIZE && buff_p < ( (NP + 1) % MAXBUFFER ) * BUFFERSIZE) ){//将数据填入缓冲区memcpy(AUDIOBUFFER+buff_p*RECBUFFER,data->m_pcData,data->m_nSize);//保证NR最新,缓冲区相减大于0,或者0-9,1-9的情况,播放的指针向后增加if((NR+1)%MAXBUFFER>(NR+5)%MAXBUFFER){if(( (buff_p/BUFFERSIZE) >= (NR+1)%MAXBUFFER )||( (buff_p/BUFFERSIZE) <= (NR+5)%MAXBUFFER ))NR=buff_p/BUFFERSIZE;}else{if(( (buff_p/BUFFERSIZE) >= (NR+1)%MAXBUFFER )&&( (buff_p/BUFFERSIZE) <= (NR+5)%MAXBUFFER ))NR=buff_p/BUFFERSIZE;}//存入缓冲区后,返回return;}else{//该数据如果是应写入写保护的缓冲区,则将该数据丢弃,包到达太迟,这时该包相应位置为空,包达到太快,则丢弃,超出缓冲区的范围return ;}}}
音频包格式

typedef struct MSG_AUDIO_DATA// 音频数据{INTm_nSize;            //数据内大小INTm_nSeque;          //序列号CHARm_pcData[1024*5];         //音频数据MSG_AUDIO_DATA() { Reset(); }void Reset(){ZeroMemory(m_pcData,sizeof(m_pcData));m_nSeque=0;m_nSize = 0;}} * LPMSG_AUDIO_DATA;

Timer触发后执行的函数

PlayBlock(){//NR和NP相隔大于等于2时,播放,如果小于,则播放上一个数据if((NR+MAXBUFFER-NP)%MAXBUFFER>=2){//数据清空memset(AUDIOBUFFER+((NP+MAXBUFFER-1)%MAXBUFFER)*5*RECBUFFER,RECBUFFER*BUFFERSIZE,sizeof(char));//向播放进程发送消息PostThreadMessage(m_pPlaySound->m_nThreadID,WM_PLAYSOUND_PLAYBLOCK,(WPARAM)(RECBUFFER*BUFFERSIZE),(LPARAM)(AUDIOBUFFER+NP*5*RECBUFFER));//播放指针向前增加NP=(NP+1)%MAXBUFFER;}else{//网络延迟,则播放上一块数据PostThreadMessage(m_pPlaySound->m_nThreadID,WM_PLAYSOUND_PLAYBLOCK,(WPARAM)(RECBUFFER*BUFFERSIZE),(LPARAM)(AUDIOBUFFER+NP*5*RECBUFFER));//这种情况下,NP不增加}}

结贴,欢迎讨论,我也纠结了一段时间,摸索的。



原创粉丝点击