Winamp輸入模塊編寫詳解

寫給C/C++基礎類的朋友:
    很長時間都沒有認真的來版上和網友們聊聊了,偶爾上來也是隨便轉轉,僅處理一下版務。這些日子裏來你們之中的有些人給我發了短消息,問道“嘿,哥們(大多數時候用的是‘老大’這個詞,但我並不怎麼喜歡這個稱呼,感覺有點像黑社會?),最近怎麼不見你露面啊,忙什麼呢?”而我在極爲敷衍的回答道:“在忙自己的活呢,不好意思啊。”之後也感覺到非常內疚,但是每當我一想起現在做的工作,能真正的給那些在C/C++版裏做出貢獻的網友們留下點東西,我也便覺得心安理得了。

 


    在正式開始之間我還是想說一些廢話,如果你是老手就向下拖動右邊的滾動條30-50像素吧。Winamp是衆所周知的世界上最好最流行的音樂(媒體)播放軟件,它所支持的音頻格式之多另其它同類軟件望塵莫及。而這種廣泛支持性又是由於它採用了非常合理巧妙的插件式設計,每個人都可以用winamp來播放自己發明的音頻格式,只要你寫出了相應的輸入插件。但這篇文章不會教你用小波變換等高難度數學算法來編寫比MP3或APE更爲優秀的音頻編碼格式,如果你企圖在文章裏找到這些東西,我還是勸你到此爲止,關掉瀏覽器,然後去圖書館翻看《信號學》。在這篇文章裏我首先會教你怎樣編寫一個符合Winamp插件規範的輸出插件,你一定會問,我剛纔說的不是輸入插件嗎?其實這兩種插件是一體的,你向下看便會得到答案。在練習編寫輸出插件的的同時,我還將幫你瞭解電聲的基礎原理、瞭解Winamp的設計結構、熟悉WAVE(PCM)的文件格式、聲卡的基本工作機制等。最後,我們還會用DirectX來完成這個音頻的輸出模塊。這是一系列非常令人激動的主題,但請你還是先不要急着熱血沸騰,下面這幾盆涼水是一定要潑的。在繼續看下去之間請確定你有這些條件或能力:可以隨時打開VC.net編寫測試程序、裝有DirectX8.1SDK、能夠熟練的開發一個並不十分複雜的SDK程序,熟悉C++……好了,想來想去要求的條件就這麼多了,我爲了避免嚇跑大批的讀者還是儘量使用了較溫合的詞彙。還有一點要說的是,我希望你能在一個愉快的心境中閱讀全文,而不是像蹲在廁所裏讀《量子力學》那種感覺一樣,確定我們可以開始了嗎?

    爲了從哪裏開始這個問題我傷透了腦筋,想來想去畢竟這偏文章是發表在網上的,那麼就從你移動鼠標開始吧,現在再打開一個瀏覽器,進入下面的網址:http://www.winamp.com/nsdn/winamp2x/dev/plugins/out.jhtml這裏是Winamp插件開發的主頁,他美其名曰NSDN,可是和MSDN相比,它除了一點原代碼之外什麼也沒有。你現在需要做的是下載那個OUT_MINISDK,鏈接在:http://ftpwa.newaol.com/customize/component/nsdn/winamp2x/out_minisdk.zip,你打開這個壓縮包你會看到三個文件in2.h、out.h和out_raw.c,什麼,你還看到了別的文件?哦,在那是還有一些別的文件,但我們現在只需要這三個。把它們解壓出來,然後打開他們,因爲你在聽我講述Winamp的設計結構的時候需要對應着看看實現的代碼。

    當我第一次看到這個插件的封裝結構時我驚呆了,原來Winamp比我想象中的更懶,它其實什麼都沒有做,僅僅是一個界面,調用了一些輸入的接口,僅此而已。我們先看一下In_Module和Out_Module的結構:

typedef struct
{
 int nVer; // 模塊版本號
 char *szDesc; //模塊描述信息
 HWND hMainWnd; // Winamp的主窗體句柄(由Winamp來填寫)
 HINSTANCE hDllInstance;  // DLL實例句柄(由Winamp來填寫)
 char *szFileExt; // 擴展名過濾器,格式參見GetOpenFileName
 int nIsSeekable; // 是否可索引媒體,是-你可以拖動進度條,否-反之
 int UsesOutputPlug; // 是否使用輸出插件?你想在這個模塊裏搞定一切?

// 下面都是函數指針,將被Winamp調用
 void (*Config)(HWND hwndParent); // 配置對話框
 void (*About)(HWND hwndParent);  // 關於對話框
 void (*Init)();  // 初始化
 void (*Quit)();  // 退出
 // szFile - 傳入的文件名,szTitle - 傳出的標題,nLen - 轉出的時間長度,毫秒。
 // 如果szFile傳NULL,則返回當前播放文件的信息
 void (*GetFileInfo)(char *szFile, char *szTitle, int *nLen);
 int (*InfoBox)(char *szFile, HWND hwndParent); // 彈出文件信息對話框
 int (*IsOurFile)(char *szFile); // 檢查文件格式
 int (*Play)(char *szFile); // 開始播放文件szFile,返回0正常,-1錯誤
 void (*Pause)(); // 暫停處理
 void (*UnPause)(); // 取消暫停
 int (*IsPaused)(); // 是否斬停?1是暫停,0不是
 void (*Stop)();  // 停止播放
 int (*GetLength)(); // 取得長度,毫秒單位
 int (*GetOutputTime)();  // 獲取當前時間,一般調用out模塊的同名函數即可
 void (*SetOutputTime)(int nTime); // 索引到某一時刻
 void (*SetVolume)(int volume); // 音量調節,從0 - 255
 void (*SetPan)(int pan); // 左右聲道平衡,從-127 - 127
 
//下面的函數多和AVS、可視化效果、均衡器等有關,具體咱們用不到,就暫時不講了。
 void (*SAVSAInit)(int maxlatency_in_ms, int srate);
 void (*SAVSADeInit)(); // call in Stop()
 void (*SAAddPCMData)(void *PCMData, int nch, int bps, int timestamp);
 int (*SAGetMode)();
 void (*SAAdd)(void *data, int timestamp, int csa);
 void (*VSAAddPCMData)(void *PCMData, int nch, int bps, int timestamp);
 int (*VSAGetMode)(int *specNch, int *waveNch);
 void (*VSAAdd)(void *data, int timestamp);
 void (*VSASetInfo)(int nch, int srate);
 int (*dsp_isactive)();
 int (*dsp_dosamples)(short int *samples, int numsamples, int bps, int nch, int srate);
 void (*EQSet)(int on, char data[10], int preamp);
 void (*SetInfo)(int bitrate, int srate, int stereo, int synched);

 Out_Module *outMod; // 看看,Winamp終於露出馬腳了吧?
} In_Module;


typedef struct
{
// 下面有些和In_Module一樣,就不贅述了。
 int nVer;
 char *szDesc;
 int nId; // 自己給一個ID,不知道有什麼用,反正大於65536就行了。
 HWND hMainWindow;
 HINSTANCE hDllInstance;
 void (*Config)(HWND hwndParent);
 void (*About)(HWND hwndParent);
 void (*Init)();
 void (*Quit)();
 // nSample - 採樣率, nChannels - 聲道數,1或2
 // nBitPerSamp - 每採樣的位率,nBufLen、nPreBufLen - 緩衝長度,咱們用不到
 // 返回大於0正常播放,小於0失敗
 int (*Open)(int nSample, int nChannels, int nBitPerSamp, int nBufLen, int nPreBufLen);
 void (*Close)(); // 關閉輸出設備
 // pBuf - 內存數據塊,nLen - 數據塊的長度
 int (*Write)(char *pBuf, int len); // 返回0成功,其它不成功
 int (*CanWrite)(); // 表示當前狀態是否可寫
 int (*IsPlaying)(); // 表示是否正在播放
 int (*Pause)(int pause); // 暫停,稍後詳釋
 void (*SetVolume)(int volume);
 void (*SetPan)(int pan);
 void (*Flush)(int t); // 刷新緩衝
 int (*GetOutputTime)(); // 獲取輸出時間
 int (*GetWrittenTime)(); // 返回寫入的時間
} Out_Module;


你應該還在out_raw.c裏看到了這樣的代碼:
__declspec( dllexport ) Out_Module * winampGetOutModule()
{
 return &out;
}

    相信大多數有些經驗的人看到這裏就已經真像大白了,不過考慮到文章的“兼容性”我還是要詳述一翻。就拿輸出模塊來講,Winamp在啓動的時候,會去Plugins目錄中查找所有可用的輸出插件,如果找到有可用的輸出模塊,就會先把這個DLL加載進來。而這個DLL被加載時會按照編寫者的定義填寫好一個Out_Module全局對象,裏面的函數指針成員則指向已經實現的函數代碼。而Winamp調用這些函數的方法就是通過這個DLL導出的winampGetOutModule()函數來得到那個Out_Module全局對象的指針,接着它就可以通過這個全局對象的成員函數指針來調用那些函數了。是不是很巧妙呢?而插件的反饋機制則是由Windows標準的消息機制來完成,PostMessage就是最簡單的方法,這也正是每一個插件裏都會有一個HWND hMainWindow主窗體句柄的原因。事實上Winamp在播放音樂時大多數調用的都只是In_Module的接口,因爲In_Module裏包括一個Out_Module的指針,所以In_Module自動調用Out_Module相應的函數,爲Winamp完成所有的事情,Out_Module一般只是用來反饋。

    在瞭解了Winamp的工作原理之後,下面我們將開始編寫第一個Winamp輸出模塊。爲了方便調試,並且避免對你的winamp造成意外的傷害,我們會先寫一個winamp模擬程序,它會來調用Winamp的插件。就照下面的步驟去做吧,在VC.net中新建一個MFC應用程序,記住選對話框。添加兩個文件MusicPlayer.h和MusicPlayer.cpp。然後把剛纔的那兩個大結構體Out_Module和In_Module的聲明加入頭文件,接着寫一些咱們需要調用的函數聲明:

void OutInit();
void OutConfig( HWND hWnd );
void OutAbout( HWND hWnd );
int OutOpen( int nSampleRate, int nChannels, int nBitPerSample, int nBufLen, int nPreBufLen );
void OutClose( void );
void OutQuit( void );
int OutWrite( char *pBuf, int nLen );
int OutCanWrite( void );
int OutIsPlaying( void );
int OutPause( int nPause );
void OutFlush( int nT );
int OutGetWrittenTime( void );
int OutGetOutputTime( void );
void OutSetVolume( int nVolume );
void OutSetPan( int nPan );

    我們還需要在頭文件裏聲明以下幾個東東:

