FMOD Programmer Sound Freeze on Unity

Hello,

Playing Programmer Sound is freezing Unity.
All the other non-Programmer Sound has been working perfectly for the last 3 years development on this project…

Once I play few programmer sound as each dialogue line plays, it freezes the editor without any error message. It happens roughly about 7~13 Programmer Sound plays later - it’s pretty random at this point.

I am on:
Unity 2020.3.38f1
FMOD 2.01.11

Below is my code to use Programmer Sound wichi is based on https://www.fmod.com/docs/2.01/unity/examples-programmer-sounds.html

I’ve integrated into Dialogue System (Unity Asset)'s Sequencer Command btw.
The key string (in the code below that is retreived by a function GetParameter(0)) for search in Programmer Sound will be something like “Yuna_158_1” or “Min_172_44” and so on which seems to work but wanted to write it if it helps.

    // FMODWait(entrytag);
    public class SequencerCommandFMODWait : SequencerCommand
    {
        EventInstance eventInstance;
        PLAYBACK_STATE state;
        EventDescription eventCheck;
        FMOD.Studio.EVENT_CALLBACK dialogueCallback;

        private FMOD.Studio.EVENT_CALLBACK_TYPE _currentType;
        public FMOD.Studio.EVENT_CALLBACK_TYPE CurrentType
        {
            set
            {
                _currentType = value;
            }
            get
            {
                return _currentType;
            }
        }
        
        public IEnumerator Start()
        {
            dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);
            
            PlayDialogue(GetParameter(0));
            
            // Mouth Animation Start
            SequencerCommandStartTalkAnimation.StartTalkAnim(speaker, GetSubject(1), GetParameter(1));
            
            if (CurrentType != EVENT_CALLBACK_TYPE.SOUND_STOPPED)
            {
                while (IsPlaying())
                {
                    yield return null;
                }
            }
            
            //  Mouth Animation End
            SequencerCommandStopTalkAnimation.StopTalkAnim(speaker, GetSubject(1), GetParameter(1));
            
            Stop();
        }

        bool IsPlaying()
        {
            if (CurrentType == EVENT_CALLBACK_TYPE.STOPPED || CurrentType == EVENT_CALLBACK_TYPE.SOUND_STOPPED || CurrentType == EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND)
            {
                return false;
            }

            return CurrentType != EVENT_CALLBACK_TYPE.SOUND_STOPPED;
        }

        public void OnDestroy()
        {
            if (state != PLAYBACK_STATE.STOPPED)
            {
                if (IsPlaying())
                {
                    eventInstance.stop(STOP_MODE.IMMEDIATE);
                }
                
                eventInstance.release();
            }
        }
        
        void PlayDialogue(string key)
        {
            eventInstance = FMODUnity.RuntimeManager.CreateInstance("event:/Dialogue/DialogTest");

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

            eventInstance.setCallback(dialogueCallback);
            eventInstance.start();
        }
        
        [AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
        FMOD.RESULT DialogueEventCallback(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;

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

                    if (key.Contains("."))
                    {
                        FMOD.Sound dialogueSound;
                        var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(Application.streamingAssetsPath + "/" + key, soundMode, out dialogueSound);
                        if (soundResult == FMOD.RESULT.OK)
                        {
                            parameter.sound = dialogueSound.handle;
                            parameter.subsoundIndex = -1;
                            Marshal.StructureToPtr(parameter, parameterPtr, false);
                        }
                    }
                    else
                    {
                        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(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;
        }

    }

Hi,

You seem to be running into two connected issues. You’ll notice that the scripting example in the docs that you’ve linked has a static callback; the main issue is that your MonoPInvokeCallback has to be static. As a result of adding the static keyword to the callback, you’ll also notice that your line CurrentType = type; in the callback breaks. If you want to operate on non-static members like CurrentType, you need to pass them to the callback as user data, the same as you’ve done with the sound key. You cannot access them directly.

If the purpose of CurrentType is just to track the state of an event instance, not the callback type, you may find it more convenient to call eventInstance.getPlaybackState() instead. If you do need to tie some kind of behavior specifically to callback types, not just the event state, you can pass CurrentType as user data. Since you’re also passing the sound key as user data, you can use a struct or class to bundle multiple variables or method delegates together. The Timeline Callbacks Scripting Example in the Unity docs shows a basic example of using a struct in this way.

Hope this helps!

Hello,

I changed it back to static and used eventInstance.getPlaybackState() instead of CurrentType.
It seems to not occur any freeze.
But it does not change its state when the actual sound is stopped while “EVENT_CALLBACK_TYPE.SOUND_STOPPED” is called right after the file playing length is finished.
I am using this to move the character mouth move and stop.

With eventInstance.getPlaybackState(), I can not know the exact timing of the sound (file playing length) is stopped like the CurrentType.

What would be the solution here?

In that case, the solution is to pass CurrentType to the callback as user data, set its value in the callback, and then act on it outside the callback. As mentioned before, since you’re also passing the sound key as user data, you’ll want to use a struct to contain both variables; the Timeline Callbacks example I linked in my previous reply demonstrates how to do this.

Hello,

Just implemented and it seems like its working as expected.

However, could you take a look at my implementation and see if there is any problem lurking inside if that is okay?

    public class SequencerCommandFMODWait : SequencerCommand
    {
        PLAYBACK_STATE state;
        EventDescription eventCheck;
        EVENT_CALLBACK dialogueCallback;
        private EventInstance eventInstance;
        
        class VoiceData
        {
            public string key;
            public EVENT_CALLBACK_TYPE eventCallbackType;
        }

        private VoiceData voiceData;
        private GCHandle voiceDataHandle;
        
        public IEnumerator Start()
        {
            dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);

            voiceData = new VoiceData();
            voiceData.key = GetParameter(0);
            
            PlayDialogue(voiceData);
            
            // Mouth animation start
            SequencerCommandStartTalkAnimation.StartTalkAnim(speaker, GetSubject(1), GetParameter(1));
            
            if (voiceData.eventCallbackType != EVENT_CALLBACK_TYPE.SOUND_STOPPED)
            {
                while (IsPlaying())
                {
                    yield return null;
                }
            }
            
            // Mouth animation end
            SequencerCommandStopTalkAnimation.StopTalkAnim(speaker, GetSubject(1), GetParameter(1));
            
            Stop();
        }

        bool IsPlaying()
        {
            if (voiceData.eventCallbackType == EVENT_CALLBACK_TYPE.SOUND_STOPPED) return false;
            eventInstance.getPlaybackState(out state);
            return voiceData.eventCallbackType != EVENT_CALLBACK_TYPE.SOUND_STOPPED;
        }

        public void OnDestroy()
        {
            if (state != PLAYBACK_STATE.STOPPED)
            {
                eventInstance.setUserData(IntPtr.Zero);
                if (IsPlaying()) eventInstance.stop(STOP_MODE.IMMEDIATE);
                eventInstance.release();
            }
        }
        
        void PlayDialogue(VoiceData inVoiceData)
        {
            // TODO: check localization and set the bank to EN JP or KR - https://documentation.help/FMOD-Studio-API/programmer_sounds.html
            eventInstance = FMODUnity.RuntimeManager.CreateInstance("event:/Dialogue/DialogTest");

            // Pin the key string in memory and pass a pointer through the user data
            voiceDataHandle = GCHandle.Alloc(inVoiceData);
            eventInstance.setUserData(GCHandle.ToIntPtr(voiceDataHandle));

            eventInstance.setCallback(dialogueCallback);
            eventInstance.start();
            eventInstance.release();
        }
        
        [AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
        static FMOD.RESULT DialogueEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
        {
            FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr);

            // Retrieve the user data
            IntPtr voiceDataInstancePtr;
            FMOD.RESULT result = instance.getUserData(out voiceDataInstancePtr);
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError("Timeline Callback error: " + result);
            }
            else if (voiceDataInstancePtr != IntPtr.Zero)
            {
                // Get the object to store beat and marker details
                GCHandle timelineHandle = GCHandle.FromIntPtr(voiceDataInstancePtr);
                VoiceData timelineInfo = (VoiceData) timelineHandle.Target;

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

                        if (timelineInfo.key.Contains("."))
                        {
                            FMOD.Sound dialogueSound;
                            var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(
                                Application.streamingAssetsPath + "/" + timelineInfo.key, soundMode, out dialogueSound);
                            if (soundResult == FMOD.RESULT.OK)
                            {
                                parameter.sound = dialogueSound.handle;
                                parameter.subsoundIndex = -1;
                                Marshal.StructureToPtr(parameter, parameterPtr, false);
                            }
                        }
                        else
                        {
                            FMOD.Studio.SOUND_INFO dialogueSoundInfo;
                            var keyResult =
                                FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(timelineInfo.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(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
                        timelineHandle.Free();

                        break;
                    }
                }
            }

            return FMOD.RESULT.OK;
        }

    }

Much appreciated.

The only issue I can see here is that case FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND: will not execute if the user data has been freed, which causes a memory leak as your assigned sound is never released, and therefore stays in memory. You can fix this by moving the code for the callback type FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND to a separate if statement that will always be checked, even when your user data has already been freed:

        if (result != FMOD.RESULT.OK)
        {
              UnityEngine.Debug.LogError("Callback error: " + result);
        }
        else if (voiceDataInstancePtr != IntPtr.Zero)
        {
              // existing code for CREATE_PROGRAMMER_SOUND and 
              // EVENT_CALLBACK_TYPE.DESTROYED here. Only executes
              // if user data has not been freed
        } 
        
        // separate check for whether the programmer sound is getting destroyed,
        // regardless of whether the user data is valid - sound still needs
        // to be released even if user data already has been
        if (type == 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();
        }

Is this what you mean by checking the separate " FMOD.Studio.EVENT_CALLBACK_TYPE.DESTROY_PROGRAMMER_SOUND" so its always checked whether the user data is freed or not?

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

            // Retrieve the user data
            IntPtr voiceDataInstancePtr;
            FMOD.RESULT result = instance.getUserData(out voiceDataInstancePtr);
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError("Timeline Callback error: " + result);
            }
            else if (voiceDataInstancePtr != IntPtr.Zero)
            {
                // Get the object to store beat and marker details
                GCHandle voiceDataHandle = GCHandle.FromIntPtr(voiceDataInstancePtr);
                VoiceData voiceData = (VoiceData) voiceDataHandle.Target;

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

                        if (voiceData.key.Contains("."))
                        {
                            FMOD.Sound dialogueSound;
                            var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(
                                Application.streamingAssetsPath + "/" + voiceData.key, soundMode, out dialogueSound);
                            if (soundResult == FMOD.RESULT.OK)
                            {
                                parameter.sound = dialogueSound.handle;
                                parameter.subsoundIndex = -1;
                                Marshal.StructureToPtr(parameter, parameterPtr, false);
                            }
                        }
                        else
                        {
                            FMOD.Studio.SOUND_INFO dialogueSoundInfo;
                            var keyResult =
                                FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(voiceData.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(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
                        voiceDataHandle.Free();

                        break;
                    }
                }
            }
            
            if (type == 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();
            }

            return FMOD.RESULT.OK;
        }

    }

In essence, yes, but there’s some duplicate code that can be cleaned up. However, after a quick look over the rest of the code for your class, you may run into race conditions due to the code you’re executing in OnDestroy - for example, if the user data is valid for the voiceDataInstancePtr != IntPtr.Zero check, and then freed from outside the callback immediately afterwards, the callback code will try to access memory that has just been freed.

If you remove the code you have in OnDestroy, and use the following callback code, it should be fine.

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

        // Retrieve the user data
        IntPtr voiceDataInstancePtr;
        FMOD.RESULT result = instance.getUserData(out voiceDataInstancePtr);
        if (result != FMOD.RESULT.OK)
        {
            UnityEngine.Debug.LogError("Callback error: " + result);
        }
        else 
        {
            // Get the voice data struct to access the sound key and assign to the callback type
            GCHandle voiceDataHandle = GCHandle.FromIntPtr(voiceDataInstancePtr);
            VoiceData voiceData = (VoiceData)voiceDataHandle.Target;

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

                        if (voiceData.key.Contains("."))
                        {
                            FMOD.Sound dialogueSound;
                            var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(
                                Application.streamingAssetsPath + "/" + voiceData.key, soundMode, out dialogueSound);
                            if (soundResult == FMOD.RESULT.OK)
                            {
                                parameter.sound = dialogueSound.handle;
                                parameter.subsoundIndex = -1;
                                Marshal.StructureToPtr(parameter, parameterPtr, false);
                            }
                        }
                        else
                        {
                            FMOD.Studio.SOUND_INFO dialogueSoundInfo;
                            var keyResult =
                                FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(voiceData.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(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
                        voiceDataHandle.Free();
                        break;
                    }
            }
        }

        return FMOD.RESULT.OK;
    }

