Problem with loading multiple audio files to FMOD on demand

Hello, I’m using fmod for Unity.
What I’m trying to do is, download some random WAV or MP3 songs from the web and play a certain part of them on demand.

To do this,

  1. I’ve set up a programmer instrument like below and exported it to FMOD banks, then copied them to my Unity project.

  2. I’ve referred to this sample code and created these C# source files on my own.

<ProgrammerAudioManager.cs>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ProgrammerAudioManager : MonoBehaviour
{
    public ProgrammerSoundPlayer soundPlayer;

    public void PlayDownloadedSound(string filePath)
    {
        soundPlayer.PlaySound(filePath);
    }
}

<ProgrammerSoundPlayer.cs>

using System;
using UnityEngine;
using FMOD.Studio;
using FMODUnity;
using System.Runtime.InteropServices;

public class ProgrammerSoundPlayer : MonoBehaviour
{
    [SerializeField] private EventReference programmerSoundEvent;
    private EventInstance soundEvent;

    private void Start()
    {
        soundEvent = RuntimeManager.CreateInstance(programmerSoundEvent);
    }

    public void PlaySound(string filePath)
    {
        soundEvent.start();
        soundEvent.setCallback(ProgrammerSoundCallback, EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND);
        soundEvent.setUserData(GCHandle.ToIntPtr(GCHandle.Alloc(filePath)));
    }

    private FMOD.RESULT ProgrammerSoundCallback(EVENT_CALLBACK_TYPE type, IntPtr stringPtr, IntPtr parameters)
    {
        if (type != EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND)
            return FMOD.RESULT.OK;

        PROGRAMMER_SOUND_PROPERTIES properties = (PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameters, typeof(PROGRAMMER_SOUND_PROPERTIES));

        if (properties.sound != IntPtr.Zero)
        {
            FMOD.Sound sound = new FMOD.Sound(properties.sound);
            sound.release();
        }
        
        GCHandle handle = GCHandle.FromIntPtr(stringPtr);
        string filePath = handle.Target as string;

        FMOD.CREATESOUNDEXINFO exinfo = new FMOD.CREATESOUNDEXINFO();
        exinfo.cbsize = Marshal.SizeOf(typeof(FMOD.CREATESOUNDEXINFO));
        exinfo.length = 0;

        FMOD.Sound newSound;
        FMOD.RESULT result = RuntimeManager.CoreSystem.createSound(filePath, FMOD.MODE.LOOP_NORMAL | FMOD.MODE.CREATESTREAM, ref exinfo, out newSound);
        properties.sound = newSound.handle;
        properties.subsoundIndex = -1;
        Marshal.StructureToPtr(properties, parameters, false);

        handle.Free();

        return FMOD.RESULT.OK;
    }

    private void OnDestroy()
    {
        soundEvent.release();
    }
}

But when I just click on the play button, the Unity editor just freezes.
What am I doing wrong?

Sorry for the delayed response!

The main issue here is that you’re calling handle.Free() at the end of the CREATE_PROGRAMMER_SOUND callback, which for me appears to be causing an ArgumentException: GCHandle value belongs to a different domain error. This usually occurs when the GCHandle is attempted to be retrieved from an IntPtr after the handle has been freed.

The recommended way to handle this would be to encapsulate each callback type as we do in our Unity Programmer Sounds scripting example so that you can free the handle at the appropriate time - I’ve edited provided ProgrammerSoundPlayer to follow this:

using System;
using UnityEngine;
using FMOD.Studio;
using FMODUnity;
using System.Runtime.InteropServices;

public class ProgrammerSoundPlayer : MonoBehaviour
{
    [SerializeField] private EventReference programmerSoundEvent;
    private EventInstance soundEvent;
    private FMOD.Studio.EVENT_CALLBACK soundCallback;

    private void Start()
    {
        soundEvent = RuntimeManager.CreateInstance(programmerSoundEvent);
        soundEvent.set3DAttributes(this.transform.position.To3DAttributes());

        // 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
        soundCallback = new FMOD.Studio.EVENT_CALLBACK(ProgrammerSoundCallback);
    }

    public void PlaySound(string filePath)
    {
        soundEvent.start();
        soundEvent.setCallback(soundCallback);
        soundEvent.setUserData(GCHandle.ToIntPtr(GCHandle.Alloc(filePath)));
    }

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

        // Retrieve the user data
        IntPtr stringPtr;
        instance.getUserData(out stringPtr);

        // Get the string object
        GCHandle stringHandle = GCHandle.FromIntPtr(stringPtr);
        String key = stringHandle.Target as String;

        switch (type)
        {
            case FMOD.Studio.EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
                {
                    FMOD.MODE soundMode = FMOD.MODE.LOOP_NORMAL | FMOD.MODE.CREATESTREAM;
                    var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));

                    FMOD.Sound dialogueSound;
                    var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(key, soundMode, out dialogueSound);
                    if (soundResult == FMOD.RESULT.OK)
                    {
                        parameter.sound = dialogueSound.handle;
                        parameter.subsoundIndex = -1;
                        Marshal.StructureToPtr(parameter, parameterPtr, false);
                    }

                    break;
                }
            case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND:
                {
                    var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));
                    var sound = new FMOD.Sound(parameter.sound);
                    sound.release();

                    break;
                }
            case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROYED:
                {
                    // Now the event has been destroyed, unpin the string memory so it can be garbage collected
                    stringHandle.Free();

                    break;
                }
        }
        return FMOD.RESULT.OK;
    }

    private void OnDestroy()
    {
        soundEvent.release();
    }
}

You’ll also note a couple of other changes i.e. the callback is static, with the [MonoPInvokeCallback] attribute, and the callback delegate is explicitly assigned to a local member with new - these ensure that the callback will function correctly on AOT platforms without crashing. You can read more info on this in the Callbacks section of our Unity docs.