diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index da1900d..a2c48d4 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -9,6 +9,7 @@ using System.Linq; using ADOFAIRenderer.API; using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Data; +using ADOFAIRenderer.Patch.Engine; using ADOFAIRenderer.UI; using ADOFAIRenderer.UI.Window; using ADOFAIRenderer.Util; @@ -424,6 +425,7 @@ internal class RenderManager : MonoBehaviour { AudioMixer.UpdateBGM(AudioTime, dspTimeBase); AudioMixer.Commit((frameCount + 1) / Configuration.RenderFPS); + EffectSource.DeactivateMappedSources(AudioMixer); } if (!texture) return; diff --git a/Patch/Engine/AudioPatches.cs b/Patch/Engine/AudioPatches.cs index 0202995..9d9c2f1 100644 --- a/Patch/Engine/AudioPatches.cs +++ b/Patch/Engine/AudioPatches.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using ADOFAIRenderer.Core; using ADOFAIRenderer.Util.Audio; using ADOFAIRenderer.Util.Audio.Clip; @@ -51,7 +52,10 @@ public static class AudioManagerPlayPatch 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); + var mixer = RenderManager.Instance.AudioMixer; + if (mixer == null) return; + mixer.RegisterSoundEvent(__result, snd, relativeStartTime, -1.0, volume, 1.0f, RenderManager.Instance.AudioTime); + EffectSource.DeactivateAtFrameEnd(__result); } } } @@ -103,18 +107,53 @@ public static class AudioManagerMakeSourcePatch var state = PlayWithEndTimePatch.PendingState; if (state == null || !__result) return; PlayWithEndTimePatch.PendingState = null; - RenderManager.Instance.AudioMixer?.MapSource(__result, state); + var mixer = RenderManager.Instance.AudioMixer; + if (mixer == null) return; + mixer.MapSource(__result, state); + EffectSource.DeactivateAtFrameEnd(__result); } } /// /// AudioSource members of an effect source mapped to a 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. +/// The patches apply only while returns true: rendering is capturing (RenderManager.IsCapturingActive, +/// stage Render), the render has an audio mixer (created when the render starts, cleared when it ends), and the source is mapped +/// in that mixer (mappings are created only when RenderAudio is on). Otherwise they run the Unity implementation, so sources +/// without a mapping (the song, menu sounds) and all sources outside rendering behave as in the game. +/// +/// The game object of a mapped source is deactivated at the end of the frame it was mapped in: the mixer produces its sound and the +/// state answers every member above, so Unity does not process the source. The game activates it again when AudioManager.MakeSource +/// reuses it. Active AudioSources add main-thread time per frame in proportion to their count, and dense levels keep thousands of +/// scheduled effect sources alive. /// internal static class EffectSource { - /// State mapped to while capturing, with the current render time in . + /// Sources mapped during the current frame, deactivated by . Main thread only. + private static readonly List PendingDeactivation = new(); + + /// Deactivates the game object of at the end of the current frame (). + public static void DeactivateAtFrameEnd(AudioSource source) => PendingDeactivation.Add(source); + + /// + /// Deactivates the game objects of the sources mapped this frame that are still mapped and not destroyed. + /// Called by RenderManager.OnPostFrame after the audio commands of the frame are committed, which runs only in stage Render with + /// an audio mixer; sources are added only by the render-time registrations (AudioManager._Play and scrConductor.PlayWithEndTime + /// patches with RenderAudio on). + /// + public static void DeactivateMappedSources(OfflineAudioMixer mixer) + { + foreach (var source in PendingDeactivation) + { + if (source && mixer.TryGetSourceState(source, out _)) source.gameObject.SetActive(false); + } + PendingDeactivation.Clear(); + } + + /// + /// State mapped to in the mixer of the current render, with the current render time in . + /// False outside rendering (stage below Render), without a mixer, or for a source that is not mapped. + /// public static bool TryGet(AudioSource source, out SourceState state, out double now) { state = null!;