日期:2012-12-08  浏览次数:20586 次

关键字: DirectSound 9 VB.NET DirectX 声音控制 声音引擎 作者:董含君

转载请注明来自 http://blog.csdn.net/a11s

目的:制作一个声效控制器,减少内存占用量,封装directsound,低CPU消耗,满足人的实际需要,以及易用性提高.

本来不打算继续的,前几天跟denghe研究了一下相关的问题.讨论到太多的音效对机器的影响,尤其是内存占用以及释放.发现有必要做一个专门管理音效的控制器.

由于跟denghe的制作目的不同,所以实现起来大相径庭.他全部都要实现gameobject接口,对于游戏来说,这是可行的.但是我做控制器的目的并不仅仅是用于游戏.而是更加像是提供一个处理声音的服务.我还不敢称它为声音引擎.毕竟可以实现的功能还比较少.



问题的出现

1 当你打算播放音效的时候(就是wav),你可以new buffer(filename , ....) 这个我们都知道的. 但是new 之后,整个wav将会被装入内存.我们称之为静态载入.当你进攻敌人的时候,比如子弹的音效或者刀剑的音效.很多都是重复的.这时,你可以用刚才new 出来的 buffer 直接 play.

2 问题如果这么简单我就没有必要写此文了.当一个声音没有播放完成,另外一个重复的声音接着响起的时候,很显然不能直接play了,那么你怎么办?先stop它然后重新从头开始播放?很显然,也不行.那么直接再new 一个可以了吧.当然可以,除非你自己做混音(需要很多声学的知识,以及数学),否则只能使用两个buffer不同时间同时播放的方法.

3 看似解决了问题.我们把问题继续推广到多个音效(wav),以及大量重复播放的时候.用问题2的解决办法 new 出大量的buffer 就会出现问题了. 不但管理起来十分复杂,而且他会消耗掉恐怖的内存.并且你的系统资源开销非常可观. 看似这样的现象再游戏中是很容易出现的. 所以迫切需要一种管理方法应对这种现象.

于我不同的是,denghe提供了一个SoundPool方案,就是依赖游戏的脚本引擎.管理wav文件的载入以及卸载.每个有效的wav在每个场景载入的时候就创建多个备份,你可以给每个wav建立一定数目的缓冲buffer.比如建立4个,然后根据需要自动指向下一个.然后在4个之间循环.对于较短的音效而言,完全可以满足人类听觉的需要.所以对于游戏来说可以从根本上解决内存的无限浪费的局面,而且每种音效都不互相干扰..只要有可靠的脚本引擎负责加载以及释放即可.所需的总空间为 文件的总占用体积+ 每个文件的buffer数量*该文件的大小,实际上很多音效是与用户交互的,所以没办法预料某些音效的实际频率.做最坏的打算,你付出的实际内存=wav文件的个数*buffer数量.对于所需音效数量较小的程序来说,可以符合要求.但是某些较大的场景也许付出的代价需要重新考虑了.但是这个方案对游戏来说是比较好的方案.

4 我的解决方案:

4.1 建立Layer: 在我看来,声音可以分层次的.比如一个格斗游戏.Player1说得话,可以放在Layer1. Player 说的话可以放在Layer2,格斗时的声效--就是那些噼里啪啦的声音 可以放在Layer3 , 场景开始的时候 "Ready GO",以及"K.O." 都可以放在Layer4

4.2 建立Layer缓冲区:这样分类完成之后,你会发现,如果Layer之间相互影响是不合适的,但是Layer内部,人说话的时候,如果同时出现两句话是不是就有点不自然了?? 或者打斗的时候,那些噼里啪啦的声音过于常见,而且比较短.人们热火朝天的时候,不会可以在乎是否刚才的已经播放完?? 把这些互斥的因素放在一个同一个Layer里面是符合实际需要的.

4.3 建立步骤:

为了适应速度的需要,所以不能考虑再游戏进行的时候从硬盘加载.所以一开始就要全部加载到内存.播放的时候由于每个wav只有一个buffer,所以需要动态的创建副本进行播放.副本的管理可以交给控制器.比如play 方法.play( SoundKey,targetLayer)

4.4 性能分析: 主要是内存占用. 所需内存= 每个文件的大小的和 + Layer的数量*每个Layer中Buffer的数量

这些都是可控的,当然buffer越大,效果越好,而Layer往往是根据实际需要一开始就确定好的,所以内存使用可以稳定在一个数值,值得庆幸的是,这个数值是可控的,甚至可以根据计算机的内存容量临时改变buffer的数量(呵呵,一般不会有人会这么想的)


5 关键技术问题: 看似上面的想法不错,但是要实行起来,下列问题需要考虑

5.1 如何管理全部的buffer?对于习惯了面向对象的我们也许很少考虑这个问题,你可以用任何的高级手段,甚至想到用dataset.... 不是不可一,但是我比较古典,使用了一个buffer的二维数组,已经够用了.曾经想用arrylist,但是想不出他跟数组相比能给我带来多少实际的优势.

5.2 如何决定在哪个buffer播放? 既然buffer是数组或者其他形式,根据Layer的划分,从属于Layer.我仅仅针对二维数组来说,buffer[tarLayer][tarBuffer] 正好填充二维数组的两个参数.

5.3 既然是循环利用的,如何指定合适的buffer?

这个问题比较复杂,我封装在了控制器内部,用一个枚举的办法指定一个合适的buffer,一下是步骤

function enumBuffer(tarLayer as integer) as integer 传递过来要使用那个layer,返回合适的bufferID

a.循环检查当前layer是否有空闲的buffer,如果还有为nothing 或者 null 的buffer,毫无疑问他将会是最合适的buffer,所以直接退出循环返回他的id

b.如果不满足a,那么看看是否有disposed或者已经stop的buffer,他们已经不再播放了.也是合适的,找到一个之后也就没有必要继续循环,直接返回该ID

c. 如果不满足b,那么遍历当前layer的全部buffer,找到播放时间最长的buffer.播放时间长意味着来到的时间最长.我们可以请他休息了,返回这个id

d.如果满足c的有多个,那么按照第一个返回,或者最后一个返回.

5.4 播放的时候我们要注意什么?

为了节约空间当然要清空 刚才被枚举出来的buffer,然后创建一个wavbuffer列表的一个副本进行播放.

由于这时不能从硬盘得到资料,所以只能考虑用buffer.clone()的方法,或者直接new 一个新的buffer(利用wavbuffer的Stream)

6 具体结构说明

类: XSoundList 实质是一个filestream的hashtable,提供一个关键字,一个filename,创建一个filestream..起到了存储器的作用,将原始资料在这里集合,方便控制器读取

类:XBGMList 跟XSoundList类似,不过这里用来存储较大的文件,不使用静态加载,提供多种格式.用于背景音乐(难道你打算加载CD音轨或者几分钟的wav到内存?太疯狂了)

类:XSoundManEx 控制器.具体管里播放哪一个以及内部缓冲的数量,音量大小等等.也就是对directSound的封装类,使用它可以用layer的思想管理SoundBuffer

类:XSoundMan 标准控制器,由于XSoundManEx使用了太多的DirectSound特性,不符合OOP封装的思想,所以对于普通应用而言,应当使用这个来代替XSoundManEx,除非你打算重新封装原始的Ex.

类:XSoundSimp 简易控制器.只要有一个Layer就够用,超级简单的控制器,第一步加载,第二步播放 没了,已经重写了析构,不必担心泄漏,甚至每次新建一个XSoundSimp也比你直接new 多个buffer 节省内存,而且不必进行DirectSound的初始化

7 附加的功能

声音音量的调整,背景音乐与SoundBuffer的分离处理,动态加载wav(虽然不建议这么做).循环播放.等等,如果不够用可以从XSoundManEx继续封装

8 已知存在的不足以及下个版本的计划.

Layer之间的音量并没有独立,这点有悖于Layer之间的互不干涉.

BGM没有区分Layer,由于设计的时候,判定BGM划分Layer的意义不大.所以设定BGM只有一个Layer.中间使用也是竞争的.虽然用建立较大的BGMbu