Skip to content

Commit

Permalink
feat: FMOD fade-outs (#564)
Browse files Browse the repository at this point in the history
* Added Fade out extensions

* Patches to allow fade outs

* Slight refactoring

* Fixed compiler error

* Fix fade out not working as intended

---------

Co-authored-by: Lee23 <[email protected]>
  • Loading branch information
Metious and LeeTwentyThree authored Nov 13, 2024
1 parent 536af8c commit 9d4a89e
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 17 deletions.
54 changes: 54 additions & 0 deletions Nautilus/Extensions/FModExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using FMOD;
using FMODUnity;
using Nautilus.Patchers;
using Nautilus.Utility;

namespace Nautilus.Extensions;

/// <summary>
/// Contains extension methods for the FMOD system.
/// </summary>
public static class FModExtensions
{
/// <summary>
/// Adds a fade-out point for the specified sound.
/// </summary>
/// <param name="sound">The sound to add a fade-out to</param>
/// <param name="seconds">The duration of the fade-out.</param>
/// <remarks>Fades are only triggered when an emitter respects them. E.G: when calling <c>FMOD_CustomEmitter.Stop(STOP_MODE.ALLOWFADEOUT)</c>.</remarks>
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);
}

/// <summary>
/// Adds a fade-out point for the specified channel.
/// </summary>
/// <param name="channel">The channel to add a fade-out to</param>
/// <param name="seconds">The duration of the fade-out. The fade-out starts at the current time.</param>
/// <param name="dspClock">The DSP clock at the point where the fade was added.<br/>
/// DSP clock consists of 48_000 ticks per second. For more information, please refer to the <see href="https://documentation.help/fmod-studio-api/FMOD_Channel_GetDSPClock.html">FMOD docs</see>.</param>
/// <remarks>This method only applies the fade-out one time. If you want the fade to stay everytime the sound is played, consider using <see cref="AddFadeOut(FMOD.Sound,float)"/>.</remarks>
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;
}
}
60 changes: 53 additions & 7 deletions Nautilus/Patchers/CustomSoundPatcher.cs
Original file line number Diff line number Diff line change
@@ -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<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 readonly Dictionary<IntPtr, FadeInfo> FadeOuts = new();
internal static List<AttachedChannel> AttachedChannels = new();

private static readonly Dictionary<string, Channel> PlayedChannels = new();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
22 changes: 12 additions & 10 deletions Nautilus/Utility/AudioUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public const MODE StandardSoundModes_Stream = StandardSoundModes_2D | MODE.CREATESTREAM;

private static FMOD.System FMOD_System => RuntimeManager.CoreSystem;

/// <summary>
Expand Down Expand Up @@ -84,14 +84,8 @@ public static IEnumerable<Sound> CreateSounds(IEnumerable<string> 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);
}

/// <summary>
Expand All @@ -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;
}

/// <summary>
Expand Down

0 comments on commit 9d4a89e

Please sign in to comment.