SHA256
283 lines
12 KiB
C#
283 lines
12 KiB
C#
using System;
|
|
using ADOFAIRenderer.Core;
|
|
using ADOFAIRenderer.Util.Audio;
|
|
using ADOFAIRenderer.Util.Audio.Clip;
|
|
using HarmonyLib;
|
|
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
|
|
namespace ADOFAIRenderer.Patch.Engine;
|
|
|
|
/// <summary>
|
|
/// Records the file path of each external clip the game loads (always active, also outside rendering,
|
|
/// because levels load their song and PlaySound files before a render starts).
|
|
/// FindOrLoadAudioClipExternal returns the clip already in audioLib when its key exists, so the path is recorded
|
|
/// only when the key is missing and the file is about to be loaded.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(AudioManager), nameof(AudioManager.FindOrLoadAudioClipExternal))]
|
|
public static class AudioManagerFindOrLoadAudioClipExternalPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static void Prefix(AudioManager __instance, string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path)) return;
|
|
if (__instance.audioLib.ContainsKey(AudioClipPaths.KeyOf(path))) return;
|
|
AudioClipPaths.Record(path);
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch(typeof(AudioManager), "_Play")]
|
|
public static class AudioManagerPlayPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
public static bool Prefix(string snd, double time, AudioMixerGroup group, float volume, int priority)
|
|
{
|
|
if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio)
|
|
{
|
|
// Cache the clip on the main thread
|
|
RenderManager.Instance.AudioMixer?.CacheClip(snd);
|
|
}
|
|
return true; // Let the original method execute to prevent NullReferenceException
|
|
}
|
|
|
|
[HarmonyPostfix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static void Postfix(AudioSource __result, string snd, double time, AudioMixerGroup group, float volume, int priority)
|
|
{
|
|
if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio && __result != null)
|
|
{
|
|
// Convert absolute DSP time to relative time in seconds from render start
|
|
double relativeStartTime = time - RenderManager.Instance.dspTimeBase;
|
|
|
|
// Register event to the offline mixer (default pitch = 1.0f) and map it to the AudioSource
|
|
RenderManager.Instance.AudioMixer?.RegisterSoundEvent(__result, snd, relativeStartTime, -1.0, volume, 1.0f, RenderManager.Instance.AudioTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// scrConductor.PlayWithEndTime: registers a looping effect voice until the end time. The AudioSource it creates with
|
|
/// AudioManager.MakeSource is mapped to that voice (<see cref="AudioManagerMakeSourcePatch"/>).
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(scrConductor), "PlayWithEndTime")]
|
|
public static class PlayWithEndTimePatch
|
|
{
|
|
/// <summary>State registered by the running PlayWithEndTime call, until its MakeSource returns. Main thread only.</summary>
|
|
internal static SourceState? PendingState;
|
|
|
|
[HarmonyFinalizer]
|
|
public static void Finalizer() => PendingState = null;
|
|
|
|
[HarmonyPrefix]
|
|
public static bool Prefix(string snd, double time, double endTime, float volume, int priority)
|
|
{
|
|
if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio)
|
|
{
|
|
var mixer = RenderManager.Instance.AudioMixer;
|
|
if (mixer != null)
|
|
{
|
|
// Cache the clip on the main thread
|
|
mixer.CacheClip(snd);
|
|
|
|
// Convert absolute DSP times to relative times in seconds from render start
|
|
double relativeStartTime = time - RenderManager.Instance.dspTimeBase;
|
|
double relativeEndTime = endTime - RenderManager.Instance.dspTimeBase;
|
|
|
|
// Register looping sound event to the offline mixer (default pitch = 1.0f)
|
|
PendingState = mixer.RegisterSoundEvent(null, snd, relativeStartTime, relativeEndTime, volume, 1.0f, RenderManager.Instance.AudioTime);
|
|
}
|
|
}
|
|
return true; // Let the original method execute
|
|
}
|
|
}
|
|
|
|
/// <summary>Maps the AudioSource created inside scrConductor.PlayWithEndTime to its state (<see cref="PlayWithEndTimePatch.PendingState"/>).</summary>
|
|
[HarmonyPatch(typeof(AudioManager), nameof(AudioManager.MakeSource))]
|
|
public static class AudioManagerMakeSourcePatch
|
|
{
|
|
[HarmonyPostfix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static void Postfix(AudioSource __result)
|
|
{
|
|
var state = PlayWithEndTimePatch.PendingState;
|
|
if (state == null || !__result) return;
|
|
PlayWithEndTimePatch.PendingState = null;
|
|
RenderManager.Instance.AudioMixer?.MapSource(__result, state);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AudioSource members of an effect source mapped to a <see cref="SourceState"/> during rendering: the patches below skip the
|
|
/// Unity implementation and read or change the state instead (isPlaying, time, timeSamples, pitch, SetScheduledEndTime, Stop).
|
|
/// Sources without a mapping (the song, menu sounds) use the Unity implementation.
|
|
/// </summary>
|
|
internal static class EffectSource
|
|
{
|
|
/// <summary>State mapped to <paramref name="source"/> while capturing, with the current render time in <paramref name="now"/>.</summary>
|
|
public static bool TryGet(AudioSource source, out SourceState state, out double now)
|
|
{
|
|
state = null!;
|
|
now = 0;
|
|
if (!RenderManager.IsCapturingActive) return false;
|
|
var mixer = RenderManager.Instance.AudioMixer;
|
|
if (mixer == null || !mixer.TryGetSourceState(source, out state)) return false;
|
|
now = RenderManager.Instance.AudioTime;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AudioSource.isPlaying getter (<see cref="EffectSource"/>): true from the scheduling call until Stop(), the scheduled end time or
|
|
/// the end of the clip, including before the scheduled start. AudioManager.Update returns a source to its reusable pool once this is false.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(AudioSource), "isPlaying", MethodType.Getter)]
|
|
public static class AudioSourceIsPlayingPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, ref bool __result)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
__result = state.IsPlaying(now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>AudioSource.time getter (<see cref="EffectSource"/>): clip position in seconds while playing, 0 while not playing.</summary>
|
|
[HarmonyPatch(typeof(AudioSource), "time", MethodType.Getter)]
|
|
public static class AudioSourceTimeGetterPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, ref float __result)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
__result = state.GetTime(now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AudioSource.time setter (<see cref="EffectSource"/>): moves the playback position (clip seconds) from the current render time.
|
|
/// Set before the scheduled start (ffxPlaySound sets it right after AudioManager.Play), it becomes the start position.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(AudioSource), "time", MethodType.Setter)]
|
|
public static class AudioSourceTimePatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, float value)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
state.SetTime(value, now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>AudioSource.timeSamples getter (<see cref="EffectSource"/>): clip position in samples of the clip.</summary>
|
|
[HarmonyPatch(typeof(AudioSource), "timeSamples", MethodType.Getter)]
|
|
public static class AudioSourceTimeSamplesGetterPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, ref int __result)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
__result = state.GetTimeSamples(now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>AudioSource.timeSamples setter (<see cref="EffectSource"/>): moves the playback position (samples of the clip).</summary>
|
|
[HarmonyPatch(typeof(AudioSource), "timeSamples", MethodType.Setter)]
|
|
public static class AudioSourceTimeSamplesPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, int value)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
state.SetTimeSamples(value, now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>AudioSource.pitch getter (<see cref="EffectSource"/>): the last pitch set.</summary>
|
|
[HarmonyPatch(typeof(AudioSource), "pitch", MethodType.Getter)]
|
|
public static class AudioSourcePitchGetterPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, ref float __result)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out _)) return true;
|
|
__result = state.GetPitch();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>AudioSource.pitch setter (<see cref="EffectSource"/>): changes the playback rate from the current render time.</summary>
|
|
[HarmonyPatch(typeof(AudioSource), "pitch", MethodType.Setter)]
|
|
public static class AudioSourcePitchPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, float value)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
state.SetPitch(value, now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>AudioSource.SetScheduledEndTime (<see cref="EffectSource"/>): DSP time (absolute) at which playback stops.</summary>
|
|
[HarmonyPatch(typeof(AudioSource), "SetScheduledEndTime", typeof(double))]
|
|
public static class AudioSourceSetScheduledEndTimePatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance, double time)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
state.SetEndTime(time - RenderManager.Instance.dspTimeBase, now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AudioSource.Stop() (<see cref="EffectSource"/>): stops playback at the current render time. The mapping stays, so the source
|
|
/// keeps reporting the stopped state. AudioManager.Update calls Stop() on sources it returns to the pool, which are no longer
|
|
/// playing, so their voices are left unchanged.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(AudioSource), "Stop", new Type[0])]
|
|
public static class AudioSourceStopPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
// ReSharper disable once InconsistentNaming
|
|
public static bool Prefix(AudioSource __instance)
|
|
{
|
|
if (!EffectSource.TryGet(__instance, out var state, out var now)) return true;
|
|
state.Stop(now);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AudioManager.StopAllSounds destroys every source in liveSources (all AudioManager._Play and scrConductor.PlayWithEndTime sources),
|
|
/// so every effect voice stops at the current render time, including ones scheduled to start later.
|
|
/// Callers include scrConductor.PlayHitTimes (from its second call), ScrubMusicToTile/Time, KillAllSoundsCoroutine,
|
|
/// scrController.FailAction and scnGame.Play.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(AudioManager), nameof(AudioManager.StopAllSounds))]
|
|
public static class AudioManagerStopAllSoundsPatch
|
|
{
|
|
[HarmonyPrefix]
|
|
public static void Prefix()
|
|
{
|
|
if (RenderManager.IsCapturingActive && RenderManager.Instance.Configuration.AudioConfig.RenderAudio)
|
|
{
|
|
RenderManager.Instance.AudioMixer?.StopAllEffects(RenderManager.Instance.AudioTime);
|
|
}
|
|
}
|
|
} |