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();


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值