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 a new track DiscoKapot.
|
||||||
- Added an accessibility option to reduce the intensity of overly distracting visual effects.
|
- 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.
|
- 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
|
## 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;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
|
@ -17,7 +16,6 @@ using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Networking;
|
|
||||||
|
|
||||||
namespace MuzikaGromche
|
namespace MuzikaGromche
|
||||||
{
|
{
|
||||||
|
|
@ -1023,75 +1021,47 @@ namespace MuzikaGromche
|
||||||
// Sort in place by name
|
// Sort in place by name
|
||||||
Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks);
|
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
|
#if DEBUG
|
||||||
foreach (var track in Tracks)
|
GlobalBehaviour.Instance.StartCoroutine(PreloadDebugAndExport(Tracks));
|
||||||
{
|
|
||||||
track.Debug();
|
|
||||||
}
|
|
||||||
Exporter.ExportTracksJSON(Tracks);
|
|
||||||
#endif
|
#endif
|
||||||
Config = new Config(base.Config);
|
Config = new Config(base.Config);
|
||||||
DiscoBallManager.Load();
|
DiscoBallManager.Load();
|
||||||
PoweredLightsAnimators.Load();
|
PoweredLightsAnimators.Load();
|
||||||
Harmony = new Harmony(MyPluginInfo.PLUGIN_NAME);
|
Harmony = new Harmony(MyPluginInfo.PLUGIN_NAME);
|
||||||
Harmony.PatchAll(typeof(GameNetworkManagerPatch));
|
Harmony.PatchAll(typeof(GameNetworkManagerPatch));
|
||||||
Harmony.PatchAll(typeof(JesterPatch));
|
Harmony.PatchAll(typeof(JesterPatch));
|
||||||
Harmony.PatchAll(typeof(EnemyAIPatch));
|
Harmony.PatchAll(typeof(EnemyAIPatch));
|
||||||
Harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch));
|
Harmony.PatchAll(typeof(PoweredLightsAnimatorsPatch));
|
||||||
Harmony.PatchAll(typeof(AllPoweredLightsPatch));
|
Harmony.PatchAll(typeof(AllPoweredLightsPatch));
|
||||||
Harmony.PatchAll(typeof(DiscoBallTilePatch));
|
Harmony.PatchAll(typeof(DiscoBallTilePatch));
|
||||||
Harmony.PatchAll(typeof(DiscoBallDespawnPatch));
|
Harmony.PatchAll(typeof(DiscoBallDespawnPatch));
|
||||||
Harmony.PatchAll(typeof(SpawnRatePatch));
|
Harmony.PatchAll(typeof(SpawnRatePatch));
|
||||||
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
Harmony.PatchAll(typeof(DeathScreenGameOverTextResetPatch));
|
||||||
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
Harmony.PatchAll(typeof(ScreenFiltersManager.HUDManagerScreenFiltersPatch));
|
||||||
NetcodePatcher();
|
Harmony.PatchAll(typeof(ClearAudioClipCachePatch));
|
||||||
Compatibility.Register(this);
|
NetcodePatcher();
|
||||||
}
|
Compatibility.Register(this);
|
||||||
else
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static IEnumerator PreloadDebugAndExport(ISelectableTrack[] tracks)
|
||||||
|
{
|
||||||
|
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()
|
private static void NetcodePatcher()
|
||||||
{
|
{
|
||||||
var types = Assembly.GetExecutingAssembly().GetTypes();
|
var types = Assembly.GetExecutingAssembly().GetTypes();
|
||||||
|
|
@ -3038,6 +3008,7 @@ namespace MuzikaGromche
|
||||||
Plugin.Log.LogInfo($"SetTrackClientRpc {name}");
|
Plugin.Log.LogInfo($"SetTrackClientRpc {name}");
|
||||||
if (Plugin.FindTrackNamed(name) is { } track)
|
if (Plugin.FindTrackNamed(name) is { } track)
|
||||||
{
|
{
|
||||||
|
AudioClipsCacheManager.LoadAudioTrack(track);
|
||||||
CurrentTrack = Config.OverrideCurrentTrack(track);
|
CurrentTrack = Config.OverrideCurrentTrack(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3117,7 +3088,18 @@ namespace MuzikaGromche
|
||||||
if (behaviour.CurrentTrack == null || behaviour.CurrentTrack.LoadedIntro == null || behaviour.CurrentTrack.LoadedLoop == null)
|
if (behaviour.CurrentTrack == null || behaviour.CurrentTrack.LoadedIntro == null || behaviour.CurrentTrack.LoadedLoop == null)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Plugin.Log.LogError("CurrentTrack is not set!");
|
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
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue