MFC播放声音和录音的实现(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shufac/article/details/20649303

上一篇通过Win32控制台程序简单地完成了声音的录取和回放,但是这个过程都只是是在内存中进行的,没有进行文件的操作,这样录取的声音也就无法保存。这一篇介绍一下用MFC实现录音并生成wave文件,最后存储到指定的目录的方法。

新建一个MFC对话框应用程序,命名为VoiceRecord 打开资源视图,Dialog目录下的IDD_VOICERECORD_DIALOG,往这个对话框中添加3Button控件,修改对应的属性栏中的CaptionID,分别为:

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;  
}  

最后的最后,不要忘了添加对声音的头文件支持(第一篇中已做过介绍)。

经过上面的步骤,一个简易版的录音机已经实现。在此基础上,添加两个定时器以及显示录音和放音时间的编辑框,在添加一个进度条控件等,基本上可以完成一个比较完善的小型录音机了,有兴趣的可以试一试。

(待完善)

猜你喜欢

转载自blog.csdn.net/shufac/article/details/20649303