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.
             
            
              
              
              
            
            
           
          
            
            
              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();
    }
  }
}