I see.

So what I’ve did is:

Removed this specific code from OnDestroy()

                eventInstance.setUserData(IntPtr.Zero);

and also removed checking which was a separate if statement that will always be checked even when your use data has already been freed which was outside of the switch statement.

       if (type == 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();
        }

Im sum, below is the complete code:

  public class SequencerCommandFMODWait : SequencerCommand
    {
        PLAYBACK_STATE state;
        EventDescription eventCheck;
        EVENT_CALLBACK dialogueCallback;
        private EventInstance eventInstance;
        
        class VoiceData
        {
            public string key;
            public EVENT_CALLBACK_TYPE eventCallbackType;
        }

        private VoiceData voiceData;
        private GCHandle voiceDataHandle;
        
        public IEnumerator Start()
        {
            dialogueCallback = new FMOD.Studio.EVENT_CALLBACK(DialogueEventCallback);

            voiceData = new VoiceData();
            voiceData.key = GetParameter(0);

            PlayDialogue(voiceData);
            
            // Mouth animation start
            SequencerCommandStartTalkAnimation.StartTalkAnim(speaker, GetSubject(1), GetParameter(1));
            
            if (voiceData.eventCallbackType != EVENT_CALLBACK_TYPE.SOUND_STOPPED)
            {
                while (IsPlaying())
                {
                    yield return null;
                }
            }
            
            // Mouth animation end
            SequencerCommandStopTalkAnimation.StopTalkAnim(speaker, GetSubject(1), GetParameter(1));
            
            Stop();
        }

        bool IsPlaying()
        {
            if (voiceData.eventCallbackType == EVENT_CALLBACK_TYPE.SOUND_STOPPED || voiceData.eventCallbackType == EVENT_CALLBACK_TYPE.STOPPED || voiceData.eventCallbackType == EVENT_CALLBACK_TYPE.DESTROYED) return false;
            eventInstance.getPlaybackState(out state);
            return voiceData.eventCallbackType != EVENT_CALLBACK_TYPE.SOUND_STOPPED;
        }
        
        public void OnDestroy()
        {
            if (state != PLAYBACK_STATE.STOPPED)
            {
                if (IsPlaying())
                {
                    eventInstance.stop(STOP_MODE.IMMEDIATE);
                }
                eventInstance.release();
            }
        }
        
        void PlayDialogue(VoiceData inVoiceData)
        {
            // TODO: check localization and set the bank to EN JP or KR - https://documentation.help/FMOD-Studio-API/programmer_sounds.html
            eventInstance = FMODUnity.RuntimeManager.CreateInstance("event:/Dialogue/DialogTest");

            // Pin the key string in memory and pass a pointer through the user data
            voiceDataHandle = GCHandle.Alloc(inVoiceData);
            eventInstance.setUserData(GCHandle.ToIntPtr(voiceDataHandle));

            eventInstance.setCallback(dialogueCallback);
            eventInstance.start();
            eventInstance.release();
        }
        
        [AOT.MonoPInvokeCallback(typeof(FMOD.Studio.EVENT_CALLBACK))]
        static FMOD.RESULT DialogueEventCallback(FMOD.Studio.EVENT_CALLBACK_TYPE type, IntPtr instancePtr, IntPtr parameterPtr)
        {
            FMOD.Studio.EventInstance instance = new FMOD.Studio.EventInstance(instancePtr);

            // Retrieve the user data
            IntPtr voiceDataInstancePtr;
            FMOD.RESULT result = instance.getUserData(out voiceDataInstancePtr);
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError("Timeline Callback error: " + result);
            }
            else if (voiceDataInstancePtr != IntPtr.Zero)
            {
                // Get the object to store beat and marker details
                GCHandle voiceDataHandle = GCHandle.FromIntPtr(voiceDataInstancePtr);
                VoiceData voiceData = (VoiceData) voiceDataHandle.Target;

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

                        if (voiceData.key.Contains("."))
                        {
                            FMOD.Sound dialogueSound;
                            var soundResult = FMODUnity.RuntimeManager.CoreSystem.createSound(
                                Application.streamingAssetsPath + "/" + voiceData.key, soundMode, out dialogueSound);
                            if (soundResult == FMOD.RESULT.OK)
                            {
                                parameter.sound = dialogueSound.handle;
                                parameter.subsoundIndex = -1;
                                Marshal.StructureToPtr(parameter, parameterPtr, false);
                            }
                        }
                        else
                        {
                            FMOD.Studio.SOUND_INFO dialogueSoundInfo;
                            var keyResult =
                                FMODUnity.RuntimeManager.StudioSystem.getSoundInfo(voiceData.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(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
                        voiceDataHandle.Free();

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

    }

What you have there should be fine. You can also:

  1. Remove the if (voiceDataInstancePtr != IntPtr.Zero) line from your callback, leaving just the else, as that particular if statement isn’t needed now
  2. Remove the extra eventInstance.release() from OnDestroy() since you’re already calling eventInstance.release() in PlayDialogue()

Neither of the above things will cause errors in your code if you leave them in, but they’re both unneeded.

Thanks for the explanation. This should fix this thread’s issue for sure.

One additional question:
Rarely a few lines are not playing at all.

I can see that it used the correct Key and the file itself is good. Bank and table also includes the specific sound for sure.

Which line in above code is a good line to put Debug.Log or some sort of msg to show that the Programmer Sound have allocated memory for the sound and played?

Thank you.

I would recommend setting the logging level to “Log” in FMOD Settings, as it will enable you to see when a sound is being created, as well as details like its path, which you can check against your expected path. As for checking what’s happening in the callback, inserting a breakpoint in the FMOD.Studio.EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND case so that you can step through it would probably be your best bet.

Since the occurrence is rare, however, if you don’t want to have the game pause every time a correct sound is created as well, a Debug.Log() call in the the various switch cases would be a good idea so that you can see which callback types are being used. Logging the various FMOD functions in the callback that return FMOD.Result is also a good idea, as you’ll be able to see which functions, if any, aren’t working.