Get instance from ChannelControl in SYSTEM_CALLBACK_TYPE.ERROR

I have registered a callback to get errors that looks like this and im trying to get the name of the thing to debug it but CHANNELGROUP is an interface and i cant figure out a way to determine the underlying type since all i have is the raw pointer.

        errorCallback = new FMOD.SYSTEM_CALLBACK(ERROR_CALLBACK);
        var result = system.setCallback(errorCallback, FMOD.SYSTEM_CALLBACK_TYPE.ERROR);

        private static FMOD.RESULT ERROR_CALLBACK(IntPtr system, FMOD.SYSTEM_CALLBACK_TYPE type, IntPtr commanddata1, IntPtr commanddata2, IntPtr userdata)
        {
            FMOD.ERRORCALLBACK_INFO callbackInfo = Marshal.PtrToStructure<FMOD.ERRORCALLBACK_INFO>(commanddata1);

            if(callbackInfo.instancetype == ERRORCALLBACK_INSTANCETYPE.CHANNELGROUP)
            {
                // get the name or other identifier of the callbackInfo.instance
            }
        }

We provide parameterized constructors which you can use to wrap an IntPtr in its corresponding type. i.e

if (callbackInfo.instancetype == ERRORCALLBACK_INSTANCETYPE.CHANNELGROUP)
{
    FMOD.ChannelGroup cg = new FMOD.ChannelGroup(callbackInfo.instance);
    cg.getName(out string name, 256);
}
else if (callbackInfo.instancetype == ERRORCALLBACK_INSTANCETYPE.CHANNEL)
{
    FMOD.Channel c = new FMOD.Channel(callbackInfo.instance);
    channel.getName(out string name, 256);
}

Ok but what do i do if it is an CHANNELCONTROL which is an IChannelControl interface in c#?

I am not sure what an IChannelControl is, it doesn’t appear to be part of the FMOD Unity Integration that we ship, but I think I see what you mean- how do you take the instance pointer for a ChannelControl and convert it into either the Channel or ChannelGroup.
If that is what you are after, you can do a similar thing to what I mentioned above but test the conversion from IntrPtr to the corresponding type with a successful API call:

if (callbackInfo.instancetype == FMOD.ERRORCALLBACK_INSTANCETYPE.CHANNELCONTROL)
{
    FMOD.Channel c = new FMOD.Channel(callbackInfo.instance);
    if (c.getCurrentSound(out FMOD.Sound sound) == FMOD.RESULT.OK)
    {
        sound.getName(out string soundName, 256);
        Debug.Log("soundName: " + soundName);
    }
    FMOD.ChannelGroup cg = new FMOD.ChannelGroup(callbackInfo.instance);
    if (cg.getName(out string cgName, 256) == FMOD.RESULT.OK)
    {
        Debug.Log("cgName: " + cgName);
    }
}

I tried doing that but all i get out is garbage like a “�” or some random characters such as “xzK7”. Also it seems like both often return the same string.

The IChannelControl is in
Assets/Plugins/FMOD/src/fmod.cs:2064
and it is implemented by Channel and ChannelGroup.
Im guessing its just a straight port of the abstract base class thats used in c++ which is not really useful in c# since there seem to be no way to check the pointer type.

There is also a channelcontrol_type but that only seems to be used for a CHANNELCONTROL_CALLBACK.

Also seems strange to me that the callback returns the abstract type instead of the actual type.

Thanks, I have no idea why I couldn’t find this when searching previously.
Not sure why we have IChannelControl either, outside of maybe for composite structures- but yes it’s probably just mirroring the C++ API.

Can you please tell me the value of each callbackInfo member during one of these garbage name logs, and send me an updated snippet of your callback?

Here are two examples of results and the code I use. I remove the execution paths that arent taken for brevity.

I dont get any ok results so it makes sense that I get garbage out but if I cant get the sound of the ChannelGroup because it was stolen, its not a very useful instance. What I actually want is both the stolen and the thief as that would make it easier to debug.

 FMOD.ERRORCALLBACK_INFO
functionname "ChannelControl::stop" nativeUtf8Ptr 0x7ffe21a5ce88
functionparams "" nativeUtf8Ptr 0xf4323f030
instance 0x203c0041
instancetype CHANNELCONTROL
result ERR_CHANNEL_STOLEN

channel.getCurrentSound ERR_CHANNEL_STOLEN
sound.getName ERR_INVALID_PARAM
sound description "c:\game\x.sourcegen.componentaccessgenerator\x.sourcegen.componentaccessgenerator.componentmanagergeneration.generator\temp\generatedcode\y.shared\gameplayconditionchecker__componentmanagergeneration_sls_795.g.cs"

