forked from nikita/muzika-gromche
Load audio clips on demand, implement cache
Reduces cold-boot memory usage by 400 MB for the current playlist of 58 audio files (27.8 MB).
This commit is contained in:
parent
f83f2a72ba
commit
a4cee92d00
|
|
@ -8,6 +8,7 @@
|
|||
- Added a new track DiscoKapot.
|
||||
- Added an accessibility option to reduce the intensity of overly distracting visual effects.
|
||||
- Seasonal content like New Year's songs (IkWilJe, Paarden, DiscoKapot) will only be available for selection during their respective seasons.
|
||||
- Reduced memory usage by almost 400 MB, thanks to loading audio clips on demand (not preloading all tracks at launch).
|
||||
|
||||
## MuzikaGromche 1337.9001.3 - v73 Happy New Year Edition
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
using HarmonyLib;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace MuzikaGromche;
|
||||
|
||||
internal static class AudioClipsCacheManager
|
||||
{
|
||||
// Cache of file names to loaded AudioClips.
|
||||
// Cache is cleared at the end of each round.
|
||||
static readonly Dictionary<string, AudioClip> Cache = [];
|
||||
|
||||
// In-flight requests
|
||||
static readonly Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> Requests = [];
|
||||
|
||||
// Not just isDone status, but also whether all requests have been processed.
|
||||
public static bool AllDone => Requests.Count == 0;
|
||||
|
||||
public static void LoadAudioTrack(IAudioTrack track)
|
||||
{
|
||||
GlobalBehaviour.Instance.StartCoroutine(LoadAudioTrackCoroutine(track));
|
||||
}
|
||||
|
||||
static IEnumerator LoadAudioTrackCoroutine(IAudioTrack track)
|
||||
{
|
||||
List<UnityWebRequest> requests = [];
|
||||
requests.Capacity = 2;
|
||||
|
||||
LoadAudioClip(requests, track.AudioType, track.FileNameIntro, clip => track.LoadedIntro = clip);
|
||||
LoadAudioClip(requests, track.AudioType, track.FileNameLoop, clip => track.LoadedLoop = clip);
|
||||
|
||||
yield return new WaitUntil(() => requests.All(request => request.isDone));
|
||||
|
||||
if (requests.All(request => request.result == UnityWebRequest.Result.Success))
|
||||
{
|
||||
foreach (var request in requests)
|
||||
{
|
||||
foreach (var (fileName, (Request, Setters)) in Requests)
|
||||
{
|
||||
if (request == Request)
|
||||
{
|
||||
Plugin.Log.LogDebug($"Audio clip loaded successfully: {fileName}");
|
||||
var clip = DownloadHandlerAudioClip.GetContent(request);
|
||||
Cache[fileName] = clip;
|
||||
foreach (var setter in Setters)
|
||||
{
|
||||
setter(clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var failed = Requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl());
|
||||
Plugin.Log.LogError("Could not load audio file " + string.Join(", ", failed));
|
||||
}
|
||||
|
||||
// cleanup
|
||||
foreach (var request in requests)
|
||||
{
|
||||
// collect matching keys first to avoid mutating Requests while iterating it
|
||||
var fileNames = Requests
|
||||
.Where(kv => kv.Value.Request == request)
|
||||
.Select(kv => kv.Key)
|
||||
.ToArray();
|
||||
|
||||
foreach (var fileName in fileNames)
|
||||
{
|
||||
if (Requests.TryGetValue(fileName, out var tuple) && tuple.Request != null)
|
||||
{
|
||||
tuple.Request.Dispose();
|
||||
}
|
||||
|
||||
Requests.Remove(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static readonly string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
static void LoadAudioClip(List<UnityWebRequest> requests, AudioType audioType, string fileName, Action<AudioClip> setter)
|
||||
{
|
||||
if (Cache.TryGetValue(fileName, out var cachedClip))
|
||||
{
|
||||
Plugin.Log.LogDebug($"Found cached audio clip: {fileName}");
|
||||
setter(cachedClip);
|
||||
}
|
||||
else if (Requests.TryGetValue(fileName, out var tuple))
|
||||
{
|
||||
Plugin.Log.LogDebug($"Found existing in-flight request for audio clip: {fileName}");
|
||||
tuple.Setters.Add(setter);
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin.Log.LogDebug($"Sending request to load audio clip: {fileName}");
|
||||
var request = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{fileName}", audioType);
|
||||
request.SendWebRequest();
|
||||
Requests[fileName] = (request, [setter]);
|
||||
requests.Add(request);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
// Iterate over LoadedClipsCache keys and values, cross join with Plugin.Tracks list,
|
||||
// find AudioTracks that reference the key (file name), and null their corresponding loaded tracks;
|
||||
// then destroy tracks and clear the cache.
|
||||
|
||||
Plugin.Log.LogDebug($"Clearing {Cache.Count} cached audio clips and {Requests.Count} pending requests");
|
||||
|
||||
if (Cache.Count > 0)
|
||||
{
|
||||
var allTracks = Plugin.Tracks.SelectMany(t => t.GetTracks()).ToArray();
|
||||
|
||||
foreach (var (fileName, clip) in Cache)
|
||||
{
|
||||
foreach (var track in allTracks)
|
||||
{
|
||||
// Null out any references to this clip on matching file names.
|
||||
if (track.FileNameIntro == fileName)
|
||||
{
|
||||
track.LoadedIntro = null;
|
||||
}
|
||||
if (track.FileNameLoop == fileName)
|
||||
{
|
||||
track.LoadedLoop = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (clip != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(clip);
|
||||
}
|
||||
}
|
||||
|
||||
Cache.Clear();
|
||||
}
|
||||
|
||||
foreach (var (fileName, (Request, Setters)) in Requests)
|
||||
{
|
||||
if (Request != null)
|
||||
{
|
||||
Request.Abort();
|
||||
Request.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Requests.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(RoundManager))]
|
||||
static class ClearAudioClipCachePatch
|
||||
{
|
||||
[HarmonyPatch(nameof(RoundManager.DespawnPropsAtEndOfRound))]
|
||||
[HarmonyPatch(nameof(RoundManager.OnDestroy))]
|
||||
[HarmonyPrefix]
|
||||
static void OnDestroy(RoundManager __instance)
|
||||
{
|
||||
var _ = __instance;
|
||||
AudioClipsCacheManager.Clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace MuzikaGromche;
|
||||
|
||||
// A global MonoBehaviour instance to run coroutines from non-MonoBehaviour or static context.
|
||||
internal static class GlobalBehaviour
|
||||
{
|
||||
sealed class AdhocBehaviour : MonoBehaviour;
|
||||
|
||||
static AdhocBehaviour? instance = null;
|
||||
|
||||
public static MonoBehaviour Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
var go = new GameObject("MuzikaGromche_GlobalBehaviour", [
|
||||
typeof(AdhocBehaviour),
|
||||
])
|
||||
{
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
Object.DontDestroyOnLoad(go);
|
||||
instance = go.GetComponent<AdhocBehaviour>();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ using LethalConfig.ConfigItems.Options;
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
|
|
@ -17,7 +16,6 @@ using System.Security.Cryptography;
|
|||
using System.Text;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace MuzikaGromche
|
||||
{
|
||||
|
|
@ -1023,50 +1021,8 @@ namespace MuzikaGromche
|
|||
// Sort in place by name
|
||||
Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks);
|
||||
|
||||
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
Dictionary<string, (UnityWebRequest Request, List<Action<AudioClip>> Setters)> requests = [];
|
||||
requests.EnsureCapacity(Tracks.Length * 2);
|
||||
|
||||
foreach (var track in Tracks.SelectMany(track => track.GetTracks()))
|
||||
{
|
||||
foreach (var (fileName, setter) in new (string, Action<AudioClip>)[]
|
||||
{
|
||||
(track.FileNameIntro, clip => track.LoadedIntro = clip),
|
||||
(track.FileNameLoop, clip => track.LoadedLoop = clip),
|
||||
})
|
||||
{
|
||||
if (requests.TryGetValue(fileName, out var tuple))
|
||||
{
|
||||
tuple.Setters.Add(setter);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = UnityWebRequestMultimedia.GetAudioClip($"file://{dir}/{fileName}", track.AudioType);
|
||||
request.SendWebRequest();
|
||||
requests[fileName] = (request, [setter]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!requests.Values.All(tuple => tuple.Request.isDone)) { }
|
||||
|
||||
if (requests.Values.All(tuple => tuple.Request.result == UnityWebRequest.Result.Success))
|
||||
{
|
||||
|
||||
foreach (var (fileName, tuple) in requests)
|
||||
{
|
||||
var clip = DownloadHandlerAudioClip.GetContent(tuple.Request);
|
||||
foreach (var setter in tuple.Setters)
|
||||
{
|
||||
setter(clip);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
foreach (var track in Tracks)
|
||||
{
|
||||
track.Debug();
|
||||
}
|
||||
Exporter.ExportTracksJSON(Tracks);
|
||||
GlobalBehaviour.Instance.StartCoroutine(PreloadDebugAndExport(Tracks));
|
||||
#endif
|
||||
Config = new Config(base.Config);
|
||||
DiscoBallManager.Load();
|
||||
|
|
@ -1082,15 +1038,29 @@ namespace MuzikaGromche
|
|||
Harmony.PatchAll(typeof(SpawnRatePatch));
|
||||
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
||||
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
||||
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
||||
NetcodePatcher();
|
||||
Compatibility.Register(this);
|
||||
}
|
||||
else
|
||||
|
||||
#if DEBUG
|
||||
static IEnumerator PreloadDebugAndExport(ISelectableTrack[] tracks)
|
||||
{
|
||||
var failed = requests.Values.Where(tuple => tuple.Request.result != UnityWebRequest.Result.Success).Select(tuple => tuple.Request.GetUrl());
|
||||
Logger.LogError("Could not load audio file " + string.Join(", ", failed));
|
||||
foreach (var track in tracks.SelectMany(track => track.GetTracks()))
|
||||
{
|
||||
AudioClipsCacheManager.LoadAudioTrack(track);
|
||||
}
|
||||
yield return new WaitUntil(() => AudioClipsCacheManager.AllDone);
|
||||
Log.LogDebug("All tracks preloaded, exporting to JSON");
|
||||
|
||||
foreach (var track in tracks)
|
||||
{
|
||||
track.Debug();
|
||||
}
|
||||
Exporter.ExportTracksJSON(tracks);
|
||||
AudioClipsCacheManager.Clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
private static void NetcodePatcher()
|
||||
{
|
||||
|
|
@ -3038,6 +3008,7 @@ namespace MuzikaGromche
|
|||
Plugin.Log.LogInfo($"SetTrackClientRpc {name}");
|
||||
if (Plugin.FindTrackNamed(name) is { } track)
|
||||
{
|
||||
AudioClipsCacheManager.LoadAudioTrack(track);
|
||||
CurrentTrack = Config.OverrideCurrentTrack(track);
|
||||
}
|
||||
}
|
||||
|
|
@ -3117,7 +3088,18 @@ namespace MuzikaGromche
|
|||
if (behaviour.CurrentTrack == null || behaviour.CurrentTrack.LoadedIntro == null || behaviour.CurrentTrack.LoadedLoop == null)
|
||||
{
|
||||
#if DEBUG
|
||||
if (behaviour.CurrentTrack == null)
|
||||
{
|
||||
Plugin.Log.LogError("CurrentTrack is not set!");
|
||||
}
|
||||
else if (AudioClipsCacheManager.AllDone)
|
||||
{
|
||||
Plugin.Log.LogError("Failed to load audio clips, no in-flight requests running");
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin.Log.LogDebug($"Waiting for audio clips to load");
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue