8.3 音频接收方案
音频的接收除了实时通话以外还有音频存储和回放,在前面已经介绍了语音数据传输,本节将介绍音频存储和回放是如何实现的。
8.3.1 音频存储方案
在使用语音聊天程序时,有时需要把对方说的话录制下来,本例就是实现接收语音数据并保存的功能,接收的数据将保存为WAV文件,运行语音存储程序如图8.27 、图8.28所示。

图8.27 视频单屏显示(服务器端) 图8.28 视频单屏显示(客户端)
保存的文件如图8.29所示。

图8.29 保存的文件
客户端程序设计步骤如下:
实例位置:光盘\mr\8\8.3\8.3.1\01
(1)创建一个基于对话框的应用程序,向对话框中添加一个IP控件和2个按钮控件,对话框资源设计如图8.30所示。

图8.30 对话框设计
(2)在应用程序的InitInstance方法中初始化套接字。
WSADATA wsd;
WSAStartup(MAKEWORD(2,2),&wsd);
(3)从CSocket类中派生一个子类CClientSocket。在头文件中引用Afxsock.h头文件,目的是使用CSocket类;引用主对话框的头文件,并对主对话框进行前导声明,因为在CClientSocket类中需要定义主对话框类指针。
(4)在对话框的头文件中引入winmm.lib库,并声明如下变量。
#include "ClientSocket.h"
#include <MMSystem.h>
#pragma comment(lib, "winmm.lib")
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
/////////////////////////////////////////////////////////////////////////////
// CClientDlg dialog
class CClientSocket;
class CClientDlg : public CDialog
{
// Construction
public:
void StartRecord();
void InitAudioDevice();
void ReceiveData();
CClientDlg(CWnd* pParent = NULL); // standard constructor
CClientSocket* m_pClient;
HWAVEIN m_hWaveIn; //录音设备
WAVEFORMATEX waveform; //声音格式
WAVEHDR lpInWaveHdr[2]; //录音缓存
char lpInbuf[4096];
char lpInbuf1[4096];
BOOL m_Change;
……//此处代码省略
};
(5)在对话框中定义一个CClientSocket对象指针,创建套接字。
BOOL CClientDlg::OnInitDialog()
{
……//此处代码省略
m_pClient = new CClientSocket(this);
if(!m_pClient->Create()) //创建套接字
{
delete m_pClient;
MessageBox("套接字创建失败.");
return FALSE;
}
return TRUE;
}
(6)处理“连接”按钮的单击事件,连接服务器,并在连接以后开启定时器。
void CClientDlg::OnButconnect()
{
UpdateData(TRUE);
CString sip;
m_IP.GetWindowText(sip); //读取服务器名称
int port;
port = 700; //获取端口
if(! m_pClient->Connect(sip,port)) //连接服务器
{
MessageBox("连接服务器失败!");
return;
}
}
(7)向对话框中添加ReceiveData方法,用于接收服务器端传过来的连接成功信息。
void CClientDlg::ReceiveData()
{
char buffer[2];
//接收传来的数据
int factdata = m_pClient->Receive(buffer,2);
buffer[factdata] = '\0';
if(buffer[0]='~')
MessageBox("连接成功");
}
(8)向对话框中添加InitAudioDevice方法,该方法可以初始化音频设备。
void CClientDlg::InitAudioDevice()
{
waveform.wFormatTag = WAVE_FORMAT_PCM ; // 采样方式,PCM(脉冲编码调制)
waveform.nChannels = 2; // 双声道
waveform.nSamplesPerSec = 11025; // 采样率11.025KHz
waveform.nAvgBytesPerSec = 11025; // 数据率11.025KB/s
waveform.nBlockAlign = 1; // 最小块单元,wBitsPerSample×nChannels/8
waveform.wBitsPerSample = 8; // 样本大小为8bit
waveform.cbSize = 0;
lpInWaveHdr[0].dwBufferLength = 4096;
lpInWaveHdr[0].lpData = lpInbuf;
lpInWaveHdr[0].dwBytesRecorded = 0;
lpInWaveHdr[0].dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
lpInWaveHdr[0].dwLoops = 1;
lpInWaveHdr[0].dwUser = 0;
lpInWaveHdr[0].lpNext = NULL;
lpInWaveHdr[0].reserved = 0;
lpInWaveHdr[1].dwBufferLength = 4096;
lpInWaveHdr[1].lpData = lpInbuf1;
lpInWaveHdr[1].dwBytesRecorded = 0;
lpInWaveHdr[1].dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
lpInWaveHdr[1].dwLoops = 1;
lpInWaveHdr[1].dwUser = 0;
lpInWaveHdr[1].lpNext = NULL;
lpInWaveHdr[1].reserved = 0;
//打开录音设备
waveInOpen(&m_hWaveIn,WAVE_MAPPER ,&waveform,(DWORD)m_hWnd,0,CALLBACK_WINDOW);
for(int i=0;i<=1;i++)
waveInPrepareHeader(m_hWaveIn,&lpInWaveHdr[i],4096);
StartRecord();
}
(9)处理“发送”按钮的单击事件,调用InitAudioDevice方法初始化音频设备。
void CClientDlg::OnButsend()
{
InitAudioDevice();
}
(10)向对话框中添加StartRecord方法,开始录音。
void CClientDlg::StartRecord()
{
//准备录音缓冲区
if(m_Change)
waveInAddBuffer(m_hWaveIn,&lpInWaveHdr[0],sizeof(WAVEHDR));
else
waveInAddBuffer(m_hWaveIn,&lpInWaveHdr[1],sizeof(WAVEHDR));
//开始录音
waveInStart(m_hWaveIn);
}
(11)手动添加MM_WIM_DATA消息,消息处理函数为OnWinData,该消息在录音缓冲区满时触发,在该消息中将缓冲区的数据发送到服务器端。
void CClientDlg::OnWinData()
{
if(m_Change)
m_pClient->Send(lpInWaveHdr[0].lpData,4096);
else
m_pClient->Send(lpInWaveHdr[1].lpData,4096);
m_Change = !m_Change;
StartRecord();
}
(12)处理对话框的WM_DESTROY消息,在窗口关闭时关闭套接字和录音设备。
void CClientDlg::OnDestroy()
{
CDialog::OnDestroy();
for(int i=0;i<=1;i++)
{
waveInUnprepareHeader(m_hWaveIn,&lpInWaveHdr[i],4096);
}
if(m_pClient != NULL)
delete m_pClient;
}
服务器端程序设计步骤如下:
实例位置:光盘\mr\8\8.3\8.3.1\02
(1)创建一个基于对话框的应用程序,向对话框中添加一个静态文本控件、一个群组控件和3个按钮控件,对话框设计如图8.31 所示。

