Skip to content

Commit

Permalink
feat: Custom FMOD support for PlayableDirector (#554)
Browse files Browse the repository at this point in the history
* Added attaching functionality for FMOD channel

* Added FMOD support for playable behaviour
  • Loading branch information
Metious committed Sep 13, 2024
1 parent a404202 commit 5a7fa36
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 2 deletions.
37 changes: 37 additions & 0 deletions Nautilus/Handlers/CustomSoundHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,41 @@ public static bool TryGetCustomSoundChannel(int id, out Channel channel)
{
return CustomSoundPatcher.EmitterPlayedChannels.TryGetValue(id, out channel);
}


/// <summary>
/// Attaches the specified channel to the given transform. This results in the sound position following the <paramref name="transform"/>.
/// </summary>
/// <param name="channel">The channel to attach.</param>
/// <param name="transform">The transform which the channel will follow.</param>
public static void AttachChannelToGameObject(Channel channel, Transform transform)
{
var index = CustomSoundPatcher.AttachedChannels.FindIndex(x => x.Channel.handle == channel.handle);
var attachedChannel = new CustomSoundPatcher.AttachedChannel(channel, transform);
if (index == -1)
{
CustomSoundPatcher.AttachedChannels.Add(attachedChannel);
}
else
{
CustomSoundPatcher.AttachedChannels[index] = attachedChannel;
}

CustomSoundPatcher.SetChannel3DAttributes(channel, transform);
}

/// <summary>
/// Detaches the specified channel from any game object.
/// </summary>
/// <param name="channel">The channel to detach.</param>
public static void DetachChannelFromGameObject(Channel channel)
{
var index = CustomSoundPatcher.AttachedChannels.FindIndex(x => x.Channel.handle == channel.handle);
if (index == -1)
{
InternalLogger.Warn($"{nameof(CustomSoundHandler)}: The specified channel is not attached to any game object.");
}

CustomSoundPatcher.AttachedChannels.RemoveAt(index);
}
}
1 change: 1 addition & 0 deletions Nautilus/Nautilus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<PackageReference Include="PolySharp" Version="1.13.1" PrivateAssets="all" />

<Publicize Include="Newtonsoft.Json" />
<Publicize Include="FMODUnity" />
</ItemGroup>

<ItemGroup>
Expand Down
216 changes: 214 additions & 2 deletions Nautilus/Patchers/CustomSoundPatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@
using FMODUnity;
using HarmonyLib;
using Nautilus.FMod.Interfaces;
using Nautilus.Handlers;
using Nautilus.Utility;
using UnityEngine;
using UnityEngine.Playables;

namespace Nautilus.Patchers;

internal class CustomSoundPatcher
{
internal record struct AttachedChannel(Channel Channel, Transform Transform);

internal static readonly SelfCheckingDictionary<string, Sound> CustomSounds = new("CustomSounds");
internal static readonly SelfCheckingDictionary<string, Bus> CustomSoundBuses = new("CustomSoundBuses");
internal static readonly SelfCheckingDictionary<string, IFModSound> CustomFModSounds = new("CustoomFModSounds");
internal static readonly Dictionary<int, Channel> EmitterPlayedChannels = new();
internal static List<AttachedChannel> AttachedChannels = new();

private static readonly Dictionary<string, Channel> PlayedChannels = new();
private static readonly Dictionary<FMODEventPlayableBehavior, Channel> PlayableBehaviorChannels = new();
private static readonly List<AttachedChannel> _attachedChannelsToRemove = new();

internal static void Patch(Harmony harmony)
{
Expand Down Expand Up @@ -68,7 +75,212 @@ public static bool FMODExtension_GetLength_Prefix(string path, ref int __result)
__result = 0;
return false;
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.PerformSeek))]
[HarmonyPrefix]
public static bool FMODEventPlayableBehavior_PerformSeek_Prefix(FMODEventPlayableBehavior __instance)
{
if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel))
{
return true;
}

if (__instance.seek < 0)
{
return true;
}

channel.setPosition((uint)__instance.seek, TIMEUNIT.MS);
__instance.seek = -1;
return false;
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.PlayEvent))]
[HarmonyPrefix]
public static bool FMODEventPlayableBehavior_PlayEvent_Prefix(FMODEventPlayableBehavior __instance)
{
if (string.IsNullOrEmpty(__instance.eventName))
{
return true;
}

if (string.IsNullOrEmpty(__instance.eventName) || !CustomSounds.TryGetValue(__instance.eventName, out Sound soundEvent)
&& !CustomFModSounds.ContainsKey(__instance.eventName)) return true;

Channel channel;
if (CustomFModSounds.TryGetValue(__instance.eventName, out var fModSound))
{
if (!fModSound.TryPlaySound(out channel))
return false;
}
else if (CustomSoundBuses.TryGetValue(__instance.eventName, out Bus bus))
{
if (!AudioUtils.TryPlaySound(soundEvent, bus, out channel))
return false;
}
else
{
return false;
}

PlayableBehaviorChannels[__instance] = channel;

channel.setPaused(true);

__instance.PerformSeek();

if (__instance.TrackTargetObject)
{
CustomSoundHandler.AttachChannelToGameObject(channel, __instance.TrackTargetObject.transform);
}
else
{
SetChannel3DAttributes(channel, Vector3.zero);
}

channel.setVolume(__instance.currentVolume);

channel.setPaused(false);

return false;
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.OnExit))]
[HarmonyPrefix]
public static bool FMODEventPlayableBehavior_OnExit_Prefix(FMODEventPlayableBehavior __instance)
{
if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel))
{
return true;
}

