Thank you Cameron.
Unity is on 2018.1.1f1 (64bit)
and FMOD 1.10.05
The file is edited but just invoking a callback and I’m passing the emitter EventInstance → I assume the error comes from there. Any tipp how to avoid this ?
using System;
using System.Runtime.InteropServices;
using UnityEngine;
class ScriptUsageTimeline : MonoBehaviour
{
// Variables that are modified in the callback need to be part of a seperate class.
// This class needs to be ‘blittable’ otherwise it can’t be pinned in memory.
[StructLayout(LayoutKind.Sequential)]
class TimelineInfo
{
public int currentMusicBar = 0;
public int currentMusicBeat = 0;
public FMOD.StringWrapper lastMarker = new FMOD.StringWrapper();
}
TimelineInfo timelineInfo;
GCHandle timelineHandle;
FMOD.Studio.EVENT_CALLBACK beatCallback;
FMOD.Studio.EventInstance musicInstance;
public FMODUnity.StudioEventEmitter battleMusicEmitter;
static Battle battleScript;
void Start()
{
timelineInfo = new TimelineInfo();
battleScript = GetComponent<Battle>();
// Explicitly create the delegate object and assign it to a member so it doesn't get freed
// by the garbage collected while it's being used
beatCallback = new FMOD.Studio.EVENT_CALLBACK(BeatEventCallback);
//musicInstance = FMODUnity.RuntimeManager.CreateInstance("event:/music/battles/hati_skol_battlemusic");
musicInstance = battleMusicEmitter.EventInstance;
// Pin the class that will store the data modified during the callback
timelineHandle = GCHandle.Alloc(timelineInfo, GCHandleType.Pinned);
// Pass the object through the userdata of the instance
musicInstance.setUserData(GCHandle.ToIntPtr(timelineHandle));
musicInstance.setCallback(beatCallback, FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_BEAT | FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_MARKER);
//musicInstance.start();
}
void OnDestroy()
{
musicInstance.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
musicInstance.release();
timelineHandle.Free();
}
[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
static FMOD.RESULT BeatEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, FMOD.Studio.EventInstance instance, IntPtr parameterPtr)
{
// Retrieve the user data
IntPtr timelineInfoPtr;
instance.getUserData(out timelineInfoPtr);
// Get the object to store beat and marker details
GCHandle timelineHandle = GCHandle.FromIntPtr(timelineInfoPtr);
TimelineInfo timelineInfo = (TimelineInfo)timelineHandle.Target;
switch (type)
{
case FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_BEAT:
{
var parameter = (FMOD.Studio.TIMELINE_BEAT_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.TIMELINE_BEAT_PROPERTIES));
if(parameter.bar != timelineInfo.currentMusicBar)
battleScript.TriggerPhase();
battleScript.BeatTest();
timelineInfo.currentMusicBar = parameter.bar;
timelineInfo.currentMusicBeat = parameter.beat;
//Debug.Log(String.Format("Current Bar = {0}, Last Marker = {1}", timelineInfo.currentMusicBar, (string)timelineInfo.lastMarker));
}
break;
case FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_MARKER:
{
var parameter = (FMOD.Studio.TIMELINE_MARKER_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.TIMELINE_MARKER_PROPERTIES));
timelineInfo.lastMarker = parameter.name;
Debug.Log(timelineInfo.lastMarker);
}
break;
}
return FMOD.RESULT.OK;
}
Thanks for your patience, it looks like there is a chance that callback will be called one last time as the FMOD system releases but after the GCHandle has already been freed.
By overwriting the musicInstances user data in the OnDestroy, we can avoid this.
So the problem is that this doesn’t always work the way you’d expect. In the example the event instance is managed, but when you are borrowing the instance from e.g. a studio event emitter it might be destroyed before you can set its userData to IntPtr.Zero (but still inexplicably trigger a callback, which was happening in my case).
May I suggest the example be updated with the following instead?
UserData userData;
try
{
var instance = new EventInstance(instancePtr);
instance.getUserData(out var userDataPtr);
var userDataHandle = GCHandle.FromIntPtr(userDataPtr);
userData = (UserData)userDataHandle.Target;
}
catch (Exception e)
{
// ArgumentException catches "GCHandle value belongs to a different domain"
// InvalidOperationException catches "GCHandle value cannot be Zero"
// The later is thrown when user data is not set: catch it if you don't want a simple mistake to force an editor freeze.
if (e is ArgumentException || e is InvalidOperationException)
{
return RESULT.OK;
}
throw e;
}
The try catch here is much safer in my opinion, as you’re not avoiding the symptom but providing the cure.
New tactic: since this threw another exception, and since it’s really annoying that these exceptions keep freezing the editor or play music continuously I opted for something even better instead:
try
{
// same as above
}
catch (Exception e)
{
Debug.LogError(e.Message);
return RESULT.OK;
}
This is the real fix: no blocking of editor, nice workflow, clear error messaging: beautiful.