多媒体编程——声音播放(2)
第二部分 使用DirectSound进行声音播放。
前一节讲解了如何使用WaveOut进行声音播放,只是讲解个原理。由于缓存切换时有数据有卡顿,所以播放效果不流畅。下面讲讲怎么使用DirectSound进行声音播放,这也是通常实际开发中的选择,使用DirectSound进行开发,由于它API抽象得更好,并且采用异步事件通知的方式通知上层填充数据,所以可以避免数据输入不连续。
DirectSound是微软DirectX SDK的一部分,所以使用DirectSound需要下载DirectXSDK,版本不限。
使用DirectSound播放声音,需要HWND参数作为通知事件消息循环载体,所以例子代码是在MFC工程下的。
建立一个MFC工程,直接在OnInitDialog里面添加播放代码。
需要以下头文件和lib库生命。
#include "atlbase.h"
#include "mmsystem.h"
#pragma comment(lib,"winmm.lib")
#include "dsound.h"
#pragma comment(lib,"dsound.lib")
#pragma comment(lib,"dxguid.lib")
需要以下结构体定义
/* WAV Riff文件头*/
typedef struct _RIFF_HEADER
{
char szRiffID[4]; //'R','I','F','F'
DWORD dwRiffSize; //从下一字节,到文件结束的字节数。加上就是整个文件的大小
char szRiffFormat[4]; //'W','A','V','E'
}RIFF_HEADER,*LP_RIFF_HEADER;
typedef struct _WAV_FORMAT
{
WORD wFormatTag ; //格式种类1为PCM
WORD wTracks ; //声道数
DWORD dwSamplesPerSec ; //采样频率
DWORD dwAvgBytesPerSec; //每秒的字节数,是大B。
WORD wBlockAlign ; //数据的调整数,按B计算
WORD wBitsPerSample ; //样本采样位数
}WAV_FORMAT,*LP_WAV_FORMAT;
/* 数据块头*/
typedef struct _DATA_CHUNK
{
char szDataID[4]; //'d','a','t','a'
DWORD dwDataSize;// 接下来数据的长度
}DATA_CHUNK,*LP_DATA_CHUNK;
typedef struct _FMT_CHUNK
{
char szFmtID[4] ; // 'f','m','t',' '
DWORD dwFmtSize ; //一般等于,表示WAVE_FORMAT的字节数
WAV_FORMAT wavFormat; //这个结构体大小刚好为
}FMT_CHUNK,*LP_FMT_CHUNK;
完整的OnInitDialog代码如下:
解释都在代码注释里。
对象释放就省略了,实际开发中可不能这么草率噢。
BOOL CDxSoundPlayerDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
::CoInitialize(NULL);
LPCTSTR pWavFilePath = _T("C:/Lenka - Trouble Is A Friend.wav") ;
CFile file;
file.Open(pWavFilePath,CFile::modeRead|CFile::shareDenyWrite);
RIFF_HEADER riffHeader ;
memset(&riffHeader,0,sizeof(RIFF_HEADER));
file.Read(&riffHeader,sizeof(RIFF_HEADER));
FMT_CHUNK fmtBlock ;
memset(&fmtBlock,0,sizeof(FMT_CHUNK));
file.Read(&fmtBlock,sizeof(FMT_CHUNK));
DATA_CHUNK dataBlock;
memset(&dataBlock,0,sizeof(DATA_CHUNK));
file.Read(&dataBlock,sizeof(DATA_CHUNK));
//前面部分先读取WAV文件的头,得到音频格式信息。
//初始化DirectSound。
LPDIRECTSOUND8 pDirectSound = NULL;
HRESULT hr = DirectSoundCreate8(NULL,&pDirectSound,NULL);
if(FAILED(hr))
return 0;
//需要设置HWND作为参数。
hr = pDirectSound->SetCooperativeLevel(this->m_hWnd, DSSCL_NORMAL);
if(FAILED(hr))
return hr ;
DSBUFFERDESC stDirectDesc ;//DirectSound格式参数结构体
WAVEFORMATEX stWaveFormat ;//标准WAVE,PCM音频格式结构体。
stWaveFormat.cbSize = sizeof(WAVEFORMATEX);
stWaveFormat.nAvgBytesPerSec = fmtBlock.wavFormat.dwAvgBytesPerSec;
stWaveFormat.nBlockAlign = fmtBlock.wavFormat.wBlockAlign;
stWaveFormat.nChannels = fmtBlock.wavFormat.wTracks;
stWaveFormat.nSamplesPerSec = fmtBlock.wavFormat.dwSamplesPerSec;
stWaveFormat.wBitsPerSample = fmtBlock.wavFormat.wBitsPerSample;
stWaveFormat.wFormatTag = fmtBlock.wavFormat.wFormatTag; //其实这个值的含义是WAVE_FORMAT_PCM
stDirectDesc.dwSize = sizeof(DSBUFFERDESC);
stDirectDesc.lpwfxFormat = &stWaveFormat ;
stDirectDesc.lpwfxFormat->wFormatTag= WAVE_FORMAT_PCM ;
stDirectDesc.lpwfxFormat->cbSize= sizeof(WAVEFORMATEX);
stDirectDesc.dwBufferBytes = stWaveFormat.nAvgBytesPerSec / 8 ;//一个缓存的数据代表1/8秒的数据长度
stDirectDesc.guid3DAlgorithm = DS3DALG_DEFAULT;
stDirectDesc.dwReserved = 0;
stDirectDesc.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY| DSBCAPS_GLOBALFOCUS |DSBCAPS_CTRLVOLUME | DSBCAPS_LOCSOFTWARE|DSBCAPS_CTRLFREQUENCY;
LPDIRECTSOUNDBUFFER pSoundBuffer = NULL; //创建一个缓存。
LPDIRECTSOUNDBUFFER8 pDirectSoundBuffer = NULL;
hr = pDirectSound->CreateSoundBuffer(&stDirectDesc,&pSoundBuffer,NULL);
if(FAILED(hr))
return hr ;
hr = pSoundBuffer->QueryInterface(IID_IDirectSoundBuffer8,(LPVOID*)&pDirectSoundBuffer);
if(FAILED(hr))
return hr ;
/*
DirectSound的工作方式是,一个缓存分成多个片段。
可以分片段写入,并且为每一个片段设置一个事件通知。
当该片段的缓存已经播放完毕,事件被通知出来。
*/
LPDIRECTSOUNDNOTIFY pSoundNotify = NULL;//通知操作对象
hr = pSoundBuffer->QueryInterface(IID_IDirectSoundNotify,(LPVOID*)&pSoundNotify);
if(FAILED(hr))
return hr ;
DSBPOSITIONNOTIFY notifyPosition[4] ; //将缓存分成个片段。
HANDLE notifyEvent[4] ;
int notifyCount = sizeof(notifyPosition)/sizeof(DSBPOSITIONNOTIFY);
DWORD dwPieceLen= (stDirectDesc.dwBufferBytes/notifyCount) ; //每一个缓存片段的长度。
for(int idx=0; idx<notifyCount;idx++)
{
notifyPosition[idx].dwOffset =(idx + 1) * dwPieceLen; //设置这个事件通知的位置
notifyPosition[idx].hEventNotify= notifyEvent[idx]= CreateEvent(NULL,FALSE,FALSE,NULL) ;//事件对象句柄。
}
//注意,最后一个notify的位置,刚好等于总长度,会有一个问题
//一个buffer 有两种播放模式,一种单此播放,一种循环播放。
//我们是要循环往里面填写数据,所以一定是循环播放;但是循环播放模式下,最后一个事件通知位置永远不会被触发
//所以我们需要得到最后一个缓存片段播放完成的消息,又不能刚好让这个片段的通知位置等于总长度,于是就有下面这句代码
notifyPosition[notifyCount-1].dwOffset-= 2 ;//最后一个通知位置往前移动两个字节,这样它就会被触发了,并且差不多是最后缓存的位置。
hr = pSoundNotify->SetNotificationPositions(notifyCount,(LPDSBPOSITIONNOTIFY)¬ifyPosition);
if(FAILED(hr))
return hr ;
//播放前首先要一次性填充满缓存。
LPBYTE pPlayBuffer = NULL ;
DWORD dwBufferLen= 0 ;
//第一步,锁定整个缓存区域
hr = pDirectSoundBuffer->Lock(0,0,(LPVOID*)&pPlayBuffer,&dwBufferLen,NULL, NULL ,DSBLOCK_ENTIREBUFFER);
if(FAILED(hr))
return hr ;
//第二步,从文件读取这么多的数据,写入缓存。
UINT readLen= file.Read(pPlayBuffer, dwBufferLen);
if(readLen != dwBufferLen)
return 0 ;
//第三步,解锁
hr = pDirectSoundBuffer->Unlock(pPlayBuffer,dwBufferLen, NULL,NULL);
if(FAILED(hr))
return hr ;
//播放第二个大步骤,开始DirectSound播放线程
hr = pDirectSoundBuffer->Play(0,0,DSBPLAY_LOOPING);
if(FAILED(hr))
return hr ;
//播放第三大步骤,等待缓存事件通知
do
{
int bufIndex = WaitForMultipleObjects(notifyCount, notifyEvent,FALSE, INFINITE)- WAIT_OBJECT_0 ;
//WAIT函数会一直等待某个缓存片段被播放完毕,当这个函数返回时,index代表的缓存片段已经播放完毕了。
hr = pDirectSoundBuffer->Lock(bufIndex * dwPieceLen,dwPieceLen, (LPVOID*)&pPlayBuffer,&dwBufferLen,NULL,NULL,0) ;
if(FAILED(hr))
break ;
readLen = file.Read(pPlayBuffer, dwBufferLen); //读文件,同时写入缓存。
hr = pDirectSoundBuffer->Unlock(pPlayBuffer, dwBufferLen,NULL, NULL);//解锁缓存。
if(FAILED(hr))
break ;
} while (readLen == dwBufferLen);
::CoUninitialize();
return TRUE; // 除非将焦点设置到控件,否则返回TRUE
}