Chinaunix首页 | 论坛 | 博客
  • 博客访问: 845772
  • 博文数量: 756
  • 博客积分: 40000
  • 博客等级: 大将
  • 技术积分: 4980
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-13 14:40
文章分类

全部博文(756)

文章存档

2011年(1)

2008年(755)

我的朋友

分类:

2008-10-13 16:06:42

实时语音通信的实现

作者:解放军炮兵学院 十四队


 
引言
  本人虽已学习VC++一年半载,仍觉捉襟见肘,好在有VCKBASE的帮忙,确实学到了不少东西,也成了我每次上民网必到之处(阁下有所不知,鄙人接受最为严格的管理,上民网是要申请的)。近日在做一个通信 方面的程序,实时的语音和视频通信当然是大家所喜欢的。本文将向您展示局域网环境下实时语音通信的的一个解决方案(视频这一块正在做,估计很快就能出炉),Winxp环境下测试效果良好,并且具有网络 拥塞处理机制,您不妨一看。
  本文以第26期 栾义明 先生的为基础的,在此深表感谢。雷同之处将不再赘述,主要做了以下发展:

  • (1) 利用多线程机制,实现录音、网络传输、放音同时进行。
  • (2) 网络壅塞处理,保证数据不丢失。
  • 例子程序运行画面:



    下面且看我细细道来:

    (一)首先定义了一个声音数据“块”

    struct CAudioData
    {
    	PBYTE lpdata; //指向语音数据,注意这里内存区域是动态申请释放的
    	DWORD dwLength;//语音数据长度
    }
    
    接下来申明两个循环队列和相关指针。
    //InBlocks,OutBlocks非别为两个常数
    CAudioData m_AudioDataIn[InBlocks],m_AudioDataOut[OutBlocks];
    int   nAudioIn, nSend, //录入、发送指针
         nAudioOut, nReceive;//接收、播放指针
    
    // 对于录音和放音都存在和网络的同步问题,主要靠这些指针进行协调

    讨论:如图所示,几个指针的相互追逐,这种机制在处理网络拥塞上应该有普遍的应用意义
     

       
  • (1)正常网速下:nAudioIn 在 nSend 之前, nReceive 在 nAuioOu t之前,周而复始的走下去。
  • (2)超快网速下:发送端:-->nSend追上nAudioIn-->“空转”(绕了一圈又回来了)--〉
    接收端:因为录、放音的采样频率设置为相等,故不可能出现 nReceive 在n AudioOut 之后,
    即收到的声音文件太多,来不及播放的现象。
  • (3)超慢网速下:(极端情况,网速几乎为0也没关系)
    发送端:nAudioIn 绕一圈反追上 nSend,于是将数据接在当前块的尾部,以待发送
    接收端:nAudioOut 追上 nReceive 后,发现没有数据可播放了,就“空转”。
  • 综合以上情况,相关实现如下:

    (二)声音的录制与播放

    (1)录音处理

    void CRecTestDlg::OnMM_WIM_DATA(UINT wParam,LONG lParam)
    {
          int nextBlock = (nAudioIn+1)% InBlocks;	
    	if(m_AudioDataIn[nextBlock].dwLength!=0)//下一“块”没发走
    	{  //把PWAVEHDR(即pBUfferi)里的数据接到当前“块”的末尾
               m_AudioDataIn[nAudioIn].lpdata  
    		= (PBYTE)realloc (m_AudioDataIn[nAudioIn].lpdata ,
                     (((PWAVEHDR) lParam)->dwBytesRecorded+m_AudioDataIn[nAudioIn].dwLength)) ;
    		if (m_AudioDataIn[nAudioIn].lpdata == NULL)
    		{//...出错处理
    			return ;
    		}
    	        CopyMemory ((m_AudioDataIn[nAudioIn].lpdata+m_AudioDataIn[nAudioIn].dwLength), 
    				   ((PWAVEHDR) lParam)->lpData,
    				   ((PWAVEHDR) lParam)->dwBytesRecorded) ;//(*destination,*resource,nLen);	
    		m_AudioDataIn[nAudioIn].dwLength +=((PWAVEHDR) lParam)->dwBytesRecorded;        
    	}
    	else //把PWAVEHDR(即pBUfferi)里的数据拷贝到下一“块”中
    	{
    		nAudioIn = (nAudioIn+1)% InBlocks;
    		m_AudioDataIn[nAudioIn].lpdata = (PBYTE)realloc
    			(0,((PWAVEHDR) lParam)->dwBytesRecorded);
    		CopyMemory(m_AudioDataIn[nAudioIn].lpdata,
    			    ((PWAVEHDR) lParam)->lpData,
    				((PWAVEHDR) lParam)->dwBytesRecorded) ;
    	   m_AudioDataIn[nAudioIn].dwLength =((PWAVEHDR) lParam)->dwBytesRecorded;
    
    	}
    	// Send out a new buffer	
    	waveInAddBuffer (hWaveIn, (PWAVEHDR) lParam, sizeof (WAVEHDR)) ;
    	return ;	
    }
    
    (2)放音处理
    void CRecTestDlg::OnMM_WOM_DONE(UINT wParam,LONG lParam)
    { //释放播放完的缓冲区,并准备新的数据  
    	free(m_AudioDataOut[nAudioOut].lpdata);
    	m_AudioDataOut[nAudioOut].lpdata = reinterpret_cast(malloc(1));
    	m_AudioDataOut[nAudioOut].dwLength = 0;
     
           nAudioOut= (nAudioOut+1)%OutBlocks;
    	((PWAVEHDR)lParam)->lpData          = (LPTSTR)m_AudioDataOut[nAudioOut].lpdata ;
    	((PWAVEHDR)lParam)->dwBufferLength  = m_AudioDataOut[nAudioOut].dwLength ;
       	waveOutPrepareHeader (hWaveOut,(PWAVEHDR)lParam,sizeof(WAVEHDR));
           waveOutWrite(hWaveOut,(PWAVEHDR)lParam,sizeof(WAVEHDR));
       return;
    }
    
    (三)套接字发送、接收线程
      其实,经过刚才的讨论,现在这两个线程的运作很简单---只是循环地操作nReceive和nSend指针。首先发送(接收)声音块的长度,然后发送(接收)声音内容。注意:拿CSocket::Send(buffer,count)为例,其返回值(发送出去的字结数)只是1到count之间的某值,所以要添加检测机制,否则将出现错误,这也是socket编程必须注意的。本文是用一个循环,直到发送出去的字节总数等于“块”的长度才发送第二个数据块的信息。
    例外这两个线程稍加改动即可实现多人的语音会议。
    UINT Audio_Listen_Thread(LPVOID lParam)
    {
    	CRecTestDlg *pdlg = (CRecTestDlg*)lParam;
    	CSocket m_Server;
    	DWORD 	length;
    	if(!m_Server.Create(4002))
    		AfxMessageBox("Listen Socket create error"+pdlg->GetError(GetLastError()));
    	if(!m_Server.Listen()) 
    		AfxMessageBox("m_server.Listen ERROR"+pdlg->GetError(GetLastError()));
    	CSocket recSo;
    	if(! m_Server.Accept(recSo))
    		AfxMessageBox("m_server.Accept() error"+pdlg->GetError(GetLastError()));
    	m_Server.Close();	
    	int ret ;
    	while(1)
    	{   //开始循环接收声音文件,首先接收文件长度
    		ret = recSo.Receive(&length,sizeof(DWORD));		
    		if(ret== SOCKET_ERROR )
    			AfxMessageBox("服务器端接收声音文件长度出错,原因: "+pdlg->GetError(GetLastError()));
    		if(ret!=sizeof(DWORD))
    		{
    			AfxMessageBox("接收文件头错误,将关闭该线程");
    			recSo.Close();
    			return -1;
    		}//接下来开辟length长的内存空间
    		pdlg->m_AudioDataOut[pdlg->nReceive].lpdata =(PBYTE)realloc (0,length);
    		if (pdlg->m_AudioDataOut[pdlg->nReceive].lpdata == NULL)
    		{
    			AfxMessageBox("erro memory_ReceiveAudio");
    			recSo.Close();
    			return -1;
    		}
    		else//内存申请成功,可以进行循环检测接受
    		{
    			DWORD dwReceived = 0,dwret;
    			while(length>dwReceived)
    			{
    				dwret = recSo.Receive((pdlg->m_AudioDataOut[pdlg->nReceive].lpdata+dwReceived),
    					(length-dwReceived));
    				dwReceived +=dwret;
    				if(dwReceived ==length)
    				{
    					pdlg->m_AudioDataOut[pdlg->nReceive].dwLength = length;
    					break;
    				}
    			}
    		}//本轮声音文件接收完毕	
    		pdlg->nReceive=(pdlg->nReceive+1)%OutBlocks;
    	}
    	recSo.Close();
    	return 0;
    }
    
    UINT Audio_Send_Thread(LPVOID lParam)
    {                                    
    	CRecTestDlg *pdlg = (CRecTestDlg*)lParam;
    	CSocket m_Client;
    	m_Client.Create();
    	if( m_Client.Connect("127.0.0.1",4002))
    	{		
    		DWORD ret, length;
    		int count=0;
    		while(1)//循环使用指针nSend
    		{
    			length =pdlg->m_AudioDataIn[pdlg->nSend].dwLength;			
    			if(length !=0)
    			{   //首先发送块的长度
    				if(((ret = m_Client.Send(&length,sizeof(DWORD)))
    				     != sizeof(DWORD))||(ret==SOCKET_ERROR))
    				{   
    					AfxMessageBox("声音文件头传输错误!"+pdlg->GetError(GetLastError()));
    					pdlg->OnOK();
    					break;	
    				}//其次发送块的内容,循环检测是否发送完毕
    				DWORD dwSent = 0;//已经发送掉的字节数
    				while(1)//==============================发送声音数据开始
    				{
    					ret = m_Client.Send((pdlg->m_AudioDataIn[pdlg->nSend].lpdata+dwSent),
    					                     (length-dwSent));
    					if(ret==SOCKET_ERROR)//检错
    					{
    						AfxMessageBox("声音文件传输错误!"+pdlg->GetError(GetLastError()));
    						break;			
    					}
    					else //发送未发送完的
    					{
    						dwSent += ret;
    						if(dwSent ==length)//发送完毕,则释放当前“块”
    						{   
    							free(pdlg->m_AudioDataIn[pdlg->nSend].lpdata);
    							pdlg->m_AudioDataIn[pdlg->nSend].dwLength = 0;
    							break;
    						}
    					}	
    				}  //======================================发送声音数据结束
    			}
    			pdlg->nSend = (pdlg->nSend +1)% InBlocks;
    		}
    		
    	}
    	else 
    		AfxMessageBox("Socket连接失败"+pdlg->GetError(GetLastError()));
    	m_Client.Close();
    	return 0;
    }      
    存在的问题
  • (1) 一旦添加声音控制waveSetGetVolume(),耳机就变成单声的,打开系统的音量控制,发现“波形”选项完全不平衡。
  • (2) 声音的录入运用双缓冲技术,使得无懈可击,但是在播放时,采用双缓冲调试时未能取得成功,相反使用单缓冲却基本上能够满足一般的音效。
  • (3) 可能还有尚未暴露的错误,恳请广大朋友不吝赐教。E-mail:
  •   Finally,Thank Candy Lee(my special friend) for her help.
    --------------------next---------------------

    首先,CPU占用100%,不知道什么原因
    第二,我怀疑你的拥塞控制没有作用,因为每录完一块,nAudioIn+1,假设已录到第4块,而第1块还没有发完,你的处理办法是仍然录在当前块
    if(m_AudioDataIn[nextBlock].dwLength!=0)//下一“块”没发走
    {  //把PWAVEHDR(即pBUfferi)里的数据拷贝到当前“块”中
    .....
    }
    ,那么就会将第4块的录音给冲掉 ( talent529 发表于 2008-5-8 20:59:00)
     
    TCP一样的可以做到延时很小.0.5秒以内.BUFFER 应该是一个ARRAY 或者内存块.发的时间去Get()然后再discard.
    while (TRUE)
    {
    if(WAIT_OBJECT_0!=::WaitForSingleObject(=_SEND_BUFFER.m_hBufEnough,INFINITE))
    {
    return 1;
    } packet=_SEND_BUFFER_.Get();
    if (packet==NULL)
    {
    continue;
    }
    if(!SendPack(*packet,this))
    {
    break;
    }
    _SEND_BUFFER_.Discard();
    } ( heyunet 发表于 2007-11-21 14:55:00)
     
    "例外这两个线程稍加改动即可实现多人的语音会议"
    我想实现两台机器的语音通话 我将m_Client.Connect("192.168.1.100",4002) 改成了我同学机器的IP
    但是一直报错“SOCKET连接失败” “一般错误”  不知道是怎么回事 请指点 ( karl412 发表于 2007-5-3 15:14:00)
     
    声音有延迟主要是因为作者的缓冲区设的很大(#define  INP_BUFFER_SIZE 16384)
    大约采样需要2秒,才第一次触发OnMM_WIM_DATA,所以感觉延迟大,可以试着改小INP_BUFFER_SIZE。和tcp无关,用udp实现,主要难点是如何协调数据包的到达顺序,而不窜声。 ( bifei 发表于 2007-1-18 20:16:00)
     
    CPU资源占用居高不下。 ( magicdigua 发表于 2006-10-30 8:30:00)
     
    我的发送线程是在else语句处return 的-1
    到了CSocket::send内部后,判断如果是-1就直接返回-1
    ( ydbcsdn 发表于 2006-6-16 9:36:00)
     
    我想请问一句:
     CSocket的Send 函数的确经常返回-1,我跟踪到send函数的内部后发现,在他的发送函数里已经有了发送数据数据长度的检测,显然,我们不需要对Send函数进行额外的处理!!
      我的新问题,我的Send函数返回的并不是一个长度,而是返回-1,很明显是CSysnSocket::Send 就返回了-1,当为-1时他就不检测长度了,而是直接返回了。
    while ((nResult = CAsyncSocket::Send(lpBuf, nBufLen, nFlags)) == SOCKET_ERROR)
    {
    if (GetLastError() == WSAEWOULDBLOCK)
    {
    if (!PumpMessages(FD_WRITE))
    return SOCKET_ERROR;
    }
    else
    return SOCKET_ERROR;
    } ( ydbcsdn 发表于 2006-6-16 9:34:00)
     
    双/多缓冲播放文件要使用定时机制,即采样率/缓冲长度为调用waveOutWrite的周期,如果是网络接收播放,每收到一个包填入当前缓冲区,填满后调用waveOutWrite加入播放队列就行了,这个播放队列是waveOutOpen之后默认建立的,不能通过放完一个缓冲后的回调函数中再调用waveOutWrite的方式来实现流畅播放 ( supercjj 发表于 2006-1-18 17:29:00)
     
    这是什么水平??太初手了
    一点也不PF,但是<<基于API的录音机程序>>作者栾义明写文档的功夫和分享精神个人还是很欣赏的 ( 逍遥剑侠 发表于 2006-1-4 14:10:00)
     
    您好,我想请教一下,用UDP方式和TCP方式怎么没有变化
    因为TCP方式延迟大约3到5秒钟,我想用UDP方式来改变延迟问题。希望能得到指教! ( ziying1211 发表于 2005-11-24 11:34:00)
     
    .......................................................

    --------------------next---------------------


    阅读(318) | 评论(0) | 转发(0) |
    给主人留下些什么吧!~~