Skype是免費的語音通話軟件,不但可以點對點用電腦進行免費的語音通話,而且只需花費低廉的費用就可以直接呼叫固定電話或手機,Skype以優秀的通話質量而贏得了全世界不少用戶的親睐,我就是Skype的忠實用戶,下圖就是我的Skype截圖:
我常常使用Skype和台灣同胞還有國外的朋友進行聯系,有時因為業務需要需要將語音通話錄音並保留下來,在我有這個想法的那個時候(2006年)Skype官方並沒有提供錄音功能,咱們是做程序的嘛,沒有的功能可以自己來添加啊,這也是為什麼我酷愛編程的原因。
應廣大網友的要求,現將該程序的編程思路和源代碼貢獻出來與大家共勉,希望能給對音頻編程有興趣的朋友提供一點點幫助,那我就心滿意足了。
剛開始編寫這個程序的時候,我試著用常規的錄音方式對聲卡進行錄音,既然是通話錄音,我們希望能將自己的聲音和對方的聲音同時紀錄下來。首先,我們要將對方的聲音錄下來,那就只能選取“立體聲混音”通道進行錄音,但此時“麥克風”通道的聲音將被丟棄,也就是說在Skype裡對方將聽不到我說話了;其次,如果我們還要將我自己的聲音錄下來,就得開啟“麥克風”通道錄音,但是在Skype通話過程中,“麥克風”通道已經被Skype占用了,我們的程序無法再次進行錄音,看來常規的錄音方式行不通。
於是,我想到了Windows音頻的底層處理機制,任何語音軟件的音頻數據處理到最後都離不開 Windows 的底層音頻 Win32 API 函數,查一下MSDN 庫就能得知,這些函數都在 MultiMed.chm 幫助文件中:
Waveform Functions
The following functions are used with waveform audio.
auxGetDevCaps
auxGetNumDevs
auxGetVolume
auxOutMessage
auxSetVolume
PlaySound
sndPlaySound
waveInAddBuffer
waveInClose
waveInGetDevCaps
waveInGetErrorText
waveInGetID
waveInGetNumDevs
waveInGetPosition
waveInMessage
waveInOpen
waveInPrepareHeader
waveInProc
waveInReset
waveInStart
waveInStop
waveInUnprepareHeader
waveOutBreakLoop
waveOutClose
waveOutGetDevCaps
waveOutGetErrorText
waveOutGetID
waveOutGetNumDevs
waveOutGetPitch
waveOutGetPlaybackRate
waveOutGetPosition
waveOutGetVolume
waveOutMessage
waveOutOpen
waveOutPause
waveOutPrepareHeader
waveOutProc
waveOutReset
waveOutRestart
waveOutSetPitch
waveOutSetPlaybackRate
waveOutSetVolume
waveOutUnprepareHeader
waveOutWrite
有了這些函數,我就想到了一個辦法,那就是用系統鉤子改變這些函數的原地址,在Skype調用這些Win32 API函數之前先進入我的程序,我將Skype的音頻數據“偷偷地”拷貝一份傳遞給我自己的應用程序,再還給Skype,這樣就可以神不知鬼不覺地將通話中的語音數據取出來,再加上自己的mp3壓縮保存到硬盤文件即可。
以上便是整個Skype錄音的全部思路,現在開始介紹代碼。
在本程序中需要監視的Win32 API函數有:
waveInOpen – 打開一個音頻輸入設備(錄音)
waveInClose – 關閉一個音頻輸入設備(錄音)
waveOutOpen – 打開一個音頻輸出設備(回放)
waveOutClose – 關閉一個音頻輸出設備(回放)
waveInPrepareHeader – 為音頻輸入設備准備一個內存數據緩沖(錄音)
waveOutWrite – 將語音數據塊發送至音頻輸出設備進行播放(回放)
由於我們的程序需要嵌入到Skype程序中,所以我們只能使用dll的形式來編寫這個程序,我現在需要寫一個修改Win32 API函數地址的類,在這裡我直接引用了《Windows 核心編程》隨書代碼中的 CAPIHook 類,我提供的源代碼裡就有這個類,這個類可以修改Win32 API函數的地址,當我們修改好API函數地址以後,Skype調用前面所說的6個函數時系統會自動調用我們的函數,請看代碼:
//
// 定義函數變量
//
typedef MMRESULT (WINAPI *PFN_waveInOpen) ( LPHWAVEIN phwi,
UINT uDeviceID,
LPWAVEFORMATEX pwfx,
DWORD dwCallback,
DWORD dwCallbackInstance,
DWORD fdwOpen );
typedef MMRESULT (WINAPI *PFN_waveInClose) ( HWAVEIN hwi );
typedef MMRESULT (WINAPI *PFN_waveOutOpen) ( LPHWAVEOUT phwo,
UINT uDeviceID,
LPWAVEFORMATEX pwfx,
DWORD dwCallback,
DWORD dwCallbackInstance,
DWORD fdwOpen );
typedef MMRESULT (WINAPI *PFN_waveOutClose) ( HWAVEOUT hwo );
typedef MMRESULT (WINAPI *PFN_waveInPrepareHeader) ( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh );
typedef MMRESULT (WINAPI *PFN_waveOutWrite) ( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );
//
// 修改Win32 API函數地址
//
CAPIHook g_waveInOpen("winmm.dll", "waveInOpen", (PROC) Hook_waveInOpen, TRUE);
CAPIHook g_waveInClose("winmm.dll", "waveInClose", (PROC) Hook_waveInClose, TRUE);
CAPIHook g_waveOutOpen("winmm.dll", "waveOutOpen", (PROC) Hook_waveOutOpen, TRUE);
CAPIHook g_waveOutClose("winmm.dll", "waveOutClose", (PROC) Hook_waveOutClose, TRUE);
CAPIHook g_waveInPrepareHeader("winmm.dll", "waveInPrepareHeader", (PROC) Hook_waveInPrepareHeader, TRUE);
CAPIHook g_waveOutWrite("winmm.dll", "waveOutWrite", (PROC) Hook_waveOutWrite, TRUE);
說明:
CAPIHook g_waveInOpen("winmm.dll", "waveInOpen", (PROC) Hook_waveInOpen, TRUE);
這段代碼實現了 "winmm.dll" 庫中 "waveInOpen"函數地址的修改,修改後的地址為“Hook_waveInOpen”,也就是說,以後Skype調用函數“waveInOpen”系統會自動先調用我們的函數“Hook_waveInOpen”。其他幾個函數修改原理相同。
至此我們已經成功地修改了Win32 API函數,由於Skype在調用這些API函數時會將音頻數據傳遞給系統,剛好系統又先調用我們的函數,那我們就可以得到Skype的音頻數據,看下面代碼:
//
// 修改 waveOutWrite 函數地址
//
CAPIHook g_waveOutWrite("winmm.dll", "waveOutWrite", (PROC) Hook_waveOutWrite, TRUE);
//
// This is the waveOutWrite replacement function
//
MMRESULT WINAPI Hook_waveOutWrite ( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh )
{
//
// Skype 在調用 waveOutWrite 函數進行音頻回放(播放對方的聲音)時我們將得
// 到這些音頻數據,這些數據就存放在 pwh->lpData 緩沖中,我們調用
// SendDataToMainWnd 函數將數據發送至主窗口進行壓縮和保存處理
//
SendDataToMainWnd ( pwh->lpData,
pwh->dwBytesRecorded > 0 ? pwh->dwBytesRecorded : pwh->dwBufferLength,
ENUM_CATCHSOUNDTYPE_waveOutWrite );
// Call the original waveOutWrite function
MMRESULT nResult = ((PFN_waveOutWrite)(PROC) g_waveOutWrite )
(hwo, pwh, cbwh);
// Return the result back to the caller
return(nResult);
}
以上代碼中將“偷取”到的音頻數據通過 SendDataToMainWnd 函數發送給主窗口,至此,回放音頻數據偷取成功了。
接下來我們在“偷取”錄音數據(即通話中我說話的音頻數據),看代碼:
//
// 修改 waveInPrepareHeader 函數地址
//
CAPIHook g_waveInPrepareHeader("winmm.dll", "waveInPrepareHeader", (PROC) Hook_waveInPrepareHeader, TRUE);
//
// This is the waveInPrepareHeader replacement function
//
MMRESULT WINAPI Hook_waveInPrepareHeader ( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh )
{
// 我們都知道,常規錄音是用多個錄音緩沖輪流交替使用的方式來得到來自
// 硬件設備的語音數據的,當任何一個錄音緩沖數據滿的時候,錄音程序將
// 調用 Win32 API waveInPrepareHeader 函數來准備下一個錄音緩沖,調用這
// 個函數時傳遞的 LPWAVEHDR 指針中剛好有已經錄好的音頻數據,我們
// 用同樣的方式將數據取走。
SendDataToMainWnd ( pwh->lpData,
pwh->dwBytesRecorded > 0 ? pwh->dwBytesRecorded : pwh->dwBufferLength,
ENUM_CATCHSOUNDTYPE_waveInPrepareHeader );
// Call the original waveInPrepareHeader function
MMRESULT nResult = ((PFN_waveInPrepareHeader)(PROC) g_waveInPrepareHeader )
(hwi, pwh, cbwh);
// Return the result back to the caller
return(nResult);
}
至此,Skype通話過程中音頻輸入和輸出的數據(即對方講話和我自己講話的聲音)已經全部“偷取”到了,接下來只要壓縮成mp3格式即可,mp3壓縮代碼網上很多,隨便下載一個來用就可以了,我用的是“hw_mp3_enc.dll library”,效果一般,但用做電話錄音足亦。
一個有趣的功能:我們錄音後的mp3文件播放時,我可以讓對方的聲音在左聲道,我自己的聲音在右聲道,好像兩個人面對面在對話一樣。其實做起來並不難,從上面的代碼我們知道,其實輸入和輸出的音頻數據是獨立獲取的,我們在合並到mp3文件時,將輸入的數據存為左聲道,輸出的數據存為右聲道即可。
既然叫“Skype答錄機”,除了有錄音功能外,還應該有自動應答功能,要實現這個功能有兩個辦法:
a) 當來電震鈴超過規定的次數時自動提機,將錄音通道切換到“立體聲混音”,然後播放之前准備好的一個語音文件(如:您好,我現在不在電腦旁,有事請留言),本軟件使用的就是這種方式;
b) 當來電震鈴超過規定的次數時自動提機,然後播放之前准備好的一個語音文件(如:您好,我現在不在電腦旁,有事請留言)數據直接傳遞至上面的.dll文件相關函數中,然後 waveInPrepareHeader 函數中將系統從麥克風中錄制的聲音替換掉,這種方式比較難控制,但可以實 現很多奇怪的效果,比如通話變聲等。
需要注意的地方:該程序是通過鉤子方式截取Skype的音頻數據,所以程序的執行效率要求很高,對於慢速處理的操作(如:壓縮mp3數據、數據存盤等)最好是放到其他線程中處理,否則會影響Skype通話質量,造成通話斷斷續續的感覺,錄音數據也可能會丟失。
軟件執行界面
主界面:
☆ 配置界面:
說明:
由於該軟件為“深圳市偉信科技開發有限公司”的商業軟件,這個軟件是為了“無線Skype話機”而開發的,使用該話機可以離開電腦像操作普通手機一樣進行免費的Skype通話了。
詳情請見:http://www.viction.net
出於商業道德的考慮,我不能將整個工程源代碼全部公開,但有關Skype錄音和應答方面的關鍵性代碼已經全部包含在文件包裡了,只要稍加修改就可以添加到自己的工程項目中了。
以上程序在Windows XP/ Skype2.5 上測試通過,但Skype3.0 以後的版本錄音好像有點問題,因為時間的關系,我尚未查找其中的原因,我初步猜測 Skype 在調用 waveInPrepareHeader 函數前將數據清除掉了,可以試著捕捉 MM_WIM_DATA 消息來獲取錄音數據,因為工作比較忙,所以沒時間來做嘗試,有興趣的朋友可以來完成它,如果有朋友完成了請將你的方法email給我,多謝。
謝謝曹昌利、陳容清、周偉波等幾位同志對本軟件的嚴格測試。
本文配套源碼