1. 程式人生 > >DirectSound採集播放聲音技術文件

DirectSound採集播放聲音技術文件

Windows上的採集聲音播放我們一般都用DirectSound來實現,下面我們重點來介紹一下使用DirectSound來實現音訊採集播放技術。

1.音訊採集部分:

首先我們需要枚舉出系統裡面的音訊裝置物件,我們用DirectSoundCaptureEnumerate()方面枚舉出系統音訊採集的裝置,這個方法帶有兩個引數,一個指定枚舉出裝置執行的回撥函式,一個上下文引數指標,首先我們定義枚舉出裝置執行的回撥函式宣告

//lpGuid 裝置GUID
//lpcstrDescription 裝置描述資訊
//lpcstrModule DirectSound的驅動程式對應於該裝置的模組名稱
//lpContext 之前設定上下文指標
BOOL CALLBACK DSEnumCallback(
         LPGUID lpGuid,
         LPCSTR lpcstrDescription,
         LPCSTR lpcstrModule,
         LPVOID lpContext
)
然後我們在定義一個結構體使用者存放我們枚舉出的裝置資訊
typedef struct _DevItem
{
	CString strName;
	GUID guid;
} DevItem;
//用來儲存枚舉出來的採集裝置物件;
std::vector<DevItem>	m_CapDevices;
定義好這些我們就可以呼叫DirectSoundCaptureEnumerate()方法來列舉系統裝置資訊了
HRESULT hr = S_OK;
hr = DirectSoundCaptureEnumerate(DSEnumCallback, (LPVOID)&m_CapDevices);
然後我們在DSEnumCallback函式裡面把枚舉出來的裝置儲存到m_CapDevices裡面
BOOL CALLBACK DSEnumCallback(LPGUID lpGuid, LPCSTR lpcstrDescription, LPCSTR lpcstrModule, LPVOID lpContext)
{
    std::vector<DevItem> *pLst = (std::vector<DevItem> *) lpContext;
    if (pLst)
    {
        DevItem item;
        memset(&item, 0, sizeof(item));
        item.strName = lpcstrDescription;
        if(lpGuid)
            item.guid = *lpGuid;
        else
            item.guid = GUID_NULL;
        pLst->push_back(item);
        return TRUE;
    }
    return FALSE;
}
枚舉出裝置後,我們就可以用枚舉出裝置GUID來建立採集裝置IDirectSoundCapture介面物件了
	//DS採集裝置
	CComPtr<IDirectSoundCapture>		m_pMicDevice;
	//strGuid 剛剛枚舉出裝置出來的GUID,如果為NULL則建立系統預設音訊裝置
	HRESULT hr = DirectSoundCaptureCreate(&strGuid, &m_pMicDevice, 0);
IDirectSoundCapture介面建立好,我們就可以使用IDirectSoundCapture介面的方法了,主要有下面幾個方法
DECLARE_INTERFACE_(IDirectSoundCapture, IUnknown)
{
    // IDirectSoundCapture methods
    STDMETHOD(CreateCaptureBuffer)  (THIS_ LPCDSCBUFFERDESC pcDSCBufferDesc, LPDIRECTSOUNDCAPTUREBUFFER *ppDSCBuffer, LPUNKNOWN pUnkOuter) PURE;
    STDMETHOD(GetCaps)              (THIS_ LPDSCCAPS pDSCCaps) PURE;
    STDMETHOD(Initialize)           (THIS_ LPCGUID pcGuidDevice) PURE;
};
我們主要使用CreateCaptureBuffer方法,GetCaps方法主要是可以獲取錄音裝置的效能,Initialize方法是做一些初始化的方面的工作,通過IDirectSoundCapture建立裝置物件是不需要呼叫Initialize方法的,只有通過CoCreateInstance方法建立裝置物件才需要呼叫Initialize方法,接著我們用已經建立m_pMicDevice裝置物件來建立IDirectSoundCaptureBuffer介面,主要用到CreateCaptureBuffer方法來建立,看下這個方法說明:
//pcDSCBufferDesc 指向DSCBUFFERDESC結構體指標
//用來儲存建立好的IDirectSoundCaptureBuffer物件的指標
//pUnkOuter 目前填NULL
HRESULT CreateCaptureBuffer(
         LPCDSCBUFFERDESC pcDSCBufferDesc,
         LPDIRECTSOUNDCAPTUREBUFFER * ppDSCBuffer,
         LPUNKNOWN pUnkOuter
)
首先我們要了解WAVEFORMATEX結構體和DSCBUFFERDESC結構體,DSCBUFFERDESC結構體最後一個成員就是WAVEFORMATEX結構體,WAVEFORMATEX結構體主要用來設定採集生意的格式,取樣率,通道數目,取樣位深,位元速率等資訊。DSCBUFFERDESC主要是增加描述採集緩衝區的大小的一些資訊
typedef struct
{
  WORD  wFormatTag;	//設定波形聲音的格式,更多的資訊請參考MSDN
  WORD  nChannels;	//設定音訊檔案的通道數量,對於單聲道的聲音,此此值為1。對於立體聲,此值為2
  DWORD  nSamplesPerSec;//設定每個聲道播放和記錄時的樣本頻率
  DWORD  nAvgBytesPerSec;//設定請求的平均資料傳輸率,單位byte/s
  WORD  nBlockAlign;	//以位元組為單位設定塊對齊。塊對齊是指最小資料的原子大小。
  WORD  wBitsPerSample;	//每個樣本的取樣位深
  WORD  cbSize;			//額外資訊的大小
} WAVEFORMATEX; *PWAVEFORMATEX;
typedef struct _DSCBUFFERDESC
{
    DWORD           dwSize;//結構體大小用於區分不同版本
    DWORD           dwFlags;//指定裝置的一些能力,預設為0
    DWORD           dwBufferBytes;//採集緩衝區大小
    DWORD           dwReserved;//保留欄位
    LPWAVEFORMATEX  lpwfxFormat;//WAVEFORMATEX結構體指標
} DSCBUFFERDESC, *LPDSCBUFFERDESC;
建立裝置緩衝區物件示例
	WAVEFORMATEX wfx;
	memset(&wfx, 0, sizeof(WAVEFORMATEX));
	wfx.wFormatTag = WAVE_FORMAT_PCM;
	wfx.nSamplesPerSec = 48000;
	wfx.nChannels = 2;
	wfx.wBitsPerSample = 16;
	wfx.nBlockAlign = 4;
	wfx.nAvgBytesPerSec = 48000 * 4;
	int nDSIBufferSzie = 3 * 48000 * 2 * (16 / 8); //採集緩衝3秒資料;
	DSCBUFFERDESC bufProp;
	memset(&bufProp, 0, sizeof(DSCBUFFERDESC));
	bufProp.dwSize = sizeof(DSCBUFFERDESC);
	bufProp.dwBufferBytes = nDSIBufferSzie;
	bufProp.lpwfxFormat = &wfx;
	hr = m_pMicDevice->CreateCaptureBuffer(&bufProp, &m_pMicBuffer, 0);
