k-hole is the game development and design blog of kyle kukshtel
subscribe to my newsletter to get posts delivered directly to your inbox

FMOD Crashing with Programmer sound callback is not set for instrument error


Debugging some FMOD interop stuff

March 1, 2023 Programming C# FMOD

FMOD - Asset Store

Debugging FMOD Callback Issues Crashing Unity

UPDATE: New fix at the end of the post

I had a weird issue in FMOD recently that I was struggling to find any information on so just capturing it here in hopes to help other people that run into something similar.

I recently set up callbacks in Unity for FMOD event instances. The code was much simpler that I thought it would be, and consisted of basically just creating a delegate of the correct type and passing that to the event instance. Here’s the full code:

FMOD.Studio.EVENT_CALLBACK FMODCallback;
public void InitializeAudio() {
    try {
        FMODInstance = RuntimeManager.CreateInstance(Sound.FMODPath);
        FMODCallback = FMODEventCallback;
        FMODInstance.setCallback(FMODCallback);
    }
    catch (Exception) {
        FMODInstance = RuntimeManager.CreateInstance(GameManager.AudioEvents.EMPTY.FMODPath);
    }
}

[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
FMOD.RESULT FMODEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr _event, IntPtr parameterPtr) {
    switch (type)
    {
        case FMOD.Studio.EVENT_CALLBACK_TYPE.SOUND_STOPPED:
        case FMOD.Studio.EVENT_CALLBACK_TYPE.STOPPED:
            if(State != AudioState.STOPPED) {
                Debug.Log($"{Name} pool item was stopped internally, marking as stopped");
                State = AudioState.STOPPED;
            }
            break;
    }
    return FMOD.RESULT.OK;
}

This worked fine for it’s normal use case. The reason I implemented callbacks was because we maintain a shim object that maps to internal FMOD instances to allow for a better API, but there were some things that would happen in FMOD independent to our own instance handing that we wanted to listen to (like audio stopping). So now when audio stops internally in FMOD, we just make sure our State matches the latest. Cool. This worked.

But when I was switching scenes, the game would crash.

NullReferenceException: Object reference not set to an instance of an object
  at Cantata.AudioPoolItem.FMODEventCallback (FMOD.Studio.EVENT_CALLBACK_TYPE type, System.IntPtr _event, System.IntPtr parameterPtr) [0x00001] in C:\Users\kyle\Workspace\cantata\Assets\Scripts\Audio\AudioPoolItem.cs:97

Line 97 here was the if statement in the STOPPED case.

I was also seeing these weird errors:

[FMOD] ShadowEventInstance::createProgrammerSound : Programmer sound callback is not set for instrument ''.

[FMOD] EventInstance::createProgrammerSoundImpl : Programmer sound callback for instrument '' returned no sound.

I was stumped. State is an enum so literally couldn’t be null. Inside the if was just a debug statement and an assignment. What was nulling out?


So some context on when I was switching scenes. The issue that was happening was when a user tried to load a save inside an existing game, the game would crash. Loading fresh saves from the main menu was fine. So what was special about loading from inside a game?

Well for audio, the main thing we do is destroy all instances of audio we created for that match (so as to not persist them across matches). This has always worked prior to us doing a callback, but now them nulling out meant something was wonk with callback stuff. I looked around online and found basically nothing and was honestly almost stumped. What was even possible to be null?

Well…

See this line here?

[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]

This isn’t really documented in FMOD but using my own knowledge I have a pretty good guess - basically its a way to grab the function and bind it as a callback so as not to get stripped during app trimming for AOT compilation. I think. However, knowing that FMOD was needing to PInvoke for callbacks to its own native layer and thinking about that boundary got me thinking about the state” of things that are possible. The managed layer in FMOD just holds a pointer to the native object, and we’re not literally cleaning up the native object (afaik you can’t even do that - you can release the pointer but that’s it).

So what if something happened where the native object and the managed object fell out of sync? Like, say, maybe if the managed object was GC’d after being destroyed for a scene switch, but FMOD still wanted to call back to that object after its native instance was destroyed? That could maybe result in a null reference right? Maybe?

But again a function itself can’t be null in that sense. So what’s the issue?

Well here’s the code that works without crashing:

[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
FMOD.RESULT FMODEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr _event, IntPtr parameterPtr) {
    switch (type)
    {
        case FMOD.Studio.EVENT_CALLBACK_TYPE.SOUND_STOPPED:
        case FMOD.Studio.EVENT_CALLBACK_TYPE.STOPPED:
            if(State != AudioState.STOPPED) {
                State = AudioState.STOPPED;
            }
            break;
    }
    return FMOD.RESULT.OK;
}

Do you even see the difference?

All I did was remove this line:

Debug.Log($"{Name} pool item was stopped internally, marking as stopped");

What? So here’s my best guess.

What I think was happening was that the managed object was being cleared out by me (calling ReleaseInstance()). This worked as intended, but the instance callback function on the native side was still trying to hit the callback function with a pointer. The function was pinned somehow in memory so that it could still get hit, BUT the object instance the callback function was on” was null.

Because the object itself was null, the string (reference type!) variable Name was resolving null and crashing. Interestingly enough, State is also on the object, but since it’s an enum its a value type so is stored on the stack instead of the managed heap. I think!

Anyways, here’s the possible learning:

Don’t use reference type variables in the FMOD callback function.

Hope this helps people who run into something similar!


UPDATE:

So turns out my fix still wasn’t working in builds (but working in editor). I once again couldn’t figure out what wasn’t working. I searched around more for callback information, and found this useful thread that pointed out to these docs.

One thing that stood out to me was that their example callback function was static. So the function itself is pinned in memory instead of needing a possible GC’d object reference.

I changed my callback function to also be static, and bing bang boom, it works now. So here’s the better learning:

Make sure your callback is static.

Here’s the updated code. Only major change is needing to grab the event instance reference via the callback pointer, but it’s painless.

public void InitializeAudio()
{
    try
    {
        FMODInstance = RuntimeManager.CreateInstance(Sound.FMODPath);
        FMODInstance.setCallback(FMODEventCallback);
    }
    catch (Exception)
    {
        FMODInstance = RuntimeManager.CreateInstance(GameManager.AudioEvents.EMPTY.FMODPath);
    }
}

[AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
static FMOD.RESULT FMODEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr _event, IntPtr parameterPtr)
{
    var instance = new FMOD.Studio.EventInstance(_event);
    var poolItem = GameManager.AudioManager.GetPoolItemByInstance(instance);
    if(poolItem != null)
    {
        switch (type)
        {
            case FMOD.Studio.EVENT_CALLBACK_TYPE.SOUND_STOPPED:
            case FMOD.Studio.EVENT_CALLBACK_TYPE.STOPPED:
                if(poolItem.State != AudioState.STOPPED)
                {
                    poolItem.State = AudioState.STOPPED;
                }
                break;
        }
    }
    return FMOD.RESULT.OK;
}

Hope this is it! Thanks for reading.



Date
March 1, 2023




subscribe to my newsletter to get posts delivered directly to your inbox