MFC播放聲音和錄音的實現(三)
上一篇通過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
這裡還有一個建立目錄的問題,錄音檔案肯定是存放在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;
}
最後的最後,不要忘了新增對聲音的標頭檔案支援(第一篇中已做過介紹)。
經過上面的步驟,一個簡易版的錄音機已經實現。在此基礎上,新增兩個定時器以及顯示錄音和放音時間的編輯框,在新增一個進度條控制元件等,基本上可以完成一個比較完善的小型錄音機了,有興趣的可以試一試。
(待完善)