獲取到IDirectSoundCaptureBuffer緩衝區後,我們就可以用該介面提供的方法來控制採集開始,停止,獲取位置,獲取資料操作了,看下這個介面提供的方法
DECLARE_INTERFACE_(IDirectSoundCaptureBuffer, IUnknown)
{
    // IDirectSoundCaptureBuffer methods
    STDMETHOD(GetCaps)              (THIS_ LPDSCBCAPS pDSCBCaps) PURE;
    STDMETHOD(GetCurrentPosition)   (THIS_ LPDWORD pdwCapturePosition, LPDWORD pdwReadPosition) PURE;
    STDMETHOD(GetFormat)            (THIS_ LPWAVEFORMATEX pwfxFormat, DWORD dwSizeAllocated, LPDWORD pdwSizeWritten) PURE;
    STDMETHOD(GetStatus)            (THIS_ LPDWORD pdwStatus) PURE;
    STDMETHOD(Initialize)           (THIS_ LPDIRECTSOUNDCAPTURE pDirectSoundCapture, LPCDSCBUFFERDESC pcDSCBufferDesc) PURE;
    STDMETHOD(Lock)                 (THIS_ DWORD dwOffset, DWORD dwBytes, LPVOID *ppvAudioPtr1, LPDWORD pdwAudioBytes1,
                                           LPVOID *ppvAudioPtr2, LPDWORD pdwAudioBytes2, DWORD dwFlags) PURE;
    STDMETHOD(Start)                (THIS_ DWORD dwFlags) PURE;
    STDMETHOD(Stop)                 (THIS) PURE;
    STDMETHOD(Unlock)               (THIS_ LPVOID pvAudioPtr1, DWORD dwAudioBytes1, LPVOID pvAudioPtr2, DWORD dwAudioBytes2) PURE;
};
我們主要用到Start、Stop、GetCurrentPosition、Lock、Unlock幾個方法,首先我們要啟動採集,主要就是呼叫Start()方法
hr = m_pMicBuffer->Start(DSCBSTART_LOOPING);
重點是要使用DSCBSTART_LOOPING這個引數,表示迴圈不斷的採集,啟動開始採集以後我們後續主要就是從緩衝取出採集到的資料了,首先我們要根據GetCurrentPosition()獲取讀指標的位置,這個函式有兩個引數,主要用來獲取當前採集資料的位置和可以讀取資料的位置。
//pdwCapturePosition 獲取從緩衝區起始現在採集資料的偏移位置
//pdwReadPosition 	獲取從緩衝器起始現在可以讀取資料的偏移位置
HRESULT GetCurrentPosition(LPDWORD pdwCapturePosition,LPDWORD pdwReadPosition)
這裡我們應該記錄上一次讀取緩衝區的位置,然後判斷本次可以讀取的資料長度是否滿足條件,如果滿足讀取條件就去讀取資料,如果不滿足條件就返回等待下次讀取資料
	int nBufLen = 192000/1000 * 40;	//每次讀取40毫秒的資料
	unsigned char *pBuffer = new unsigned char[nBufLen+1];
	int nDSIMicReadPtr = 0; //表示上次讀取緩衝區的位置
	int nDSIMicReadLen = 0;	//本次讀取緩衝區的長度
	DWORD dwPtr = 0;
	DWORD dwReadPtr = 0;
	m_pMicBuffer->GetCurrentPosition(&dwPtr, &dwReadPtr);
	if(dwReadPtr < (DWORD)nDSIMicReadPtr)
	{
		//nDSIBufferSzie代表之前建立緩衝區的大小
		nDSIMicReadLen = nDSIBufferSzie - (nDSIMicReadPtr - dwReadPtr);
	}
	else
	{
		nDSIMicReadLen = dwReadPtr - nDSIMicReadPtr;
	}
	if(nDSIMicReadLen < nBufLen) //資料不夠讀取長度,迴圈一下次讀取
	{
		delete[] pBuffer;
		return;
	}
當可以去讀取資料的時候,我們就可以用Lock函式先鎖定資料然後讀取資料了,該函式用法如下
	//dwOffset 鎖住緩衝區資料的起始位置
	//dwBytes 鎖定資料的長度
	//ppvAudioPtr1 讀取資料指標1
	//pdwAudioBytes1 讀取資料指標1的長度
	//ppvAudioPtr2 讀取資料指標2
	//pdwAudioBytes2 讀取資料指標2的長度
	//dwFlags 標誌位可以修改鎖的一些事件,一般設定0
	HRESULT Lock(
         DWORD dwOffset,DWORD dwBytes,
         LPVOID * ppvAudioPtr1,LPDWORD  pdwAudioBytes1,
         LPVOID * ppvAudioPtr2,LPDWORD pdwAudioBytes2,
         DWORD dwFlags
	)
呼叫這個函式,我們就可以鎖定自己希望獲取的資料緩衝區了,然後根據返回的資料指標1和資料指標1來從緩衝區複製資料了。有人可能會問為什麼要返回兩個緩衝區指標呢,根據我個人理解IDirectSoundCaptureBuffer的緩衝區用到的是環形buffer,這樣當我們要拷取的資料剛好穿過環形buffer的頭尾時必須要提供兩個指標才能拷貝完成使用者需要的資料,所以這裡要返回兩個緩衝區的指標,下面我看下獲取資料的程式碼
	void *ptr1 = 0;
	void *ptr2 = 0;
	DWORD len1 = 0;
	DWORD len2 = 0;
	HRESULT hr = m_pMicBuffer->Lock(nDSIMicReadPtr, nBufLen, &ptr1, &len1, &ptr2, &len2, 0);
	if(FAILED(hr)) return;
	nBufLen = 0;
	//pBuffer在上面獲取資料位置時已經建立
	if ((NULL != ptr1) && (len1 > 0))
	{
		memcpy(pBuffer, ptr1, len1);
		nDSIMicReadPtr += len1;
		nBufLen = len1;
	}
	if ((NULL != ptr2) && (len2 > 0))
	{
		memcpy(pBuffer+len1, ptr2, len2);
		nDSIMicReadPtr += len2;
		nBufLen += len2;
	}
	nDSIMicReadPtr = nDSIMicReadPtr % nDSIBufferSzie;
	m_pMicBuffer->Unlock(ptr1, len1, ptr2, len2);
	/*
	//對獲取的採集的資料進行各種處理
	*/
	delete[] pBuffer;
