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;
}
///