图8.31 对话框设计
(2)在应用程序的InitInstance方法中初始化套接字。
WSADATA wsd;
WSAStartup(MAKEWORD(2,2),&wsd);
(3)从CSocket类派生新类CClientSocket和CServerSocket,在头文件中引用对话框的头文件和afxsock.h头文件,并对对话框类进行前导声明。
(4)在对话框类的头文件中引用MMSystem.h头文件,连接winmm.lib库文件并声明如下变量。
#include "ServerSocket.h"
#include "ClientSocket.h"
#include <MMSystem.h>
#pragma comment(lib, "winmm.lib")
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
/////////////////////////////////////////////////////////////////////////////
// CServerDlg dialog
class CServerSocket;
class CClientSocket;
class CServerDlg : public CDialog
{
// Construction
public:
void ReceiveData(CClientSocket* socket);
void AcceptConnect();
CServerDlg(CWnd* pParent = NULL); // standard constructor
CServerSocket* m_pServer;
CClientSocket* m_pClient;
LPSTR lptr;
HANDLE hData;
int nBuf;
int size;
CString strName;
WAVEFORMATEX waveform; //声音格式
……//此处代码省略
};
(5)绑定套接字,并开始监听客户端。
void CServerDlg::OnButlisten()
{
if(!m_pServer->Create(700))
{
MessageBox("套接字创建失败");
delete m_pServer;
m_pServer = NULL;
return;
}
if (!m_pServer->Listen())
MessageBox("监听失败");
hData = GlobalAlloc(GMEM_MOVEABLE, 4096);
lptr = (char*)GlobalLock(hData);
}
(6)在对话框中添加AcceptConnect方法,用于接受客户端的连接,如果连接成功就向客户端发送确认信息。
void CServerDlg::AcceptConnect()
{
if(m_pServer->Accept(*m_pClient))
{
char a[1];
a[0] = '~';
m_pClient->Send(a,1);
}
}
(7)处理“选择”按钮的单击事件,获得要保存为文件的路径名。
void CServerDlg::OnButpath()
{
CFileDialog dlg(false,NULL,NULL,NULL,"文件(*.wav)|*.wav||");
if(dlg.DoModal()==IDOK)
{
strName = dlg.GetPathName();
strName += ".wav";
m_path.SetWindowText(strName);
}
}
(8)在对话框中添加ReceiveData方法,用于接受客户端的发送的数据。
void CServerDlg::ReceiveData(CClientSocket *socket)
{
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE,4096);
char* lpBuf = (char*)GlobalLock(hGlobal);
memset(lpBuf,0,4096);
size = socket->Receive(lpBuf,4096);
memcpy(lptr+(nBuf-1)*4096,lpBuf,4096*sizeof(BYTE));
GlobalUnlock(hData);
hData = GlobalReAlloc(hData, (++nBuf)*4096, GMEM_MOVEABLE);
lptr = (char*)GlobalLock(hData);
GlobalUnlock(hGlobal);
GlobalFree(hGlobal);
}
(9)处理“保存”按钮的单击事件,将接收的数据保存为WAV文件。
void CServerDlg::OnButsave()
{
if(strName.IsEmpty())
{
MessageBox("请选择保存的文件名");
}
waveform.nChannels = 2;
waveform.wFormatTag = WAVE_FORMAT_PCM;
waveform.cbSize = 0;
waveform.wBitsPerSample = 8;
waveform.nSamplesPerSec = 11052;
waveform.nBlockAlign = waveform.nChannels * (waveform.wBitsPerSample / 8);
waveform.nAvgBytesPerSec = waveform.nSamplesPerSec * waveform.nBlockAlign;
//保存
HMMIO hmmio;
MMCKINFO ciRiffChunk;
MMCKINFO ciSubChunk;
MMIOINFO mmioInfo;
TCHAR file[255];
mmioInfo.dwFlags = 0;
mmioInfo.fccIOProc = mmioStringToFOURCC("WAV", 0);
mmioInfo.pIOProc = NULL;
mmioInfo.wErrorRet = 0;
mmioInfo.htask = 0;
mmioInfo.cchBuffer = 0;
mmioInfo.pchBuffer = 0;
mmioInfo.pchNext = 0;
mmioInfo.pchEndRead = 0;
mmioInfo.pchEndWrite = 0;
mmioInfo.lBufOffset = 0;
mmioInfo.lDiskOffset = 0;
mmioInfo.adwInfo[0] = 0;
mmioInfo.adwInfo[1] = 0;
mmioInfo.adwInfo[2] = 0;
mmioInfo.adwInfo[3] = 0;
mmioInfo.dwReserved1 = 0;
mmioInfo.dwReserved2 = 0;
mmioInfo.hmmio = 0;
wsprintf(file,_T("%s"),strName);
hmmio = mmioOpen(file, &mmioInfo,
MMIO_CREATE | MMIO_WRITE | MMIO_ALLOCBUF);
mmioSeek(hmmio, 0, SEEK_SET);
ciRiffChunk.fccType = mmioFOURCC('W', 'A', 'V', 'E');
ciRiffChunk.cksize = 0L;
mmioCreateChunk(hmmio, &ciRiffChunk, MMIO_CREATERIFF);
ciSubChunk.ckid = mmioStringToFOURCC("fmt", 0);
ciSubChunk.cksize = sizeof(WAVEFORMATEX)-2;
mmioCreateChunk(hmmio, &ciSubChunk, 0);
mmioWrite(hmmio, (HPSTR)&waveform, sizeof(WAVEFORMATEX));
mmioAscend(hmmio, &ciSubChunk, 0);
ciSubChunk.ckid = mmioStringToFOURCC("data", 0);
ciSubChunk.cksize = (nBuf-1)*4096+size;
mmioCreateChunk(hmmio, &ciSubChunk, 0);
mmioWrite(hmmio, (HPSTR)lptr,(LONG)(nBuf-1)*4096+size);
mmioAscend(hmmio, &ciSubChunk, 0);
mmioAscend(hmmio, &ciRiffChunk, 0);
mmioFlush(hmmio, 0);
mmioClose(hmmio, 0);
MessageBox("已保存");
}
(10)处理对话框的WM_DESTROY消息,在窗口关闭时关闭套接字。
void CServerDlg::OnDestroy()
{
CDialog::OnDestroy();
GlobalUnlock(hData);
GlobalFree(hData);
if(m_pServer != NULL)
delete m_pServer;
if(m_pClient != NULL)
delete m_pClient;
}
8.3.2 音频回放设计方案
存储声音时通常都是保存成WAV文件,WAV是一种常见的音频文件格式,可以使用MCI指令播放WAV文件,MCI指令是Windows系统中多媒体硬件的接口,通过该指令还可以实现播放CD、控制光驱、控制音量等操作。
播放wav文件的步骤如下:
(1)向工程中导入winmm.lib库。
(2)定义MCIDEVICEID变量,用来设置设备ID号,定义MCI_OPEN_PARMS变量,用来设置打开设备类型,定义MCI_PLAY_PARMS变量,用来设置播放回调函数。
(3)使用mciSendCommand函数使用MCI_OPEN命令打开播放wav文件的设备,获得mciOpenParms变量的wDeviceID值。
(4)通过MCI_OPEN_PARMS变量设置lpstrElementName属性,该属性保存的是wav文件的路径,并通过mciSendCommand函数的将文件打开。
(5)使用mciSendCommand函数的MCI_PLAY命令播放文件。
音频回放程序运行如图8.32 所示。

