您的位置:首页 > 其它

DirectSound采集播放声音技术文档

2013-12-12 17:40 351 查看
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();
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: