Programmer sound - Unity freezing

Hi there,

I’m trying to modify the programmer sound example code to play an arbitrary file. The end goal is to allow users to play their own audio files in the game. Unfortunately when ever I try to trigger a sound, Unity freezes.

here’s the script:

using System;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

public class MP3Player : MonoBehaviour
{
    FMOD.Studio.EVENT_CALLBACK dialogueCallback;

    [FMODUnity.EventRef]
    public string testEvent;
    
    void Start()
    {
        // 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
        dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);
    }

    void PlayDialogue(string key)
    {
        Debug.Log("init event instance");
        var dialogueInstance = FMODUnity.RuntimeManager.CreateInstance(testEvent);

        // Pin the key string in memory and pass a pointer through the user data
        GCHandle stringHandle = GCHandle.Alloc(key, GCHandleType.Pinned);
        dialogueInstance.setUserData(GCHandle.ToIntPtr(stringHandle));

        

        dialogueInstance.setCallback(dialogueCallback);

        Debug.Log("Instance Callback set");
        dialogueInstance.start();
        dialogueInstance.release();
    }

    [AOT.MonoPInvokeCallback (typeof (FMOD.Studio.EVENT_CALLBACK))]
    static FMOD.RESULT DialogueEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, FMOD.Studio.EventInstance instance, IntPtr parameterPtr)
    {

        // 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 | FMOD.MODE.NONBLOCKING;
                    var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));

                    if (key.Contains("."))
                    {
                        Debug.Log("key contains a period");
                        Debug.Log("ideally, key path is " + Application.dataPath + "/" + key);
                        FMOD.Sound dialogueSound;
                        var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(Application.dataPath + "/" + key, soundMode, out dialogueSound);
                        if (soundResult == FMOD.RESULT.OK)
                        {
                            parameter.sound = dialogueSound.handle;
                            parameter.subsoundIndex = -1;
                            Marshal.StructureToPtr(parameter, parameterPtr, false);
                        }
                    }
                    else
                    {
                        Debug.Log("key didn't contain a period");
                        FMOD.Studio.SOUND_INFO dialogueSoundInfo;
                        var keyResult = FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(key, out dialogueSoundInfo);
                        if (keyResult != FMOD.RESULT.OK)
                        {
                            break;
                        }
                        FMOD.Sound dialogueSound;
                        var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(dialogueSoundInfo.name_or_data, soundMode | dialogueSoundInfo.mode, ref dialogueSoundInfo.exinfo, out dialogueSound);
                        if (soundResult == FMOD.RESULT.OK)
                        {
                            parameter.sound = dialogueSound.handle;
                            parameter.subsoundIndex = dialogueSoundInfo.subsoundindex;
                            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();
                    sound.handle = 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;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            PlayDialogue("testmp3.mp3");
        }
        if (Input.GetKeyDown(KeyCode.B))
        {
            PlayDialogue("testwav.wav");
        }
    }
}

I’m wondering if part of the issue is including the file extension in the string (“testmp3.mp3”), or using Application.dataPath when creating the sound.

Also, in general if there’s a way to create + find FMOD logs in the event of a freeze, please let me know. I’m always trying to practice better debugging.

Ok, I dug a bit more into the Core API and am able to get a sound to play using the Core API with the exact same file path string that is freezing Unity when used as a programmer sound. Here’s the full script:

using System;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

public class MP3Player : MonoBehaviour
{

    FMOD.Studio.EVENT_CALLBACK dialogueCallback;

    [FMODUnity.EventRef]

    public string testEvent;

