聲音是人類傳遞資訊的重要途徑,如果應用程式中包含聲音資訊,就可以大大增強它的親合力;另外在科研開發過程中,聲音訊號的處理也是一個很重要的科學研究領域。Visual C++作為一個強大的開發工具,當然是聲音處理的首選工具,但是在當前Visual C++相關的編程資料中,無論是大部頭的參考書,還是一些電腦雜誌,對音效檔的處理都是泛泛的涉及一下,許多編程愛好者都感到對該部分的內容瞭解不是很透徹,筆者結合自己的學習和開發過程中積累的一些經驗,在本執行個體中來和廣大編程愛好者們探討一下音效檔的處理,當然本執行個體也不可能包括聲音處理內容的方方面面,只是希望它能夠對剛剛涉及到聲音處理領域的朋友們起到一個引路的作用,協助他們儘快進入聲音處理的更深奧空間。
當前電腦系統處理音效檔有兩種辦法:一是使用現成的軟體,如微軟的錄音機、SoundForge、CoolEdit等軟體可以實現對聲音訊號進行錄音、編輯、播放的處理,但它們的功能是有限的,為了更靈活,更大限度地處理聲音資料,就不得不使用另外一種方法,既利用微軟提供的多媒體服務,在Windows環境下自己編寫程式來進行聲音處理來實現一些特定的功能。下面就開始介紹音效檔的格式和在Windows環境下使用Visual C++開發工具進行音效檔編程處理的方法。
一、實現方法
1、RIFF檔案結構和WAVE檔案格式
Windows支援兩種RIFF(Resource Interchange File Format,"資源互動檔案格式")格式的音頻檔案:MIDI的RMID檔案和波形音頻檔案格式WAVE檔案,其中在電腦領域最常用的數字化音效檔格式是後者,它是微軟專門為Windows系統定義的波形檔案格式(Waveform Audio),由於其副檔名為"*.wav",因而該類檔案也被稱為WAVE檔案。為了突出重點,有的放矢,本文涉及到的音效檔所指的就是WAVE檔案。常見的WAVE語音檔案主要有兩種,分別對應於單聲道(11.025KHz採樣率、8Bit的採樣值)和雙聲道(44.1KHz採樣率、16Bit的採樣值)。這裡的採樣率是指聲音訊號在進行"模→數"轉換過程中單位時間內採樣的次數。採樣值是指每一次採樣周期內聲音類比訊號的積分值。對於單聲道音效檔,採樣資料為八位的短整數(short int 00H-FFH);而對於雙聲道立體聲音效檔,每次採樣資料為一個16位的整數(int),高八位和低八位分別代表左右兩個聲道。WAVE檔案資料區塊包含以脈衝編碼調製(PCM)格式表示的樣本。在進行聲音編程處理以前,首先讓我們來瞭解一下RIFF檔案和WAVE檔案格式。
RIFF檔案結構可以看作是樹狀結構,其基本構成是稱為"塊"(Chunk)的單元,每個塊有"標誌符"、"資料大小"及"資料"所組成,塊的結構1所示:
| 塊的標誌符(4BYTES) |
| 資料大小 (4BYTES) |
| 資料 |
圖一、 塊的結構
從可以看出,其中"標誌符"為4個字元所組成的代碼,如"RIFF","LIST"等,指定塊的標誌ID;資料大小用來指定塊的資料域大小,它的尺寸也為4個字元;資料用來描述具體的聲音訊號,它可以由若干個子塊構成,一般情況下塊與塊是平行的,不能相互嵌套,但是有兩種類型的塊可以嵌套子塊,他們是"RIFF"或"LIST"標誌的塊,其中RIFF塊的層級最高,它可以包括LIST塊。另外,RIFF塊和LIST塊與其他塊不同,RIFF塊的資料總是以一個指定檔案中資料存放區格式的四個字元碼(稱為格式類型)開始,如WAVE檔案有一個"WAVE"的格式類型。LIST塊的資料總是以一個指定列表內容的4個字元碼(稱為清單類型)開始,例如副檔名為".AVI"的視頻檔案就有一個"strl"的清單類型。RIFF和LIST的塊結構如下:
| RIFF/LIST標誌符 |
| 資料1大小 |
| 資料1 |
格式/清單類型 |
| 資料 |
圖二、RIFF/LIST塊結構
WAVE檔案是非常簡單的一種RIFF檔案,它的格式類型為"WAVE"。RIFF塊包含兩個子塊,這兩個子塊的ID分別是"fmt"和"data",其中"fmt"子塊由結構PCMWAVEFORMAT所組成,其子塊的大小就是sizeofof(PCMWAVEFORMAT),資料群組成就是PCMWAVEFORMAT結構中的資料。WAVE檔案的結構如三所示:
| 標誌符(RIFF) |
| 資料大小 |
| 格式類型("WAVE") |
| "fmt" |
| Sizeof(PCMWAVEFORMAT) |
| PCMWAVEFORMAT |
| "data" |
| 聲音資料大小 |
| 聲音資料 |
圖三、WAVE檔案結構
Typedef struct { WAVEFORMAT wf;//波形格式; WORD wBitsPerSample;//WAVE檔案的採樣大小; }PCMWAVEFORMAT; WAVEFORMAT結構定義如下: typedef struct { WORD wFormatag;//編碼格式,包括WAVE_FORMAT_PCM,WAVEFORMAT_ADPCM等 WORD nChannls;//聲道數,單聲道為1,雙聲道為2; DWORD nSamplesPerSec;//採樣頻率; DWORD nAvgBytesperSec;//每秒的資料量; WORD nBlockAlign;//塊對齊; }WAVEFORMAT; |
"data"子塊包含WAVE檔案的數字化波形聲音資料,其存放格式依賴於"fmt"子塊中wFormatTag成員指定的格式種類,在多聲道WAVE檔案中,樣本是交替出現的。如16bit的單聲道WAVE檔案和雙聲道WAVE檔案的資料採樣格式分別四所示:
16位單聲道:
| 採樣一 |
採樣二 |
…… |
| 低位元組 |
高位元組 |
低位元組 |
高位元組 |
…… |
16位雙聲道:
| 採樣一 |
…… |
| 左聲道 |
右聲道 |
…… |
| 低位元組 |
高位元組 |
低位元組 |
高位元組 |
…… |
圖四、WAVE檔案資料採樣格式
2、音效檔的聲音資料的讀取操作
操作音效檔,也就是將WAVE檔案開啟,擷取其中的聲音資料,根據所需要的聲音資料處理演算法,進行相應的數學運算,然後將結果重新儲存與WAVE格式的檔案中去。可以使用CFILE類來實現讀取操作,也可以使用另外一種方法,拿就是使用Windows提供的多媒體處理函數(這些函數都以mmino打頭)。這裡就介紹如何使用這些相關的函數來擷取音效檔的資料,至於如何進行處理,那要根據你的目的來選擇不同的演算法了。WAVE檔案的操作流程如下:1)調用mminoOpen函數來開啟WAVE檔案,擷取HMMIO類型的檔案控制代碼;2)根據WAVE檔案的結構,調用mmioRead、mmioWrite和mmioSeek函數實現檔案的讀、寫和定位操作;3)調用mmioClose函數來關閉WAVE檔案。
下面的函數代碼就是根據WAVE檔案的格式,實現了讀取雙聲道立體聲資料,但是在使用下面的代碼過程中,注意需要在程式中連結Winmm.lib庫,並且包含標頭檔"Mmsystem.h"。
BYTE * GetData(Cstring *pString) //擷取音效檔資料的函數,pString參數指向要開啟的音效檔; { if (pString==NULL) return NULL; HMMIO file1;//定義HMMIO檔案控制代碼; file1=mmioOpen((LPSTR)pString,NULL,MMIO_READWRITE); //以讀寫入模式開啟所給的WAVE檔案; if(file1==NULL) { MessageBox("WAVE檔案開啟失敗!"); Return NULL; } char style[4];//定義一個四位元組的資料,用來存放檔案的類型; mmioSeek(file1,8,SEEK_SET);//定位到WAVE檔案的類型位置 mmioRead(file1,style,4); if(style[0]!='W'||style[1]!='A'||style[2]!='V'||style[3]!='E') //判斷該檔案是否為"WAVE"檔案格式 { MessageBox("該檔案不是WAVE格式的檔案!"); Return NULL; } PCMWAVEFORMAT format; //定義PCMWAVEFORMAT結構對象,用來判斷WAVE檔案格式; mmioSeek(file1,20,SEEK_SET); //對開啟的檔案進行定位,此時指向WAVE檔案的PCMWAVEFORMAT結構的資料; mmioRead(file1,(char*)&format,sizeof(PCMWAVEFORMAT));//擷取該結構的資料; if(format.wf.nChannels!=2)//判斷是否是立體聲聲音; { MessageBox("該音效檔不是雙通道立體聲檔案"); return NULL; } mmioSeek(file1,24+sizeof(PCMWAVEFORMAT),SEEK_SET); //擷取WAVE檔案的聲音資料的大小; long size; mmioRead(file1,(char*)&size,4); BYTE *pData; pData=(BYTE*)new char[size];//根據資料的大小申請緩衝區; mmioSeek(file1,28+sizeof(PCMWAVEFORMAT),SEEK_SET);//對檔案重新置放; mmioRead(file1,(char*)pData,size);//讀取聲音資料; mmioClose(file1, MMIO_FHOPEN);//關閉WAVE檔案; return pData; } |
3、使用MCI方法操作音效檔
WAVE音效檔一個最基本的操作就是將檔案中的聲音資料播放出來,用Windows提供的API函數BOOL sndPlaySound(LPCSTR lpszSound, UINT fuSound)可以實現小型WAV檔案的播放,其中參數lpszSound 為所要播放的音效檔,fuSound為播放音效檔時所用的標誌位。例如實現Sound.wav 檔案的非同步播放,只要調用函數sndPlaySound("c:/windows/Sound.wav",SND_ASYNC)就可以了,由此可以看到sndPlaySound函數使用是很簡單的。但是當WAVE檔案大於100K時,這時候系統無法將聲音資料一次性的讀入記憶體,sndPlaySound函數就不能進行播放了。為瞭解決這個問題,你的一個選擇就是用MCI方法來操作音效檔了。在使用MCI方法之前,首先需要在你開發的項目設定Project->Setting->Link->Object/library modules中加入winmm.lib。並在標頭檔中包括"mmsystem.h"標頭檔。
MicroSoft API提供了MCI(The Media Control Interface)的方法mciSendCommand()和mciSendString()來完成WAVE檔案的播放,這裡僅介紹mciSendCommand()函數的使用。
原型:DWORD mciSendCommand(UINT wDeviceID,UINT wMessage,DWORD dwParam1,DWORD dwParam2);
參數:wDeviceID:接受訊息的裝置ID;
Message:MCI命令訊息;
wParam1:命令的標誌位;
wParam2:所使用參數區塊的指標
返值:調用成功,返回零;否則,返回雙字中的低字存放有錯誤資訊。
在使用MCI播放音效檔時,首先要開啟音訊裝置,為此要定義MCI_OPEN_PARMS變數 OpenParms,並設定該結構的相應分量:
OpenParms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_WAVEFORM_AUDIO;//WAVE類型 OpenParms.lpstrElementName = (LPCSTR) Filename;//開啟的音效檔名; OpenParms.wDeviceID = 0;//開啟的音訊裝置的ID |
mciSendCommand (NULL, MCI_OPEN,MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT, (DWORD)(LPVOID) &OpenParms)函數調用發送MCI_OPEN命令後,返回的參數 OpenParms中成員變數的wDeviceID指明開啟了哪個裝置。需要關閉音訊裝置時只要調用mciSendCommand (m_wDeviceID, MCI_CLOSE, NULL, NULL)就可以了。
播放WAVE檔案時,需要定義MCI_PLAY_PARMS變數PlayParms,對該變數進行如下設定:PlayParms.dwFrom = 0,這是為了指定從什麼地方(時間)播放WAVE檔案,設定好以後,調用函數mciSendCommand (m_wDeviceID, MCI_PLAY,MCI_FROM, (DWORD)(LPVOID)&PlayParms));就實現了WAVE音效檔的播放。
另外,調用mciSendCommand (m_wDeviceID, MCI_PAUSE, 0,(DWORD)(LPVOID)&PlayParms)實現了暫停功能。調用mciSendCommand (m_wDeviceID, MCI_STOP, NULL, NULL)實現停止功能等,可以看出,這些不同的功能實現都是依靠參數"Message"取不同的值來實現的。 不同的Message和dwParam1、dwParam2的組合還可以實現檔案的 跳躍功能。如下面的代碼實現了跳轉到WAVE檔案末端的操作:mciSendCommand (m_wDeviceID, MCI_SEEK, MCI_SEEK_TO_END, NULL)。
4、DirectSound操作WAVE檔案的方法
MCI雖然調用簡單,功能強大,可以滿足音效檔處理的基本需要,但是MCI也有它的缺點,那就是它一次只能播放一個WAVE檔案,有時在實際應用中,為了實現混音效果,需要同時播放兩個或兩個以上的WAVE檔案時,就需要使用微軟DirectX技術中的DirectSound了,該技術直接操作底層音效卡裝置,可以實現八個以上WAV檔案的同時播放。
實現DirectSound需要以下幾個步驟:1.建立及初始化DirectSound;2.設定應用程式的聲音裝置優先順序別方式,一般為DSSCL_NORMAL;2. 將WAV檔案讀入記憶體,找到格式塊、資料區塊位置及資料長度;3.建立聲音緩衝區;4.載入聲音資料;5.播放及停止: PCMWAVEFORMAT結構定義如下:
二、編程步驟
1、 啟動Visual C++6.0產生一個單文檔視圖結構的應用程式,將該程式命名為"playsound";
2、 在程式的主菜單中添加"MCI Play"、"PlaySound"菜單,並使用Class Wizard添加相應的訊息響應函說,分別用不同的方法來處理音效檔;
3、 在程式的"Link"設定中添加"dsound.lib、dxguid.lib、winmm.lib"庫,程式的視圖類中包含"mmsystem.h"檔案,程式的Debug目錄下添加待播放的音效檔"chimes.wav和sound.wav";
4、 添加代碼,編譯運行程式;
三、程式碼
//////////////////////////////////////////////////// void CPlaysoundView::OnMciplay()//下面的代碼實現了WAVE音效檔的播放: { // TODO: Add your command handler code here MCI_OPEN_PARMS mciOpenParms; MCI_PLAY_PARMS PlayParms; mciOpenParms.dwCallback=0; mciOpenParms.lpstrElementName="d:/chimes.wav"; mciOpenParms.wDeviceID=0; mciOpenParms.lpstrDeviceType="waveaudio"; mciOpenParms.lpstrAlias=" "; PlayParms.dwCallback=0; PlayParms.dwTo=0; PlayParms.dwFrom=0; mciSendCommand(NULL,MCI_OPEN,MCI_OPEN_TYPE|MCI_OPEN_ELEMENT,(DWORD)(LPVOID)&mciOpenParms);//開啟音訊裝置; mciSendCommand(mciOpenParms.wDeviceID,MCI_PLAY,MCI_WAIT,(DWORD)(LPVOID)&PlayParms);//播放WAVE音效檔; mciSendCommand(mciOpenParms.wDeviceID,MCI_CLOSE,NULL,NULL);//關閉音訊裝置; } ////////////////////////////////////////////////////////////////////////////// /*下面的函數利用DirectSound技術實現了一個WAVE音效檔的播放(注意項目設定中要包含"dsound.lib、dxguid.lib"的內容),代碼和注釋如下:*/ void CPlaysoundView::OnPlaySound() { // TODO: Add your command handler code here LPVOID lpPtr1;//指標1; LPVOID lpPtr2;//指標2; HRESULT hResult; DWORD dwLen1,dwLen2; LPVOID m_pMemory;//記憶體指標; LPWAVEFORMATEX m_pFormat;//LPWAVEFORMATEX變數; LPVOID m_pData;//指向語音資料區塊的指標; DWORD m_dwSize;//WAVE檔案中語音資料區塊的長度; CFile File;//Cfile對象; DWORD dwSize;//存放WAV檔案長度; //開啟sound.wav檔案; if (!File.Open ("d://sound.wav", CFile::modeRead |CFile::shareDenyNone)) return ; dwSize = File.Seek (0, CFile::end);//擷取WAVE檔案長度; File.Seek (0, CFile::begin);//定位到開啟的WAVE檔案頭; //為m_pMemory分配記憶體,類型為LPVOID,用來存放WAVE檔案中的資料; m_pMemory = GlobalAlloc (GMEM_FIXED, dwSize); if (File.ReadHuge (m_pMemory, dwSize) != dwSize)//讀取檔案中的資料; { File.Close (); return ; } File.Close (); LPDWORD pdw,pdwEnd; DWORD dwRiff,dwType, dwLength; if (m_pFormat) //格式塊指標 m_pFormat = NULL; if (m_pData) //資料區塊指標,類型:LPBYTE m_pData = NULL; if (m_dwSize) //資料長度,類型:DWORD m_dwSize = 0; pdw = (DWORD *) m_pMemory; dwRiff = *pdw++; dwLength = *pdw++; dwType = *pdw++; if (dwRiff != mmioFOURCC ('R', 'I', 'F', 'F')) return ;//判斷檔案頭是否為"RIFF"字元; if (dwType != mmioFOURCC ('W', 'A', 'V', 'E')) return ;//判斷檔案格式是否為"WAVE"; //尋找格式塊,資料區塊位置及資料長度 pdwEnd = (DWORD *)((BYTE *) m_pMemory+dwLength -4); bool m_bend=false; while ((pdw < pdwEnd)&&(!m_bend)) //pdw檔案沒有指到檔案末尾並且沒有擷取到聲音資料時繼續; { dwType = *pdw++; dwLength = *pdw++; switch (dwType) { case mmioFOURCC('f', 'm', 't', ' ')://如果為"fmt"標誌; if (!m_pFormat)//擷取LPWAVEFORMATEX結構資料; { if (dwLength < sizeof (WAVEFORMAT)) return ; m_pFormat = (LPWAVEFORMATEX) pdw; } break; case mmioFOURCC('d', 'a', 't', 'a')://如果為"data"標誌; if (!m_pData || !m_dwSize) { m_pData = (LPBYTE) pdw;//得到指向聲音資料區塊的指標; m_dwSize = dwLength;//擷取聲音資料區塊的長度; if (m_pFormat) m_bend=TRUE; } break; } pdw = (DWORD *)((BYTE *) pdw + ((dwLength + 1)&~1));//修改pdw指標,繼續迴圈; } DSBUFFERDESC BufferDesc;//定義DSUBUFFERDESC結構對象; memset (&BufferDesc, 0, sizeof (BufferDesc)); BufferDesc.lpwfxFormat = (LPWAVEFORMATEX)m_pFormat; BufferDesc.dwSize = sizeof (DSBUFFERDESC); BufferDesc.dwBufferBytes = m_dwSize; BufferDesc.dwFlags = 0; HRESULT hRes; LPDIRECTSOUND m_lpDirectSound; hRes = ::DirectSoundCreate(0, &m_lpDirectSound, 0);//建立DirectSound對象; if( hRes != DS_OK ) return; m_lpDirectSound->SetCooperativeLevel(this->GetSafeHwnd(), DSSCL_NORMAL); //設定聲音裝置優先順序別為"NORMAL"; //建立聲音資料緩衝; LPDIRECTSOUNDBUFFER m_pDSoundBuffer; if (m_lpDirectSound->CreateSoundBuffer (&BufferDesc, &m_pDSoundBuffer, 0) == DS_OK) //載入聲音資料,這裡使用兩個指標lpPtr1,lpPtr2來指向DirectSoundBuffer緩衝區的資料,這是為了處理大型WAVE檔案而設計的。dwLen1,dwLen2分別對應這兩個指標所指向的緩衝區的長度。 hResult=m_pDSoundBuffer->Lock(0,m_dwSize,&lpPtr1,&dwLen1,&lpPtr2,&dwLen2,0); if (hResult == DS_OK) { memcpy (lpPtr1, m_pData, dwLen1); if(dwLen2>0) { BYTE *m_pData1=(BYTE*)m_pData+dwLen1; m_pData=(void *)m_pData1; memcpy(lpPtr2,m_pData, dwLen2); } m_pDSoundBuffer->Unlock (lpPtr1, dwLen1, lpPtr2, dwLen2); } DWORD dwFlags = 0; m_pDSoundBuffer->Play (0, 0, dwFlags); //播放WAVE聲音資料; } |
四、小結
為了更好的說明DiretSound編程的實現,筆者使用了一個函數來實現所有的操作,當然讀者可以將上面的內容封裝到一個類中,從而更好的實現程式的封裝性,至於如何?就不需要筆者多說了,真不明白的話,找本C++的書看看(呵呵)。如果定義了類,那麼就可以一次聲明多個對象來實現多個WAVE音效檔的混合播放。也許細心的讀者朋友會發現,在介紹WAVE檔案格式的時候我們介紹了PCMWAVEFORMAT結構,但是在代碼的實現讀取WAVE檔案資料部分,我們使用的卻是LPWAVEFORMATEX結構,那末是不是我們有錯誤呢?其實沒有錯,對於PCM格式的WAVE檔案來說,這兩個結構是完全一樣的,使用LPWAVEFORMATEX結構不過是為了方便設定DSBUFFERDESC對象罷了。
操作WAVE音效檔的方法很多,靈活的運用它們可以靈活地操作WAVE檔案,這些函數的詳細用途讀者可以參考MSDN。本執行個體只是對WAVE檔案的操作作了一個膚淺的介紹,希望可以對讀者朋友起到拋磚引玉的作用.