// 用於從DLL中獲取上面說的那個接口的函數指針
typedef In_Module* (WINAPI *PWINAMPGETIN2MOD)( void );
// 用於方便的初始化對象
void InitModules( In_Module *pInModule, Out_Module *pOutModule );

    現在回到MusicPlayer.cpp裏,實現上面聲明的那些函數,先把大括號寫好,有返回值的函數都返回0,讓整個工程能夠順利的通過編譯,稍後我們再回來填寫這些函數裏的代碼。在這之前我們先得實現一些輸入模塊的結構體裏沒有填好而等着Winamp來填的函數指針,如果我們不填,輸入模塊就會調用空地址,結果可想而知。下面是這些函數的聲明及實現,直接放到MusicPlayer.cpp文件的頂部即可,不會佔太多空間吧? :)

void SAVSAInit(int maxlatency_in_ms, int srate){}
void SAVSADeInit(){}
void SAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int SAGetMode(){return 0;}
void SAAdd(void *data, int timestamp, int csa){}
void VSAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int VSAGetMode(int *specNch, int *waveNch){return 0;}
void VSAAdd(void *data, int timestamp){}
void VSASetInfo(int nch, int srate){}
int dsp_isactive(){return 0;}
int dsp_dosamples(short int *samples, int numsamples, int bps, int nch, int srate){return 0;}
void EQSet(int on, char data[10], int preamp){}
void SetInfo(int bitrate, int srate, int stereo, int synched){}

    上面這些函數都是用於可視化效果的,咱們當然不需要,所以用最簡單的代碼實現一下就行了!下面再實現一下InitModules函數:

void InitModules( In_Module *pInModule, Out_Module *pOutModule )
{
 // 如果你用MFC的話,可以用下面的方法得到主窗體指針
 // 如果不是,那你需要另想辦法,比如全局變量
 g_hWnd = ::AfxGetMainWnd()->GetSafeHwnd();
 // 給傳進來的輸出模塊結構體填值,使和在其被調用時將調用到上面相應的函數
 pOutModule->description = "Creamdog DirectSount Out Module v1.0";
 pOutModule->About = OutAbout;
 pOutModule->CanWrite = OutCanWrite;
 pOutModule->Open = OutOpen;
 pOutModule->Close = OutClose;
 pOutModule->Config = OutConfig;
 pOutModule->Flush = OutFlush;
 pOutModule->GetOutputTime = OutGetOutputTime;
 pOutModule->GetWrittenTime = OutGetWrittenTime;
 pOutModule->hDllInstance = ::AfxGetInstanceHandle();
 pOutModule->hMainWindow = g_hWnd;
 pOutModule->id = 32;
 pOutModule->Init = OutInit;
 pOutModule->IsPlaying = OutIsPlaying;
 pOutModule->Pause = OutPause;
 pOutModule->Quit = OutQuit;
 pOutModule->SetPan = OutSetPan;
 pOutModule->SetVolume = OutSetVolume;
 pOutModule->version = 100;
 pOutModule->Write = OutWrite;
 pOutModule->Init();

 // 輸入模塊的大部分功能函數在你加載DLL時已經填好
 // 這裏只需要填一下那寫“無用而危險”的函數指針
 pInModule->outMod = pOutModule;
 pInModule->hMainWindow = pOutModule->hMainWindow;
 pInModule->hDllInstance = pOutModule->hDllInstance;
 pInModule->SAVSAInit = SAVSAInit;
 pInModule->SAVSADeInit = SAVSADeInit;
 pInModule->SAAddPCMData = SAAddPCMData;
 pInModule->SAGetMode = SAGetMode;
 pInModule->SAAdd = SAAdd;
 pInModule->VSAAddPCMData = VSAAddPCMData;
 pInModule->VSAGetMode = VSAGetMode;
 pInModule->VSAAdd = VSAAdd;
 pInModule->VSASetInfo = VSASetInfo;
 pInModule->dsp_isactive = dsp_isactive;
 pInModule->dsp_dosamples = dsp_dosamples;
 pInModule->EQSet = EQSet;
 pInModule->SetInfo = SetInfo;
 // 最後初始化一下
 pInModule->Init();
}

看看程序能不能正常編譯?如果可以,那麼開始我們的主題吧,現在回過頭來填寫上面那些聲明在頭文件裏的重要的函數。你還是不要指望這一次我們就能播出音樂來,因爲對於一些初學者來說,他們的基礎知識離此還差一些,所以我需要先講述一下電聲學的基本原理,如果你是老鳥,那麼就敬請您再一次向下拖動右邊的滾動條了。

衆所周知,聲音是由於空氣的振動而使耳膜產生諧振而引起的,振動的頻率便是聲波的頻率,一般人耳的接受能力是20Hz到20000Hz,低於此泛圍的叫次聲波,高於此泛圍的叫超聲波。不同的音調的頻率當然不一樣,比如do、re、mi就是三種不同的頻率。那麼,不同的樂器發出同要頻率的聲音,但我們卻可以聽出它們的不同,這又是怎麼回事呢?這就是音色的差別,事實上不同的樂器之間的差別並不在頻率上而是在波形上,比如同樣頻率的聲波,在一個週期的1/4階段部分內陡升而在3/4階段內緩升和1/4階段部分內緩升而在3/4階段內陡升所產生的聲音是截然不一樣的。

