PreviewEvent works for custom programmer events, but only sometimes

So I have custom AI voiceover tools in the editor for prototyping, and it requires previewing them from the editor:

      if (Application.isPlaying)
      {
        _eventInstance = RuntimeManager.CreateInstance(eventPath);
      }
      #if UNITY_EDITOR
      else
      {
        EditorEventRef eventRef = EventManager.EventFromPath(eventPath);
        EditorUtils.LoadPreviewBanks();
        EditorUtils.PreviewEvent(eventRef, new Dictionary<string, float>());
      }
      #endif

      // Pin the key string in memory and pass a pointer through the user data
      _voiceDataHandle = GCHandle.Alloc(_voiceData);
      Log(_eventInstance.setUserData(GCHandle.ToIntPtr(_voiceDataHandle)), "setUserData");

      Log(_eventInstance.setCallback(DialogueEventCallback), "setCallback");
      Log(_eventInstance.start(),                       "start");
      Log(_eventInstance.release(),                     "release");

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

      // Retrieve the user data
      if (!StaticLog(instance.getUserData(out IntPtr voiceDataInstancePtr))) return RESULT.OK;
      if (voiceDataInstancePtr == IntPtr.Zero) return RESULT.OK;

      // 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 EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
        {
          MODE soundMode = MODE.CREATESTREAM;
          PROGRAMMER_SOUND_PROPERTIES parameter = (PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr,
            typeof(PROGRAMMER_SOUND_PROPERTIES));

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

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

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

          break;
        }
      }

      return RESULT.OK;
    }

This works fine in the editor, except when it doesn’t. Sometimes PreviewEvent just returns a null pointer.

Hi,

A few questions:

  • What version of FMOD for Unity are you using?
  • Do you find that your run into this issue immediately, or only after PreviewEvent works without issue several times?
  • Correct me if I’m wrong, but are you missing an assignment of the EventInstance returned by PreviewEvent() to _eventInstance?

Okay now that’s pretty hilarious. Sorry for the bother, I was indeed missing an assignment. Not sure how that ever worked.

No problem, happy to hear that it’s now working.

So now I’m getting a new hilarious bug.

When I play the event in the editor, nothing happens.
But when I try in playmode, the editor freezes on SetListenerLocation of all places

New code:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using AOT;
using FMOD;
using FMOD.Studio;
using FMODUnity;
using UnityEngine;
using Debug = UnityEngine.Debug;

#if UNITY_EDITOR
using System.Collections.Generic;
#endif

namespace Dialogs.VoiceOver.Utils
{
  public class VoiceOverPlayer
  {
    EventInstance _eventInstance;
    PLAYBACK_STATE _state;
    VoiceData _voiceData;
    GCHandle _voiceDataHandle;
    static FMOD.Studio.System _system;

    #if UNITY_EDITOR
    static FMOD.System CoreSystem
    {
      get
      {
        if (StaticLog(EditorUtils.System.getCoreSystem(out FMOD.System core))) return core;
        throw new Exception("GetCoreSystem failed");
      }
    }
    #else
    static FMOD.System CoreSystem => RuntimeManager.CoreSystem;
    #endif

    public bool IsVoicePlaying()
    {
      if (_voiceData == null ||
          _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 bool HasAlreadyStopped()
    {
      return _state == PLAYBACK_STATE.STOPPED;
    }

    public void PlayDialogue(VoiceOverPath path)
    {
      _voiceData = new VoiceData
      {
        Path = path,
      };
      string eventPath = "event:/Dialogue/Dialogue";

      if (Application.isPlaying)
      {
        _eventInstance = RuntimeManager.CreateInstance(eventPath);
      }
      #if UNITY_EDITOR
      else
      {
        EditorEventRef eventRef = EventManager.EventFromPath(eventPath);
        EditorUtils.LoadPreviewBanks();
        EditorUtils.PreviewEvent(eventRef, new Dictionary<string, float>());
        _eventInstance = EditorUtils.PreviewEvent(eventRef, new Dictionary<string, float>());
      }
      #endif

      // Pin the key string in memory and pass a pointer through the user data
      _voiceDataHandle = GCHandle.Alloc(_voiceData);
      Log(_eventInstance.setUserData(GCHandle.ToIntPtr(_voiceDataHandle)), "setUserData");

      Log(_eventInstance.setCallback(DialogueEventCallback), "setCallback");
      Log(_eventInstance.start(),                       "start");
      Log(_eventInstance.release(),                     "release");
    }

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

      // Retrieve the user data
      if (!StaticLog(instance.getUserData(out IntPtr voiceDataInstancePtr))) return RESULT.OK;
      if (voiceDataInstancePtr == IntPtr.Zero) return RESULT.OK;

      // 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 EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
        {
          MODE soundMode = MODE.CREATESTREAM;
          PROGRAMMER_SOUND_PROPERTIES parameter = (PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr,
            typeof(PROGRAMMER_SOUND_PROPERTIES));

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

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

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

          break;
        }
      }

      return RESULT.OK;
    }

    bool Log(RESULT result, string label = "")
    {
      if (result == RESULT.OK) return true;
      Debug.Log($"Dialogue System: Sequencer: TTS({_voiceData.Path}): FMOD failed at {label} : {result}");
      return false;
    }

    static bool StaticLog(RESULT result, string label = "")
    {
      if (result == RESULT.OK) return true;
      Debug.Log($"Dialogue System: Sequencer: TTS(): FMOD failed at {label} : {result}");
      return false;
    }

    class VoiceData
    {
      public EVENT_CALLBACK_TYPE EventCallbackType;
      public VoiceOverPath Path;
    }

    public void Destroy(bool stopSound = false)
    {
      if (HasAlreadyStopped()) return;
      _eventInstance.release();
      if (stopSound && IsVoicePlaying()) _eventInstance.stop(STOP_MODE.ALLOWFADEOUT);
      _eventInstance.clearHandle();
    }
  }
}

This is probably caused by accessing EditorUtils.System during play mode, which was not what I intended, but the freeze is probably not intended behavior.

Well turns out throwing an exception in a FMOD callback is a very bad idea, and Application.isPlaying is not accessible outside of the main thread.

It’s crazy the thing you take for granted when you’re used to a single thread.

1 Like

The final code if anyone is interested:

using System;
using System.Runtime.InteropServices;
using AOT;
using FMOD;
using FMOD.Studio;
using FMODUnity;
using UnityEngine;
using Debug = UnityEngine.Debug;

#if UNITY_EDITOR
using System.Collections.Generic;
#endif

namespace Dialogs.VoiceOver.Utils
{
  public class VoiceOverPlayer
  {
    EventInstance _eventInstance;
    PLAYBACK_STATE _state;
    VoiceData _voiceData;
    GCHandle _voiceDataHandle;

    #if UNITY_EDITOR
    static FMOD.System CoreSystem
    {
      get
      {
        if (RuntimeManager.IsInitialized) return RuntimeManager.CoreSystem;
        if (StaticLog(EditorUtils.System.getCoreSystem(out FMOD.System core))) return core;
        throw new Exception("GetCoreSystem failed");
      }
    }
    #else
    static FMOD.System CoreSystem => RuntimeManager.CoreSystem;
    #endif

    public bool IsVoicePlaying()
    {
      if (_voiceData == null ||
          _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 bool HasAlreadyStopped()
    {
      return _state == PLAYBACK_STATE.STOPPED;
    }

    public void PlayDialogue(VoiceOverPath path)
    {
      _voiceData = new VoiceData
      {
        Path = path,
      };
      string eventPath = "event:/Dialogue/Dialogue";

      if (Application.isPlaying)
      {
        _eventInstance = RuntimeManager.CreateInstance(eventPath);
      }
      #if UNITY_EDITOR
      else
      {
        EditorEventRef eventRef = EventManager.EventFromPath(eventPath);
        EditorUtils.LoadPreviewBanks();
        _eventInstance = EditorUtils.PreviewEvent(eventRef, new Dictionary<string, float>());
      }
      #endif

      // Pin the key string in memory and pass a pointer through the user data
      _voiceDataHandle = GCHandle.Alloc(_voiceData);
      Log(_eventInstance.setUserData(GCHandle.ToIntPtr(_voiceDataHandle)), "setUserData");

      Log(_eventInstance.setCallback(DialogueEventCallback), "setCallback");
      Log(_eventInstance.start(),                       "start");
      Log(_eventInstance.release(),                     "release");
    }

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

      // Retrieve the user data
      if (!StaticLog(instance.getUserData(out IntPtr voiceDataInstancePtr))) return RESULT.OK;
      if (voiceDataInstancePtr == IntPtr.Zero) return RESULT.OK;

      // 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 EVENT_CALLBACK_TYPE.CREATE_PROGRAMMER_SOUND:
        {
          MODE soundMode = MODE.CREATESTREAM;
          PROGRAMMER_SOUND_PROPERTIES parameter = (PROGRAMMER_SOUND_PROPERTIES)Marshal.PtrToStructure(parameterPtr,
            typeof(PROGRAMMER_SOUND_PROPERTIES));

          try
          {

            RESULT soundResult = CoreSystem.createSound(
              voiceData.Path.FullPath,
              soundMode,
              out Sound dialogueSound);
            if (soundResult == RESULT.OK)
            {
              parameter.sound = dialogueSound.handle;
              parameter.subsoundIndex = -1;
              Marshal.StructureToPtr(parameter, parameterPtr, false);
            }
          }
          catch (Exception e)
          {
            Debug.LogError(e);
            return RESULT.ERR_INITIALIZED;
          }

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

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

          break;
        }
      }

      return RESULT.OK;
    }

    bool Log(RESULT result, string label = "")
    {
      if (result == RESULT.OK) return true;
      Debug.Log($"Dialogue System: Sequencer: TTS({_voiceData.Path}): FMOD failed at {label} : {result}");
      return false;
    }

    static bool StaticLog(RESULT result, string label = "")
    {
      if (result == RESULT.OK) return true;
      Debug.Log($"Dialogue System: Sequencer: TTS(): FMOD failed at {label} : {result}");
      return false;
    }

    class VoiceData
    {
      public EVENT_CALLBACK_TYPE EventCallbackType;
      public VoiceOverPath Path;
    }

    public void Destroy(bool stopSound = false)
    {
      if (_eventInstance.isValid()) _eventInstance.release();
      if (stopSound && !HasAlreadyStopped() && IsVoicePlaying()) _eventInstance.stop(STOP_MODE.ALLOWFADEOUT);
      _eventInstance.clearHandle();
    }
  }
}