利用DirectSound编程实现实时混音
摘要:将多个音频文件或多路音频数据同时输出到音频输出设备上,就可同时听到多个不同的声音,这就是混音。在游戏开发,网络视频会议开发中都会用到混音技术,本文详细介绍如何利用DirectSound实现几路不同的音频进行实时的混音。
关键词:DirectSound混音
在游戏开发中比较常用的音效素材都是比较短的,所以一般常用的API是playsound函数,比如我们要在游戏背景中播放一个test.wav音效素材,只要简单的调用下面的函数即可
PlaySound("test.wav",NULL,SND_FILENAME|SND_ASYNC);
事实上我们看到,国内的游戏大致上都可以用PlaySound搞定。但是既然是简单,从功能上就要受限了,如果遇到复杂的场景就没法用playsound实现了,比如在场景中既有开门的声音,又有砍人的声音,你如果用playsound就没法同时听到两种声音,只能是一个声音完了再听到另外一个声音,这时,就需要混音了。
在网络视频会议开发中,如果不同的客户端同时发言,如何将多个不同端点的话音数据经网络传输到达某一个端点,经该端点的Wave设备输出,能同时听到多个人的话音,从而实现局域网络中多方的话音交谈,这也需要用到混音技术。
在网络上实现话音交谈,特别强调实时性,要尽量保证话音的平滑、连续,因此为了保证话音数据连续,减少话音数据存储带来的延时,在具体实现中,话音的录制和播放都不采用文件的形式,录制和播放的话音数据都存在缓冲区中。在Windows系统中,一般情况下,高层Wave接口函数无法直接播放缓冲区中的话音数据,而必须用底层函数来实现,常用的是Windows API中的Wave函数。将Wave数据在Wave设备上输出使用的是WaveOutWrite函数,但是该函数不支持多路Wave数据的同时播放,为了能达到多路Wave数据同时播放的效果,对缓冲区中多路Wave数据进行必要的预处理后,再提交给Wave输出设备播放,实现原理如图1所示。
图1 多路Wave混音的实现原理
这种混音的方式效果跟你采用的算法有很大关系,但是如果我们采用DirectSound进行混音,就简单多了,我们只需要将我们要混音的内容传给它,DirectSound会在内部自动进行混音的。下面我们就进入DirectSound混音编程。
在了解DirectSound如何混音前我们先来看看DirectSound是如何播放一段wave音频的。
这里简单的介绍一下播放声音的步骤。
第一步:创建一个设备对象,设置设备对象的协作度。
在代码中你可以通过调用DirectSoundCreate8函数来创建一个支持IDirectSound8接口的对象,这个对象通常代表缺省的播放设备。当然你可以枚举可用的设备,然后将设备的GUID传递给DirectSoundCreate8函数。
如果没有声音输出设备,这个函数就返回error,或者,在VXD驱动程序下,如果声音输出设备正被某个应用程序通过waveform格式的API函数所控制,该函数也返回error。
下面是创建对象的代码:
LPDIRECTSOUND8 lpDirectSound;
HRESULT hr = DirectSoundCreate8(NULL, & lpDirectSound, NULL));
注意:DirectSound虽然基于COM,但是你并不需要初始化COM库,这些DirectSound都帮你做好了,当然,如果使用DMOS特技,就要自己初始化COM库了。
因为Windows是一个多任务操作环境,在同一个时刻有可能多个应用程序共用同一个设备,通过协作水平,DirectX就可以保证这些应用程序在访问设备的时候不会冲突,每个DirectSound应用程序都有一个协作度,用来确定来接近设备的程度,当你创建完设备对象后,一定要调用IDirectSound8::SetCooperativeLevel来设置协作度,否则,会听不到声音的。
HRESULThr = lpDirectSound->SetCooperativeLevel(hwnd, DSSCL_PRIORITY);
if(FAILED(hr))
ErrorHandler(hr); // Adderror-handling here.
第二步:创建一个辅助Buffer,也叫后备缓冲区
可以通过IDirectSound8::CreateSoundBuffer来创建buffer对象,这个对象主要用来获取处理数据,这种buffer称作辅助缓冲区,以和主缓冲区区别开来,DirectSound通过把几个后备缓冲区的声音混合到主缓冲区中,然后输出到声音输出设备上,达到混音的效果。
第三步:获取PCM类型的数据
将WAV文件或者其他资源的数据读取到缓冲区中。
第四步,将数据读取到缓冲区
可以通过 IDirectSoundBuffer8::Lock.方法来准备一个辅助缓冲区来进行写操作,通常这个方法返回一个内存地址,数据从私人buffer中复制到这个地址中,然后调用IDirectSoundBuffer8::Unlock方法。
第五步:播放缓冲区中的数据
可以通过IDirectSoundBuffer8::Play方法来播放缓冲区中的音频数据,可以通过IDirectSoundBuffer8::Stop来暂停播放数据,可以反复的停止,播放,音频数据,如果你同时创建了几个buffer,那么就可以同时来播放这些数据,这些声音会自动进行混音的。
DirectSound混音很简单,我们只要在一个设备上创建几个辅助的缓冲区,然后将数据读取到缓冲区中,同时的播放,DirectSound就会自动在主缓冲区自动混音的。至于同时可以播放几个辅助缓冲区则有硬件设备的性能决定。
在WDM驱动模式下,混音的工作由核心混音器来完成,不同的辅助缓冲区可能具有不同的WAV格式(例如,不同的采样频率),在必要的时候,辅助缓冲区的格式要转换成主缓冲区,或者核心混音器的格式。
在VXD驱动模式下,如果你的辅助缓冲区都采用相同的音频格式,并且硬件的音频格式也和你的音频格式匹配,此时,混音器不用作任何的转换。你的应用程序可以创建一个主缓冲区,然后通过IDirectSoundBuffer8::SetFormat来设置硬件的输出格式。要注意,只有你的协作度一定要是Priority Cooperative Level.,并且,一定要创建辅助缓冲区前设置主缓冲区,DirectSound会将你的设置保存下来。
在WDM模式下,对主缓冲区的的设置没有作用,因为主缓冲区的格式是由内核混音器来决定的。
下面看看如何进行混音,假设我们的背景需要混音的素材是下面的三个wave文件,"test1 .wav" "test2.wav""test3.wav"。首先定义一下需要的几个变量:
LPDIRECTSOUND8 g_pDS = NULL;
LPDIRECTSOUNDBUFFER g_pDsbuffer[3] = NULL;
CWaveFile* g_pWaveFile;//
WAVEFORMATEX g_wfxInput; //输入的音频格式
这里简单介绍一下CWaveFile类,DirectSound里封装了一个CWaveFile类用来操作wav文件,可以通过open来写入文件的头信息,write来写入文件的数据,Getsize函数获取文件的长度,close关闭文件。你可以在DirectSound的路径下找到这个类的定义(SDK root)\samples\C++\Common\Src\Dsutil.cpp。
首先初始化DirectSound
BOOLInitDirectSound()
{
if( FAILED( hr = DirectSoundCreate(NULL, & g_pDS, NULL ) ) )
return FALSE;
if( FAILED( hr = g_pDS ->SetCooperativeLevel( hwnd, DSSCL_PRIORITY ) ) )
returnFALSE;
returnTRUE;
}
在初始化DirectSound的时候,创建设备对象最简单的方法就是通过DirectSoundCreate8函数,这个函数的第一个参数指定了和这个对象邦定的设备的GUID,你可以通过枚举设备来获取这个设备的GUID, 如果这个参数也可以为NULL,缺省的系统的声音输出设备就是DSDEVID_DefaultPlayback。当你创建完设备对象后,一定要调用SetCooperativeLevel来设置协作度,否则,你不会听到声音的。DirectSound定义了三种水平:DSSCL_NORMAL、 DSSCL_PRIORITY和 DSSCL_WRITEPRIMARY
在Priority层次的协作度下,应用程序可以有优先权使用硬件资源,比如使用硬件进行混音,当然也可以设置主缓冲区的媒体格式,游戏程序应该采用这个层次的协作度,这个层次的协作度在允许应用程序控制采用频率和位深度的同时,也给应用程序很大的权力,这个层次的协作度允许其他应用程序的声音和游戏的音频同时被听到,不影响。
下面的函数加载wave文件,然后将音频数据读取到缓冲区中,然后通过DirectSound创建了的静态辅助缓冲区,将音频数据copy到DirectSound的静态辅助缓冲区,然后就可以播放了。
LPDIRECTSOUNDBUFFERLoadWaveFile(LPSTR lpzFileName)
{
DSBUFFERDESCdsbdesc;
HRESULThr;
BYTE*pBuffer;
DWORDdwSizeRead;
LPDIRECTSOUNDBUFFERlpdsbStatic=NULL;
if(FAILED( hr = g_pWaveFile->Open( lpzFileName, &g_wfxInput, WAVEFILE_WRITE) ) )
returnNULL;
DWORDdwSize = g_pWaveFile->GetSize();
pBuffer= new BYTE[dwSize];
g_pWaveFile->Read(pBuffer,dwSize,&dwSizeRead);
if(dwSizeRead> 0)
{
memset(dsbdesc,0,sizeof(DSBUFFERDESC));
dsbdesc.dwSize = sizeof(DSBUFFERDESC);
dsbdesc.dwFlags =DSBCAPS_STATIC;
dsbdesc.dwBufferBytes =dwSizeRead;
dsbdesc.lpwfxFormat = g_wfxInput;
if ( FAILED( g_pDS->CreateSoundBuffer(&dsbdesc, & lpdsbStatic,NULL ) ) )
{
g_pWaveFile->Close();
delete pBuffer;
return NULL;
}
LPVOID lpvWrite;
DWORD dwLength;
if (DS_OK == lpdsbStatic ->Lock(
0, // Offset at which to start lock.
0, // Size of lock; ignored because of flag.
&lpvWrite, // Gets address of first part of lock.
&dwLength, // Gets size of first part of lock.
NULL, // Address of wraparound not needed.
NULL, // Size of wraparound not needed.
DSBLOCK_ENTIREBUFFER)) // Flag.
{
memcpy(lpvWrite, pBuffer,dwLength);
lpdsbStatic ->Unlock(
lpvWrite, // Address oflock start.
dwLength, // Size of lock.
NULL, // No wraparoundportion.
0); // No wraparound size.
}
}
delete pBuffer;
return lpdsbStatic;
}
这里简单的介绍一下DirectSound的辅助缓冲区,在DirectSound中,辅助缓冲区分两类,一种是Static Buffer,这种buffer主要用于播放那些比较短的音频,可以将文件中的音频数据全部copy到Static buffer中,如果音频文件比较大,未了限制内存的开销,就要用到Streamingbuffer,一般来说,Streaming buffer只能包含几秒钟的数据量,然后在播放的过程中不断的更新streaming buffer中的数据。静态缓冲区的创建和管理和流缓冲区很相似,唯一的区别就是它们使用的方式不一样,静态缓冲区只填充一次数据,然后就可以play,然而,流缓冲区是一边play,一边填充数据。
上面创建的就是得静态的buffer,如果你要播放比较长的音频文件,你就要使用streaming buffer了。流缓冲区用来播放那些比较长的声音,因为数据比较长,没法一次填充到缓冲区中,一边播放,一边将新的数据填充到buffer中。
可以通过IDirectSoundBuffer8::Play函授来播放缓冲区中的内容,注意在该函数的参数中一定要设置DSBPLAY_LOOPING标志。
通过IDirectSoundBuffer8::Stop方法中断播放,该方法会立即停止缓冲区播放,因此你要确保所有的数据都被播放,你可以通过拖动播放位置或者设置通知位置来实现。
将音频流倒入缓冲区需要下面三个步骤
1、确保你的缓冲区已经做好接收新数据的准备。你可以拖放播放的光标位置或者等待通知
2、调用IDirectSoundBuffer8::Lock.函数锁住缓冲区的位置,这个函数返回一个或者两个可以写入数据的地址
3、使用标准的copy数据的方法将音频数据写入缓冲区中
4、IDirectSoundBuffer8::Unlock.,解锁
IDirectSoundBuffer8::Lock可能返回两个地址的原因在于你锁定内存的数量是随机的,有时锁定的区域正好包含buffer的起始点,这时,就会返回两个地址,举个例子:
假设锁定了30,000字节,偏移位置为20,000字节,也就是开始位置,如果你的缓冲区的大小为40,000字节,此时就会给你返回四个数据:
1、内存地址的偏移位置20,000,
2、从偏移位置到buffer的最末端的字节数,也是20,000,你要在第一个地址写入20,000个字节的内容
3、偏移量为0的地址
4、从起始点开始的字节数,也就是10,000字节,你要将这个字节数的内容写入第二个地址。如果不包含零点,最后两个数值为NULL和0。
当然,也有可能锁定buffer的全部内存,建议在播放的时候不要这么做,通过更新所有buffer中的一部份,例如,可能在播放广标到达1/2位置前要将第一个1/4内存更新成新的数据,一定不要更新play光标和Write光标间的内容。
下面的这个函数演示了如果向streaming buffer中填充音频数据,在调用这个函数之前,一定要确保你的streaming buffer是空的,但如何知道buffer是空闲没有数据呢?一个更有效的方法采用通知机制,通过IDirectSoundNotify8::SetNotificationPositions方法,可以设置任何一个小于buffer的位置来触发一个事件,然后响应处理函数中调用下面的函数将音频数据copy到DirectSound的Streaming buffer中。
BOOLAppWriteDataToBuffer(
LPDIRECTSOUNDBUFFER8 lpDsb, // The buffer.
DWORD dwOffset, // Our own write cursor.
LPBYTE lpbSoundData, // Start of our data.
DWORD dwSoundBytes) // Size of block to copy.
{
LPVOID lpvPtr1;
DWORD dwBytes1;
LPVOID lpvPtr2;
DWORD dwBytes2;
HRESULT hr;
// Obtain memory address of write block. This will be in two parts
// if the block wraps around.
hr = lpDsb->Lock(dwOffset, dwSoundBytes, &lpvPtr1,&dwBytes1,&lpvPtr2, &dwBytes2, 0);
// If the buffer was lost, restore and retry lock.
if (DSERR_BUFFERLOST == hr)
{
lpDsb->Restore();
hr = lpDsb->Lock(dwOffset, dwSoundBytes, &lpvPtr1,&dwBytes1,&lpvPtr2, &dwBytes2, 0);
}
if (SUCCEEDED(hr))
{
// Write to pointers.
CopyMemory(lpvPtr1, lpbSoundData, dwBytes1);
if (NULL != lpvPtr2)
{
CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2);
}
// Release the data back to DirectSound.
hr = lpDsb->Unlock(lpvPtr1, dwBytes1, lpvPtr2,dwBytes2);
if (SUCCEEDED(hr))
{
// Success.
return TRUE;
}
}
// Lock, Unlock, or Restore failed.
return FALSE;
}
将音频数据复制到DirectSound 的辅助缓冲区中,剩下的工作就是play buffer了,play很简单的,只是简单地调用Buffer提供的Play函数就可以了,复杂的工作DirectSound会替我们做好的。
voidPlay(LPDIRECTSOUNDBUFFER lpdsbStatic)
{
if ( lpdsbStatic == NULL ) return;
lpdsbStatic->SetCurrentPosition(0);
lpdsbStatic->Play(0,0,0);
}
现在混音的主要代码已经完成,你可以用下面的代码来对三段wave文件进行实时混音了。
VoidStartMixer()
{
InitDirectSound();
g_pDsbuffer[0] = LoadWaveFile("test1.wav");
g_pDsbuffer[1] = LoadWaveFile("test2.wav");
g_pDsbuffer[2] = LoadWaveFile("test3.wav");
Play(g_pDsbuffer[0]);
Play(g_pDsbuffer[1]);
Play(g_pDsbuffer[2]);
}
现在就可以同时听到三段wave音频了,这里要提醒一下,如果想将DirectSound的混音用到网络视频会议中,你就要做一些额外的工作了,现在的网络视频会议系统一般都是用Directshow技术开发的,因为在Directshow用播放音频的都是通过一个filter来访问声卡的,所以可以将DirectSound封装成一个filter,然后将这个filter加入到你的Graph图表中。