本博关于2D声音和3D声音的播放问题。
1、fmod中区分2D声音和3D声音。在fmod中如果音频有:
这个音频是3D的,如果没有则是2D声音。具体到unity中,我们可以通过EventBrowser查看声音的属性:
查看到。
代码中判断方法,参考StudioEventEmitter的Play()方法:
eventDescription.is3D(out is3D);
2、2D、3D声音的区别:
在fmod中的,对应2D声音,即使没有加StudioListener组件,也会听到2D的声音。也不要以为是默认摄像机上的AudioListener听到的,因为我们把场景中所有的unity自己的AudioListener去除掉,依然可以听到fmod的2D声音,答案就是fmod为我们提供默认的耳朵以监听2D声音。
在fmod中的,3D声音,需要添加StudioListener组件,而且必须保证耳朵在声音的有效位置处,才能听到3D声音。有效位置是3D声音的最近和最远距离之间。超过这个范围则听不到3D声音。
在有多个StudioListener情况下,同一个3D声音会不会听到多次,答案是不会。fmod只会取离3D声音最近的耳朵监听3D声音。
声音有没有方向,目前不清楚,但是测试下来,没有。
耳朵有没有方向,答案是有的。我们尝试把播放3D声音,同时,移动StudioListener的位置,发现左右耳朵听到的声音会有变化。同时如果我们把物体旋转一个角度,也会发现左右耳朵听到的声音不同,这也符合实际的物理规律。
3、耳朵到底放在哪里,是角色还是摄像机,待思考。
fmod调试unity卡死的问题:http://www.fmod.org/questions/question/visual-studio-freezes-when-debugging-with-fmod-installed/
4、fmod中的master.string读取,检查所有事件的字符串:
在fmod项目导入unity之后,有一个Master Bank.strings,它包含了所有事件的名字。
这个最简单的方式,是查看:GUIDs,因为其是可读的。
5、在升级fmod之后,对原插件代码进行的修改有哪些?
(1)升级到1.10.09之后,会出现vs调试就崩溃的情况,此时参考:
http://www.fmod.org/questions/question/visual-studio-freezes-when-debugging-with-fmod-installed/
具体的修改方法是:
在RuntimeManager中的Initialize()中,将studioInitFlags的赋值修改如下:
#if UNITY_EDITOR
//FIX FOR CRASHES
studioInitFlags = FMOD.Studio.INITFLAGS.SYNCHRONOUS_UPDATE;
#else
//ORIGINAL LINE
studioInitFlags = FMOD.Studio.INITFLAGS.NORMAL | FMOD.Studio.INITFLAGS.DEFERRED_CALLBACKS;
#endif
(2)在苹果包上把应用切到后台,然后再切回前台声音消失,修改RuntimeManager中的
OnApplicationFocus方法,在其后加入OnApplicationPause方法。在其OnApplicationPause方法中加入恢复声音的方法:
void OnApplicationPause(bool pauseStatus)
{
if (studioSystem.isValid())
{
// Strings bank is always loaded
if (loadedBanks.Count > 1)
PauseAllEvents(pauseStatus);
if (pauseStatus)
{
lowlevelSystem.mixerSuspend();
}
else
{
lowlevelSystem.mixerResume();
}
}
}
(3)测试在Update中测试是否可以屏蔽经常检测拷贝FMODStudioCache的方法。
(4)事件命名的时候,不要有空格,否则会导致事件找不到。
如事件起名字叫:hello world,最好写成hello_world。
6、Refresh Bank按钮执行了什么操作?
反序列化EventCache,这个类是ScriptableObject的子类。
7、项目移动之后,fmod报错如下:
报错原因:FMODStudioCache.asset缓存文件,记录原先的拷贝路劲。
解决方法:所以可以将此文件删除,然后fmod会自动重新生成一个。
8、升级fmod遇到的问题,同时存在高低两个版本的dll,导致报错
解决方法:关闭unity,删除低版本的dll,注意只有关闭unity才能将其之前版本的dll删除掉,因为开启unity,dll处于占用状态无法删除。
9、fmod中音频找不到,引起手机包崩溃
原因是在RuntimeManager中的:
public static FMOD.Studio.EventDescription GetEventDescription(string path)
{
//try
//{
return GetEventDescription(PathToGUID(path));
//}
//catch (EventNotFoundException)
//{
// throw new EventNotFoundException(path);
//}
}
public static FMOD.Studio.EventDescription GetEventDescription(Guid guid)
{
FMOD.Studio.EventDescription eventDesc;
if (Instance.cachedDescriptions.ContainsKey(guid) && Instance.cachedDescriptions[guid].isValid())
{
eventDesc = Instance.cachedDescriptions[guid];
}
else
{
var result = Instance.studioSystem.getEventByID(guid, out eventDesc);
if (result != FMOD.RESULT.OK)
{
Debug.LogError("GetEventDescription [FMOD] Event not found guid=" + guid + " result=" + result);
//throw new EventNotFoundException(guid);
}
if (eventDesc.isValid())
{
Instance.cachedDescriptions[guid] = eventDesc;
}
else
{
Debug.LogError("eventDesc is not valid guid=" + guid);
}
}
return eventDesc;
}
在找不到音频事件的时候抛出了异常,外面又没有补货异常,所以崩溃了。
解决的方法是改为Debug.LogError,输出一个log即可,达到了提示,音频缺失就缺失,反正要能保证游戏继续运行下去。
10、队友声音播放听不到的问题
原因是在组件中,对fmod没有正确的初始化:
public void OnEntityLoaded(XGameObject go)
{
Transform t = go.Find("");
if (t != null/* && _emitter != null*/)
{
cachedGo = t.gameObject;
}
}
这里是判断物体是否加载完毕了,如果加载完毕了,缓存实体,但是不应该判断_emitter为null,才去缓存。
这样导致fmod声音依赖实体没有初始化,所以声音是播放了,但是在要给不确定的位置。
而队友的声音一定是一个3D的声音,对于3D的声音要设置正确位置才能听得到。
11、音频的切换
在遇到boss的时候,要切换背景音乐。如果采用的是两个音频叠加成一个背景音的情况下。
比如一个场景的背景音为A+B,两个音乐组成。
正常情况下播放的是A,而遇到boss的时候切换成B。
那么则需要通过参数设置的形式:
SetBGMParam("Boss", 1);
而如果此时fmod的状态不是有效的,也就是invalid的,那么这个设置是无效的。
所以一定要保证设置参数的时候,fmod是valid的,如果不是,则需要缓存参数,等到fmod为valid的状态,再重新设置参数。
如何判断fmod是否valid,则需要通过如下的代码:
public bool IsValid(AudioChannel channel)
{
FMODUnity.StudioEventEmitter e = GetEmitter(channel);
if (e != null)
{
return e.EventInstance.isValid();
}
return false;
}
这样就可获得fmod的状态了。
12、出现[FMOD] OutputWASAPI::mixerThread : Starvation detected in WASAPI output buffer!
报错。
尝试解决方法:
https://gameinstitute.qq.com/community/detail/103142
https://blog.csdn.net/boluns/article/details/52451999
在RuntimeManager中的FMOD.RESULT Initialize()方法中添加:
result = FMOD.Studio.System.create(out studioSystem);
CheckInitResult(result, "FMOD.Studio.System.create");
result = studioSystem.getCoreSystem(out coreSystem);
CheckInitResult(result, "FMOD.Studio.System.getCoreSystem");
// 默认是1024的buffer,开大一点,尝试解决因为buffer导致崩溃的问题
#if UNITY_EDITOR || UNITY_STANDALONE
CoreSystem.setDSPBufferSize(2048, 4);
#endif
其中的CoreSystem.setDSPBufferSize(2048, 4);是将缓冲开到2048。
这个只是尝试解决方法,不知道可行不。
这个问题的参考网址:https://qa.fmod.com/search?q=OutputWASAPI%3A%3AmixerThread%20%3A%20Starvation%20detected%20in%20WASAPI%20output%20buffer!%20category%3A7
13、fmod的设置参考网址
https://fmod.com/resources/documentation-unity?version=2.0&page=settings.html#logging-level
14、今天又遇到一个问题,就是停背景音停不了的问题。
问题描述:
进入场景A,使用频道action,播放背景音bg1
切换到战斗场景B,使用频道action,播放背景音bg2
战斗胜利,需要播放两段音乐musicA,musicB
原先做法是:
使用同一个频道action播放musicA,以打断的方式,也就是此时打断了战斗场景2的背景音bg2,然后播放了musicA,这步是正确的。
然后再以非打断的方式播放musicB,此时你能听到两个声音musicA和musicB,这是正确的。
但是,但是,当我们切换到场景A的时候,应该播放bg1,但是此时我们还听到musicA的声音。这是为啥呢?
原因剖析:
每次load场景之后,都会调用这样的接口:
public void PlayBGM(string bgm, AudioChannel channel = AudioChannel.Action)
{
if (!hasSound)
return;
if (string.IsNullOrEmpty(bgm)) return;
if (m_bgmFmod == null)
{
m_bgmFmod = XAudioMgr.singleton.FModBus.GetRuntimeFMOD() as IXFmod;
}
m_bgmFmod.StartEvent("event:/" + bgm, channel, true); //这里是打断的方式
}
所以当播放musicA的时候,action被musicA占用,而播放musicB的时候,以非打断的方式播放,那么action频道此时被musicA和musicB公共用,但是此时的句柄已经指向了musicB时候的句柄了。musicA那个instance则不受控制,直到自己消亡。
所以当你切回到场景A的时候,虽然以打断的方式,使用action频道,但是此时只能停止的(假设musicB还在播放)是musicB。
所以当然你能够听到musicA(因为它没有人能够停止掉)。
解决方法:
使用action频道,以打断的方式播放musicA,但是使用另外一个频道播放musicB。由于musicB声音很短,所以播放完自己停止了。不用关心,而回到场景A的时候,bg1以打断的方式使用action频道,此时能够只听到bg1声音,因为同为action频道的musicA被打断了。而另外一个频道的musicB由于时长太短(路飞的一个胜利语气而已,在结算界面出来之后一会就完了,所以不用关心)。这样就解决了。
那么fmod代码中的关于时刻保持一个最新频道的引用的代码在这里:
if (!instance.isValid()) //假设当前频道的实例不可用,那么则直接回收句柄
{
instance.clearHandle();
}
// Let previous oneshot instances play out
if (instance.isValid()) //假设当前频道的实例可用,那么同样进行释放和回收,只时多了一个release()方法。
{
instance.release();
instance.clearHandle();
}
if (!instance.isValid()) //那么此时当前频道的实例绝对是不用了,所以重新创建了一个实例,这也就是证明了,如果以非打断的方式播放同一个频道的话,那么过一会你在想停止前面的声音,则是不可控制的。
{
eventDescription.createInstance(out instance);
同一个频道使用的例子:
播放A——》非打断播放B
此时你想停止A,那么是不可能的了,最多也就停止了B,因为A的句柄已经回收了。你拿不到,此时只能任由其死亡。
而这种方式,最好的应用场景是,UI的点击,都是非打断的方式播放。
其他的都应该以打断的方式播放。
15、人物跑步声音的添加
总的思路是:制作短暂的跑步的音频,以定时器的方式,每隔一段时间播放一次。
private XTimerMgr.ElapsedEventHandler m_playMoveSoundHandler = new XTimerMgr.ElapsedEventHandler(PlayMoveSound);
private void PlayMoveSound(object obj)
{
//XAudioMgr.singleton.PlaySound(XEntityMgr.singleton.CombatPlayer, AudioChannel.Motion, "Tone/Role/Luffy/Select"); //->测试
XAudioMgr.singleton.PlaySound(XEntityMgr.singleton.CombatPlayer, AudioChannel.Motion, XAudioStateDefine.XState_Audio_Move);
float interval = GetStepInvalTime(XEntityMgr.singleton.CombatPlayer);
m_moveTrack1 = XTimerMgr.singleton.SetTimer(interval, m_playMoveSoundHandler, null);
XDebug.singleton.AddGreenLog("play move sound interval=" + interval);
}
循环播放,那么什么时候停止播放呢?
public void OnStateChange(ulong id,int state)
{
XStateType eStage = (XStateType)(state);
switch (eStage)
{
case XStateType.Move:
if (XEntityMgr.singleton.CombatPlayer.ID == id)
{
float inval = GetStepInvalTime(XEntityMgr.singleton.CombatPlayer);
if (m_moveTrack1 == 0) m_moveTrack1 = XTimerMgr.singleton.SetTimer(inval * 0.5f, m_playMoveSoundHandler, null);
}
break;
default:
if (XEntityMgr.singleton.CombatPlayer.ID == id)
{
if (m_moveTrack1 != 0) XTimerMgr.singleton.KillTimer(m_moveTrack1);
m_moveTrack1 = 0;
XDebug.singleton.AddGreenLog("stop move sound");
}
break;
}
}
可以看到再切换到idle的时候,就kill掉定时器,然后就不会再循环播放跑步声音了。
16、今天遇到一个bug,是停不了声音的问题。
public override void OnGraphStart (Playable playable)
{
if(_runtimeFmod == null) //如果不判断空,那么此时会重新申请一个fmod,那么前面的fmod会丢失,导致不能停止声音。
{
_runtimeFmod = XRuntimeFmod.GetFMOD();
}
}
17、遇到timeline中,重复播放声音的问题。
当函数OnBehaviourPlay反复进入的时候时候,会导致声音播放。这里加了个保护,就是讲播放的音频,如果
已经播放了 ,则不再播放, fpa.isExecuted = true; 设置为已经播放。
public override void OnBehaviourPlay(Playable playable, FrameData info)
{
var fpa = curveAsset;
if (_runtimeFmod != null && fpa != null && !fpa.isExecuted)
{
fpa.isExecuted = true;
_runtimeFmod.StartEvent (fpa.clip, AudioChannel.Action, true);
RTimeline.singleton.RegistFmod(fpa.clip, _runtimeFmod);
}
}
}
18、今天遇到一个问题,就是在timeline里面,有一个qte的按钮,如果点击了qte按钮,则瞬间切换到第二段音乐。
但是此时和最后的动作声音不匹配了。
最终的原因是,跳转的时候,跳转到了到switch=2的下一个时间点了,导致动作被截断了一部分,所以导致,动作播放完毕,声音还在的情况。
解决的方法是,切换声音的时候,跳转的时间点要一致才行。
这里从第一个signal,直接跳转到了后面的那个紫色的箭头了。此时音乐是切换了,但是动作则是被阶段了。所以要第二个signal和紫色箭头的跳转时间,重叠即可。
19、速度和音量大小的控制
控制速度的接口:
m_emitter.EventInstance.setPitch(0.1f);
控制音量的接口:
FMOD.RESULT x = m_emitter.EventInstance.setVolume(0.2f);
20、控制event不要刷新:
const int FilePollTimeSeconds = 5; //fmod默认是每隔5秒就刷新,所以会导致git不能拉取远程的故,可以设置一个变量,在编辑器下控制这个变量。让其不要自动刷新。
21、不运行unity,在编辑器下,预览fmod的声音
这个使用StudioEventEmitter组件,直接在获取使其播放。
如下:
[ExecuteInEditMode]
public class NewBehaviourScript : MonoBehaviour
{
public StudioEventEmitter emitter;
private void OnEnable()
{
emitter.Play();
}
private void OnDisable()
{
emitter.Stop();
}
}
这个这个在不运行unity的时候,隐藏和显示物体,会触发播放和停止。
但是不会正确播放声音,还需要一个update的方法。
这个脚本挂在一个物体上,使其一直执行Update方法,让其调用:FMODUnity.RuntimeManager.StudioSystem.update()方法,这样才能在非运行的状态下,播放和停止声音。
可以参考:fmod提供的那个timeline的track方法: