diff --git a/Nautilus/Extensions/FModExtensions.cs b/Nautilus/Extensions/FModExtensions.cs new file mode 100644 index 000000000..aa8927736 --- /dev/null +++ b/Nautilus/Extensions/FModExtensions.cs @@ -0,0 +1,54 @@ +using FMOD; +using FMODUnity; +using Nautilus.Patchers; +using Nautilus.Utility; + +namespace Nautilus.Extensions; + +/// +/// Contains extension methods for the FMOD system. +/// +public static class FModExtensions +{ + /// + /// Adds a fade-out point for the specified sound. + /// + /// The sound to add a fade-out to + /// The duration of the fade-out. + /// Fades are only triggered when an emitter respects them. E.G: when calling FMOD_CustomEmitter.Stop(STOP_MODE.ALLOWFADEOUT). + public static void AddFadeOut(this Sound sound, float seconds) + { + if (!sound.hasHandle()) + { + InternalLogger.Error("AddFadeOut: Sound object is missing. Please provide a valid sound object."); + return; + } + + CustomSoundPatcher.FadeOuts[sound.handle] = new CustomSoundPatcher.FadeInfo(sound, seconds); + } + + /// + /// Adds a fade-out point for the specified channel. + /// + /// The channel to add a fade-out to + /// The duration of the fade-out. The fade-out starts at the current time. + /// The DSP clock at the point where the fade was added.
+ /// DSP clock consists of 48_000 ticks per second. For more information, please refer to the FMOD docs. + /// This method only applies the fade-out one time. If you want the fade to stay everytime the sound is played, consider using . + public static void AddFadeOut(this Channel channel, float seconds, out ulong dspClock) + { + if (!channel.hasHandle()) + { + InternalLogger.Error("AddFadeOut: Channel object is invalid. Fade operation is cancelled."); + dspClock = 0; + return; + } + + RuntimeManager.CoreSystem.getSoftwareFormat(out int samplesRate, out _, out _); + + channel.getDSPClock(out _, out ulong parentClock); + channel.addFadePoint(parentClock, 1f); + channel.addFadePoint(parentClock + (ulong)(samplesRate * seconds), 0f); + dspClock = parentClock; + } +} \ No newline at end of file diff --git a/Nautilus/Patchers/CustomSoundPatcher.cs b/Nautilus/Patchers/CustomSoundPatcher.cs index 7d4a2de76..a46fa87ba 100644 --- a/Nautilus/Patchers/CustomSoundPatcher.cs +++ b/Nautilus/Patchers/CustomSoundPatcher.cs @@ -1,24 +1,29 @@ +using System; using System.Collections.Generic; using FMOD; using FMOD.Studio; using FMODUnity; using HarmonyLib; +using Nautilus.Extensions; using Nautilus.FMod.Interfaces; using Nautilus.Handlers; using Nautilus.Utility; using UnityEngine; using UnityEngine.Playables; +using STOP_MODE = FMOD.Studio.STOP_MODE; namespace Nautilus.Patchers; internal class CustomSoundPatcher { internal record struct AttachedChannel(Channel Channel, Transform Transform); + internal record struct FadeInfo(Sound Sound, float Seconds); internal static readonly SelfCheckingDictionary CustomSounds = new("CustomSounds"); internal static readonly SelfCheckingDictionary CustomSoundBuses = new("CustomSoundBuses"); internal static readonly SelfCheckingDictionary CustomFModSounds = new("CustoomFModSounds"); internal static readonly Dictionary EmitterPlayedChannels = new(); + internal static readonly Dictionary FadeOuts = new(); internal static List AttachedChannels = new(); private static readonly Dictionary PlayedChannels = new(); @@ -159,10 +164,14 @@ public static bool FMODEventPlayableBehavior_OnExit_Prefix(FMODEventPlayableBeha return false; } - if (__instance.stopType != FMODUnity.STOP_MODE.None) + if (__instance.stopType == FMODUnity.STOP_MODE.Immediate) { channel.stop(); } + else if (__instance.stopType == FMODUnity.STOP_MODE.AllowFadeout) + { + TryFadeOutBeforeStop(channel); + } PlayableBehaviorChannels.Remove(__instance); __instance.isPlayheadInside = false; @@ -440,11 +449,18 @@ public static bool FMOD_CustomEmitter_Play_Prefix(FMOD_CustomEmitter __instance) [HarmonyPatch(typeof(FMOD_CustomEmitter), nameof(FMOD_CustomEmitter.Stop))] [HarmonyPrefix] - public static bool FMOD_CustomEmitter_Stop_Prefix(FMOD_CustomEmitter __instance) + public static bool FMOD_CustomEmitter_Stop_Prefix(FMOD_CustomEmitter __instance, STOP_MODE stopMode) { if (!EmitterPlayedChannels.TryGetValue(__instance.GetInstanceID(), out var channel)) return true; - channel.stop(); + if (stopMode == STOP_MODE.IMMEDIATE) + { + channel.stop(); + } + else + { + TryFadeOutBeforeStop(channel); + } __instance._playing = false; __instance.OnStop(); @@ -486,7 +502,8 @@ public static bool FMOD_CustomEmitter_ReleaseEvent_Prefix(FMOD_CustomEmitter __i if (__instance.asset == null || !CustomSounds.ContainsKey(__instance.asset.path) && !CustomFModSounds.ContainsKey(__instance.asset.path)) return true; if (!EmitterPlayedChannels.TryGetValue(__instance.GetInstanceID(), out var channel)) return false; // known sound but not played yet - channel.stop(); + TryFadeOutBeforeStop(channel); + EmitterPlayedChannels.Remove(__instance.GetInstanceID()); return false; @@ -761,14 +778,22 @@ public static bool FMOD_CustomEmitter_Play_Prefix(FMOD_CustomEmitter __instance) [HarmonyPatch(typeof(FMOD_CustomEmitter), nameof(FMOD_CustomEmitter.Stop))] [HarmonyPrefix] - public static bool FMOD_CustomEmitter_Stop_Prefix(FMOD_CustomEmitter __instance) + public static bool FMOD_CustomEmitter_Stop_Prefix(FMOD_CustomEmitter __instance, STOP_MODE stopMode) { if (!EmitterPlayedChannels.TryGetValue(__instance.GetInstanceID(), out Channel channel)) { return true; } - channel.stop(); + if (stopMode == STOP_MODE.ALLOWFADEOUT) + { + TryFadeOutBeforeStop(channel); + } + else + { + channel.stop(); + } + __instance._playing = false; __instance.OnStop(); @@ -827,7 +852,9 @@ public static bool FMOD_CustomEmitter_ReleaseEvent_Prefix(FMOD_CustomEmitter __i return false; // known sound but not played yet } - channel.stop(); + TryFadeOutBeforeStop(channel); + + EmitterPlayedChannels.Remove(__instance.GetInstanceID()); return false; @@ -923,4 +950,23 @@ internal static void SetChannel3DAttributes(Channel channel, Vector3 position) ATTRIBUTES_3D attributes = position.To3DAttributes(); channel.set3DAttributes(ref attributes.position, ref attributes.velocity); } + + private static bool TryFadeOutBeforeStop(Channel channel) + { + if (channel.getCurrentSound(out var sound) != RESULT.OK || !FadeOuts.TryGetValue(sound.handle, out var fadeOut)) + { + channel.stop(); + return false; + } + + channel.getDelay(out ulong _, out ulong _, out bool stopChannels); + + if (stopChannels) + return false; + + RuntimeManager.CoreSystem.getSoftwareFormat(out var samplesRate, out _, out _); + channel.AddFadeOut(fadeOut.Seconds, out var dspClock); + channel.setDelay(0, dspClock + (ulong)(samplesRate * fadeOut.Seconds)); + return true; + } } \ No newline at end of file diff --git a/Nautilus/Utility/AudioUtils.cs b/Nautilus/Utility/AudioUtils.cs index af1ef081f..8e4666aab 100644 --- a/Nautilus/Utility/AudioUtils.cs +++ b/Nautilus/Utility/AudioUtils.cs @@ -26,7 +26,7 @@ public static partial class AudioUtils /// For music, PDA voices and any 2D sounds that can have more than one instance at a time. /// public const MODE StandardSoundModes_Stream = StandardSoundModes_2D | MODE.CREATESTREAM; - + private static FMOD.System FMOD_System => RuntimeManager.CoreSystem; /// @@ -84,14 +84,8 @@ public static IEnumerable CreateSounds(IEnumerable soundPaths, MO public static bool TryPlaySound(Sound sound, string busPath, out Channel channel) { channel = default; - Bus bus = RuntimeManager.GetBus(busPath); - if (bus.getChannelGroup(out ChannelGroup channelGroup) != RESULT.OK || !channelGroup.hasHandle()) - { - bus.lockChannelGroup(); - } - return bus.getChannelGroup(out channelGroup) == RESULT.OK && - channelGroup.getPaused(out bool paused) == RESULT.OK && - FMOD_System.playSound(sound, channelGroup, paused, out channel) == RESULT.OK; + var bus = RuntimeManager.GetBus(busPath); + return TryPlaySound(sound, bus, out channel); } /// @@ -108,9 +102,17 @@ public static bool TryPlaySound(Sound sound, Bus bus, out Channel channel) { bus.lockChannelGroup(); } - return bus.getChannelGroup(out channelGroup) == RESULT.OK && + + var success = bus.getChannelGroup(out channelGroup) == RESULT.OK && channelGroup.getPaused(out bool paused) == RESULT.OK && FMOD_System.playSound(sound, channelGroup, paused, out channel) == RESULT.OK; + + if (!success) + { + return false; + } + + return true; } ///