/*! \cond PRIVATE */
#if ADDRESSABLES_ENABLED
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace DarkTonic.MasterAudio {
///
/// This class will handle unloading and load audio data for Addressable Audio Clips. I use T for AudioClip only but this could be used for anything.
///
// ReSharper disable once CheckNamespace
public class AddressableTracker {
public AsyncOperationHandle AssetHandle { get; private set; }
public int UnusedSecondsLifespan { get; private set; }
// Keep track of this because when there are none, you can release the addressable to reclaim memory.
public List AudiosSourcesUsingReference { get; } = new List();
public AddressableTracker(AsyncOperationHandle assetHandle, int unusedSecondsLifespan) {
AssetHandle = assetHandle;
UnusedSecondsLifespan = unusedSecondsLifespan;
}
}
public static class AudioAddressableOptimizer {
// maybe use a different Dictionary for each T you need, or a different similar class
private static readonly Dictionary> AddressableTasksByAddressableId = new Dictionary>();
private static readonly object SyncRoot = new object(); // to lock below
///
/// Start Coroutine when calling this, passing in success and failure action delegates.
///
///
///
///
///
///
public static IEnumerator PopulateSourceWithAddressableClipAsync(AssetReference addressable, SoundGroupVariation variation, int unusedSecondsLifespan,
System.Action successAction,
System.Action failureAction) {
var isWarmingCall = MasterAudio.IsWarming; // since this may change by the time we load the asset, we store it so we can know.
if (!IsAddressableValid(addressable)) {
if (failureAction != null) {
failureAction();
}
if (isWarmingCall) {
DTMonoHelper.SetActive(variation.GameObj, false); // should disable itself
}
yield break;
}
var addressableId = GetAddressableId(addressable);
AsyncOperationHandle loadHandle;
AudioClip addressableClip;
var shouldReleaseLoadedAssetNow = false;
if (AddressableTasksByAddressableId.ContainsKey(addressableId)) {
loadHandle = AddressableTasksByAddressableId[addressableId].AssetHandle;
addressableClip = loadHandle.Result;
} else {
loadHandle = Addressables.LoadAssetAsync(addressable);
while (!loadHandle.IsDone) {
yield return MasterAudio.EndOfFrameDelay;
}
addressableClip = loadHandle.Result;
if (addressableClip == null || loadHandle.Status != AsyncOperationStatus.Succeeded) {
var errorText = "";
if (loadHandle.OperationException != null) {
errorText = " Exception: " + loadHandle.OperationException.Message;
}
MasterAudio.LogError("Addressable file for '" + variation.GameObjectName + "' could not be located." + errorText);
if (failureAction != null) {
failureAction();
}
if (isWarmingCall) {
DTMonoHelper.SetActive(variation.GameObj, false); // should disable itself
}
yield break;
}
lock (SyncRoot) {
if (!AddressableTasksByAddressableId.ContainsKey(addressableId)) {
AddressableTasksByAddressableId.Add(addressableId, new AddressableTracker(loadHandle, unusedSecondsLifespan));
} else {
// race condition reached. Another load finished before this one. Throw this away and use the other, to release memory.
shouldReleaseLoadedAssetNow = true;
addressableClip = AddressableTasksByAddressableId[addressableId].AssetHandle.Result;
}
}
}
if (shouldReleaseLoadedAssetNow) {
Addressables.Release(loadHandle);
}
if (!AudioUtil.AudioClipWillPreload(addressableClip)) {
MasterAudio.LogWarning("Audio Clip for Addressable file '" + addressableClip.CachedName() + "' of Sound Group '" + variation.ParentGroup.GameObjectName + "' has 'Preload Audio Data' turned off, which can cause audio glitches. Addressables should always Preload Audio Data. Please turn it on.");
}
variation.LoadStatus = MasterAudio.VariationLoadStatus.Loaded;
var stoppedBeforePlay = variation.IsStopRequested;
if (stoppedBeforePlay) {
// do nothing, but don't call the delegate or set audio clip for sure!
} else {
variation.VarAudio.clip = addressableClip;
if (successAction != null) {
successAction();
}
}
}
public static void AddAddressablePlayingClip(AssetReference addressable, AudioSource holderSource) {
if (!IsAddressableValid(addressable)) {
return;
}
var addressableId = GetAddressableId(addressable);
if (!AddressableTasksByAddressableId.ContainsKey(addressableId)) {
Debug.Log("Addressable not found in loaded map: id = '" + addressable + "'. Aborting recording play.");
return;
}
MasterAudio.RemoveAddressableFromDelayedRelease(addressableId);
var tracker = AddressableTasksByAddressableId[addressableId];
if (tracker.AudiosSourcesUsingReference.Contains(holderSource)) {
return; // already added before somehow, don't duplicate.
}
tracker.AudiosSourcesUsingReference.Add(holderSource);
}
public static void RemoveAddressablePlayingClip(AssetReference addressable, AudioSource holderSource, bool forceRemove = false) {
if (!IsAddressableValid(addressable)) {
return;
}
var addressableId = GetAddressableId(addressable);
if (!AddressableTasksByAddressableId.ContainsKey(addressableId)) {
return;
}
var audioSources = AddressableTasksByAddressableId[addressableId].AudiosSourcesUsingReference;
audioSources.Remove(holderSource);
// none playing, release!
ReleaseAddressableIfNoUses(addressable, forceRemove);
}
public static void MaybeReleaseAddressable(string addressableId, bool forceRelease = false) {
if (!AddressableTasksByAddressableId.ContainsKey(addressableId)) {
return;
}
var tracker = AddressableTasksByAddressableId[addressableId];
if (forceRelease || tracker.UnusedSecondsLifespan == 0) {
var deadHandle = tracker.AssetHandle;
AddressableTasksByAddressableId.Remove(addressableId);
Addressables.Release(deadHandle);
} else {
MasterAudio.AddAddressableForDelayedRelease(addressableId, tracker.UnusedSecondsLifespan);
}
}
public static bool IsAddressableValid(AssetReference addressable) {
if (addressable == null) {
return false;
}
#if UNITY_EDITOR
return addressable.editorAsset != null;
#else
return addressable.RuntimeKeyIsValid();
#endif
}
public static IEnumerator PopulateAddressableSongToPlaylistControllerAsync(MusicSetting setting, AssetReference addressable,
PlaylistController playlistController, PlaylistController.AudioPlayType playType) {
if (!IsAddressableValid(addressable)) {
yield break;
}
var addressableId = GetAddressableId(addressable);
AsyncOperationHandle loadHandle;
AudioClip addressableClip;
var shouldReleaseLoadedAssetNow = false;
if (AddressableTasksByAddressableId.ContainsKey(addressableId)) {
loadHandle = AddressableTasksByAddressableId[addressableId].AssetHandle;
addressableClip = loadHandle.Result;
} else {
loadHandle = Addressables.LoadAssetAsync(addressable);
while (!loadHandle.IsDone) {
yield return MasterAudio.EndOfFrameDelay;
}
addressableClip = loadHandle.Result;
if (addressableClip == null || loadHandle.Status != AsyncOperationStatus.Succeeded) {
var errorText = "";
if (loadHandle.OperationException != null) {
errorText = " Exception: " + loadHandle.OperationException.Message;
}
MasterAudio.LogError("Addressable file for PlaylistController '" + playlistController.ControllerName + "' could not be located." + errorText);
yield break;
}
lock (SyncRoot) {
if (!AddressableTasksByAddressableId.ContainsKey(addressableId)) {
AddressableTasksByAddressableId.Add(addressableId, new AddressableTracker(loadHandle, 0));
} else {
// race condition reached. Another load finished before this one. Throw this away and use the other, to release memory.
shouldReleaseLoadedAssetNow = true;
addressableClip = AddressableTasksByAddressableId[addressableId].AssetHandle.Result;
}
}
}
if (shouldReleaseLoadedAssetNow) {
Addressables.Release(loadHandle);
}
if (!AudioUtil.AudioClipWillPreload(addressableClip)) {
MasterAudio.LogWarning("Audio Clip for Addressable file '" + addressableClip.CachedName() + "' of Playlist Controller '" + playlistController.ControllerName + "' has 'Preload Audio Data' turned off, which can cause audio glitches. Addressables should always Preload Audio Data. Please turn it on.");
}
// Figure out how to detect stop before loaded, if needed
var stoppedBeforePlay = false;
if (stoppedBeforePlay) {
// do nothing, but don't call the delegate or set audio clip for sure!
} else {
playlistController.FinishLoadingNewSong(setting, addressableClip, playType);
}
}
#region Helper methods
private static bool IsAnyOfAddressableClipPlaying(AssetReference addressable) {
var addressableId = GetAddressableId(addressable);
if (!AddressableTasksByAddressableId.ContainsKey(addressableId)) {
return false;
}
return AddressableTasksByAddressableId[addressableId].AudiosSourcesUsingReference.Count > 0;
}
private static void ReleaseAddressableIfNoUses(AssetReference addressable, bool forceRemove = false) {
if (IsAnyOfAddressableClipPlaying(addressable)) {
return;
}
var addressableId = GetAddressableId(addressable);
MaybeReleaseAddressable(addressableId, forceRemove);
}
private static string GetAddressableId(AssetReference addressable) {
return addressable.RuntimeKey.ToString();
}
#endregion
}
}
#endif
/*! \endcond */