channelGroup.getName ERR_INVALID_HANDLE
channelGroup description "event:/AMB/Local/Amb_Local/Fire/AMB_Local_Fire_Torch_Generic_Loop_FILTER"

-------------
 FMOD.ERRORCALLBACK_INFO
functionname "ChannelControl::stop" nativeUtf8Ptr 0x7ffe21a5cc40
functionparams "" nativeUtf8Ptr 0xf4323efe0
instance 0x203c0041
instancetype CHANNELCONTROL
result ERR_CHANNEL_STOLEN

channel.getCurrentSound ERR_CHANNEL_STOLEN
sound.getName ERR_INVALID_PARAM 
sound description "" <-- square is ETX

channelGroup.getName ERR_INVALID_HANDLE 
channelGroup description "xq\t��" <-- square is SOH

--------------

The code for getting the data above

[AOT.MonoPInvokeCallback(typeof(FMOD.SYSTEM_CALLBACK))]
private static FMOD.RESULT ERROR_CALLBACK(IntPtr system, FMOD.SYSTEM_CALLBACK_TYPE type, IntPtr commanddata1, IntPtr commanddata2, IntPtr userdata)
{
    FMOD.ERRORCALLBACK_INFO callbackInfo = Marshal.PtrToStructure<FMOD.ERRORCALLBACK_INFO>(commanddata1);

    var func = (string)callbackInfo.functionname;
    var param = (string)callbackInfo.functionparams;
    var instancetype = callbackInfo.instancetype;
    var instance = callbackInfo.instance;

    var description = "";
    const int NAME_MAX_LENGTH = 256;
    switch (instancetype)
    {
        case FMOD.ERRORCALLBACK_INSTANCETYPE.CHANNELCONTROL:
        {
            FMOD.Channel channel = new FMOD.Channel(instance);
            var result1 = channel.getCurrentSound(out FMOD.Sound sound);
            var result2 = sound.getName(out description, NAME_MAX_LENGTH);

            FMOD.ChannelGroup channelGroup = new FMOD.ChannelGroup(instance);
            var result3 = channelGroup.getName(out description, NAME_MAX_LENGTH);
            break;
        }
    }
}

My actual code looks like this

switch (instancetype)
{
    case FMOD.ERRORCALLBACK_INSTANCETYPE.CHANNELCONTROL:
    {
        var channel = new FMOD.Channel(instance);
        if (channel.getCurrentSound(out FMOD.Sound sound) == FMOD.RESULT.OK)
        {
            var result = sound.getName(out description, NAME_MAX_LENGTH);
            return result == FMOD.RESULT.OK;
        }
        else
        {
            FMOD.ChannelGroup channelGroup = new FMOD.ChannelGroup(instance);
            var result = channelGroup.getName(out description, NAME_MAX_LENGTH);
            return result == FMOD.RESULT.OK;
        }
    }
}

While we are on the topic of stolen audio logs, we also have another debug log thats set up like this

var debugCallback = new FMOD.DEBUG_CALLBACK(DEBUG_CALLBACK);
var result = FMOD.Debug.Initialize(fmodSettings.LoggingLevel, FMOD.DEBUG_MODE.CALLBACK, debugCallback, null);

[AOT.MonoPInvokeCallback(typeof(FMOD.DEBUG_CALLBACK))]
private static FMOD.RESULT DEBUG_CALLBACK(FMOD.DEBUG_FLAGS flags, IntPtr filePtr, int line, IntPtr funcPtr, IntPtr messagePtr)
{
    FMOD.StringWrapper funcWrapper = new FMOD.StringWrapper(funcPtr);
    FMOD.StringWrapper messageWrapper = new FMOD.StringWrapper(messagePtr);

    string func = (string)funcWrapper;
    string message = (string)messageWrapper;

    RuntimeUtils.DebugLog($"[FMOD] {func} : {message}", flags);

    return FMOD.RESULT.OK;
}

And the message looks like this
"[FMOD] ChannelI::updateVirtualState : Channel 0x23EC002D Stolen with audibility 0.180135"

Is there any way to get that pointer and instancetype without having to parse the string?

Okay in the context of channel stealing I can see why you wouldn’t be getting a workable Channel from the instance pointer.

We don’t have a way to get the pointer or instance type during a DEBUG_CALLBACK, that callback is really just intended for passing you a formatted string message to give to your own logging system.

