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消息,从而实现循环接受数据并播放。