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!;