DirectSound採集播放聲音技術文件
阿新 • • 發佈:2019-01-29
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裡面枚舉出裝置後,我們就可以用枚舉出裝置GUID來建立採集裝置IDirectSoundCapture介面物件了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; }
//DS採集裝置
CComPtr<IDirectSoundCapture> m_pMicDevice;
//strGuid 剛剛枚舉出裝置出來的GUID,如果為NULL則建立系統預設音訊裝置
HRESULT hr = DirectSoundCaptureCreate(&strGuid, &m_pMicDevice, 0);
IDirectSoundCapture介面建立好,我們就可以使用IDirectSoundCapture介面的方法了,主要有下面幾個方法我們主要使用CreateCaptureBuffer方法,GetCaps方法主要是可以獲取錄音裝置的效能,Initialize方法是做一些初始化的方面的工作,通過IDirectSoundCapture建立裝置物件是不需要呼叫Initialize方法的,只有通過CoCreateInstance方法建立裝置物件才需要呼叫Initialize方法,接著我們用已經建立m_pMicDevice裝置物件來建立IDirectSoundCaptureBuffer介面,主要用到CreateCaptureBuffer方法來建立,看下這個方法說明: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; };
//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();