音箱中喇叭的原理其實很簡單,就是通過電磁感應現象,將變化的電流轉爲盆膜的振動而產生空氣的振動,所以只要有在人耳能夠接受的頻率泛圍並且足夠強大的振動的電流輸入喇叭,人耳在足夠近的聲場距離內就可以得到聲壓了(聽到聲音)。當然在多媒體有源音箱上除了喇叭還有運放(電流放大器)和功放(功率放大器)等器件,這些器件的大致作用就是將輸入的電流和功率放大至喇叭可以發出聲音的泛圍內。電腦上的聲卡的作用就是將傳遞給聲卡的數字音頻數據經過緩衝、處理、分流、轉換等操作再通過接口將載有模擬信號的電流傳送到音箱,令其發聲。

現在對於電聲的原理因該比較清楚了,可是聲波電信號是一條光滑的曲線,做爲只有1和0表示的計算機數據該如何表示這種模擬信號呢?下面我們將引入一個概念“採樣率”。比如一種聲波的頻率是1000Hz,也就代表它一秒鐘內將振動1000次,即1000個週期,那麼如果需要使這個聲音從數字波還原成聲波之後還能聽個響,那麼每一個週期裏你就需要至少記錄8個採樣點才能較爲完好的重放。那麼這段數據波的採樣率就是8*1000 = 8000 Samp/Sec。再來看一個概念“位率”,這個很好理解,如果是8位,那麼就是用一個char類型的數據來記錄每一個採樣點,最大振幅當然就是-128 – 127,如果是16位,那就用一個short來記錄,泛圍是-32768 – 32767,你不要擔心用小的位率造成振幅減小,而導至聲音減少,因爲聲卡得到數據後不管是什麼類型的數據,它都會映射到一個泛圍內,也就是說8位裏127的音量和16位裏32767的音量是一樣的,只不過16位可以表現更多的動態細節。

    現在你是不是會很焦急的發問,說了這麼多,那數據倒底是怎麼存的呢?別急,我舉個例子,比如一個1000Hz的正弦波,我們對它進行數字採樣,採樣率是8000,位率是16Bit(每一個採樣點數據都是short型),單聲道,那麼它的採樣點數據因該是0,16384,32768,16384,0,-16384,-32768,-16384、0、16384……這樣循環1000次就代表1秒的1000Hz聲波。目前的聲卡一般都支持16和8位率,44100、22050、11025等採樣率,至於爲什麼採樣率不是整數,這是爲了有效減少條幅波現象,這個原理有點複雜,如果你有興趣可以參考一下相關書籍,我在此就不贅述了。下面我寫的這個小程序可以生成指定頻率、採樣率、位率、時間長度的數據聲波。

typedef struct tagWAVEINFO
{
 WORD cbSize;
 DWORD dwSamplePerSec;
 double dSeconds;
 WORD wChannels;
 WORD wBitsPerSample;
 DWORD dwHz;
 BYTE byVolume;
 DWORD dwMode;
} WAVEINFO, *PWAVEINFO;

// 用PWAVEINFO 指定你需要的聲波的類型,函數將按照你的要求創建數據
// pwf用於傳出符合WAVE標準的波型信息,pBufLen傳出生成的數據長度
// 函數返回生成的數據的指針
void* CreateWaveData( const PWAVEINFO pwi, LPWAVEFORMATEX pwf, DWORD *pBufLen )
{
 if ( pwi->cbSize != sizeof(WAVEINFO) )
 {
  return NULL;
 }
 double dCurTime;
 int nCurValue;
 double dCircle = 1.0 / (double)pwi->dwHz;  // 週期時間
 double dSecPerSample = 1.0 / (double)pwi->dwSamplePerSec; // 採樣點距
 WORD wBytePerSample = pwi->wBitsPerSample / 8; // 每採樣字節數
 DWORD dwSampleCount = (DWORD)ceil( pwi->dSeconds * (double)pwi->dwSamplePerSec ); // 全部採樣點數
 *pBufLen = dwSampleCount * wBytePerSample * pwi->wChannels;  // 數據長度
 void *pData = new char[*pBufLen];
 for ( DWORD i = 0; i < dwSampleCount; i++ )
 {
  dCurTime = dSecPerSample * (double)i / dCircle;
  dCurTime = ( dCurTime - (int)dCurTime ) * dCircle;
  nCurValue = (int)(sin( dCurTime * M_PI * 2 / dCircle ) *
   pow( 2.0, pwi->wBitsPerSample ) / 2.0 * (double)pwi->byVolume / 255.0);
  for ( WORD j = 0; j < pwi->wChannels; j++ )
  {
   for ( WORD k = 0; k < wBytePerSample; k++ )
   {
    ((char*)pData)[ ( i * pwi->wChannels + j ) * wBytePerSample + k ] =
     ( nCurValue << ( ( 3 - k ) * 8) ) >> 24;
   }
  }
 }
 pwf->cbSize = sizeof(WAVEFORMATEX);
 pwf->wFormatTag = WAVE_FORMAT_PCM;
 pwf->nChannels = pwi->wChannels;
 pwf->nSamplesPerSec = pwi->dwSamplePerSec;
 pwf->wBitsPerSample = pwi->wBitsPerSample;
 pwf->nBlockAlign = wBytePerSample * pwi->wChannels;
 pwf->nAvgBytesPerSec = pwi->dwSamplePerSec * pwf->nBlockAlign;
 return pData;
}

    在你確定你足夠了解上面所說的電聲原理之後,下面我將帶你到Win32下的WAVE(PCM)格式中去遊一遭。PCM的全稱是:pulse code modulation(脈衝編碼調製),說白了就是間隔採樣的機制。WAVE文件的結構如下:
作用           長度    類型    默認值   註釋
文件標識        4     char[4]   “RIFF”    Resource Interchange File Format(資源交換文件格式)WAVE是其中的一種
文件數據長度    4     DWORD  N/A      不包括文件標識和數據長度的裸數據的長度
格式標識        4     char[4]   “WAVE”   以此來判斷是否WAVE文件
信息頭標識      4     char[4]   “fmt ”     表示下面開始信息頭描述
信息頭長度      4     DWORD  16或12   表示下面的信息頭數據有多長
聲道數          2     WORD    1或2    表示單聲道或立體聲
採樣率          4     DWORD  44100等
每秒數據長度    4     DWORD  N/A      位率 / 8 * 採樣率 * 聲道數
每數據包長度    4     WORD    N/A      位率 / 8 * 聲道數
位率            2     WORD    16或8
數據標識        4     char[4]   “data”     表示下面這一塊是數據塊
數據長度        4     DWORD  N/A      表示數據有多長
數據           N/A    N/A      N/A      數據
其它信息       不需要

上面這些信息是順序存儲的,而且信息頭剛好和WAVEFORMATEX結構體對映起來,只是順序有點亂,可千萬不要搞差了。很簡單是吧?現在你可以用上面的那個聲波生成器生成一段聲音數據,然後寫一個WAVE文件試一下,看能不能用Winamp放出來。

說了這麼多,相信菜鳥也對這些東東有些瞭解了,那麼現在我們回過頭去繼續編寫我們的輸出模塊。先確定一下我們要編寫怎樣的一個輸出模塊?輸出到聲卡嗎?那太難了,讓我們先來點簡單的測一下,輸出一個WAVE文件。文件操作模塊我個人認爲使用STL庫裏的fstream是很方便的,所以你得先在MusicPlayer.cpp的頂上定一個文件全局變量來打開你需要寫的文件:

ofstream file( “文件名”, ios_base::out | ios_base::binary );

    另外還需要兩個全局變量:

DWORD g_dwWrited; // 當前寫入的數據的長度
DWORD g_dwBytePerSec; // 每秒字節數

    在調用In_Module的Play函數後,也就是音樂開始播放後,In_Module會調用Out_Module的Open函數,並指定數據信息,所以我們應該在這裏寫下Wave 文件的頭信息,按照上面說的WAVE文件格式,我們這樣寫Open函數:

int OutOpen( int nSampleRate, int nChannels, int nBitPerSample, int nBufLen, int nPreBufLen )
{
 DWORD dwFlag;
 dwFlag = (DWORD)'RIFF';
 file.write( (char*)&dwFlag, 4 );
 file.write( (char*)&dwFlag, 4 ); // 空出4個字節寫數據長度
 dwFlag = (DWORD)'WAVE';
 file.write( (char*)&dwFlag, 4 );
 dwFlag = (DWORD)'fmt ';
 file.write( (char*)&dwFlag, 4 );

 WAVEFORMATEX wf;
 ZeroMemory( &wf, sizeof(wf) );
 wf.cbSize = sizeof(wf);
 wf.nSamplesPerSec = nSampleRate;
 wf.wBitsPerSample = nBitPerSample;
 wf.nChannels = nChannels;
 wf.nBlockAlign = nBitPerSample / 8 * nChannels;
 wf.nAvgBytesPerSec = wf.nBlockAlign * nSampleRate;
 wf.wFormatTag = WAVE_FORMAT_PCM;

 g_dwBytePerSec = wf.nAvgBytesPerSec;

 file.write( (char*)&wf.cbSize, sizeof(wf.cbSize) ); // 寫低位
 wf.cbSize = 0;
 file.write( (char*)&wf.cbSize, sizeof(wf.cbSize) ); // 寫高位,0
 file.write( (char*)&wf.wFormatTag, sizeof(wf)-sizeof(wf.cbSize) ); //跟據偏移量一次性寫入
 dwFlag = (DWORD)'data';
 file.write( (char*)&dwFlag, 4 );
}

    在開始播放之後,In_Module會反覆執行下面的過程:調用Out_Module的OutCanWrite函數確定是否可寫後就會調用Out_Module的OutWrite函數向其寫數據。因爲在這裏是文件操作,所以我們在OutCanWrite函數使終返回0x1000就行。

int OutCanWrite( void )
{
 return 0x1000;
}

    OutWrite函數當然更簡單,只需要簡單的把數據寫入文件,並把數據長度累加就行了。

int OutWrite( char *pBuf, int nLen )
{
 file.write( pBuf, nLen );
 g_dwWrited += nLen;
}

這樣數據就會一段一段的寫入創建的WAVE文件,但是仍然存在問題,Out_Module不知道何時結束,而In_Module也不可以在寫入結束後便馬上終斷正在撥放緩衝中的內容的Out_Module。但In_Module在寫入結束後會向主窗體發送一個USER+2的消息,表示寫入結束,然後主窗體在這裏就因該使用計時器一類的機制調用Out_Module的GetOutputTime來獲取輸出模塊的當前播放的時間,所以這個函數因該這樣寫:

int OutGetOutputTime( void )
{
 return g_dwWrited / g_dwBytePerSec * 1000;
}

    當主窗體發現輸出的時間入全部時間長度相等時,則因立即調用Out_Module的OutClose事件。OutClose因該文件中填寫兩個空缺的數據,一個是文件數據長度,一個是裸數據長度。
void OutClose( void )
{
 DWORD dwSize = (DWORD)file.tellg() - 8;
 file.seekg( 4, ios_base::beg );
 file.write( (char*)&dwSize, sizeof(dwSize) );
 file.seekg( 32, ios_base::cur );
 file.write( (char*)&g_dwWrited, sizeof(g_dwWrited) );
file.close();
}

