曾經學習過Directshow的開發,對於Dsound壹直沒有仔細的學習,以前只是知道Dsound是做音頻開發的,我壹直以為它和Dshow的結構體系差不多,經過仔細學習後,發現,其實他們完全兩碼事。DirectSound雖然也基於COM,但不象Dshow那樣多個的filter組成鏈表。
閑話少說,下面我們看看DirectSound到底能幫我們做些什麽。
1、播放WAVE格式的音頻文件或者資源。
2、可以同時播放多個音頻。
3、Assign high-priority sounds to hardware-controlled buffers
4、播放3D立體聲音
5、在聲音中添加特技效果,比如回聲,動態的改變特技的參數等
6、將麥克風或者其他音頻輸入設備的聲音錄制成wave格式的文件
DirectSound就能做這麽多事情,到這裏,我都有點懷疑DirectSound是不是就是封裝了mmio系列和wav系列的函數。因為這些底層的API也能夠完成這些事情。這裏我們主要討論壹下,如果使用Directsound進行錄音,並保存成wave格式的文件。
在開始工作之前,要先介紹DirectSound錄音用到的三個非常重要的對象:
·IDirectSoundCapture8 ,設備對象,根據妳錄音的設備創建的設備對象,利用該對象可以獲取設備的屬性。
·IDirectSoundCaptureBuffer8,緩沖區對象,該對象由設備對象創建,主要用來操作音頻數據
·IDirectSoundNotify8 ,事件通知對象,該對象用來通知應用程序從緩沖區中將數據取走,寫入文件保存起來。
利用DirectSound錄音的主要思路,就是先根據選擇的錄音設備創建設備對象,然後通過設備對象創建輔助緩沖區對象,開始錄音的時候,設備將數據寫入緩沖區,應用程序主動的從緩沖區將數據讀出來寫文件即可,就實現了錄音功能。這裏簡單介紹壹下dsound的通知功能,應用程序會創建壹個通知對象,然後將通知對象邦定,然後設定通知位置(position),什麽是通知位置呢,比如緩沖區的大小為4000字節,如果妳想當數據達到緩沖區壹半的時候能得到通知開始copy數據,那麽此時妳就可以將通知位置設定為2000,通知位置可以任意的設定,當緩沖區的數據達到妳設定的位置時,就會通知應用程序將緩沖區的數據copy到文件中,緩沖區是循環利用的,當緩沖區填充滿了以後,就會從頭開始充填數據,所以,緩沖區就是壹邊讀,壹邊寫的過程。
下面我講壹下錄音的主要步驟,可以使大家的思路更清晰壹些
1、枚舉錄音的設備
2、根據選擇的設備創建設備對象
3、利用設備對象創建緩沖區對象
4、設置通知機制
5、創建工作線程,用來將緩沖區的數據寫入文件。
先來定義壹下用到的數據
LPDIRECTSOUNDCAPTURE8 g_pDSCapture = NULL;//設備對象指針
LPDIRECTSOUNDCAPTUREBUFFER g_pDSBCapture = NULL;//緩沖區對象指針
LPDIRECTSOUNDNOTIFY8 g_pDSNotify = NULL;//用來設置通知的對象接口
GUID g_guidCaptureDevice = GUID_NULL; //設備id
BOOL g_bRecording = FALSE; //是否正在錄音
WAVEFORMATEX g_wfxInput; //輸入的音頻格式
DSBPOSITIONNOTIFY g_aPosNotify[ NUM_REC_NOTIFICATIONS + 1 ]; //設置通知標誌的數組
HANDLE g_hNotificationEvent; //通知事件
BOOL g_abInputFormatSupported[20];
DWORD g_dwCaptureBufferSize; //錄音用緩沖區的大小
DWORD g_dwNextCaptureOffset;//偏移位置
DWORD g_dwNotifySize;// 通知位置
CWaveFile* g_pWaveFile;//
枚舉錄音的設備
如果妳的程序只是想從用戶缺省的設備上進行聲音的錄制,那麽就沒有必要來枚舉出系統中的所有錄音的設備,當妳調用DirectSoundCaptureCreate8 或者另外壹個函數DirectSoundFullDuplexCreate8的時候,其實就默認指定了壹個缺省的錄音設備。
當然,在下面的情況下,妳就必須要枚舉系統中所有的設備,例如,妳的應用程序並不支持所有的輸出設備,或者妳的應用程需要兩個或者多個設備,或者妳希望用戶自己來選擇輸出設備。
枚舉設備,妳首先要定義壹個回調函數,這個回調函數可以被系統中的每個設備來調用,妳可以在各函數做任何事情,這個函數的命名也沒有任何的限制,但是函數應該以DSEnumCallback為原型,如果枚舉沒有結束,這個回調函數就返回TRUE,如果枚舉結束,例如妳找到合適的設備,這個函數就要返回FALSE。
下面是回調函數的壹個例子,這個函數將枚舉的每壹個設備都添加到壹個combox中,將設備的GUID保存到壹個item 中,這個函數的前三個參數由設備的驅動程序提供,第四個參數有DirectSoundCaptureEnumerate函數提供,這個參數可以是任意的32位值,這個例子裏是combox的句柄,
BOOL CALLBACK DSEnumProc(LPGUID lpGUID,
LPCTSTR lpszDesc,
LPCTSTR lpszDrvName,
LPVOID lpContext )
{
HWND hCombo = (HWND)lpContext;
LPGUID lpTemp = NULL;
if (lpGUID != NULL) // NULL only for "Primary Sound Driver".
{
if ((lpTemp = (LPGUID)malloc(sizeof(GUID))) == NULL)
{
return(TRUE);
}
memcpy(lpTemp, lpGUID, sizeof(GUID));
}
//下面的代碼主要主要是將設備添加到CComboBox,其實妳完全直接將CComboBox指針傳遞過來,直接的添加,這裏采用的是給combox窗口發送消息的方法,
ComboBox_AddString(hCombo, lpszDesc);
ComboBox_SetItemData(hCombo,
ComboBox_FindString(hCombo, 0, lpszDesc),
lpTemp );
free(lpTemp);
return(TRUE);
}
枚舉設備通常都是在對話框初始化的時候才進行的,我們假設hCombo就是combox句柄,hDlg就對話框的句柄,看看我們怎麽來枚舉設備的吧。
if (FAILED(DirectSoundCaptureEnumerate ((LPDSENUMCALLBACK)DSEnumProc,
(VOID*)&hCombo)))
{
EndDialog(hDlg, TRUE);
return(TRUE);
}
在這個例子中,combox的句柄作為參數傳遞到DirectSoundEnumerate函數中,然後又被傳遞到回調函數中,這個參數妳可以是妳想傳遞的任意的32位值。
註:第壹個被枚舉的設備通常稱為Primary sound driver,並且回調函數的lpGUID為NULL,這個設備就是用戶通過控制面板設置的缺省的錄音聲音設備,
創建設備對象
妳可以通過DirectSoundCaptureCreate8或者DirectSoundFullDuplexCreate8函數直接創建設備對象,該函數返回壹個指向IDirectSoundCapture8接口的指針
if( FAILED( hr = CoInitialize(NULL) ) )
return hr;
if(pDeviceGuid)
{
if(FAILED( hr = DirectSoundCaptureCreate(pDeviceGuid,&g_pDSCapture,NULL)))
return hr;
}
else
{
if(FAILED(hr= DirectSoundCaptureCreate(&DSDEVID_DefaultCapture ,&g_pDSCapture,NULL)))
return hr
}
其中pDeviceGuid是從枚舉的combox中選擇的設備的ID。
現在創建了設備對象妳可以通過IDirectSoundCapture8::GetCaps方法來獲取錄音設備的性能,這個函數的參數是壹個DSCCAPS類型的結構,在傳遞這個參數之前,壹定要初始化該結構的dwSize成員變量。同時,妳可以通過這個結構返回設備支持的聲道數,以及類似WAVEINCAPS結構的其他設備屬性
創建錄音的緩沖區對象
我們可以通過IDirectSoundCapture8::CreateCaptureBuffer來創建壹個錄音的buffer對象,這個函數的壹個參數采用DSCBUFFERDESC類型的結構來說明buffer的壹些特性,這個結構的最後壹個成員變量是壹個WAVEFORMATEX結構,這個結構壹定要初始化成泥需要的wav格式。
說明壹下,如果妳的應用程序壹邊播放的同時進行錄制,如果妳錄制的buffer格式和妳的主緩沖buffer不壹樣,那麽妳創建錄制buffer對象就會失敗,原因在於,有些聲卡只支持壹種時鐘,不能同時支持錄音和播放同時以兩種不同的格式進行。
下面的函數,演示了如何創建壹個錄音的buffer對象,這個buffer對象能夠處理1秒的數據,註意,這裏傳遞的錄音設備對象參數壹定要通過DirectSoundCaptureCreate8來創建,而不是早期的DirectSoundCaptureCreate接口,否則,buffer對象不支持IDirectSoundCaptureBuffer8接口。
HRESULT CreateCaptureBuffer(LPDIRECTSOUNDCAPTURE8 pDSC,
LPDIRECTSOUNDCAPTUREBUFFER8* ppDSCB8)
{
HRESULT hr;
DSCBUFFERDESC dscbd;
LPDIRECTSOUNDCAPTUREBUFFER pDSCB;
WAVEFORMATEX wfx ={WAVE_FORMAT_PCM, 2, 44100, 176400, 4, 16, 0};
// wFormatTag, nChannels, nSamplesPerSec, mAvgBytesPerSec,
// nBlockAlign, wBitsPerSample, cbSize
if ((NULL == pDSC) || (NULL == ppDSCB8)) return E_INVALIDARG;
dscbd.dwSize = sizeof(DSCBUFFERDESC);
dscbd.dwFlags = 0;
dscbd.dwBufferBytes = wfx.nAvgBytesPerSec;
dscbd.dwReserved = 0;
dscbd.lpwfxFormat = &wfx; //設置錄音用的wave格式
dscbd.dwFXCount = 0;
dscbd.lpDSCFXDesc = NULL;
if (SUCCEEDED(hr = pDSC->CreateCaptureBuffer(&dscbd, &pDSCB, NULL)))
{
hr = pDSCB->QueryInterface(IID_IDirectSoundCaptureBuffer8, (LPVOID*)ppDSCB8);
pDSCB->Release();
}
return hr;
}
妳可以通過IDirectSoundCaptureBuffer8::GetCaps方法來獲取錄音buffer的大小,但壹定要記得初始化DSCBCAPS結構類型參數的dwSize成員變量。
為了獲取buffer中數據的格式,妳可以通過IDirectSoundCaptureBuffer8::GetFormat.方法來獲取buffer中的數據格式,這個函數通過WAVEFORMATEX結構返回音頻數據的信息,如果我們想知道壹個錄音buffer目前的狀態如何,可以通過IDirectSoundCaptureBuffer8::GetStatus來獲取,這個函數通過壹個DWORD類型的參數來表示該buffer是否正在錄音,
IDirectSoundCaptureBuffer8::GetCurrentPosition方法可以獲取buffer中read指針和錄制指針的偏差。Read指針指向填充到該buffer中的數據的最末端,capture指針則指向復制到硬件的數據的末端,妳read指針指向的前段數據都是安全數據,妳都可以安全的復制。
錄音buffer對象通知機制
為了安全的定期的從錄音buffer中copy數據,妳的應用程序就要知道,什麽時候read指針指向了特定的位置,壹個方法是通過IDirectSoundCaptureBuffer8::GetCurrentPosition.方法來獲取read指針的位置,另外壹個更有效的方法采用通知機制,通過IDirectSoundNotify8::SetNotificationPositions方法,妳可以設置任何壹個小於buffer的位置來觸發壹個事件,切記,當buffer正在running的時候,不要設置。
如何來設置壹個觸發事件呢,首先要得到IDirectSoundNotify8接口指針,妳可以通過buffer對象的QuerInterface來獲取這個指針接口,對於妳指定的任何壹個position,妳都要通過CreateEvent方法,創建壹個win32內核對象, 然後將內核對象的句柄賦給DSBPOSITIONNOTIFY結構的hEventNotify成員,通過該結構的dwOffset來設置需要通知的位置在buffer中的偏移量。
最後將這個結構或者結構數組,傳遞給SetNotificationPositions函數,下面的例子設置了NUM_REC_NOTIFICATIONS個通知,當position達到g_dwNotifySize時會觸發壹個通知,依次類推。
HRESULT InitNotifications()
{
HRESULT hr ;
g_hNotificationEvent = CreateEvent(NULL,FALSE,FALSE,NULL); //創建事件
if(g_pDSBCapture == NULL)
return E_FAIL;
if(FAILED(hr = g_pDSBCapture ->QueryInterface(IID_IDirectSoundNotify,(VOID**)&g_pDSNotify)))
return hr;
for( INT i = 0; i < NUM_REC_NOTIFICATIONS; i++ )
{
g_aPosNotify[i].dwOffset = (g_dwNotifySize * i) + g_dwNotifySize - 1;
g_aPosNotify[i].hEventNotify = g_hNotificationEvent;
}
if(FAILED( hr =g_pDSNotify->SetNotificationPositions( NUM_REC_NOTIFICATIONS, g_aPosNotify ) ) )
return hr;
return S_OK;
}