基於Visual C++6.0的聲音文件操作

一、前言

  當前Visual C++相關的編程資料中,無論是大部頭的參考書,還是一些計算機雜誌,對聲音文件的處理都是泛泛的涉及一下,許多編程愛好者都感到對該部分的內容瞭解不是很透徹,本文希望能夠給剛剛涉及到聲音處理領域的朋友們起到一個引路的作用,幫助他們儘快進入聲音處理的更深奧空間。

  當前計算機系統處理聲音文件有兩種辦法:一是使用現成的軟件,如微軟的錄音機、SoundForge、CoolEdit等軟件可以實現對聲音信號進行錄音、編輯、播放的處理,但它們的功能是有限的,爲了更靈活,更大限度地處理聲音數據,就不得不使用另外一種方法,既利用微軟提供的多媒體服務,在Windows環境下自己編寫程序來進行聲音處理來實現一些特定的功能。下面就開始介紹聲音文件的格式和在Windows環境下使用Visual C++開發工具進行聲音文件編程處理的方法,本文所有的程序代碼都在Windows2000、Visual C++6.0環境下編譯通過,運行正常。

  二、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文件結構

  PCMWAVEFORMAT結構定義如下:

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文件數據採樣格式

三、聲音文件的聲音數據的讀取操作

  操作聲音文件,也就是將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;
}



  四、使用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)。

  下面的代碼實現了WAVE聲音文件的播放:

void CTest1View::OnMciPlayWave() 
{
// 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文件的方法

  MCI雖然調用簡單,功能強大,可以滿足聲音文件處理的基本需要,但是MCI也有它的缺點,那就是它一次只能播放一個WAVE文件,有時在實際應用中,爲了實現混音效果,需要同時播放兩個或兩個以上的WAVE文件時,就需要使用微軟DirectX技術中的DirectSound了,該技術直接操作底層聲卡設備,可以實現八個以上WAV文件的同時播放。

  實現DirectSound需要以下幾個步驟:1.創建及初始化DirectSound;2.設定應用程序的聲音設備優先級別方式,一般爲DSSCL_NORMAL;2. 將WAV文件讀入內存,找到格式塊、數據塊位置及數據長度;3.創建聲音緩衝區;4.載入聲音數據;5.播放及停止:

  下面的函數利用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文件的操作作了一個膚淺的介紹,希望可以對讀者起到拋磚引玉的作用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章