至此一個WAVE文件就完全寫好了。主程序的調用還有一些代碼,不過很簡單。在應用程序初始化的時候,你需要加載Winamp的in_mp3.dll。

 In_Module *pInModule;
 HMODULE hIn2Mod;
 hIn2Mod = LoadLibrary( "in_mpg123.dll" );
 if ( NULL == hIn2Mod )
 {
  MessageBox( NULL, "未能加載輸入模塊!", "錯誤", MB_ICONHAND );
  return FALSE;
 }
 PWINAMPGETIN2MOD pInFunc = (PWINAMPGETIN2MOD)GetProcAddress( hIn2Mod, "winampGetInModule2" );
 pInModule = pInFunc();
// 這樣你就或得了來自Winamp的插件in_mp3.dll的In_Module對象。
// 而Out_Module是剛纔我們寫好的東東,實例化一個就行。
 Out_Module OutModule;
// 下面用我們先前寫好的函數進行初始化。
 InitModules( pInModule, &OutModule );

    現在你就可以調用pInModule->Play( “文件名” );來播放——事實上是複製音樂了。結束後你可以用Winamp打開寫好的文件試聽一下,因該沒有什麼問題。如果你覺得你看完上面這麼長的內容仍然感覺到飢餓的話,下面將是更加令人振奮的主題,讓我們用DirectX來寫一個基於測試程序的真正的媒體播放器。首先要確定是的,你瞭解DirectX嗎?如果你從來沒有寫過DirectX的程序,可能下面的內容對你來說就有點難了,至少你要確定你在VC.net下寫過COM應用的程序。但是問題又出現了,這麼長的代碼我該怎麼講呢?乾脆就先貼出的原代碼,再在後面講一下難點算啦。


//MusicPlayer.cpp
#include "StdAfx.h"
#include "MusicPlayer.h"

HWND g_hWnd;
HANDLE g_hThread;
IDirectSound8 *g_pDS8; // DirectSount 的指針
IDirectSoundBuffer8 *g_pDSB; // DirectSound緩衝的指針
volatile DWORD dwWritePointer;
bool bStop, bPause;
DWORD dwBytePerSec;
DWORD dwWrapCount;
DWORD dwBufLen = 0x200000; // 緩衝的長度

DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
 DWORD dwStatus, dwCurPlay;
 while( !bStop && g_pDSB )
 {
  Sleep( 50 );
  g_pDSB->GetStatus( &dwStatus );
  if ( bPause || 0 == ( dwStatus & DSBSTATUS_PLAYING ) )
  {
   continue;
  }
  g_pDSB->GetCurrentPosition( &dwCurPlay, NULL );
  double dTemp = ( (double)dwWrapCount * dwBufLen + dwCurPlay ) * 1000.0 / dwBytePerSec;
  PostMessage( g_hWnd, WM_USER + 4, dwWrapCount, (DWORD)dTemp );
 }
 PostMessage( g_hWnd, WM_USER + 4, 0, 0 );
 g_hThread = NULL;
 return 0;
}

// 配置對話框
void OutConfig( HWND hWnd )
{
 MessageBox( hWnd, "Config", "About", MB_OK );
}

// 關於對話框
void OutAbout( HWND hWnd )
{
 MessageBox( hWnd,"About Creamdog Message!", "About", MB_OK );
}

// 初始化
void OutInit()
{
 CoInitialize(NULL);
 DirectSoundCreate8( NULL, &g_pDS8, NULL );
 g_pDS8->SetCooperativeLevel( g_hWnd, DSSCL_PRIORITY );
}

// 打開設備
int OutOpen( int nSampleRate, int nChannels, int nBitPerSample, int nBufLen, int nPreBufLen )
{
 if ( !OutIsPlaying() )
 {
  return -1;
 }

 bStop = false;
 bPause = false;
 dwWritePointer = 0;
 dwWrapCount = 0;

 WAVEFORMATEX wf;
 ZeroMemory( &wf, sizeof(wf) );
 wf.cbSize = sizeof(wf);
 wf.nSamplesPerSec = nSampleRate;
 wf.wBitsPerSample = nBitPerSample;
 wf.nChannels = nChannels;
 wf.nBlockAlign = nBitPerSample / 8 * nChannels;
 wf.nAvgBytesPerSec = wf.nBlockAlign * nSampleRate;
 wf.wFormatTag = WAVE_FORMAT_PCM;
 dwBytePerSec = wf.nAvgBytesPerSec;

 DSBUFFERDESC BufDesc;
 ZeroMemory( &BufDesc, sizeof(BufDesc) );
 BufDesc.dwSize = sizeof(BufDesc);
 BufDesc.dwFlags = DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_LOCSOFTWARE | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_STICKYFOCUS;
 BufDesc.lpwfxFormat = &wf;
 BufDesc.dwBufferBytes = dwBufLen;
 BufDesc.guid3DAlgorithm = DS3DALG_DEFAULT;

 HRESULT hr = g_pDS8->CreateSoundBuffer( &BufDesc, (LPDIRECTSOUNDBUFFER*)&g_pDSB, NULL );
 if ( S_OK != hr )
 {
  return -1;
 }

 g_hThread = CreateThread( NULL, 0, ThreadProc, 0, 0, NULL );

 if( FAILED( g_pDSB->Play( 0, 0, DSBPLAY_LOOPING ) ) )
 {
  return -1;
 }
 PostMessage( g_hWnd, WM_USER + 3, (WPARAM)wf.nAvgBytesPerSec, (LPARAM)0 );
 return 0;
}

// 準備數據塊寫入
int OutWrite( char *pBuf, int nLen )
{
 VOID *pMem1, *pMem2;
 DWORD dwSize1, dwSize2;
 if ( FAILED( g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 ) ) )
 {
  g_pDSB->Restore();
  g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 );
 }
 CopyMemory( pMem1, pBuf, dwSize1 );
 if ( pMem2 )
 {
  CopyMemory( pMem2, pBuf + dwSize1, dwSize2 );
 }
 g_pDSB->Unlock( pMem1, dwSize1, pMem2, dwSize2);
 dwWritePointer += nLen;
 return 0;
}

int OutCanWrite( void )
{
 DWORD dwCurPlay;
 static DWORD dwLastPlay = 0;
 if ( bPause || NULL == g_pDSB )
 {
  return 0;
 }
 g_pDSB->GetCurrentPosition( &dwCurPlay, NULL );
 if ( dwCurPlay < dwLastPlay )
 {
  dwWrapCount++;
 }
 dwLastPlay = dwCurPlay;
 long lPlayPos = (long)dwCurPlay + (long)dwWrapCount * (long)dwBufLen;
 long lWritePos = (long)dwWritePointer;
 if ( lWritePos - lPlayPos > (long)(dwBufLen / 2) )
 {
  return 0;
 }
 return dwBufLen / 2;
}

int OutIsPlaying( void )
{
 DWORD dwStatus = 0;
 if ( g_pDSB )
 {
  g_pDSB->GetStatus( &dwStatus );
 }
 return 0 == ( dwStatus & DSBSTATUS_PLAYING );
}

int OutPause( int nPause )
{
 if ( !g_pDSB )
 {
  return -1;
 }
 DWORD dwStatus;
 g_pDSB->GetStatus( &dwStatus );
 if ( 0 == ( dwStatus & DSBSTATUS_PLAYING ) )
 {
  bPause = false;
  if( FAILED( g_pDSB->Play( 0, 0, DSBPLAY_LOOPING ) ) )
  {
   return -1;
  }
 }
 else
 {
  bPause = true;
  g_pDSB->Stop();
 }

 static int nLastPause = 0;
 int nTemp = nLastPause;
 nLastPause = nPause;
 return nTemp;
}

// 關閉設備
void OutClose( void )
{
 if ( g_hThread )
 {
  bStop = true;
  WaitForSingleObject( g_hThread, INFINITE );
 }
 if ( g_pDSB )
 {
  g_pDSB->Stop();
  g_pDSB->Release();
  g_pDSB = NULL;
 }
}

void OutQuit()
{
 OutClose();
 if ( g_pDS8 )
 {
  g_pDS8->Release();
 }
}

void OutSetVolume( int nVolume )
{
 if ( g_pDSB )
 {
  g_pDSB->SetVolume( ( nVolume * ( DSBVOLUME_MAX - DSBVOLUME_MIN ) ) / 255 + DSBVOLUME_MIN );
 }
}

void OutSetPan( int nPan )
{
}

void OutFlush( int nT )
{
 DWORD dwFlushPos = ( nT / 1000 ) * dwBytePerSec;
 dwWritePointer = dwFlushPos;
 dwWrapCount = dwWritePointer / dwBufLen;
 g_pDSB->SetCurrentPosition( dwWritePointer % dwBufLen );
}

int OutGetWrittenTime( void )
{
 return 0;
}


void InitModules( In_Module *pInModule, Out_Module *pOutModule )
{
 g_hWnd = ::AfxGetMainWnd()->GetSafeHwnd();
 pOutModule->About = OutAbout;
 pOutModule->CanWrite = OutCanWrite;
 pOutModule->Open = OutOpen;
 pOutModule->Close = OutClose;
 pOutModule->Config = OutConfig;
 pOutModule->description = "Creamdog WaveOut v1.0";
 pOutModule->Flush = OutFlush;
 pOutModule->GetOutputTime = OutGetWrittenTime;
 pOutModule->GetWrittenTime = OutGetWrittenTime;
 pOutModule->hDllInstance = ::AfxGetInstanceHandle();
 pOutModule->hMainWindow = g_hWnd;
 pOutModule->id = 32;
 pOutModule->Init = OutInit;
 pOutModule->IsPlaying = OutIsPlaying;
 pOutModule->Pause = OutPause;
 pOutModule->Quit = OutQuit;
 pOutModule->SetPan = OutSetPan;
 pOutModule->SetVolume = OutSetVolume;
 pOutModule->version = 100;
 pOutModule->Write = OutWrite;
 pOutModule->Init();

 pInModule->outMod = pOutModule;
 pInModule->hMainWindow = pOutModule->hMainWindow;
 pInModule->hDllInstance = pOutModule->hDllInstance;
 pInModule->SAVSAInit = SAVSAInit;
 pInModule->SAVSADeInit = SAVSADeInit;
 pInModule->SAAddPCMData = SAAddPCMData;
 pInModule->SAGetMode = SAGetMode;
 pInModule->SAAdd = SAAdd;
 pInModule->VSAAddPCMData = VSAAddPCMData;
 pInModule->VSAGetMode = VSAGetMode;
 pInModule->VSAAdd = VSAAdd;
 pInModule->VSASetInfo = VSASetInfo;
 pInModule->dsp_isactive = dsp_isactive;
 pInModule->dsp_dosamples = dsp_dosamples;
 pInModule->EQSet = EQSet;
 pInModule->SetInfo = SetInfo;
 pInModule->Init();
}


