引言
本人雖已學習VC++一年半載,仍覺捉襟見肘,好在有VCKBASE的幫忙,確實學 到了不少東西,www.vckbase.com也成了我每次上民網必到之處(閣下有所不知, 鄙人接受最為嚴格的管理,上民網是要申請的)。近日在做一個通信 方面的程序 ,實時的語音和視頻通信當然是大家所喜歡的。本文將向您展示局域網環境下實 時語音通信的的一個解決方案(視頻這一塊正在做,估計很快就能出爐),Winxp環 境下測試效果良好,並且具有網絡 擁塞處理機制,您不妨一看。
本文以第26期 栾義明 先生的《基於API的錄音機程序》為基礎的,在此深表 感謝。雷同之處將不再贅述,主要做了以下發展:
(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<PBYTE>(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: [email protected]
Finally,Thank Candy Lee(my special friend) for her help.
本文配套源碼