Beat and Marker system with FMOD in Unity

When using FMOD, often we hear the question: how do I synchronize certain gameplay elements with the background music? Can we get information about the bars and beats of an FMOD event in Unity? The FMOD Docs provide a helpful example. In this tutorial, we’ll slightly change the example to make it usable in any game situation.

Download the Unity & FMOD project for this tutorial. If you get any errors after opening the project, please delete the FMODStudioCache.asset file, as it will still contain the old path to the FMOD Studio Project.

First, we need an FMOD event that has BPM and measure information. The information can be inserted with the right mouse button on the Timeline under Add Tempo Marker.

FMOD Event with BPM and measure information
FMOD Event with BPM and measure information

Optionally, we can also insert destination markers using the right mouse button and clicking on Add Destination Marker. These marker information can be retrieved in Unity as strings. This is especially useful if you want certain parts of the music to control something in the game.

Now we create a script called BeatSystem. We add the following lines:

using System;
using System.Runtime.InteropServices;
using UnityEngine;

class BeatSystem : MonoBehaviour
{
    [StructLayout(LayoutKind.Sequential)]
    class TimelineInfo
    {
        public int currentMusicBeat = 0;
        public FMOD.StringWrapper lastMarker = new FMOD.StringWrapper();
    }

    TimelineInfo timelineInfo;
    GCHandle timelineHandle;

    FMOD.Studio.EVENT_CALLBACK beatCallback;

    public static int beat;
    public static string marker;

    public void AssignBeatEvent(FMOD.Studio.EventInstance instance)
    {
        timelineInfo = new TimelineInfo();
        timelineHandle = GCHandle.Alloc(timelineInfo, GCHandleType.Pinned);
        beatCallback = new FMOD.Studio.EVENT_CALLBACK(BeatEventCallback);
        instance.setUserData(GCHandle.ToIntPtr(timelineHandle));
        instance.setCallback(beatCallback, FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_BEAT | FMOD.Studio.EVENT_CALLBACK_TYPE.TIMELINE_MARKER);
    }

    public void StopAndClear(FMOD.Studio.EventInstance instance)
    {
        instance.setUserData(IntPtr.Zero);
        instance.stop(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
        instance.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)
    {
        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)
        {
            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.currentMusicBeat = parameter.beat;
                        beat = timelineInfo.currentMusicBeat;
                    }
                    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;
                        marker = timelineInfo.lastMarker;
                    }
                    break;
            }
        }
        return FMOD.RESULT.OK;
    }
}

The method AssignBeatEvent() allows us to assign an instance that was actually created and started somewhere else to this beat system. If we want to stop the instance, we simply call StopAndClear(). In the FMOD example, the instance was created and started in the same script, which can cause confusion. After all, we want to decide for ourselves when and where to play our sounds.

To test this beat and marker system, we create a second script:

public class Music : MonoBehaviour
{
    private FMOD.Studio.EventInstance instance;
    private BeatSystem bS;

    void Start()
    {
        bS = GetComponent<BeatSystem>();     
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            instance = FMODUnity.RuntimeManager.CreateInstance("event:/music");
            instance.start();
            bS.AssignBeatEvent(instance);
        }

        if (Input.GetKeyDown(KeyCode.LeftControl))
        {
            bS.StopAndClear(instance);
        }
    }
}

At the beginning we declare the music instance and the Beatsystem script. In the Start() method, we use GetComponent() to access the beat system. In Unity’s Update() method we do the same as in the other tutorials: pressing the space bar creates and starts the instance, with bS.AssignBeatEvent(instance) we pass the instance to the beat system. The only important thing to remember is that we can now get the beat and marker information with the BeatSystem.beat and BeatSystem.marker variables. If we press the left control key, the instance is properly stopped together with the beat system.

In the attached project I created a canvas with two text elements to visualize the information:

FMOD Studio Update 2.1 changes

You’ll encounter errors due to changes in the new FMOD Studio 2.1 update. To fix those replace the parameter FMOD.Studio.EventInstance instance in BeatEventCallback with IntPtr instancePtr. Add FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr); direct below in the static method to make it work.

↑ To the Top