本来不打算继续的,前几天跟denghe研究了一下相关的问题.讨论到太多的音效对机器的影响,尤其是内存占用以及释放.发现有必要做一个专门管理音效的控制器.
由于跟denghe的制作目的不同,所以实现起来大相径庭.他全部都要实现gameobject接口,对于游戏来说,这是可行的.但是我做控制器的目的并不仅仅是用于游戏.而是更加像是提供一个处理声音的服务.我还不敢称它为声音引擎.毕竟可以实现的功能还比较少.学习笔记,VB.NET使用DirectSound9 (8) 音效控制器
问题的出现
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列表的一个副本进行播放.学习笔记,VB.NET使用DirectSound9 (8) 音效控制器
由于这时不能从硬盘得到资料,所以只能考虑用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.中间使用也是竞争的.虽然用建立较大的BGMbuffer可以环节这方面的不足(没关系,动态加载,内存消耗不是很大).但是对于不了解其中机制的使用者来说会觉得不可思议.而且Layer内部竞争,一旦超出buffer最大值可能会影响背景音乐的使用.比如某些音效体积太大,程序人员将其作为BGM添加,这样看起来节约,但是存在隐患.除非你能确保满足枚举条件b.
9 程序演示以及源代码介绍
第一个简单应用,按下响应的键盘按键会发出音效,可以像弹钢琴一样.(你想做软波表么?)
第二个演示是利用文本驱动声音的播放,可以"录音"哦
第三个演示是频率的重复对内存以及CPU的影响(测试性能),包括背景音乐的播放以及Layer的使用
第四个被我隐藏了,本来做个游戏的.时间不是很多,没有完成,有兴趣的化可以继续完善
10 下载地址暂时无法提供,因为我找不到地方长时间稳定的存放我的代码,大约800k,而且CSDN的Blog暂时不提供rar的上传.需要的话,请加QQ群:589626 C#游戏开发,群的共享里面有rar的下载学习笔记,VB.NET使用DirectSound9 (8) 音效控制器
==============End================