上一篇通过Win32控制台程序简单地完成了声音的录取和回放,但是这个过程都只是是在内存中进行的,没有进行文件的操作,这样录取的声音也就无法保存。这一篇介绍一下用MFC实现录音并生成wave文件,最后存储到指定的目录的方法。
新建一个MFC对话框应用程序,命名为VoiceRecord, 打开资源视图,Dialog目录下的IDD_VOICERECORD_DIALOG,往这个对话框中添加3个Button控件,修改对应的属性栏中的Caption和ID,分别为:
Caption ID
开始 IDC_RECORD_START
结束 IDC_RECORD_STOP
播放 IDC_RECORD_PLAY
说明:此处的播放相当于回放刚才的录音,没有选择性。要播放指定路径音频文件参考第一篇。
给这三个按钮分别添加消息处理函数:
OnRecordStart()
OnRecordStop()
OnRecordPlay()
在往这三个函数中添加消息响应代码之前,先介绍一下关于录音Windows提供的一组函数wave***的函数,比较重要的有以下几个:
(一)相关函数
1) 打开录音设备函数
MMRESULT waveInOpen(
LPHWAVEIN phwi, //输入设备句柄
UINT uDeviceID, //输入设备ID
LPWAVEFORMATEX pwfx, //录音格式指针
DWORD dwCallback, //处理MM_WIM_***消息的回调函数或窗口句柄,线程ID
DWORD dwCallbackInstance,
DWORD fdwOpen //处理消息方式的符号位
);
2) 为录音设备准备缓存函数
MMRESULT waveInPrepareHeader( HWAVEIN hwi, LPWAVEHDR pwh, UINT bwh );
3) 给输入设备增加一个缓存
MMRESULT waveInAddBuffer( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh );
4) 开始录音
MMRESULT waveInStart( HWAVEIN hwi );
5) 清除缓存
MMRESULT waveInUnprepareHeader( HWAVEIN hwi,LPWAVEHDR pwh, UINT cbwh);
6) 停止录音
MMRESULT waveInReset( HWAVEIN hwi );
7) 关闭录音设备
MMRESULT waveInClose( HWAVEIN hwi );
8) 打开回放设备
MMRESULT waveOutOpen(
LPHWAVEOUT phwo, //输出设备句柄
UINT uDeviceID, //输出设备ID
LPWAVEFORMATEX pwfx, //放音格式指针
DWORD dwCallback, //处理MM_WIM_***消息的回调函数或窗口句柄,线程ID
DWORD dwCallbackInstance,
DWORD fdwOpen //处理消息方式的符号位
);
9) 为回放设备准备内存块
MMRESULT waveOutPrepareHeader( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );
10) 写数据(放音)
MMRESULT waveOutWrite( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );
(二)相关数据结构
Wave_audio数据格式
typedef struct {
WORD wFormatTag; //数据格式,一般为WAVE_FORMAT_PCM即脉冲编码
WORD nChannels; //声道
DWORD nSamplesPerSec; //采样频率
DWORD nAvgBytesPerSec; //每秒数据量
WORD nBlockAlign;
WORD wBitsPerSample;//样本大小
WORD cbSize;
} WAVEFORMATEX;
waveform-audio 缓存格式
typedef struct {
LPSTR lpData; //内存指针
DWORD dwBufferLength;//长度
DWORD dwBytesRecorded; //已录音的字节长度
DWORD dwUser;
DWORD dwFlags;
DWORD dwLoops; //循环次数
struct wavehdr_tag * lpNext;
DWORD reserved;
} WAVEHDR;
(三)相关消息
录音
MM_WIM_OPEN:打开设备时消息,在此期间我们可以进行一些初始化工作 。
MM_WIM_DATA:当缓存已满或者停止录音时的消息,处理这个消息可以对缓存进行重新分配,实现不限长度录音 。
MM_WIM_CLOSE:关闭录音设备时的消息。
回放
OnMM_WOM_OPEN:打开设备
OnMM_WOM_DONE:处理声音的回放
OnMM_WOM_CLOSE:关闭设备
(四)实现过程
有了上面的基础,实现录音已经不难了。
首先在 初始化函数OnInitDialog()中分配内存
//shufac
//allocate memory for wave header
pWaveHdr1=reinterpret_cast<PWAVEHDR>(malloc(sizeof(WAVEHDR)));
pWaveHdr2=reinterpret_cast<PWAVEHDR>(malloc(sizeof(WAVEHDR)));
//allocate memory for save buffer
pSaveBuffer = reinterpret_cast<PBYTE>(malloc(1));
开始录音函数的代码如下:
void CVoiceRecordDlg::OnRecordRecord()
{
//allocate buffer memory
pBuffer1=(PBYTE)malloc(INP_BUFFER_SIZE);
pBuffer2=(PBYTE)malloc(INP_BUFFER_SIZE);
if (!pBuffer1 || !pBuffer2) {
if (pBuffer1) free(pBuffer1);
if (pBuffer2) free(pBuffer2);
MessageBeep(MB_ICONEXCLAMATION);
AfxMessageBox(_T("Memory erro!"));
return;
}
//open waveform audo for input
waveform.wFormatTag=WAVE_FORMAT_PCM;
waveform.nChannels=2;
waveform.nSamplesPerSec=44100;
waveform.nAvgBytesPerSec=176400;
waveform.nBlockAlign=4;
waveform.wBitsPerSample=16;
waveform.cbSize=0;
if (waveInOpen(&hWaveIn,WAVE_MAPPER,&waveform,(DWORD)this->m_hWnd,NULL,CALLBACK_WINDOW)) {
free(pBuffer1);
free(pBuffer2);
MessageBeep(MB_ICONEXCLAMATION);
AfxMessageBox(_T("Audio can not be open!"));
}
pWaveHdr1->lpData=(LPSTR)pBuffer1;
pWaveHdr1->dwBufferLength=INP_BUFFER_SIZE;
pWaveHdr1->dwBytesRecorded=0;
pWaveHdr1->dwUser=0;
pWaveHdr1->dwFlags=0;
pWaveHdr1->dwLoops=1;
pWaveHdr1->lpNext=NULL;
pWaveHdr1->reserved=0;
waveInPrepareHeader(hWaveIn,pWaveHdr1,sizeof(WAVEHDR));
pWaveHdr2->lpData=(LPSTR)pBuffer2; //
pWaveHdr2->dwBufferLength=INP_BUFFER_SIZE;
pWaveHdr2->dwBytesRecorded=0;
pWaveHdr2->dwUser=0;
pWaveHdr2->dwFlags=0;
pWaveHdr2->dwLoops=1;
pWaveHdr2->lpNext=NULL;
pWaveHdr2->reserved=0;
waveInPrepareHeader(hWaveIn,pWaveHdr2,sizeof(WAVEHDR));
pSaveBuffer = (PBYTE)realloc (pSaveBuffer, 1) ;
// Add the buffers
waveInAddBuffer (hWaveIn, pWaveHdr1, sizeof (WAVEHDR)) ;
waveInAddBuffer (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ;
// Begin sampling
bEnding=FALSE;
dwDataLength = 0 ;
waveInStart (hWaveIn) ;
}
录音文件的保存是在一次录音结束之后进行的。因此,保存录音的操作添加到了结束录音的函数中。后面会展示这个完整的函数,这里还是简单地做一下说明:关于生成的录音文件的存储问题:
1.存到哪儿
2.命名(每一次录音文件名不能相同,否则会产生覆盖)
第一个问题还是用前面介绍的相对路径的获取方法,直接存到Debug目录下,详细参考:MFC获取系统当前路径
这里还有一个创建目录的问题,录音文件肯定是存放在Debug目录下的一个单独的文件夹中,这个问题也已经解决,
详细参考:MFC中创建目录的相关问题
第二个问题,通过添加系统时间为后缀,确保文件不会重名;关于系统时间的获取方法,可参考:MFC获取系统时间的方法
最后还有一个字符串的拼接问题,这里在MFC中字符串的拼接问题会做一个简要后续再补充。
这样完整的录音结束函数为:
void CVoiceRecordDlg::OnRecordStop()
{
// TODO: 在此添加控件通知处理程序代码
bEnding=TRUE;
//停止录音
waveInReset(hWaveIn);
//存储声音文件
CFile m_file;
CFileException fileException;
SYSTEMTIME sys2; //获取系统时间确保文件的保存不出现重名
GetLocalTime(&sys2);
//以下实现将录入的声音转换为wave格式文件
//查找当前目录中有没有Voice文件夹 没有就先创建一个,有就直接存储
TCHAR szPath[MAX_PATH];
GetModuleFileName(NULL, szPath, MAX_PATH);
CString PathName(szPath);
//获取exe目录
CString PROGRAM_PATH = PathName.Left(PathName.ReverseFind(_T('\\')) + 1);
//Debug目录下RecordVoice文件夹中
PROGRAM_PATH+=_T("RecordVoice\\");
if (!(GetFileAttributes(PROGRAM_PATH)==FILE_ATTRIBUTE_DIRECTORY))
{
if (!CreateDirectory(PROGRAM_PATH,NULL))
{
AfxMessageBox(_T("Make Dir Error"));
}
}
//kn_string strFilePath = _T("RecordVoice\\");
//GetFilePath(strFilePath);
CString m_csFileName=PROGRAM_PATH+_T("\\audio");//strVoiceFilePath
//CString m_csFileName= _T("D:\\audio");
wchar_t s[30] = {0};
_stprintf(s,_T("%d%d%d%d%d%d"),sys2.wYear,sys2.wMonth,sys2.wDay,sys2.wHour,sys2.wMinute,sys2.wSecond/*,sys2.wMilliseconds*/);
m_csFileName.Append(s);
m_csFileName.Append(_T(".wav"));
m_file.Open(m_csFileName,CFile::modeCreate|CFile::modeReadWrite, &fileException);
DWORD m_WaveHeaderSize = 38;
DWORD m_WaveFormatSize = 18;
m_file.SeekToBegin();
m_file.Write("RIFF",4);
//unsigned int Sec=(sizeof + m_WaveHeaderSize);
unsigned int Sec=(sizeof pSaveBuffer + m_WaveHeaderSize);
m_file.Write(&Sec,sizeof(Sec));
m_file.Write("WAVE",4);
m_file.Write("fmt ",4);
m_file.Write(&m_WaveFormatSize,sizeof(m_WaveFormatSize));
m_file.Write(&waveform.wFormatTag,sizeof(waveform.wFormatTag));
m_file.Write(&waveform.nChannels,sizeof(waveform.nChannels));
m_file.Write(&waveform.nSamplesPerSec,sizeof(waveform.nSamplesPerSec));
m_file.Write(&waveform.nAvgBytesPerSec,sizeof(waveform.nAvgBytesPerSec));
m_file.Write(&waveform.nBlockAlign,sizeof(waveform.nBlockAlign));
m_file.Write(&waveform.wBitsPerSample,sizeof(waveform.wBitsPerSample));
m_file.Write(&waveform.cbSize,sizeof(waveform.cbSize));
m_file.Write("data",4);
m_file.Write(&dwDataLength,sizeof(dwDataLength));
m_file.Write(pSaveBuffer,dwDataLength);
m_file.Seek(dwDataLength,CFile::begin);
m_file.Close();
}
这里还需要注意一点的是写文件wave的各个字段都要要赋值,才能保证生成的wave文件有效。
录音的回放函数代码处理相对简单,不多重述。代码如下:
void CVoiceRecordDlg::OnRecordPlay()
{
//open waveform audio for output
waveform.wFormatTag = WAVE_FORMAT_PCM;
//设置不同的声音采样格式
/* waveform.nChannels = 1;
waveform.nSamplesPerSec =11025;
waveform.nAvgBytesPerSec=11025;
waveform.nBlockAlign =1;
waveform.wBitsPerSample =8; */
waveform.nChannels=2;
waveform.nSamplesPerSec=44100;
waveform.nAvgBytesPerSec=176400;
waveform.nBlockAlign=4;
waveform.wBitsPerSample=16;
if (waveOutOpen(&hWaveOut,WAVE_MAPPER,&waveform,(DWORD)this->m_hWnd,NULL,CALLBACK_WINDOW)) {
MessageBeep(MB_ICONEXCLAMATION);
AfxMessageBox(_T("Audio output erro"));
}
return ;
}
最后,还有几个Windows提供的几个消息响应函数,它们消息响应类似于鼠标操作的消息响应,这一点在前面的扫雷程序中做过详细的介绍,不做重述。
代码如下(录音和回放各三个):
LRESULT CVoiceRecordDlg::OnMM_WIM_OPEN(UINT wParam, LONG lParam)
{
// TODO: Add your message handler code here and/or call default
((CWnd *)(this->GetDlgItem(IDB_RECORD_RECORD)))->EnableWindow(FALSE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_STOP)))->EnableWindow(TRUE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_PLAY)))->EnableWindow(FALSE);
SetTimer(1,100,NULL);
return NULL;
}
这里采集声音信号还要添加一个定时器,另外在这个程序的基础上拓展做一个小型的录音机也是可以的,还需要添加两个定时器,分别控制录音盒放音时间。关于定时器的用法,做过总结,可参考:
MFC中定时器的使用
LRESULT CVoiceRecordDlg::OnMM_WIM_DATA(UINT wParam, LONG lParam)
{
// TODO: Add your message handler code here and/or call default
// Reallocate save buffer memory
pNewBuffer = (PBYTE)realloc (pSaveBuffer, dwDataLength +
((PWAVEHDR) lParam)->dwBytesRecorded) ;
if (pNewBuffer == NULL)
{
waveInClose (hWaveIn) ;
MessageBeep (MB_ICONEXCLAMATION);
AfxMessageBox(_T("erro memory"));
return TRUE;
}
pSaveBuffer = pNewBuffer ;
CopyMemory (pSaveBuffer + dwDataLength, ((PWAVEHDR) lParam)->lpData,
((PWAVEHDR) lParam)->dwBytesRecorded) ;
dwDataLength += ((PWAVEHDR) lParam)->dwBytesRecorded ;
if (bEnding)
{
waveInClose (hWaveIn) ;
return TRUE;
}
waveInAddBuffer (hWaveIn, (PWAVEHDR) lParam, sizeof (WAVEHDR)) ;
return NULL;
}
LRESULT CVoiceRecordDlg::OnMM_WIM_CLOSE(UINT wParam, LONG lParam)
{
KillTimer(1);
if (0==dwDataLength) {
return TRUE;
}
waveInUnprepareHeader (hWaveIn, pWaveHdr1, sizeof (WAVEHDR)) ;
waveInUnprepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ;
free (pBuffer1) ;
free (pBuffer2) ;
if (dwDataLength > 0)
{
//enable play
((CWnd *)(this->GetDlgItem(IDB_RECORD_RECORD)))->EnableWindow(TRUE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_STOP)))->EnableWindow(FALSE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_PLAY)))->EnableWindow(TRUE);
}
((CWnd *)(this->GetDlgItem(IDB_RECORD_RECORD)))->EnableWindow(TRUE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_STOP)))->EnableWindow(FALSE);
return NULL;
}
//放音
LRESULT CVoiceRecordDlg::OnMM_WOM_OPEN(UINT wParam, LONG lParam){
// Set up header
pWaveHdr1->lpData = (LPSTR)pSaveBuffer ; //???
pWaveHdr1->dwBufferLength = dwDataLength ;
pWaveHdr1->dwBytesRecorded = 0 ;
pWaveHdr1->dwUser = 0 ;
pWaveHdr1->dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP ;
pWaveHdr1->dwLoops = 1;
pWaveHdr1->lpNext = NULL;
pWaveHdr1->reserved = 0;
// Prepare and write
waveOutPrepareHeader (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ;
waveOutWrite (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ;
((CWnd *)(this->GetDlgItem(IDB_RECORD_RECORD)))->EnableWindow(TRUE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_STOP)))->EnableWindow(FALSE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_PLAY)))->EnableWindow(FALSE);
return NULL;
}
LRESULT CVoiceRecordDlg::OnMM_WOM_DONE(UINT wParam, LONG lParam)
{
waveOutUnprepareHeader (hWaveOut, pWaveHdr1, sizeof (WAVEHDR));
waveOutClose (hWaveOut);
return NULL;
}
LRESULT CVoiceRecordDlg::OnMM_WOM_CLOSE(UINT wParam, LONG lParam)
{
((CWnd *)(this->GetDlgItem(IDB_RECORD_RECORD)))->EnableWindow(TRUE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_STOP)))->EnableWindow(FALSE);
((CWnd *)(this->GetDlgItem(IDB_RECORD_PLAY)))->EnableWindow(TRUE);
return NULL;
}
最后的最后,不要忘了添加对声音的头文件支持(第一篇中已做过介绍)。
经过上面的步骤,一个简易版的录音机已经实现。在此基础上,添加两个定时器以及显示录音和放音时间的编辑框,在添加一个进度条控件等,基本上可以完成一个比较完善的小型录音机了,有兴趣的可以试一试。
(待完善)