If you want more details on the now destroyed Channel, you could potentially retrieve it ahead of time and store it in a dictionary, using the instance pointer to look up the corresponding sound name in the error callback. I acknowledge though that this is an unreasonable amount of effort just to figure out which sound has disappeared, and this is pretty clearly a limitation with our logging- I will suggest to the Dev team that we add some information on the stolen Sound name either in that updateVirtualState log or in the ERRORCALLBACK_INFO.

In the meantime, the recommended way to debug voice stealing is to use the FMOD Studio Profiler. You might not be able to easily figure out which Channel stole a voice from which other Channel, but with the Lifespans graph you’ll be able to see which voices are dropping in and out, which is probably the next best things.

:+1:

Thats really useful, i had done some profiling but i missed the lifetime option.
However some of what im seeing is not really explained well in lifetime link you provided.


Im guessing the bright line is the playing event and the faded lines are virtualized?

image
Whats the difference between a solid line and dotted line (referenced?)

image
Also im guessing the left and right “cap” of the line is just there to show start/stop?

image
And that this is just the dotted version of the start/stop “caps”?

I tried this but i cant manage to connect the two ends.

When i send the event i have the event instance and from that i can get a ChannelGroup that has 0 getNumChannels and then when i get the error i get a Channel that i cant get the group or sounds from since it has been stolen.

Correct in all cases, faded lines are virtual, dotted lines are referenced, and the caps are for start/stop points, with different stylings for referenced and non-referenced event instances. There are also arrows which can have multiple meanings, such as when the event enters the “Stopping” state but hasn’t yet stopped:
image

When sample data hasn’t yet loaded:
image

And when sample data has finished loading:
image

Channels aren’t created immediately, and events can own various different Channels over their lifetime, so you would need to be constantly updating this hypothetical map of events and their Channels. Additionally, the Core API layout of Channels and ChannelGroups can be a little surprising. The ChannelGroup returned from EventInstance::getChannelGroup is the event’s master track, so if you have multiple tracks in your event you will need to query the master track’s sub groups. Here is an example that recursively builds up a map of all Channels and their corresponding Sound names for debug purposes:

    Dictionary<int, string> channelNames = new Dictionary<int, string>();

    void AddChannels(ChannelGroup cg)
    {
        cg.getNumChannels(out int numChannels);
        for (int i = 0; i < numChannels; i++)
        {
            cg.getChannel(i, out Channel channel);
            channel.getCurrentSound(out Sound sound);
            sound.getName(out string name);

            int key = channel.handle.ToInt32();
            channelNames[key] = name;
        }
    }

    void AddGroupChannels(ChannelGroup cg)
    {
        cg.getNumGroups(out int numGroups);
        for (int i = 0; i < numGroups; i++)
        {
            cg.getGroup(i, out ChannelGroup subgroup);
            AddGroupChannels(subgroup);
        }
        AddChannels(cg);
    }
...
    // Call during update
    FMODUnity.RuntimeManager.CoreSystem.getMasterChannelGroup(out FMOD.ChannelGroup mCG);
    AddGroupChannels(mCG);

This doesn’t necessarily scale well as you would be traversing the entire channel hierarchy every update, but for debug purposes while we look into improving the logging it might help.

Thanks, that helps a lot, i would not have guessed thats the way to do it.

It seems to work for the most part, the only problem is that some of the stolen errors seem to happen when i call eventInstance.start which means that it hassent had time to cache the channels yet but it is a lot better than nothing.

It might be a tall order but it would be nice if the debug callbacks included the callstack so you can see where it was in the c# code when it triggered the error, right now it just cuts of before the callback, i assume because the call comes from the c++ side.

Yeah we should really have an explainer somewhere on how an EventInstance is structured in terms of Channels and ChannelGroups, I will write up a task for that as well.

That would be the optimization to prevent a Channel from starting at all if its audibility is too low to steal from an existing voice. I can’t think of any way to identify those voices using the current API unfortunately.

By default, the API calls you make into FMOD are pushed into a command buffer that are executed asynchronously by the Studio Update Thread, so the call stack of any log would be exclusively in FMOD and you wouldn’t be able to see the C# caller.
However, if you add FMOD.Studio.INITFLAGS.SYNCHRONOUS_UPDATE to the INITFLAGS in RuntimeManager.Initialize (Assets\Plugins\FMOD\src\RuntimeManager.cs line ~316) the Studio Update Thread won’t be created and the command will execute immediately, so you will be able to see the C# caller. For debugging this could be useful, but I probably wouldn’t leave it in for release as it would mean every API call blocks the main thread while executing.