1、利用 Directsound 编程实现实时混音 摘要:将多个音频文件或多路音频数据同时输出到音频输出设备上,就可同时听到多个不同的声音,这就是混音。在游戏开发,网络视频会议开发中都会用到混音技术,本文详细介绍如何利用 Directsound 实现几路不同的音频进行实时的混音。 关键词:Directsound 混音在游戏开发中比较常用的音效素材都是比较短的,所以一般常用的 API 是playsound()函数,比如我们要在游戏背景中播放一个 test.wav 音效素材,只要简单的调用下面的函数即可PlaySound(“test.wav“,NULL,SND_FILENAME|SND_ASYNC);
2、如此简单,事实上我们看到,国内的游戏大致上都可以用 PlaySound()搞定。但是既然是简单,从功能上就要受限了,如果遇到复杂的场景就没法用 playsound 实现了,比如在场景中既有开门的声音,又有砍人的声音,你如果用 playsound 就没法同时听到两种声音,只能是一个声音完了再听到另外一个声音,这时,就需要混音了。在网络视频会议开发中,如果不同的客户端同时发言,如何将多个不同端点的话音数据经网络传输到达某一个端点,经该端点的 Wave 设备输出,能同时听到多个人的话音,从而实现局域网络中多方的话音交谈,这也需要用到混音技术。在网络上实现话音交谈,特别强调实时性,要尽量保证话音的平滑
3、、连续,因此为了保证话音数据连续,减少话音数据存储带来的延时,在具体实现中,话音的录制和播放都不采用文件的形式,录制和播放的话音数据都存在缓冲区中。在 Windows 系统中,一般情况下,高层Wave 接口函数无法直接播放缓冲区中的话音数据,而必须用底层函数来实现,常用的是Windows API 中的 Wave 函数。将 Wave 数据在 Wave 设备上输出使用的是WaveOutWrite 函数,但是该函数不支持多路 Wave 数据的同时播放,为了能达到多路Wave 数据同时播放的效果,对缓冲区中多路 Wave 数据进行必要的预处理后,再提交给Wave 输出设备播放,实现原理如图 1 所示。
4、图 1 多路 Wave 混音的实现原理这种混音的方式效果跟你采用的算法有很大关系,但是如果我们采用 Directsound 进行混音,就简单多了,我们只需要将我们要混音的内容传给它,Directsound 会在内部自动进行混音的。下面我们就进入 Directsound 混音编程。在了解 Directsound 如何混音前我们先来看看 DirectSound 是如何播放一段 wave 音频的。这里只是简单的介绍一下播放声音的步骤。第一步,创建一个设备对象。 在你的代码中你可以通过调用 DirectSoundCreat8 函数来创建一个支持 IDirectSound8接口的对象,这个对象通常代表缺
5、省的播放设备。当然你可以枚举可用的设备,然后将设备的GUID 传递给 DirectSoundCreat8 函数。注意,Directsound 虽然基于 COM,但是你并不需要初始化 com 库,这些Directsound 都帮你做好了,当然,如果你使用 DMOs 特技,你就要自己初始化 com 库了,切记。第二步,创建一个辅助 Buffer,也叫后备缓冲区你可以通过 IDirectSound8:CreateSoundBuffer 来创建 buffer 对象,这个对象主要用来获取处理数据,这种 buffer 称作辅助缓冲区,以和主缓冲区区别开来,Direcsound 通过把几个后备缓冲区的声音混
6、合到主缓冲区中,然后输出到声音输出设备上,达到混音的效果。第三步,获取 PCM 类型的数据将 WAV 文件或者其他资源的数据读取到缓冲区中。第四步,将数据读取到缓冲区你可以通过 IDirectSoundBuffer8:Lock.方法来准备一个辅助缓冲区来进行写操作,通常这个方法返回一个内存地址,见数据从你的私人 buffer 中复制到这个地址中,然后调用IDirectSoundBuffer8:Unlock.第五步,播放缓冲区中的数据你可以通过 IDirectSoundBuffer8:Play 方法来播放缓冲区中的音频数据,你可以通过IDirectSoundBuffer8:Stop 来暂停播放数
7、据,你可以反复的莱停止,播放,音频数据,如果你同时创建了几个 buffer,那么你就可以同时来播放这些数据,这些声音会自动进行混音的。看到了吧,Directsound 混音很简单,我们只要在一个设备上创建几个辅助的缓冲区,然后将数据读取到缓冲区中,同时的播放,Directsound 就会自动在主缓冲区自动混音的。至于同时可以播放几个辅助缓冲区则有硬件设备的性能决定。 在 WDM 驱动模式下,混音的工作由核心混音器来完成,不同的辅助缓冲区可能具有不同的 WAV 格式(例如,不同的采样频率),在必要的时候,辅助缓冲区的格式要转换成主缓冲区,或者核心混音器的格式。在 VXD 驱动模式下,如果你的辅助
8、缓冲区都采用相同的音频格式,并且硬件的音频格式也和你的音频格式匹配,此时,混音器不用作任何的转换。你的应用程序可以创建一个主缓冲区,然后通过 IDirectSoundBuffer8:SetFormat 来设置硬件的输出格式。要注意,只有你的协作度一定要是 Priority Cooperative Level.,并且,一定要创建辅助缓冲区前设置主缓冲区,DirectSound 会将你的设置保存下来。在 WDM 模式下,对主缓冲区的的设置没有作用,因为主缓冲区的格式是由内核混音器来决定的。下面开始吧,让我们看看如何进行混音吧,假设我们的背景需要混音的素材是下面的三个wave 文件,“test1 .
9、wav“ “test2.wav“ “test3.wav“。首先定义一下我们需要的几个变量:LPDIRECTSOUND8 g_pDS = NULL; LPDIRECTSOUNDBUFFER g_pDsbuffer3 = NULL;CWaveFile* g_pWaveFile;/WAVEFORMATEX g_wfxInput; /输入的音频格式这里简单介绍一下 CWaveFile 类,Directsound 里封装了一个 CWaveFile 类用来操作wav 文件,可以通过 open 来写入文件的头信息,write 来写入文件的数据,Getsize 函数获取文件的长度,close 关闭文件。你可以
10、在 DirectSound 的路径下找到这个类的定义(SDK root)samplesC+CommonSrcDsutil.cpp。首先初始化 DirectsoundBOOL InitDirectSound()if ( FAILED( hr = DirectSoundCreate(NULL, / Set cooperative level.if ( FAILED( hr = g_pDS -SetCooperativeLevel( hwnd, DSSCL_PRIORITY ) ) )return FALSE;return TRUE;在初始化 Directsound 的时候,创建设备对象最简单的方法
11、就是通过DirectSoundCreate8 函数,这个函数的第一个参数指定了和这个对象邦定的设备的 GUID,你可以通过枚举设备来获取这个设备的 GUID, 如果这个参数也可以为 NULL,缺省的系统的声音输出设备就是 DSDEVID_DefaultPlayback 。当你创建完设备对象后,一定要调用IDirectSound8:SetCooperativeLevel 来设置协作度,否则,你不会听到声音的。DirectSound 定义了三种水平,DSSCL_NORMAL, DSSCL_PRIORITY, and DSSCL_WRITEPRIMARY在 Priority 层次的协作度下,应用程序
12、可以有优先权使用硬件资源,比如使用硬件进行混音,当然也可以设置主缓冲区的媒体格式,游戏程序应该采用这个层次的协作度,这个层次的协作度在允许应用程序控制采用频率和位深度的同时,也给应用程序很大的权力,这个层次的协作度允许其他应用程序的声音和游戏的音频同时被听到,不影响。下面的函数加载 wave 文件,然后将音频数据读取到缓冲区中,然后通过 Directsound创建了的静态辅助缓冲区,将音频数据 copy 到 Directsound 的静态辅助缓冲区,然后就可以play 了。LPDIRECTSOUNDBUFFER LoadWaveFile(LPSTR lpzFileName) DSBUFFERD
13、ESC dsbdesc;HRESULT hr;BYTE *pBuffer;DWORD dwSizeRead; LPDIRECTSOUNDBUFFER lpdsbStatic=NULL;if( FAILED( hr = g_pWaveFile-Open( lpzFileName, DWORD dwSize = g_pWaveFile-GetSize();pBuffer = new BYTEdwSize;g_pWaveFile-Read(pBuffer,dwSize,if(dwSizeRead 0)memset(dsbdesc,0,sizeof(DSBUFFERDESC);dsbdesc.dwSi
14、ze = sizeof(DSBUFFERDESC);dsbdesc.dwFlags =DSBCAPS_STATIC; dsbdesc.dwBufferBytes =dwSizeRead; dsbdesc.lpwfxFormat = g_wfxInput;if ( FAILED( g_pDS-CreateSoundBuffer(delete pBuffer;return NULLLPVOID lpvWrite;DWORD dwLength;if (DS_OK = lpdsbStatic -Lock(0, / Offset at which to start lock.0, / Size of l
15、ock; ignored because of flag.lpdsbStatic -Unlock(lpvWrite, / Address of lock start.dwLength, / Size of lock.NULL, / No wraparound portion.0); / No wraparound size.delete pBuffer;return lpdsbStatic; 这里我想简单的讲一下 Directsound 的辅助缓冲区,在 Directsound 中,辅助缓冲区分两类,一种是 Static Buffer,这种 buffer 主要用于播放那些比较短的音频,可以将文
16、件中的音频数据全部 copy 到 Static buffer 中,如果音频文件比较大,未了限制内存的开销,就要用到 Streaming buffer,一般来说,Streaming buffer 只能包含几秒钟的数据量,然后在播放的过程中不断的更新 streaming buffer 中的数据。静态缓冲区的创建和管理和流缓冲区很相似,唯一的区别就是它们使用的方式不一样,静态缓冲区只填充一次数据,然后就可以play,然而,流缓冲区是一边 play,一边填充数据。上面创建的就是得静态的 buffer,如果你要播放比较长的音频文件,你就要使用streaming buffer 了。流缓冲区用来播放那些比较
17、长的声音,因为数据比较长,没法一次填充到缓冲区中,一边播放,一边将新的数据填充到 buffer 中。可以通过 IDirectSoundBuffer8:Play 函授来播放缓冲区中的内容,注意在该函数的参数中一定要设置 DSBPLAY_LOOPING 标志。通过 IDirectSoundBuffer8:Stop 方法中断播放,该方法会立即停止缓冲区播放,因此你要确保所有的数据都被播放,你可以通过拖动播放位置或者设置通知位置来实现。将音频流倒入缓冲区需要下面三个步骤 1、确保你的缓冲区已经做好接收新数据的准备。你可以拖放播放的光标位置或者等待通知2、调用 IDirectSoundBuffer8:L
18、ock.函数锁住缓冲区的位置,这个函数返回一个或者两个可以写入数据的地址3、使用标准的 copy 数据的方法将音频数据写入缓冲区中4、 IDirectSoundBuffer8:Unlock.,解锁IDirectSoundBuffer8: Lock 可能返回两个地址的原因在于你锁定内存的数量是随机的,有时你锁定的区域正好包含 buffer 的起始点,这时,就会给你返回两个地址,举个例子吧假设你锁定了 30,000 字节,偏移位置为 20,000 字节,也就是开始位置,如果你的缓冲区的大小为 40,000 字节,此时就会给你返回四个数据:1、内存地址的偏移位置 20,000 ,2、从偏移位置到 b
19、uffer 的最末端的字节数,也是 20,000 ,你要在第一个地址写入20,000 个字节的内容3、偏移量为 0 的地址4、从起始点开始的字节数,也就是 10,000 字节,你要将这个字节数的内容写入第二个地址。如果不包含零点,最后两个数值为 NULL 和 0,当然,你也有可能锁定 buffer 的全部内存,建议你在播放的时候不要这么做,通过你只是更新所有 buffer 中的一部份,例如,你可能在播放广标到达 1/2 位置前要将第一个 1/4 内存更新成新的数据,你一定不要更新 play 光标和 Write 光标间的内容。 下面的这个函数演示了如果向 streaming buffer 中填充
20、音频数据,在调用这个函数之前,你一定要确保你的 streaming buffer 是空的,但如何知道 buffer 是空闲没有数据呢?一个更有效的方法采用通知机制,通过 IDirectSoundNotify8:SetNotificationPositions 方法,你可以设置任何一个小于 buffer 的位置来触发一个事件,然后响应处理函数中调用下面的函数将音频数据 copy 到 Directsound 的 Streaming buffer 中。BOOL AppWriteDataToBuffer( LPDIRECTSOUNDBUFFER8 lpDsb, / The buffer.DWORD d
21、wOffset, / 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-Lo
22、ck(dwOffset, dwSoundBytes, / If the buffer was lost, restore and retry lock. if (DSERR_BUFFERLOST = hr) lpDsb-Restore(); hr = lpDsb-Lock(dwOffset, dwSoundBytes, if (SUCCEEDED(hr) / Write to pointers. CopyMemory(lpvPtr1, lpbSoundData, dwBytes1); if (NULL != lpvPtr2) CopyMemory(lpvPtr2, lpbSoundData+d
23、wBytes1, 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 函数就可以了,复
24、杂的工作Directsound 会替我们做好的。void Play(LPDIRECTSOUNDBUFFER lpdsbStatic) if ( lpdsbStatic = NULL ) return;lpdsbStatic-SetCurrentPosition(0);lpdsbStatic-Play(0,0,0); 现在混音的主要代码已经完成,你可以用下面的代码来对三段 wave 文件进行实时混音了。Void StartMixer()InitDirectSound();g_pDsbuffer0 = LoadWaveFile(“test1.wav“);g_pDsbuffer1 = LoadWav
25、eFile(“test2.wav“);g_pDsbuffer2 = LoadWaveFile(“test3.wav“);Play (g_pDsbuffer0);Play (g_pDsbuffer1);Play (g_pDsbuffer2);现在就可以同时听到三段 wave 音频了,利用 Directsound 进行混音是不是很简单啊,这里要提醒一下,如果你想将 Directsound 的混音用到网络视频会议中,你就要做一些额外的工作了,现在的网络视频会议系统一般都是用 Directshow 技术开发的,因为在 Directshow用播放音频的都是通过一个 filter 来访问声卡的,所以你可以将 DirectSound 封装成一个filter,然后将这个 filter 加入到你的 Graph 图表中。好了,关于 Directsound 的混音我就简单介绍到这里,下一篇文档我会介绍一下,如何用Directsound 生成 3D 声音Trackback: http:/