程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 實時語音通信的實現

實時語音通信的實現

編輯:關於VC++

引言

本人雖已學習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.

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved