小彭屋

导航

C++ 语音聊天

对语音控制思路为:先在服务端录音然后通用网络传输最后在客户端播放,下面我们分别讨论录音,传输,放音的实现步骤

录音实现:

对计算机录音我们可以使用一系列API,简单过程如下
waveInOpen                        打开录音设备
waveInPrepareHeader                准备录音缓冲区
waveInAddBuffer                将缓冲区加入队列
waveInStart                        开始录音
waveInUnPrepareHeader        释放录音缓冲区
waveInReset                         停止录音
waveInClose                        关闭录音设备

放音实现:

对计算机放音,简单过程如下

waveOutOpen                打开回放设备
waveOutPrepareHeader准备放音缓冲区
waveOutWrite                开始播放
waveOutRest                停止放音
waveOutClose                关闭回放设备
放音与录音相差无几,在后面的实例中将详细说明它的的使用

文件传输:

对于未经压缩处理的音频数据,它的体积是相当壮观的,对音频数据有效的压缩可以提高传输效率,为了方便本文没有对
数据进行压缩,而直接使用TCP进行传输


连续录/放音实现方法:

为了实现声音的平滑播放,在录放音时通常准备两个以上的缓冲区,当一个缓冲区用完后,将发出一个结束消息,并自动
转入下个缓冲区。当录音完成时会发出一个 MM_WIM_DATA消息,当放音完成时会发出一个MM_WOM_DONE消息。

两个重要的结构:

1.声音采样格式
原形如下:
typedef struct {
    WORD  wFormatTag;                 //数据格式,一般为WAVE_FORMAT_PCM即脉冲编码
    WORD  nChannels;                 //声道
    DWORD nSamplesPerSec;         //采样频率
    DWORD nAvgBytesPerSec;         //每秒数据量
    WORD  nBlockAlign;
    WORD  wBitsPerSample;        //样本大小
    WORD  cbSize;
} WAVEFORMATEX;  
对于这个结构我们通常使用默认或固定的值

2.音频数据块缓存结构WAVEHDR
其声明如下:  
type struct{
LPSTR lpData;                 //指向锁定的数据缓冲区的指针
DWORD dwBufferLength;         //数据缓冲区的大小
DWORD dwByteRecorded;         //录音时指明缓冲区中的数据量
DWORD dwUser;                 //用户数据
DWORD dwFlag;                 //提供缓冲区信息的标志
DWORD dwLoops;                 //循环播放的次数
struct wavehdr_tag *lpNext; //保留
DWORD reserved;                 //保留
} WAVEHDR;

声音的采集和播放都要使用这个音频数据块结构,实际上主要用到的就是第一个成员变量lpData和第二个成员变量dwBufferLength。         


相关AIP的使用:

waveInOpen的原型如下

MMRESULT waveInOpen(
  LPHWAVEIN phwi,            //输入设备句柄一个指向HWAVEIN的指针
  UINT uDeviceID,            //输入设备ID
  LPWAVEFORMATEX pwfx,       //录音格式指针
  DWORD dwCallback,          //处理MM_***消息的回调函数或窗口句柄
  DWORD dwCallbackInstance,  
  DWORD fdwOpen              //处理消息方式的符号位
);



在打开录音设置后就要指定录音缓冲区
它原形如下:
MMRESULT waveInPrepareHeader(
  HWAVEIN hwi,                    
  LPWAVEHDR pwh,                  
  UINT cbwh                       
);
其中HWAVEIN hwi为我们上面用waveInOpen打开的句柄,pwh为音频数据块缓存结构WAVEHDR。
其它的操作都比较简单就不再一一说明了,可参照MSDN使用。


服务端实现:


在开始前我们需要加载winmm.lib库和 mmsystem.h头文件
#include <mmsystem.h>
#pragma comment(lib,"winmm")

在开始录音按钮上添加如下代码:


        m_RecStart.EnableWindow(false);                        //停用录音按钮
        m_RecStart.SetWindowText("录音中...");        //改变按钮文字
        m_exit.SetFocus();                                                //设置焦点按钮

        wavehdr=reinterpret_cast<PWAVEHDR>(malloc(sizeof(WAVEHDR)));

        //录音采样格式
        waveform.wFormatTag=WAVE_FORMAT_PCM;
        waveform.nChannels=1;
        waveform.nSamplesPerSec=11025;
        waveform.nAvgBytesPerSec=11025;
        waveform.nBlockAlign=1;
        waveform.wBitsPerSample=8;
        waveform.cbSize=0;

        //设定缓冲结构
        wavehdr->lpData=(LPTSTR)buffer;
        wavehdr->dwBufferLength=BUFFER_SIZE;
        wavehdr->dwBytesRecorded=0;
        wavehdr->dwUser=0;
        wavehdr->dwFlags=0;
        wavehdr->dwLoops=1;
        wavehdr->lpNext=NULL;
        wavehdr->reserved=0;

        //打开录音设备函数
        if (waveInOpen(&hWaveIn,WAVE_MAPPER,&waveform,(DWORD)this->m_hWnd,NULL,CALLBACK_WINDOW))
        {

                AfxMessageBox("Audio can not be open!");
        }
        

        for(int i=0;i<2;i++)//加入2个缓冲区
        {
        //为录音设备准备缓冲区
        waveInPrepareHeader(hWaveIn,wavehdr,sizeof(WAVEHDR));
        //给输入设备增加一个缓存
        waveInAddBuffer (hWaveIn, wavehdr, sizeof (WAVEHDR)) ;
        }
        waveInStart (hWaveIn) ;//开始录音

当缓存录满后系统将发出MM_WIM_DATA消息,我们添加消息处理函数,当收到MM_WIM_DATA消息时就将数据发给
客户端处理,对于添加消息的方法可以参考一下VC教程。在MM_WIM_DATA消息中发送数据代码如下:

void CCCDlg::OnMM_WIM_DATA(UINT wParam,LONG lParam)//录音完成
{

        //释放录音缓冲区
        waveInUnprepareHeader(hWaveIn,wavehdr,sizeof(WAVEHDR));
        //拷贝录音数据
        CopyMemory(buffer,wavehdr->lpData,wavehdr->dwBufferLength);
        //调用函数发送数据
        SendBuffer(buffer);
        //重新准备缓冲区
        waveInPrepareHeader(hWaveIn,wavehdr,sizeof(WAVEHDR));
        //重新加入缓冲区
        waveInAddBuffer (hWaveIn, wavehdr, sizeof (WAVEHDR)) ;
}
当录音完成后系统会自动转入下个缓冲区,继续录音,我们就释放录音缓冲区,然后拷贝数据,最后重新加入缓冲区,这样就实现了对声音的循环录制。

发送数据函数SendBuffer(buffer)代码如下:
void SendBuffer(char *buffer)
{
        WSADATA wsadata;
        SOCKET client;
        SOCKADDR_IN serveraddr;
        int port=5555;
        WORD ver=MAKEWORD(2,2);                                                        //判断winsock版本
        WSAStartup(ver,&wsadata);                                                //初始SOCKET
        client=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        serveraddr.sin_family=AF_INET;
        serveraddr.sin_port=htons(port);
        serveraddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
        connect(client,(SOCKADDR*)&serveraddr,sizeof(serveraddr));
        send(client,buffer,BUFFER_SIZE,0);//发送数据
        closesocket(client);
        WSACleanup();
}



客户端实现:

在对话框上添加监听按钮,并加入响应代码:

void CSSDlg::OnStart()
{
                m_start.SetWindowText("监听中...");                //改变按钮文字
                m_start.EnableWindow(false);                        //停用录音按钮
                hwnd=m_hWnd;
                ::SendMessage(hwnd,MM_WOM_DONE,0,0);                //发送MM_WOM_DONE消息
}

MM_WOM_DONE消息函数代码如下:

void CSSDlg::OnMM_WOM_DONE(UINT wParam,LONG lParam)//放音结束
{
        WSADATA wsadata;
        SOCKET server;
        SOCKET client;
        SOCKADDR_IN serveraddr;
        SOCKADDR_IN clientaddr;
        int port=5555;
        WORD ver=MAKEWORD(2,2);                                                        //判断winsock版本
        WSAStartup(ver,&wsadata);                                                //初始SOCKET

        char *buffer=(char *)malloc(BUFFER_SIZE);                //分配空间
        if (!buffer)
        {
                AfxMessageBox("Memory error!");
        }

        server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

        serveraddr.sin_family=AF_INET;
        serveraddr.sin_port=htons(port);
        serveraddr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);


        bind(server,(SOCKADDR*)&serveraddr,sizeof(serveraddr));
        listen(server,5);
        int len=sizeof(clientaddr);
        client=accept(server,(sockaddr *)&clientaddr,&len);

        if(recv(client,buffer,BUFFER_SIZE,0))
        {
                wavehdr->lpData=(LPTSTR)buffer;
                wavehdr->dwBufferLength=BUFFER_SIZE;
                wavehdr->dwBytesRecorded=0;
                wavehdr->dwUser=0;
                wavehdr->dwFlags=0;
                wavehdr->dwLoops=1;
                wavehdr->lpNext=NULL;
                wavehdr->reserved=0;

                waveform.wFormatTag                =        WAVE_FORMAT_PCM;
                waveform.nChannels                =        1;
                waveform.nSamplesPerSec        =        11025;
                waveform.nAvgBytesPerSec=        11025;
                waveform.nBlockAlign        =        1;
                waveform.wBitsPerSample        =        8;
                waveform.cbSize                        =        0;

        waveOutOpen(&hWaveOut,WAVE_MAPPER,&waveform,(DWORD)hwnd,NULL,CALLBACK_WINDOW)
        waveOutPrepareHeader (hWaveOut, wavehdr, sizeof (WAVEHDR))
        waveOutWrite (hWaveOut, wavehdr, sizeof (WAVEHDR))
        }

        closesocket(server);
        closesocket(client);
        WSACleanup();

}
我们用SendMessage(hwnd,MM_WOM_DONE,0,0)手动发送MM_WOM_DONE消息后,程序开始接受网络数据并进行播放,当播放结束时又自动发出一个MM_WOM_DONE消息,从而实现循环接受数据并播放。

posted on 2013-07-02 17:51  小彭屋  阅读(2455)  评论(0编辑  收藏  举报