diff --git a/CHANGELOG.md b/CHANGELOG.md index e04ddf7..c24f4d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,4 +41,9 @@ # v0.1.8 | Changed dependencies -- Added ModConfig and PeakPresence (my mod), Updated BepInEx \ No newline at end of file +- Added ModConfig and PeakPresence (my mod), Updated BepInEx + +# v0.1.9 | AudioSyncWorker & Dynamic BingBong voices + +- Centralized Audio loading in Worker class +- Now load BingBong voicelines dynamically after Sound reloads. \ No newline at end of file diff --git a/src/JordanMod/JordanMod.csproj b/src/JordanMod/JordanMod.csproj index 07a81ed..93badc5 100644 --- a/src/JordanMod/JordanMod.csproj +++ b/src/JordanMod/JordanMod.csproj @@ -8,7 +8,7 @@ JordanMod - 0.1.8 + 0.1.9 diff --git a/src/JordanMod/modules/better_bugle/BetterBugleModule.cs b/src/JordanMod/modules/better_bugle/BetterBugleModule.cs index 22bec4a..e070f1d 100644 --- a/src/JordanMod/modules/better_bugle/BetterBugleModule.cs +++ b/src/JordanMod/modules/better_bugle/BetterBugleModule.cs @@ -24,19 +24,6 @@ class BetterBugleModule : Module public override string ModuleName => "BetterBugle"; public static readonly string bugleItemName = "Bugle"; - public static readonly string SoundsDirectory = Path.Combine(BepInEx.Paths.BepInExRootPath, "bugleSounds"); - public static readonly Dictionary AudioTypes = new() - { - { "wav", AudioType.WAV }, - { "mp3", AudioType.MPEG }, - { "ogg", AudioType.OGGVORBIS }, - { "aiff", AudioType.AIFF }, - }; - - public static bool IsLoading { get; private set; } = false; - public static bool IsSyncing { get; private set; } = false; - public static int CurrentSongIndex { get; set; } = 0; - public static string CurrentSongName { get; set; } = "None"; public static bool HadConfirmation { get; set; } = false; public static bool IsPlaying = false; @@ -51,9 +38,10 @@ class BetterBugleModule : Module { if (Instance != null) return; Instance = this; - SceneManager.sceneLoaded += OnSceneLoaded; ManageLocalizedText(); - GetAudioClips(); + SceneManager.sceneLoaded += OnSceneLoaded; + AudioSyncWorker.OnAudioLoadComplete += OnAllAudioClipsLoaded; + AudioSyncWorker.GetAudioClips(); base.Initialize(); } @@ -61,13 +49,13 @@ class BetterBugleModule : Module { if (Input.GetKeyDown(ConfigHandler.SyncAudioRepository.Value)) { - Instance?.TrySyncAndLoadAudioClips(); + AudioSyncWorker.TrySyncAndLoadAudioClips(); } if (Input.GetKeyDown(ConfigHandler.FavoriteSongToggleKey.Value)) { if (Character.localCharacter == null) return; if (Song.Songs.Count == 0) return; - if (!Song.Songs.ContainsKey(CurrentSongName)) return; + if (!Song.Songs.ContainsKey(AudioSyncWorker.CurrentSongName)) return; Character character = Character.localCharacter; Optionable selectedSlot = character.refs.items.currentSelectedSlot; @@ -82,7 +70,7 @@ class BetterBugleModule : Module List supportedItemNames = ["Bugle", "Bugle_Magic", "Megaphone"]; if (!supportedItemNames.Contains(item.UIData.itemName)) return; - Song? currentSong = Song.Songs.GetValueOrDefault(CurrentSongName); + Song? currentSong = Song.Songs.GetValueOrDefault(AudioSyncWorker.CurrentSongName); if (currentSong == null) return; if (Song.FavoriteSongs.Contains(currentSong.Name)) @@ -113,7 +101,7 @@ class BetterBugleModule : Module public override void Destroy() { - ClearAudioClips(); + AudioSyncService.ClearAudioClips(); base.Destroy(); } @@ -129,105 +117,6 @@ class BetterBugleModule : Module LocalizedText.mainTable.Add("CHANGE_SONG", scrollActionLocalizations); } - public void GetAudioClips() - { - if (IsLoading || IsSyncing) return; - if (!Directory.Exists(SoundsDirectory)) return; - IsLoading = true; - Plugin.Instance.StartCoroutine(LoadAllAudioClipsCoroutine(SoundsDirectory)); - } - private void ClearAudioClips() - { - foreach (Song song in Song.Songs.Values.ToList()) - { - song.Dispose(); - } - Song.Sounds.Clear(); - Song.SoundsByHash.Clear(); - Song.Songs.Clear(); - Song.BB_VoiceLines.Clear(); - GC.Collect(); - } - private 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.Songs.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 loadCoroutines = new(); - - 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}"); - } - OnAllAudioClipsLoaded(); - } - private IEnumerator LoadAudioClipCoroutine(string filePath, string ext, string name, bool forceReload = false) - { - - Debug.Log($"Loading audio clip: {name}.{ext} from {filePath}" + (forceReload ? " (forced reload)" : "")); - - using UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip($"file://{filePath}", AudioTypes[ext]); - yield return www.SendWebRequest(); - - if (www.result != UnityWebRequest.Result.Success) - { - Debug.LogError($"Failed to load audio clip from {filePath}: {www.error}"); - yield break; - } - - bool songExists = Song.Songs.ContainsKey(name); - - Debug.Log($"Audio clip '{name}' exists: {songExists}. Force reload: {forceReload}"); - - if (songExists && !forceReload) - { - Debug.LogWarning($"Audio clip with name '{name}' already exists. Skipping duplicate."); - yield break; - } - - if (songExists && forceReload) - { - Song? previousSong = Song.Songs.TryGetValue(name, out var existingSong) ? existingSong : null; - previousSong?.Dispose(); - } - - AudioClip audioClip = DownloadHandlerAudioClip.GetContent(www); - if (audioClip == null) - { - Debug.LogError($"Failed to load audio clip from {filePath}: {www.error}"); - yield break; - } - - Song song = new(name, ext, filePath, audioClip); - song.Register(); - Debug.Log($"Loaded audio clip: {name} from {filePath}"); - } private void OnAllAudioClipsLoaded() { if (Song.Songs.Count == 0) Debug.LogWarning("No songs loaded. Please ensure audio files are in the Sounds directory."); @@ -239,86 +128,10 @@ class BetterBugleModule : Module if (Song.Songs.ContainsKey(songKeyName) && !Song.FavoriteSongs.Contains(songKeyName)) Song.FavoriteSongs.Add(songKeyName); - if (!Song.Songs.ContainsKey(CurrentSongName)) - CurrentSongName = Song.GetSongNames_Alphabetically()[CurrentSongIndex]; - - IsLoading = false; + if (!Song.Songs.ContainsKey(AudioSyncWorker.CurrentSongName)) + AudioSyncWorker.CurrentSongName = Song.GetSongNames_Alphabetically()[AudioSyncWorker.CurrentSongIndex]; } - public void TrySyncAndLoadAudioClips() - { - if (IsLoading || IsSyncing) return; - Task.Run(() => - { - SyncAndLoadAudioClipsCoroutine().GetAwaiter().GetResult(); - }); - } - private async Task SyncAndLoadAudioClipsCoroutine() - { - if (IsLoading || IsSyncing) return; - IsSyncing = true; - AudioSyncService audioSyncService = AudioSyncService.GetInstance(); - Dictionary toDownload = new(); - - string[] existingSongNames = Song.Songs.Keys.ToArray(); - 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.Songs.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 DownloadAPIAudio(apiAudio, 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)); - } - private async Task DownloadAPIAudio(AudioSyncService.APIAudioFormat apiAudio, Song? existingSong = null) - { - 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 class BetterBugleSFX : MonoBehaviourPun @@ -347,7 +160,7 @@ public class BetterBugleSFX : MonoBehaviourPun audioSource.volume = 0f; audioSource.loop = true; if (IsLocal()) BetterBugleModule.CurrentAudioSource = audioSource; - song = Song.Songs.GetValueOrDefault(BetterBugleModule.CurrentSongName); + song = Song.Songs.GetValueOrDefault(AudioSyncWorker.CurrentSongName); } private bool IsLocal() @@ -393,7 +206,7 @@ public class BetterBugleSFX : MonoBehaviourPun if (flag != hold) { - if (flag) photonView.RPC("RPC_StartBetterToot", RpcTarget.All, BetterBugleModule.CurrentSongName); + if (flag) photonView.RPC("RPC_StartBetterToot", RpcTarget.All, AudioSyncWorker.CurrentSongName); else photonView.RPC("RPC_StopBetterToot", RpcTarget.All); hold = flag; } @@ -526,7 +339,7 @@ public class BetterBugleUI : MonoBehaviour { if (customStyle == null) return; if (BetterBugleModule.CurrentAudioSource == null || !BetterBugleModule.IsPlaying) return; - Song? currentAudio = Song.Songs.FirstOrDefault(s => s.Value.Name == BetterBugleModule.CurrentSongName).Value; + Song? currentAudio = Song.Songs.FirstOrDefault(s => s.Value.Name == AudioSyncWorker.CurrentSongName).Value; if (currentAudio == null || BetterBugleModule.CurrentAudioSource.clip == null) return; float MAX_WIDTH = Screen.width - (offsetX * 2); diff --git a/src/JordanMod/modules/replace_bingbong/ReplaceBingBongModule.cs b/src/JordanMod/modules/replace_bingbong/ReplaceBingBongModule.cs index 3ed5aa5..4d5e249 100644 --- a/src/JordanMod/modules/replace_bingbong/ReplaceBingBongModule.cs +++ b/src/JordanMod/modules/replace_bingbong/ReplaceBingBongModule.cs @@ -1,4 +1,9 @@ using System; +using System.Collections; +using System.Collections.Generic; +using JordanMod.Utils; +using UnityEngine; +using UnityEngine.InputSystem; namespace JordanMod.Modules.ReplaceBingBong; @@ -6,6 +11,9 @@ namespace JordanMod.Modules.ReplaceBingBong; class ReplaceBingBongModule : Module { public override string ModuleName => "Replace BingBong Module"; + + public static bool HasReplacedSounds = false; + public static BingBongResponseData[] OriginalResponsesData = []; public override Type[] GetPatches() { @@ -16,5 +24,124 @@ class ReplaceBingBongModule : Module { base.Initialize(); LocalizedText.mainTable.Add("idk_funny", ["Test subtitle!"]); + AudioSyncWorker.OnAudioLoadComplete += OnAudioLoadComplete; } + + public override void Update() + { + base.Update(); + if (Input.GetKeyDown(KeyCode.P)) + { + Helper.FindItemByName("BingBong_Prop Variant", out Item? item); + if (item == null) return; + Debug.Log($"Found item: {item.name} in scene {item.gameObject.scene.name}"); + } + } + + private static void OnAudioLoadComplete() + { + if (!HasReplacedSounds) return; + Action_AskBingBong[] allBingBongActions = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None); + foreach (Action_AskBingBong askBingBong in allBingBongActions) { + ReplaceBingBongResponses(askBingBong); + } + } + + public static void ReplaceBingBongResponses(Action_AskBingBong askBingBong) + { + Action_AskBingBong.BingBongResponse[] currentResponses = new Action_AskBingBong.BingBongResponse[OriginalResponsesData.Length]; + for (int index = 0; index < OriginalResponsesData.Length; index++) + { + currentResponses[index] = OriginalResponsesData[index].ToBingBongResponse(); + } + + askBingBong.responses = []; + + Dictionary sfxDict = new(); + for (int i = 0; i < currentResponses.Length; i++) + { + Action_AskBingBong.BingBongResponse response = currentResponses[i]; + if (response.sfx != null && response.sfx.clips != null && response.sfx.clips.Length > 0) + { + foreach (AudioClip clip in response.sfx.clips) + { + sfxDict[response.sfx.name] = response.sfx; + } + } + } + + List voices = [.. Song.BB_VoiceLines.Values]; + + foreach (Song voice in voices) + { + AudioClip clip = voice.AudioClip; + + bool isNew = !sfxDict.ContainsKey(voice.Name); + if (isNew) + { + SFX_Instance sFX_Instance = new() + { + clips = [clip] + }; + Action_AskBingBong.BingBongResponse newResponse = new() + { + sfx = sFX_Instance, + subtitleID = "idk_funny", + mouthCurve = null, + mouthCurveTime = 1f + }; + currentResponses = [.. currentResponses, newResponse]; + } + else + { + sfxDict[voice.Name].clips = [clip]; + } + } + + askBingBong.responses = new Action_AskBingBong.BingBongResponse[currentResponses.Length]; + for (int i = 0; i < currentResponses.Length; i++) + { + askBingBong.responses[i] = currentResponses[i]; + } + } + + + + +} + +public class BingBongResponseData +{ + public AudioClip[] Clips { get; set; } = []; + public string SfxName { get; set; } = ""; + public string SubtitleID { get; set; } = ""; + public AnimationCurve? MouthCurve { get; set; } = null; + public float MouthCurveTime { get; set; } = 0f; + + public Action_AskBingBong.BingBongResponse ToBingBongResponse() + { + return new Action_AskBingBong.BingBongResponse + { + sfx = new SFX_Instance + { + name = SfxName, + clips = (AudioClip[])Clips.Clone() + }, + subtitleID = SubtitleID, + mouthCurve = MouthCurve, + mouthCurveTime = MouthCurveTime + }; + } + + public static BingBongResponseData FromBingBongResponse(Action_AskBingBong.BingBongResponse response) + { + return new BingBongResponseData + { + Clips = (AudioClip[])response.sfx.clips.Clone(), + SfxName = response.sfx.name, + SubtitleID = response.subtitleID, + MouthCurve = response.mouthCurve, + MouthCurveTime = response.mouthCurveTime + }; + } } \ No newline at end of file diff --git a/src/JordanMod/patches/BetterBuglePatch.cs b/src/JordanMod/patches/BetterBuglePatch.cs index a122915..e8c0ac1 100644 --- a/src/JordanMod/patches/BetterBuglePatch.cs +++ b/src/JordanMod/patches/BetterBuglePatch.cs @@ -45,7 +45,7 @@ public class BetterBuglePatch // BetterBugleUI.Instance?.ShowActionbar("No songs available."); // return; // } - if (BetterBugleModule.IsLoading) return; + if (AudioSyncWorker.IsLoading) return; if (!BetterBugleModule.HadConfirmation) { BetterBugleUI.Instance?.ShowActionbar("Are you sure you want to refresh songs ? Right-click again to reload."); @@ -57,7 +57,7 @@ public class BetterBuglePatch { BetterBugleModule.HadConfirmation = false; // Reset confirmation state BetterBugleUI.Instance?.ShowActionbar("Refreshing songs..."); - BetterBugleModule.Instance?.GetAudioClips(); + AudioSyncService.GetAudioClips(); } } @@ -72,7 +72,7 @@ public class BetterBuglePatch private static void OnScroll(float scrollDelta) { - if (BetterBugleModule.IsLoading) return; + if (AudioSyncWorker.IsLoading) return; bool isNext = scrollDelta > 0; if (Song.Songs.Count == 0) { @@ -80,15 +80,15 @@ public class BetterBuglePatch return; } - if (isNext && BetterBugleModule.CurrentSongIndex < Song.Songs.Count - 1) BetterBugleModule.CurrentSongIndex++; - else if (isNext && BetterBugleModule.CurrentSongIndex == Song.Songs.Count - 1) BetterBugleModule.CurrentSongIndex = 0; - else if (!isNext && BetterBugleModule.CurrentSongIndex > 0) BetterBugleModule.CurrentSongIndex--; - else BetterBugleModule.CurrentSongIndex = Song.Songs.Count - 1; - BetterBugleModule.CurrentSongName = Song.GetSongNames_Alphabetically()[BetterBugleModule.CurrentSongIndex]; + if (isNext && AudioSyncWorker.CurrentSongIndex < Song.Songs.Count - 1) AudioSyncWorker.CurrentSongIndex++; + else if (isNext && AudioSyncWorker.CurrentSongIndex == Song.Songs.Count - 1) AudioSyncWorker.CurrentSongIndex = 0; + else if (!isNext && AudioSyncWorker.CurrentSongIndex > 0) AudioSyncWorker.CurrentSongIndex--; + else AudioSyncWorker.CurrentSongIndex = Song.Songs.Count - 1; + AudioSyncWorker.CurrentSongName = Song.GetSongNames_Alphabetically()[AudioSyncWorker.CurrentSongIndex]; - Song currentSong = Song.Songs[BetterBugleModule.CurrentSongName]; + Song currentSong = Song.Songs[AudioSyncWorker.CurrentSongName]; - bool isFavorite = Song.FavoriteSongs.Contains(BetterBugleModule.CurrentSongName); + bool isFavorite = Song.FavoriteSongs.Contains(AudioSyncWorker.CurrentSongName); BetterBugleUI.Instance?.ShowActionbar($" {(isFavorite ? "★" : " ")} {currentSong.RealIndex} | {currentSong.Name.Replace("_", " ")}"); } @@ -104,10 +104,10 @@ public class BetterBuglePatch if (currentItem.itemState != ItemState.Held) return; if (currentItem.TryGetComponent(out var bugleSFX)) { - Song? song = Song.Songs.GetValueOrDefault(BetterBugleModule.CurrentSongName); + Song? song = Song.Songs.GetValueOrDefault(AudioSyncWorker.CurrentSongName); if (song == null) return; - bool isFavorite = Song.FavoriteSongs.Contains(BetterBugleModule.CurrentSongName); + bool isFavorite = Song.FavoriteSongs.Contains(AudioSyncWorker.CurrentSongName); BetterBugleUI.Instance?.ShowActionbar($"{(isFavorite ? "★" : " ")} {song.RealIndex} | {song.Name}"); } }; diff --git a/src/JordanMod/patches/ReplaceBingBongPatch.cs b/src/JordanMod/patches/ReplaceBingBongPatch.cs index b21a33e..ffba091 100644 --- a/src/JordanMod/patches/ReplaceBingBongPatch.cs +++ b/src/JordanMod/patches/ReplaceBingBongPatch.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using HarmonyLib; using JordanMod.Utils; @@ -8,6 +9,14 @@ namespace JordanMod.Modules.ReplaceBingBong; public class ReplaceBingBongPatch { + [HarmonyPatch(typeof(Item), "Start")] + [HarmonyPrefix] + static void OnItemStart(Item __instance) + { + if (__instance.name != "BingBong_Prop Variant") return; + Debug.Log($"Item {__instance.name} Start in scene {__instance.gameObject.scene.name} ({__instance.gameObject.scene.buildIndex})"); + } + [HarmonyPatch(typeof(ItemActionBase), "OnEnable")] [HarmonyPrefix] static bool PreActionAskBingBongConstructorFix(ItemActionBase __instance) @@ -16,54 +25,18 @@ public class ReplaceBingBongPatch return true; Action_AskBingBong.BingBongResponse[] currentResponses = [..askBingBong.responses]; - // Each response has a .sfx which has a Object.name, store a ref to the sfx with key being sfx name - Dictionary sfxDict = new(); - for (int i = 0; i < currentResponses.Length; i++) + + if (!ReplaceBingBongModule.HasReplacedSounds) { - Action_AskBingBong.BingBongResponse response = currentResponses[i]; - if (response.sfx != null && response.sfx.clips != null && response.sfx.clips.Length > 0) + ReplaceBingBongModule.OriginalResponsesData = new BingBongResponseData[currentResponses.Length]; + for (int index = 0; index < currentResponses.Length; index++) { - foreach (AudioClip clip in response.sfx.clips) - { - sfxDict[response.sfx.name] = response.sfx; - } + ReplaceBingBongModule.OriginalResponsesData[index] = BingBongResponseData.FromBingBongResponse(currentResponses[index]); } + ReplaceBingBongModule.HasReplacedSounds = true; } - List voices = [.. Song.BB_VoiceLines.Values]; - - foreach (Song voice in voices) - { - AudioClip clip = voice.AudioClip; - - bool isNew = !sfxDict.ContainsKey(voice.Name); - if (isNew) - { - SFX_Instance sFX_Instance = new() - { - clips = [clip] - }; - Action_AskBingBong.BingBongResponse newResponse = new() - { - sfx = sFX_Instance, - subtitleID = "idk_funny", - mouthCurve = null, - mouthCurveTime = 1f - }; - currentResponses = [.. currentResponses, newResponse]; - } - else - { - sfxDict[voice.Name].clips = [clip]; - } - } - - askBingBong.responses = new Action_AskBingBong.BingBongResponse[currentResponses.Length]; - for (int i = 0; i < currentResponses.Length; i++) - { - askBingBong.responses[i] = currentResponses[i]; - } - + ReplaceBingBongModule.ReplaceBingBongResponses(askBingBong); return true; } diff --git a/src/JordanMod/utils/AudioSyncService.cs b/src/JordanMod/utils/AudioSyncService.cs index 98de8f2..e7c3eea 100644 --- a/src/JordanMod/utils/AudioSyncService.cs +++ b/src/JordanMod/utils/AudioSyncService.cs @@ -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 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 GetAudioClips() + public static List GetAudioClips() { List 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 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 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 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 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);