if (!__instance.isPlayheadInside)
{
return false;
}

if (__instance.stopType != FMODUnity.STOP_MODE.None)
{
channel.stop();
}

PlayableBehaviorChannels.Remove(__instance);
__instance.isPlayheadInside = false;

return false;
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.ProcessFrame))]
[HarmonyPrefix]
public static bool FMODEventPlayableBehavior_ProcessFrame_Prefix(FMODEventPlayableBehavior __instance)
{
return !PlayableBehaviorChannels.ContainsKey(__instance);
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.UpdateBehavior))]
[HarmonyPrefix]
public static bool FMODEventPlayableBehavior_UpdateBehavior_Prefix(FMODEventPlayableBehavior __instance, float time, float volume)
{
if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel))
{
return true;
}

if (volume != __instance.currentVolume)
{
__instance.currentVolume = volume;
channel.setVolume(volume);
}

if (time >= __instance.OwningClip.start && time < __instance.OwningClip.end)
{
__instance.OnEnter();
}
else
{
__instance.OnExit();
}

return false;
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.OnGraphStop))]
[HarmonyPostfix]
public static void FMODEventPlayableBehavior_OnGraphStop_Postfix(FMODEventPlayableBehavior __instance)
{
if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel))
{
channel.stop();
PlayableBehaviorChannels.Remove(__instance);
}
}

[HarmonyPatch(typeof(FMODEventPlayableBehavior), nameof(FMODEventPlayableBehavior.Evaluate))]
[HarmonyPrefix]
public static bool FMODEventPlayableBehavior_Evaluate_Postfix(FMODEventPlayableBehavior __instance, double time, FrameData info, bool evaluate)
{
if (!PlayableBehaviorChannels.TryGetValue(__instance, out var channel))
{
return true;
}

if (!info.timeHeld && time >= __instance.OwningClip.start && time < __instance.OwningClip.end)
{
if (!__instance.isPlayheadInside)
{
if (time - __instance.OwningClip.start > 0.1)
{
__instance.seek = __instance.GetPosition(time);
}
__instance.OnEnter();
return false;
}
if ((evaluate || info.seekOccurred || info.timeLooped || info.evaluationType == FrameData.EvaluationType.Evaluate))
{
__instance.seek = __instance.GetPosition(time);
__instance.PerformSeek();
return false;
}
}
else
{
__instance.OnExit();
}

return false;
}

[HarmonyPatch(typeof(RuntimeManager), nameof(RuntimeManager.Update))]
[HarmonyPostfix]
public static void RuntimeManager_Update_Postfix(RuntimeManager __instance)
{
if (!__instance.studioSystem.isValid())
{
return;
}

foreach (var attachedChannel in AttachedChannels)
{
attachedChannel.Channel.isPlaying(out var isPlaying);
if (!isPlaying || !attachedChannel.Transform)
{
_attachedChannelsToRemove.Add(attachedChannel);
continue;
}

SetChannel3DAttributes(attachedChannel.Channel, attachedChannel.Transform);
}

if (_attachedChannelsToRemove.Count > 0)
{
foreach (var toRemove in _attachedChannelsToRemove)
{
AttachedChannels.Remove(toRemove);
}
_attachedChannelsToRemove.Clear();
}
}

#if SUBNAUTICA

[HarmonyPatch(typeof(FMODUWE), nameof(FMODUWE.PlayOneShotImpl))]
Expand Down Expand Up @@ -700,13 +912,13 @@ public static bool FMOD_CustomLoopingEmitter_OnPlay_Prefix(FMOD_CustomLoopingEmi
}
#endif

private static void SetChannel3DAttributes(Channel channel, Transform transform)
internal static void SetChannel3DAttributes(Channel channel, Transform transform)
{
ATTRIBUTES_3D attributes = transform.To3DAttributes();
channel.set3DAttributes(ref attributes.position, ref attributes.velocity);
}

private static void SetChannel3DAttributes(Channel channel, Vector3 position)
internal static void SetChannel3DAttributes(Channel channel, Vector3 position)
{
ATTRIBUTES_3D attributes = position.To3DAttributes();
channel.set3DAttributes(ref attributes.position, ref attributes.velocity);
Expand Down

0 comments on commit 5a7fa36

Please sign in to comment.