Set callback on an already playing EventInstance

Hi everyone!

I have an event instance that is playing between scenes, music starts in scene A, in scene B I want to listen to the music’s beat callback. Is this possible?

I don’t want to stop the music in scene A and then start it again in scene B, it abrupts the flow.

Context:
I have a section in my game where the player is outside a rave, when the player enters this scene I start playing the rave music but with a low pass filter controlled by a parameter.

In the rave scene the strobes, bears, and the elk DJ should bump to the beat, I want to listen for the music’s beat callback in the rave scene.

To clarify, we know what to do when a callback is established, we need to set the callback to an already playing event.

Also, when we get this to work, do we need to unsubscribe to the callback somehow so the callback is not run when the player is outside the rave?

So I have managed to hook up the callback to the already playing event by doing this:

void ListenForCallback()
{
	var playingAmbience = AudioManager.GetAmbienceByName(ambienceToListenFor.fmodName);

	// 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
	playingAmbience.eventInstance.setUserData(GCHandle.ToIntPtr(timelineHandle));
	
	var result = playingAmbience.eventInstance.setCallback(beatCallback, FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_BEAT | FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_MARKER);
	if(result != FMOD.RESULT.OK) { Debug.Log("Error setting callback."); }
}

When I then changed scene I got this

ArgumentException: GCHandle value belongs to a different domain
  at System.Runtime.InteropServices.GCHandle.op_Explicit (System.IntPtr value) [0x00025] in <fb001e01371b4adca20013e0ac763896>:0 
  at System.Runtime.InteropServices.GCHandle.FromIntPtr (System.IntPtr value) [0x00000] in <fb001e01371b4adca20013e0ac763896>:0 
  at AmbienceCallbackResponse.BeatEventCallback (FMOD.Studio.EVENT_CALLBACK_TYPE type, System.IntPtr instancePtr, System.IntPtr parameterPtr) [0x0004d] in D:\Github\TOEM\Assets\AmbienceCallbackResponse.cs:88 
  at (wrapper managed-to-native) FMOD.Studio.System.FMOD_Studio_System_Update(intptr)
  at FMOD.Studio.System.update () [0x00001] in D:\Github\TOEM\Assets\Plugins\FMOD\src\Runtime\wrapper\fmod_studio.cs:418 
  at FMODUnity.RuntimeManager.Update () [0x00318] in D:\Github\TOEM\Assets\Plugins\FMOD\src\Runtime\RuntimeManager.cs:493 

I fixed that by doing this on my callback listener script on scene B

private void OnDestroy()
{
	var playingAmbience = AudioManager.GetAmbienceByName(ambienceToListenFor.fmodName);
	playingAmbience.eventInstance.setUserData(IntPtr.Zero);
	
	if(timelineHandle.IsAllocated)
		timelineHandle.Free();
}

So now I’m my the point where I’ve ended up with this error…

NullReferenceException: Object reference not set to an instance of an object
  at (wrapper dynamic-method) System.Object.UnityEditor.AudioFilterGUI.call_DrawAudioFilterGUI(object,UnityEngine.MonoBehaviour)
  at (wrapper managed-to-native) FMOD.Studio.System.FMOD_Studio_System_Update(intptr)
  at FMOD.Studio.System.update () [0x00001] in D:\Github\TOEM\Assets\Plugins\FMOD\src\Runtime\wrapper\fmod_studio.cs:418 
  at FMODUnity.RuntimeManager.Update () [0x00318] in D:\Github\TOEM\Assets\Plugins\FMOD\src\Runtime\RuntimeManager.cs:493 
 
(Filename: Assets/Plugins/FMOD/src/Runtime/wrapper/fmod_studio.cs Line: 418)

Any insights or solutions are welcome :heart:

I’m on my phone right now but from reading this I’d say you at least need to set the callback of your event to null in the OnDestroy function as well. Not sure that will solve your current error but it will at least avoid additional buggy behaviour!

This should be possible using Object.DontDestroyOnLoad on the object that is playing the event and has the callback code.

Yeah, I’ve gotten everything to work, cheers guys!

The event is playing using our AudioManager which is using DontDestroyOnLoad, I’m just hooking into its callback like this.

public class AmbienceCallbackResponse : MonoBehaviour
{
[SerializeField] AudioReference ambienceToListenFor;
[SerializeField] UnityEvent onBeat;
[SerializeField] UnityEvent onMarker;

FMOD.Studio.EVENT_CALLBACK beatCallback;

// 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 FMOD.StringWrapper lastMarker = new FMOD.StringWrapper();
}

TimelineInfo timelineInfo;
GCHandle timelineHandle;

bool shouldTriggerOnBeat;
bool shouldTriggerOnMarker;

private void OnDestroy()
{
    var playingAmbience = AudioManager.GetAmbienceByName(ambienceToListenFor.fmodName);
    var result = playingAmbience.eventInstance.setUserData(IntPtr.Zero);
    if (result != RESULT.OK)
    {
        Debug.LogError(result.ToString());
    }
    result = playingAmbience.eventInstance.setCallback(null);
    if (result != RESULT.OK)
    {
        Debug.LogError(result.ToString());
    }
    
    if(timelineHandle.IsAllocated)
        timelineHandle.Free();
}

void Start()
{
    // 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);
    timelineInfo = new TimelineInfo();
    ListenForCallback();
}

private void LateUpdate()
{
    if (shouldTriggerOnBeat)
    {
        shouldTriggerOnBeat = false;
        onBeat?.Invoke();
    }

    if (shouldTriggerOnMarker)
    {
        shouldTriggerOnMarker = false;
        onMarker?.Invoke();
    }
}

void ListenForCallback()
{
    var playingAmbience = AudioManager.GetAmbienceByName(ambienceToListenFor.fmodName);

    // 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
    playingAmbience.eventInstance.setUserData(GCHandle.ToIntPtr(timelineHandle));
    
    var result = playingAmbience.eventInstance.setCallback(beatCallback, FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_BEAT | FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_MARKER);
    if(result != FMOD.RESULT.OK) { Debug.Log("Error setting callback."); }
}

/* THIS EVENT USED TO BE STATIC! MIGHT BE MESSING WITH THINGS! */
[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
FMOD.RESULT BeatEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
{
    FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr);

    // Retrieve the user data
    IntPtr timelineInfoPtr;
    FMOD.RESULT result = instance.getUserData(out timelineInfoPtr);
    if (result != FMOD.RESULT.OK)
    {
        Debug.LogError("Timeline Callback error: " + result);
    }
    else if (timelineInfoPtr != IntPtr.Zero)
    {
        // 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));
                timelineInfo.currentMusicBar = parameter.bar;
                
                shouldTriggerOnBeat = true;
                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;
                
                shouldTriggerOnMarker = true;
                break;
            }
        }
    }
    return FMOD.RESULT.OK;
}

}