Help with multiple TIMELINE_MARKER callbacks in same scene

Hi everyone!

I’m trying to have two objects react to two different FMOD Event timeline markers, currently, I can only have one as they seem to override each other. If just 1 of the 2 things are active at the same time everything works great so there’s nothing wrong with the FMOD Studio setup in my opinion, it’s something to do with my code.

Here’s one event:

And here’s the other:

Here is the code which the two objects in the scene use:

using UnityEngine;
using System.Runtime.InteropServices;
using System;
using Audio;
using FMOD;
using FMOD.Studio;
using UnityEngine.Events;
using FMODUnity;
using Debug = UnityEngine.Debug;

public class TimelineMarkerResponse : MonoBehaviour
{
    [SerializeField] bool runOnStart = true;
    
    [SerializeField] AudioReference audioReference; <---- This is just a ScriptableObject with a FMOD string inside it
    [SerializeField] UnityEvent OnBeat;
    [SerializeField] UnityEvent OnMarker;

    [SerializeField] private TimelineMarkerReactor[] reactors;
    private bool hasReactors;
    
    // Variables that are modified in the callback need to be part of a separate class.
    // This class needs to be 'blittable' otherwise it can't be pinned in memory.
    [StructLayout(LayoutKind.Sequential)]
    protected class TimelineInfo
    {
        public int currentMusicBar = 0;
        public StringWrapper lastMarker = new StringWrapper();
    }
    
    protected TimelineInfo timelineInfo;
    GCHandle timelineHandle;
    
    EVENT_CALLBACK beatCallback;
    protected EventInstance musicInstance;

    static bool shouldTriggerOnBeat;
    static bool shouldTriggerOnMarker;

    private bool isPlaying;
    protected virtual void Start()
    {
#if UNITY_EDITOR || DEVELOPMENT_BUILD
        if (FindObjectsOfType<TimelineMarkerResponse>().Length > 1)
        {
            Debug.Log("There are more than 1 TimelineMarkerResponse in current scene, make sure nothing is fucking up :)");
        }
#endif

        hasReactors = reactors != null && reactors.Length > 0;
        
        // 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 EVENT_CALLBACK(BeatEventCallback);
        timelineInfo = new TimelineInfo();
        
        if(runOnStart) StartAudio();
    }

    private void LateUpdate()
    {
        if (!isPlaying)
            return;
        
        if (shouldTriggerOnBeat)
        {
            if (hasReactors)
            {
                for (int i = 0; i < reactors.Length; i++)
                {
                    reactors[i].OnBeatTriggered();
                }
            }
            
            OnBeatTrigger();
        }

        if (shouldTriggerOnMarker)
        {
            // If this TimelineMarkerResponse has other "children" that should be triggered
            if (hasReactors)
            {
                for (int i = 0; i < reactors.Length; i++)
                {
                    // If they must match a particular name then pass in the last marker's name that will then be checked.
                    if (reactors[i].markerMustMatchName)
                    {
                        string lastMarkerName = timelineInfo.lastMarker;
                        reactors[i].OnMarkerMustMatch(lastMarkerName);
                    }
                    else
                    {
                        reactors[i].OnMarkerTriggered();
                    }
                }
            }

            OnMarkerTrigger();
        }
    }

    protected virtual void OnBeatTrigger()
    {
        shouldTriggerOnBeat = false;
        OnBeat?.Invoke();
    }

    protected virtual void OnMarkerTrigger()
    {
        shouldTriggerOnMarker = false;
        OnMarker?.Invoke();
        Debug.LogError($"Marker from {name}: {timelineInfo.lastMarker.ToString()}");
    }

    private void OnDisable()
    {
        StopAudio();
    }

    void OnDestroy()
    {
        StopAudio();
    }

    public void StopAudio()
    {
        DeAttachEventFromCallback(ref musicInstance);
        isPlaying = false;
    }
    
    public virtual void StartAudio()
    {
        // Cleanup old stuff if something is restarted
        if (isPlaying)
            StopAudio();

        musicInstance = AudioManager.CreateEventInstance(audioReference);
        musicInstance.set3DAttributes(transform.position.To3DAttributes());
        isPlaying = true;

        if (AttachCallbackToEvent(ref musicInstance) != RESULT.OK)
        {
            return;
        }

        musicInstance.start();
        musicInstance.release();
    }
    
    public void DeAttachEventFromCallback(ref EventInstance instance)
    {
        if (instance.isValid())
        {
            instance.setUserData(IntPtr.Zero);
            instance.stop(STOP_MODE.ALLOWFADEOUT);
            instance.setCallback(null);
            instance.release();
        }

        if (timelineHandle.IsAllocated)
            timelineHandle.Free();
    }
        
    public RESULT AttachCallbackToEvent(ref EventInstance instance)
    {
        // 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
        instance.setUserData(GCHandle.ToIntPtr(timelineHandle));
        
        var result = instance.setCallback(beatCallback, EVENT_CALLBACK_TYPE.TIMELINE_BEAT | EVENT_CALLBACK_TYPE.TIMELINE_MARKER);
        if (result != RESULT.OK)
        {
            Debug.Log("Error setting callback.");
            OnDestroy();
        }
        else
        {
            isPlaying = true;
        }

        return result;
    }

    [AOT.MonoPInvokeCallback(typeof(EVENT_CALLBACK))]
    static RESULT BeatEventCallback(EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
    {
        EventInstance instance = new EventInstance(instancePtr);

        // Retrieve the user data
        RESULT result = instance.getUserData(out var timelineInfoPtr);
        if (result != 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 EVENT_CALLBACK_TYPE.TIMELINE_BEAT:
                {
                    var parameter = (TIMELINE_BEAT_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(TIMELINE_BEAT_PROPERTIES));
                    timelineInfo.currentMusicBar = parameter.bar;
                    
                    shouldTriggerOnBeat = true;
                    break;
                }
                case EVENT_CALLBACK_TYPE.TIMELINE_MARKER:
                {
                    var parameter = (TIMELINE_MARKER_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(TIMELINE_MARKER_PROPERTIES));
                    timelineInfo.lastMarker = parameter.name;
                    
                    shouldTriggerOnMarker = true;
                    break;
                }
            }
        }
        return RESULT.OK;
    }
}

How can I make them work independently from each other?
Cheers! :slight_smile:

It’s a bit hard to tell from your code, but callbacks should be able to get information back from separate events. I’ve tested with a small Unity project on our end. Are you able to post a small reproduction Unity project using the script you provided (plus any dependencies)?