Unity音效插件— FMOD学习
1. Unity内置了Audio功能,并且底层也是用FMOD来实现的,那为什么还要学习使用FMOD插件来进行音效的管理呢?
下面是摘录的部分内容:
The main reason to use this is that it enables your sound designers to edit, mix and master your game audio using the FMOD Studio interface while the game is running. Using FMOD studio, you can also create, e.g., a footstep sound event that will randomize the pitch and selected sound sample without the game having to know anything else than the name of the event. Using FMOD is free for non-commercial purposes and the license prices are also pretty reasonable for commercial work.
Most commercial games with decent sound design use such live mixing and centralized sound management approach, either using FMOD, WWise or some custom tools. Unity does use fmod for its own sound, but it does not provide any mixing tools and a sound designer will go insane if he/she has to manage hundreds of sound objects in the Unity editor. The main limitation of the FMOD Studio integration is that it only supports iOS, Android, Windows, Mac but no web player. If you want your sounds to work in the web player, I suggest you check out the Clockstone Audio Toolkit that provides mixing and management on top of Unity's internal sound system.
理解起来就是:
- Unity内置的Audio内部使用的是FMOD,但是功能不够齐全,高级一点的功能如混合(Mix)等无法使用;
- 音效管理应该和Unity工程解耦合,这样子可以减轻音效设计师的负担;
- 使用FMOD后,游戏中我们只需要关心sound event名字就可以了,对具体音效资源不会产生依赖;
- 目前FMOD支持Windows, Mac OSX, Android, iOS,其实官方文档中说了对XBOX One,PlayStation系统等系统都有支持;
结合FMOD Studio的官方文档,我们可以总结出使用FMOD的如下优点:
- 使用FMOD我们可以使用更少的资源创建更加高级和丰富的音效,减少运行时内存资源消耗;
- 音效管理只需要在FMOD Studio中管理好即可,不需关心具体Unity工程,方便音效管理;
- 编程人员只需要依赖于各种字符串形式的Sound Event和简单的播放API即可,使用简单;
- 平台支持较为完善。
2. FMOD的基本使用过程
FMOD的使用过程比较简单,复杂之处在于FMOD Studio的使用,音效资源编辑完成后的使用较为简单。具体流程参见下图:
具体过程如下:
1. 使用FMOD Studio编辑器编辑原始音效资源,创建各种音效;
2. 导入Unity Game Project;
3. 通过FMOD Plugin的API进行播放。
3. 简单例子
FMOD Studio部分:
1. 下载FMOD Studio;
2. 打开FMOD Studio即创建一个新工程,Ctrl + S 确定工程保存位置;
3. Window->Audio Bin打开Audio Bin窗口,用于选择工程需要的声音文件,File->Import Audio Files选择工程需要的声音文件;
4. FMOD Studio中左侧面板Event Tab栏中,右击选择New Event,表示创建一个新的音效(下图创建了一个test音效);
5. 将Audio bin面板中的将音效文件拖到FMOD Studio的Audio 1中,如下图所示,拖入了两个声音资源进去组成了一个音效:
6. 右击test Event,选择Assign to Bank->Master bank;
7. Ctrl + s,然后File->Build,然后File->Export GUIDs。
8. over
Unity部分:
1. 下并导入Unity,此时菜单栏会多出一个FMOD选项;
2. FMOD->Import Banks,打开刚刚FMOD Studio创建的工程的Build目录,Import之,Import的资源会在Assets/StreamingAssets目录下;
3. 选择Main Camera,然后选择Component->Scripts->FMOD Listener;
4. 创建一个Empty GameObject,然后Create And Add一个Script,附加如下代码内容,即可实现播放音效:
using UnityEngine; using System.Collections; using FMOD.Studio; public class TestFMOD : MonoBehaviour { FMOD.Studio.EventInstance testInstance; string snd = "event:/test"; // Use this for initialization void Start () { testInstance = FMOD_StudioSystem.instance.GetEvent(snd); if (testInstance != null) { testInstance.start(); } } }
2. 参数详解
FMOD Studio工程
FMOD Studio工程中,所有的声音文件都是以Event的形式存在的,这些Event都是通过字符串的形式来进行表示的;
目前对FMOD Studio工具的使用只是创建一个简单的Event,在FMOD Studio的安装目录中有文档可以参考。
- 是否是STREAM类型:
- 音效是否loop:
EventInstance参数
- position:决定音量,距离越远,音量越小;
- velocity:决定音高,速度越快,音量越大。
注意点:
需要封装的其实只有两个东西,一个是更新EventInstance.updateAttribute方法,FMOD的实现在其中不停的获取Transfrom Component,这是效率很低的地方,可以提供一个只需要Tranform参数的方法来供调用;另一个提供一个Play方法供外部进行调用,参数是Event即可。
以前出现的问题:
连续播放多个声音的时候,后面的声音将前面的声音打断了。初步估计是以为当时的系统中使用了对象池,每次都是从对象池中进行取得EventInstance,而不是创建新的。实际上如果对同一个EventInstance进行重复调用start函数时,会从头开始重新播放。
例子中的声音文件播放的时长为2.6s,没有出现后面的声音阻断前面的声音的情况。
IEnumerator Play2Sound() { for(int i=0; i<2; ++i) { EventInstance instance = FMOD_StudioSystem.instance.GetEvent(snd); instance.start(); yield return new WaitForSeconds(1f); } }
而这个例子中,重复的start则会将上次的声音打断
IEnumerator Play2Sound() { for(int i=0; i<2; ++i) { if (null == testInstance) { testInstance = FMOD_StudioSystem.instance.GetEvent(snd); } testInstance.start(); yield return new WaitForSeconds(1f); } }一个音乐多次重复播放的时候容易出现刺刺的声音
泡爷的方案是在五帧之内则不允许再次播放,但是我没有遇到这种现象,测试代码如下:
IEnumerator TestLargetScaleSnd() { while (true) { for(int i=0; i<26; ++i) { EventInstance instance = FMOD_StudioSystem.instance.GetEvent(snd); instance.start(); yield return new WaitForSeconds(0.1f); } } }
在使用FMOD的时候,york同学遇到一个情况,就是如果设置一个回调函数,则在MonoDevelop设置断点时总是crash掉。而我实验的结果是在大量播放一个声音时,如果设置回调函数,则Unity会挂掉。因此暂且总结为不要使用EventInstance的callback方法设置回调函数。
实验代码如下(五个声音以上同时播放则会挂掉,少则不会):
IEnumerator TestLargetScaleSnd() { //while (true) { for(int i=0; i<5; ++i) { EventInstance instance = FMOD_StudioSystem.instance.GetEvent(snd); instance.setCallback(callbackFunc); instance.start(); yield return new WaitForSeconds(0.1f); } } }
RESULT callbackFunc(EVENT_CALLBACK_TYPE type, IntPtr eventInstance, IntPtr parameters)
{
return RESULT.OK;
}
3. 实践总结
FMOD的已知实验总结出以下三点:
1. Unity Project只需要使用FMOD.Studio.EvenInstance就可以了,Unity Project中不需要对EventInstance的内存池管理,只需要对它的position属性进行管理;
2. 我们还需要一个封装的接口来提供声音的播放,这个接口为void Play(string snd);
3. _3D_ATTRIBUTES用于设置EventInstance的属性,我们设置的属性目前只有position属性,而这个属性由于需要随着角色的移动而更新,因此需要频繁调用。而_3D_ATTRIBUTES的to3DAttributes方法每次都要去取GameObject的Transform,消耗较大,因此我们需要封装一个to3DAttributes(Transform)方法,根据缓存的Transform来进行。
to3DAttributes方法的封装如下:
public class FModUtils { static public _3D_ATTRIBUTES to3DAttributes(Transform go) { FMOD.Studio._3D_ATTRIBUTES attributes = new FMOD.Studio._3D_ATTRIBUTES(); attributes.forward = UnityUtil.toFMODVector(go.forward); attributes.up = UnityUtil.toFMODVector(go.up); attributes.position = UnityUtil.toFMODVector(go.position); if (go.rigidbody) attributes.velocity = UnityUtil.toFMODVector(go.gameObject.rigidbody.velocity); return attributes; } }
总结1和2则通过封装一个FModCom即可,需要播放声音的GameObject需要挂接这个脚本,并在需要播放声音的时候调用其中的Play接口,代码如下:
using UnityEngine; using System.Collections; using System.Collections.Generic; using FMOD.Studio; using com.morefun.kl; public class FMODCom : MonoBehaviour { [HideInInspector] public float updateTime = 0.5f; private List<EventInstance> m_soundList; private Transform m_trans; void Awake() { m_trans = this.transform; m_soundList = new List<EventInstance>(); } // Use this for initialization void Start () { StartCoroutine(updateSnd()); } // 播放声音接口 public void Play(string snd) { EventInstance sndEvent = FMOD_StudioSystem.instance.GetEvent(snd); if (null == sndEvent) return; sndEvent.set3DAttributes(FModUtils.to3DAttributes(m_trans)); sndEvent.start(); addToUpdateAttribute(sndEvent); } IEnumerator updateSnd() { while (true) { PLAYBACK_STATE state; //将stopped的音效从soundList中删除 //在循环中要删除对象,因此不能用foreach for (int i = 0; i < m_soundList.Count; ++i) { EventInstance eventIns = m_soundList[i]; eventIns.getPlaybackState(out state); if (PLAYBACK_STATE.PLAYING == state) { eventIns.set3DAttributes(FModUtils.to3DAttributes(m_trans)); } else if (PLAYBACK_STATE.STOPPED == state) { m_soundList.RemoveAt(i); i--; } } yield return new WaitForSeconds(updateTime); } } void addToUpdateAttribute(EventInstance value) { if (!m_soundList.Contains(value)) { m_soundList.Add(value); } } }
工程使用FMOD的过程如下:
1. 在Main Camere上Attach一个FMOD Listener脚本(Component->Scripts->FMOD_Listener);
2. 需要播放音效的GameObject上Attach FModCom脚本,需要播放音效的时候调用FModCom.Play接口。
4. 音频编码和解码
普通音频文件根据编码格式不同分为两类:
- 无损格式,例如WAV,PCM,TTA,FLAC,AU,APE,TAK,WavPack(WV)
- 有损格式,例如MP3,Windows Media Audio(WMA),Ogg Vorbis(OGG),AAC
我们一般使用的是WAV,MP3和OGG格式的音频文件,一般WAV是MP3和OGG文件的十倍左右大小。
音频文件的解析过程分为两部:
1. 加载入内存;
2. 解码成波形文件。
二者都是比较消耗时间的地方,解码的优化在于是否有硬件优化,IO的优化在于能否部分加载连续IO。
FMOD自身提供了音频编码格式.BANK,可以在编辑器中编辑Quality选项选择最终生成的音乐品质,我在实验中选择的Quality是80%,结果如下:
原始音效WAV文件大小为117KB,生成的BANK文件大小为50KB;
原始音效MP3文件大小是2.78MB, 生成的BANK文件大小是4.82MB。
FMOD统一了格式,使得我们不需要再关心具体的音频文件格式和转化。带来的一个缺点是音频文件在安装包中占据的体积会增大,这个需要我们调整Quality参数来进行设置。
上次飞车的分享中提到Android不支持音频的硬件解码,因此如果需要通过CPU解码游戏背景音这种类型音效则非常耗时,他们的经验是:
- 特效音一次性整体载入;
- 背景音则通过string的形式进行部分载入。
FMOD中支持上述两种方式的加载。在FMOD中,如果原始的音效资源是压缩类型的(mp3, ogg等),则可以选择是否以STREAM的方式进行加载;如果是非压缩类型的(wav等),则只能以非STREAM的方式加载进内存。
因此我们的解决方案是:
- 特效音在FMOD中设置成非STREAM方式;
- 背景音设置成STREAM方式。