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