图8.32 视频回放
音频回放程序设计步骤如下:
实例位置:光盘\mr\8\8.3\8.3.2\01
(1)创建一个基于对话框的应用程序,向对话框中添加一个静态文本控件和2个按钮控件,对话框设计如图8.33所示。

图8.33 对话框设计
(2)在StdAfx.h文件中导入winmm.lib库。
#include <MMSystem.h>
#pragma comment(lib, "winmm.lib")
(3)处理“选择文件”按钮的单击事件,用来选择WAV文件。
void CWAVPlayDlg::OnOpen()
{
CFileDialog dlg(TRUE,"文件","*.wav",OFN_HIDEREADONLY,
"声音文件(*.wav)|*.wav||",NULL);
if(dlg.DoModal()==IDOK)
{
strName = dlg.GetPathName();
m_Path.SetWindowText(strName);
}
}
(4)处理“播放”按钮的单击事件,用来播放选择的WAV文件。
void CWAVPlayDlg::OnPlay()
{
MCIDEVICEID m_nDeviceID;
MCIDEVICEID m_nElementID;
MCI_OPEN_PARMS mciOpenParms;
mciOpenParms.lpstrDeviceType=(LPSTR)MCI_DEVTYPE_WAVEFORM_AUDIO;
mciSendCommand(NULL,MCI_OPEN,MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID
| MCI_WAIT,(DWORD)(LPVOID)&mciOpenParms);
m_nDeviceID=mciOpenParms.wDeviceID;
MCI_OPEN_PARMS mciOpen;
memset(&mciOpen,0,sizeof(MCI_OPEN_PARMS));
mciOpen.lpstrElementName = strName;
mciSendCommand(m_nDeviceID,MCI_OPEN,MCI_OPEN_ELEMENT,(DWORD)(LPVOID)&mciOpen);
m_nElementID=mciOpen.wDeviceID;
MCI_PLAY_PARMS mciPlay;
mciPlay.dwCallback=(DWORD)this->GetSafeHwnd();
mciSendCommand(m_nElementID,MCI_PLAY,MCI_NOTIFY,(DWORD)(LPVOID)&mciPlay);
}