void SAVSAInit(int maxlatency_in_ms, int srate){}
void SAVSADeInit(){}
void SAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int SAGetMode(){return 0;}
void SAAdd(void *data, int timestamp, int csa){}
void VSAAddPCMData(void *PCMData, int nch, int bps, int timestamp){}
int VSAGetMode(int *specNch, int *waveNch){return 0;}
void VSAAdd(void *data, int timestamp){}
void VSASetInfo(int nch, int srate){}
int dsp_isactive(){return 0;}
int dsp_dosamples(short int *samples, int numsamples, int bps, int nch, int srate){return 0;}
void EQSet(int on, char data[10], int preamp){}
void SetInfo(int bitrate, int srate, int stereo, int synched){}


// EOF


    在初始化函數裏,下面這三句是初始化COM和DirectSound8的,我就不多說了。

 CoInitialize(NULL);
 DirectSoundCreate8( NULL, &g_pDS8, NULL );
 g_pDS8->SetCooperativeLevel( g_hWnd, DSSCL_PRIORITY );

    在OutOpen函數裏完成的工作是創建一個循環播放的緩存,其中要格外注意的是創建的參數。

 BufDesc.dwFlags = DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_LOCSOFTWARE | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_STICKYFOCUS;

    DSBCAPS_CTRLPAN和DSBCAPS_CTRLVOLUME無所謂,指定了的話你就可以通過g_pDSB->SetVolume改變音量。DSBCAPS_LOCSOFTWARE是爲更廣泛的兼容性,在很多板載聲卡和老式的ISA聲卡上並不存在很大的硬緩存,所以用這個標記強制緩存創建在內存中。DSBCAPS_GETCURRENTPOSITION2指定後,你可以能過g_pDSB->GetCurrentPos()來得到更精確的數值。DSBCAPS_STICKYFOCUS保證你的應用程序在未激活狀態仍然能夠放出聲來。謹記,這三個標記缺一不可。

g_pDSB->Play( 0, 0, DSBPLAY_LOOPING )

    這一句是讓聲音開始循環播放。比如你創建的Buffer有0x200000字節,如果指定了DSBPLAY_LOOPING,當播放到0x200000時,又會循環到一開始進行播放。當然,在Out_Module裏並不容易得到媒體的長度,所以我們創建一個可以循環播放的緩存,寫的速度當然要比播放的速度快,OutWrite函數在前面寫,g_pDSB就跟在後面播,爲了避免OutWrite寫了一圈又趕上g_pDSB的播放,我們必須限定g_pDSB的寫入速度,這當然就由OutCanWrite來實現。

 VOID *pMem1, *pMem2;
 DWORD dwSize1, dwSize2;
 if ( FAILED( g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 ) ) )
 {
  g_pDSB->Restore();
  g_pDSB->Lock( dwWritePointer % dwBufLen, nLen, &pMem1, &dwSize1, &pMem2, &dwSize2, 0 );
 }
 CopyMemory( pMem1, pBuf, dwSize1 );
 if ( pMem2 )
 {
  CopyMemory( pMem2, pBuf + dwSize1, dwSize2 );
 }
 g_pDSB->Unlock( pMem1, dwSize1, pMem2, dwSize2);
 dwWritePointer += nLen;
 return 0;

上面OutWrite函數中的代碼很清楚的表示出寫入緩衝區的方法和播放一樣是循環的,比如你的緩衝區有0x30000字節,而你當然寫入的位置是第0x20000字節,這時你鎖定了0x20000字節長度的內容,那麼剩下的0x10000字節將從緩衝區的頭部通過pMem2返回出來。dwWritePointer記錄的是所有寫入的字節數,爲了不越界的循環寫入,我用dwWritePointer % dwBufLen來計算在循環中到底該開始寫哪個位置。另外,那個線程是至關重要的,它將通過消息向主窗體發送當前播放的時間和狀態。主窗體也只是依靠此消息來實現對Out_Module的管理。
至於其它的暫停、結束後的釋放、獲取信息等函數的代碼實現並不複雜,有興趣的你看看源代碼就因該可以理解了。如果你會寫DLL的話,那麼你現在就因該已經知道符合Winamp插件接口標準的DLL是怎麼編寫的了。
至此全文即將結束,你是不是有點鬱悶呢?是因爲錯別字太多?文法太臭?還是跟本就狗屁不通胡說八道?我已經準備好挨批評了,發短消息告訴我吧!

Creamdog於2003年9月10日星期三,七小時未進米食,一氣呵成。

聲明:
1. 這篇文章所提及Winamp爲NullSoft公司Winamp2.X系列產品,該系列產品版權完全由NullSoft公司所有,如果文章任何地方侵犯了您的版權,敬請來函告知。
2. 本文所涉及源代碼並未經過測試,本人不保證其正確性和穩定性,如果對您的計算機及數據造成任何破壞性影響,本人概不負責。
3. 如果未經過NullSoft公司的許可,擅自發布使用Winamp專用插件的應用程序,將不受到法律的保護,並可能受到NullSoft公司的指控。
4. 如果文中的講解或代碼有錯漏之處,敬請指正!
5. 本文版權歸Creamdog所有,並由Creamdog首次將本文發在CSDN C/C++基礎類論壇上,如果你想將本文轉貼到別的地方,請保留原文的所有格式和字符。
6. 感謝CSDN C/C++基礎類論壇衆網友長期以來對我工作的支持。



 

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章