1. 程式人生 > >MFC播放聲音和錄音的實現(三)

MFC播放聲音和錄音的實現(三)

上一篇通過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

這裡還有一個建立目錄的問題,錄音檔案肯定是存放在Debug目錄下的一個單獨的資料夾中,這個問題也已經解決,

第二個問題,通過新增系統時間為字尾,確保檔案不會重名;關於系統時間的獲取方法,可參考: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;  
}  

最後的最後,不要忘了新增對聲音的標頭檔案支援(第一篇中已做過介紹)。

經過上面的步驟,一個簡易版的錄音機已經實現。在此基礎上,新增兩個定時器以及顯示錄音和放音時間的編輯框,在新增一個進度條控制元件等,基本上可以完成一個比較完善的小型錄音機了,有興趣的可以試一試。

(待完善)