獲取到資料後,要及時的解鎖之前的緩衝區,使用Unlock方法,引數是之前返回的資料指標1和資料指標2和資料的長度,要想不斷地獲取資料,只要不斷地迴圈執行這樣的步驟即可,獲取到資料後我們就可以用這些資料做後續的操作了,一般編碼寫成檔案等等。當不需要獲取採集資料的時候,我們只要呼叫Stop方法,停止麥克風的採集就可以了
m_pMicBuffer->Stop();

2.音訊播放部分:

音訊的播放和採集大體上流程一致,不過是採集是從IDirectSoundCaptureBuffer獲取資料,播放是需要向IDirectSoundBuffer寫資料而已,同時播放時需要建立IDirectSound裝置物件,採集是需要建立IDirectSoundCapture裝置物件,下面我們來簡單的介紹一下音訊播放的過程,首先我們定義需要建立的裝置物件,並且來建立揚聲器裝置

	CComPtr<IDirectSound>				m_pSpkDevice;			//DS播放裝置
	CComPtr<IDirectSoundBuffer>			m_pSpkBuffer;			//DS播放緩衝區
	hr = DirectSoundCreate(&strGuid, &m_pSpkDevice, 0);
建立IDirectSoundBuffer介面要用到DirectSoundCreate函式,第一個引數是建立裝置GUID,第二個引數是用來接收建立好裝置物件的指標,和建立麥克風裝置方法使用一樣,建立完裝置物件後,我們接下來先要設定播放平衡的優先順序,這個是必須要設定的,先看下設定播放平衡優先順序的方法
	//hwnd 指向一個應用程式的視窗控制代碼
	//dwLevel 設定協作級別
	HRESULT SetCooperativeLevel(
         HWND hwnd,
         DWORD dwLevel)
	}
dwLevel這個引數有下面幾個值

DSSCL_EXCLUSIVE設定獨佔模式
DSSCL_NORMAL 設定一般模式
DSSCL_PRIORITY設定優先模式
DSSCL_WRITEPRIMARY設定寫入初級水平

MSDN建議一般選擇DSSCL_PRIORITY模式即可

	//設定播放平衡優先順序
	hr = m_pSpkDevice->SetCooperativeLevel(m_hWnd, DSSCL_PRIORITY);
設定好協作級別後我們就可以建立輔助緩衝去了
	WAVEFORMATEX wfx;
	memset(&wfx, 0, sizeof(WAVEFORMATEX));
	wfx.wFormatTag = WAVE_FORMAT_PCM;
	wfx.nSamplesPerSec = 48000;
	wfx.nChannels = 2;
	wfx.wBitsPerSample = 16;
	wfx.nBlockAlign = 4;
	wfx.nAvgBytesPerSec = 48000 * 4;
	DWORD dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_STICKYFOCUS | DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLVOLUME;
	DSBUFFERDESC dsBufDesc = {0};
	dsBufDesc.dwSize = sizeof(DSBUFFERDESC);
	dsBufDesc.dwFlags = dwFlags;
	dsBufDesc.dwBufferBytes = nDSISpkBufferSzie;
	dsBufDesc.lpwfxFormat = &wfx;
	CComPtr<IDirectSoundBuffer> pSpkBuffer;
	hr = m_pSpkDevice->CreateSoundBuffer(&dsBufDesc, &pSpkBuffer, 0);
	pSpkBuffer->QueryInterface(IID_IDirectSoundBuffer, (void**)&m_pSpkBuffer);
	if(FAILED(hr) ||  (NULL == m_pSpkBuffer))
	{
		return;
	}
WAVEFORMATEX和DSBUFFERDESC這兩個引數在採集時以介紹過,不在過多介紹。建立好IDirectSoundBuffer以後,我們就可以用這個介面的方法了,這個介面主要有以下方法
DECLARE_INTERFACE_(IDirectSoundBuffer, IUnknown)
{
    // IDirectSoundBuffer methods
    STDMETHOD(GetCaps)              (THIS_ LPDSBCAPS pDSBufferCaps) PURE;
    STDMETHOD(GetCurrentPosition)   (THIS_ LPDWORD pdwCurrentPlayCursor, LPDWORD pdwCurrentWriteCursor) PURE;
    STDMETHOD(GetFormat)            (THIS_ LPWAVEFORMATEX pwfxFormat, DWORD dwSizeAllocated, LPDWORD pdwSizeWritten) PURE;
    STDMETHOD(GetVolume)            (THIS_ LPLONG plVolume) PURE;
    STDMETHOD(GetPan)               (THIS_ LPLONG plPan) PURE;
    STDMETHOD(GetFrequency)         (THIS_ LPDWORD pdwFrequency) PURE;
    STDMETHOD(GetStatus)            (THIS_ LPDWORD pdwStatus) PURE;
    STDMETHOD(Initialize)           (THIS_ LPDIRECTSOUND pDirectSound, LPCDSBUFFERDESC pcDSBufferDesc) PURE;
    STDMETHOD(Lock)                 (THIS_ DWORD dwOffset, DWORD dwBytes, LPVOID *ppvAudioPtr1, LPDWORD pdwAudioBytes1,
                                           LPVOID *ppvAudioPtr2, LPDWORD pdwAudioBytes2, DWORD dwFlags) PURE;
    STDMETHOD(Play)                 (THIS_ DWORD dwReserved1, DWORD dwPriority, DWORD dwFlags) PURE;
    STDMETHOD(SetCurrentPosition)   (THIS_ DWORD dwNewPosition) PURE;
    STDMETHOD(SetFormat)            (THIS_ LPCWAVEFORMATEX pcfxFormat) PURE;
    STDMETHOD(SetVolume)            (THIS_ LONG lVolume) PURE;
    STDMETHOD(SetPan)               (THIS_ LONG lPan) PURE;
    STDMETHOD(SetFrequency)         (THIS_ DWORD dwFrequency) PURE;
    STDMETHOD(Stop)                 (THIS) PURE;
    STDMETHOD(Unlock)               (THIS_ LPVOID pvAudioPtr1, DWORD dwAudioBytes1, LPVOID pvAudioPtr2, DWORD dwAudioBytes2) PURE;
    STDMETHOD(Restore)              (THIS) PURE;
};
我們主要還是用到GetCurrentPosition、Play、Lock、Unlock、Stop這幾個方法,基本上和IDirectSoundCaptureBuffer方法使用一樣,首先使用播放函式
hr = m_pSpkBuffer->Play(0, 0, DSBPLAY_LOOPING);
第一個引數保留值未使用預設為0,第二個引數一般是使用DSBCAPS_LOCDEFER建立裝置時使用,預設為0,重點是第三個引數要使用DSBPLAY_LOOPING這樣才會一直不斷的迴圈的播放。後面我們就可以把自己的音訊資料不斷地放到緩衝去了,如下
	ptr1 = NULL;
	ptr2 = NULL;
	len1 = 0;
	len2 = 0;
	hr = m_pSpkBuffer->Lock(nDSISpkWritePtr, nBufLen, &ptr1, &len1, &ptr2, &len2, 0);
	if(FAILED(hr)) continue;

	//pBuffer為儲存音訊的pcm資料
	if ((NULL != ptr1) && (NULL != pBuffer))
		memcpy(ptr1, pBuffer, len1);
	if ((NULL != ptr2) && (NULL != pBuffer))
		memcpy(ptr2, pBuffer+len1, len2);
	
	m_pSpkBuffer->Unlock(ptr1, len1, ptr2, len2);
	nDSISpkWritePtr = (nDSISpkWritePtr + len1 + len2) % nDSISpkBufferSzie;
和採集緩衝區取出資料一樣,不做過多說明,更準確的方法是需要使用GetCurrentPosition來判斷緩衝區還有多少資料沒有播放,根據資料多少來放資料,和採集那塊道理是一樣的,可以根據自己的需求來完善。這樣只要不斷源源的播放資料,聲音就可以正常的播放了。

當我我們播放完成後,或需要停止播放的時候只要呼叫Stop方法就可以,然後釋放相關的物件就完成聲音的播放了。

m_pSpkBuffer->Stop();