    public FMOD.Sound fmodSound;

    
    void Start()
    {
        // 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

        dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);
    }

    void PlayDialogue(string key)
    {

        Debug.Log("init event instance");
        var dialogueInstance = FMODUnity.RuntimeManager.CreateInstance(testEvent);

        // Pin the key string in memory and pass a pointer through the user data
        GCHandle stringHandle = GCHandle.Alloc(key, GCHandleType.Pinned);
        dialogueInstance.setUserData(GCHandle.ToIntPtr(stringHandle));
        dialogueInstance.setCallback(dialogueCallback);
        Debug.Log("Instance Callback set");

        dialogueInstance.start();
        dialogueInstance.release();
    }

    [AOT.MonoPInvokeCallback (typeof (FMOD.Studio.EVENT_CALLBACK))]
    static FMOD.RESULT DialogueEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, FMOD.Studio.EventInstance instance, IntPtr parameterPtr)
    {

        // 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 | FMOD.MODE.NONBLOCKING;
                    var parameter = (FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr, typeof(FMOD.Studio.PROGRAMMER_SOUND_PROPERTIES));

                    if (key.Contains("."))
                    {
                        Debug.Log("key contains a period");
                        Debug.Log("ideally, key path is " + Application.dataPath + "/" + key);

                        FMOD.Sound dialogueSound;
                        var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(Application.dataPath + "/" + key, soundMode, out dialogueSound);

                        if (soundResult == FMOD.RESULT.OK)
                        {

                            parameter.sound = dialogueSound.handle;
                            parameter.subsoundIndex = -1;
                            Marshal.StructureToPtr(parameter, parameterPtr, false);
                        }
                    }
                    else
                    {
                        Debug.Log("key didn't contain a period");
                        FMOD.Studio.SOUND_INFO dialogueSoundInfo;
                        var keyResult = FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(key, out dialogueSoundInfo);

                        if (keyResult != FMOD.RESULT.OK)
                        {
                            break;
                        }

                        FMOD.Sound dialogueSound;
                        var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(dialogueSoundInfo.name_or_data, soundMode | dialogueSoundInfo.mode, ref dialogueSoundInfo.exinfo, out dialogueSound);

                        if (soundResult == FMOD.RESULT.OK)
                        {

                            parameter.sound = dialogueSound.handle;
                            parameter.subsoundIndex = dialogueSoundInfo.subsoundindex;
                            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();
                    sound.handle = 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;
    }

    void Update()
    {

        if (Input.GetKeyDown(KeyCode.A))
        {
            PlayDialogue("testmp3.mp3");
        }

        if (Input.GetKeyDown(KeyCode.B))
        {
            PlayDialogue("testwav.wav");
        }

        if(Input.GetKeyDown(KeyCode.P))
        {
            FMODUnity.RuntimeManager.CoreSystem.createSound(Application.dataPath + "/" + "testmp3.mp3", FMOD.MODE.DEFAULT, out FMOD.Sound fmodSound);
            FMOD.ChannelGroup testChannelGroup;  
           FMODUnity.RuntimeManager.CoreSystem.createChannelGroup("name", out testChannelGroup);
            FMODUnity.RuntimeManager.CoreSystem.playSound(fmodSound, testChannelGroup, false, out FMOD.Channel soundChannel);
        }

    }

}

To reiterate, pressing A or B in this context results in a freeze, and is very difficult to debug (unless i’m missing a log file somewhere?). Pressing P works fine.

I need the sound integrated into the signal flow of the game, with relevant mixer channel routing, and ideally the ability to seek and pause. Hence why I would like the programmer sound to work. Any advice is appreciated.

This could be caused by an unsupported marshalling behavior, you could try replacing the FMOD.Studio.EventInstance in the callback code with an IntPtr, see the programmer sound example for FMOD 2.1:

https://fmod.com/resources/documentation-unity?version=2.1&page=examples-programmer-sounds.html

My version of the Studio + Unity integration (Studio 2.00.08), is throwing compiler errors on both the callback creation in the dialogueCallback creation and the EventInstance declaration using the intPointer.

Are these new additions to the 2.1 API? If so, is it safe to update a project that is late in development?

Ok, I’ve investigated a bit further:

I reverted back to the Programmer Sounds scripting example for version 2.0, creating sounds from a path pointing to Streaming Assets. When I place the test files in the Streaming Assets folder, it works as expected.

It seems like Application.dataPath is the source of the problem. I thought maybe Unity was modifying the audio files on import (since Application.dataPath points to the Assets folder in the editor), but pointing to an external directory (Application.dataPath + “…/Audio/” + key) didn’t solve the issue either.

At any rate, the Streaming Assets folder appears to be publicly accessible to the user after the app is built - is this an acceptable location for users to upload their own content, or is there a caveat I’m not aware of?

EDIT

I tried once more with an arbitrary file path (ex C:/Users/username/Desktop/audiofile.wav) and the programmer sound works fine. We’re going to try implementing a file browser solution for our game, since this feature doesn’t need to be persistent or saved.

Best to avoid Application.dataPath for Programmer Sounds.