0.1.9 | Centralized Audio loading & BingBong voices

This commit is contained in:
2026-03-08 20:22:00 +01:00
parent eecbd505f2
commit c81c950166
7 changed files with 364 additions and 262 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -15,14 +16,26 @@ class AudioSyncService
{
public static string API_BASE_URL => ConfigHandler.BugleSoundAPIURL.Value;
private static AudioSyncService? Instance { get; set; }
public static AudioSyncService GetInstance()
public async static Task<bool> DownloadAPIAudio(APIAudioFormat apiAudio, string SoundsDirectory, Song? existingSong = null)
{
Instance ??= new AudioSyncService();
return Instance;
bool success = true;
try
{
if (existingSong != null && apiAudio.Filename != existingSong.Name)
{
File.Delete(Path.Combine(SoundsDirectory, $"{existingSong.Name}.{existingSong.Extension}"));
}
await apiAudio.DownloadToFolder(SoundsDirectory);
}
catch (Exception ex)
{
Debug.LogError($"Failed to download API audio: {ex.Message}");
success = false;
}
return success;
}
public List<APIAudioFormat> GetAudioClips()
public static List<APIAudioFormat> GetAudioClips()
{
List<APIAudioFormat> audioClips = [];
@@ -48,6 +61,19 @@ class AudioSyncService
return audioClips;
}
public static void ClearAudioClips()
{
foreach (Song song in Song.Sounds.Values.ToList())
{
song.Dispose();
}
Song.Sounds.Clear();
Song.SoundsByHash.Clear();
Song.Songs.Clear();
Song.BB_VoiceLines.Clear();
GC.Collect();
}
public class APIAudioFormat
{
[JsonProperty("_id")]
@@ -105,6 +131,164 @@ class AudioSyncService
}
class AudioSyncWorker
{
private static AudioSyncWorker? Instance { get; set; }
public static AudioSyncWorker GetInstance()
{
Instance ??= new AudioSyncWorker();
return Instance;
}
public static readonly string SoundsDirectory = Path.Combine(BepInEx.Paths.BepInExRootPath, "bugleSounds");
public static readonly Dictionary<string, AudioType> AudioTypes = new()
{
{ "wav", AudioType.WAV },
{ "mp3", AudioType.MPEG },
{ "ogg", AudioType.OGGVORBIS },
{ "aiff", AudioType.AIFF },
};
public static bool IsLoading = false;
public static bool IsSyncing = false;
public static int CurrentSongIndex = 0;
public static string CurrentSongName = "None";
public static Action? OnAudioLoadComplete;
public static void GetAudioClips()
{
if (IsLoading || IsSyncing) return;
if (!Directory.Exists(SoundsDirectory)) return;
IsLoading = true;
Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory));
}
public static void TrySyncAndLoadAudioClips()
{
if (IsLoading || IsSyncing) return;
Task.Run(() =>
{
SyncAndLoadAudioClipsCoroutine().GetAwaiter().GetResult();
});
}
private static IEnumerator LoadAllAudioClipsCoroutine(string directoryPath, string[]? forceReload = null)
{
List<(string filePath, string ext, string name)> filesToLoad = new();
foreach (var ext in AudioTypes.Keys)
{
var files = Directory.GetFiles(directoryPath, $"*.{ext}");
foreach (var file in files)
{
string name = Path.GetFileNameWithoutExtension(file);
bool shouldForceReload = forceReload != null && forceReload.Contains($"{name}.{ext}");
if (!Song.Sounds.ContainsKey(name) || shouldForceReload)
{
filesToLoad.Add((file, ext, name));
}
}
}
const int BATCH_SIZE = 2;
int loadedCount = 0;
for (int i = 0; i < filesToLoad.Count; i += BATCH_SIZE)
{
List<Coroutine> loadCoroutines = [];
for (int j = i; j < i + Math.Min(BATCH_SIZE, filesToLoad.Count - i) && j < filesToLoad.Count; j++)
{
var (filePath, ext, name) = filesToLoad[j];
bool forceReloadClip = forceReload != null && forceReload.Contains($"{name}.{ext}");
Coroutine loadCoroutine = Plugin.Instance.StartCoroutine(LoadAudioClipCoroutine(filePath, ext, name, forceReloadClip));
loadCoroutines.Add(loadCoroutine);
}
foreach (var coroutine in loadCoroutines) yield return coroutine;
loadedCount += loadCoroutines.Count;
BetterBugleUI.Instance?.ShowActionbar($"Loading audio clips... {loadedCount}/{filesToLoad.Count}");
}
OnAudioLoadComplete?.Invoke();
IsLoading = false;
}
private static IEnumerator LoadAudioClipCoroutine(string filePath, string ext, string name, bool forceReload = false)
{
using UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip($"file://{filePath}", AudioTypes[ext]);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success) yield break;
bool songExists = Song.Sounds.ContainsKey(name);
if (songExists && !forceReload) yield break;
if (songExists && forceReload)
{
Song? previousSong = Song.Sounds.TryGetValue(name, out var existingSong) ? existingSong : null;
previousSong?.Dispose();
}
AudioClip audioClip = DownloadHandlerAudioClip.GetContent(www);
if (audioClip == null) yield break;
Song song = new(name, ext, filePath, audioClip);
song.Register();
}
private static async Task SyncAndLoadAudioClipsCoroutine()
{
if (IsLoading || IsSyncing) return;
IsSyncing = true;
Dictionary<AudioSyncService.APIAudioFormat, Song?> toDownload = new();
string[] existingSongNames = [.. Song.Sounds.Keys];
AudioSyncService.APIAudioFormat[] existingAPIFormats = [.. AudioSyncService.GetAudioClips()];
string[] apiExistingNames = [.. existingAPIFormats.Select(apiAudio => apiAudio.Filename)];
var songsToRemove = existingSongNames.Except(apiExistingNames).ToArray();
foreach (var songName in songsToRemove)
{
if (Song.Sounds.TryGetValue(songName, out var songToDispose))
{
songToDispose.Dispose();
songToDispose.DeleteFile();
}
}
foreach (AudioSyncService.APIAudioFormat apiAudio in existingAPIFormats)
{
Song? existingSong = Song.SoundsByHash.GetValueOrDefault(apiAudio.Hash);
if (existingSong == null || existingSong.Hash != apiAudio.Hash)
{
toDownload.Add(apiAudio, existingSong);
}
}
BetterBugleUI.Instance?.ShowActionbar($"Syncing audio bank... {toDownload.Count} changed/new files found.");
string[] filesToOverload = [];
foreach (AudioSyncService.APIAudioFormat apiAudio in toDownload.Keys)
{
bool success = await AudioSyncService.DownloadAPIAudio(apiAudio, SoundsDirectory, toDownload[apiAudio]);
if (success)
{
Debug.Log($"Successfully downloaded audio: {apiAudio.Filename}.{apiAudio.Extension}, adding to forceload");
filesToOverload = [.. filesToOverload, $"{apiAudio.Filename}.{apiAudio.Extension}"];
}
}
IsSyncing = false;
IsLoading = true;
Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory, filesToOverload));
}
}
public class Song : IDisposable
{
public static readonly Dictionary<string, Song> Sounds = new();
@@ -180,7 +364,7 @@ public class Song : IDisposable
public void DeleteFile()
{
if (AudioClip == null) return;
var filePath = Path.Combine(BetterBugleModule.SoundsDirectory, $"{Name}.{Extension}");
var filePath = Path.Combine(AudioSyncWorker.SoundsDirectory, $"{Name}.{Extension}");
if (File.Exists(filePath))
{
File.Delete(filePath);