using System; using System.Collections.Generic; using System.Threading; using Code.Infrastructure.Configs; using Cysharp.Threading.Tasks; using LitMotion; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Audio; namespace Code.Infrastructure.Audio { public interface IAudioService { UniTaskVoid PlayOneShot(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Vector3 position = new Vector3(), AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null, bool isLOD = false); UniTaskVoid PlayRepeatedly(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Vector3 position = new Vector3(), AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null, CancellationTokenSource cancellationTokenSource = null); void CancelMusic(AudioPartition groupPartition, int id); void ReleaseCancellationSource(AudioPartition groupPartition, int id); UniTask ApplyParameter(string effectName, float value, float time, Action callback = null, CancellationTokenSource cts = null); UniTask ChangeGroupVolume(AudioGroupType groupType, float value, float time, Action callback = null, CancellationTokenSource cts = null); void SetAudioListener(AudioListener listener); UniTaskVoid PlayRepeatedly(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Transform parent, AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null, CancellationTokenSource cancellationTokenSource = null); UniTaskVoid PlayOneShot(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Transform parent, AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null); } public interface IAudioServiceInitializer { UniTask LoadMixerAsset(); } public partial class AudioService : IAudioService, IAudioServiceInitializer { private IAudioFactory _audioFactory; private IAudioSourcePool _pool; private AudioMixer _mixer; private AudioListener _listener; private Dictionary _audioMixerGroups = new(); public AudioService(IAudioFactory audioFactory, IAudioSourcePool pool, IConfigLocator configLocator) { _audioFactory = audioFactory; _pool = pool; _mixerConfig = configLocator.TryGetConfigAssetReference(); } public void SetAudioListener(AudioListener listener) { if(_listener) _listener.enabled = false; _listener = listener; _listener.enabled = true; } private partial void InitializeMixer(); public async UniTask LoadMixerAsset() { var audioMixer = await Addressables.LoadAssetAsync(_mixerConfig); _mixer = audioMixer.mixerAsset; InitializeMixer(); } public async UniTaskVoid PlayOneShot(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Vector3 position = new Vector3(), AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null, bool isLOD = false) { if (!CheckClip(groupPartition, id)) return; if (!audioSource) audioSource = _pool.GetSource(sourceType); audioSource.transform.position = position; audioSource.outputAudioMixerGroup = _audioMixerGroups[audioGroupType]; if (sourceType == AudioSourceType.NoBlend) audioSource.PlayOneShot(GetSingleClip(groupPartition, id)); if (sourceType == AudioSourceType.Full3D) { audioSource.PlayOneShot(isLOD ? GetLODClip(groupPartition, id, GetLODLevelOnPosition(position)) : GetSingleClip(groupPartition, id)); } await UniTask.NextFrame(); await UniTask.WaitUntilValueChanged(audioSource, _ => !audioSource.isPlaying); _pool.ReleaseSource(audioSource); } public async UniTaskVoid PlayOneShot(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Transform parent, AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null) { if (!CheckClip(groupPartition, id)) return; if (!audioSource) audioSource = _pool.GetSource(sourceType); audioSource.GetComponent().FollowTarget = parent; audioSource.outputAudioMixerGroup = _audioMixerGroups[audioGroupType]; if (sourceType == AudioSourceType.NoBlend) audioSource.PlayOneShot(GetSingleClip(groupPartition, id)); if (sourceType == AudioSourceType.Full3D) audioSource.PlayOneShot(GetLODClip(groupPartition, id, GetLODLevelOnPosition(parent.position))); await UniTask.NextFrame(); await UniTask.WaitUntilValueChanged(audioSource, _ => !audioSource.isPlaying); _pool.ReleaseSource(audioSource); } private Dictionary _cancellationTokenSources = new(); private AssetReference _mixerConfig; public void CancelMusic(AudioPartition groupPartition, int id) { var key = new AudioPartitionIdPair(groupPartition, id); if (!_cancellationTokenSources.ContainsKey(key)) return; _cancellationTokenSources[key].Cancel(); _cancellationTokenSources[key].Dispose(); _cancellationTokenSources.Remove(key); } public void ReleaseCancellationSource(AudioPartition groupPartition, int id) { _cancellationTokenSources.Remove(new AudioPartitionIdPair(groupPartition, id)); } public async UniTaskVoid PlayRepeatedly(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Vector3 position = new(), AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null, CancellationTokenSource cancellationTokenSource = null) { if (!CheckClip(groupPartition, id)) return; if (cancellationTokenSource == null) { cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSources[new AudioPartitionIdPair(groupPartition, id)] = cancellationTokenSource; } if (!audioSource) audioSource = _pool.GetSource(sourceType); if (!audioSource.resource) { if (sourceType == AudioSourceType.NoBlend) audioSource.resource = GetSingleClip(groupPartition, id); if (sourceType == AudioSourceType.Full3D) audioSource.resource = GetLODClip(groupPartition, id, GetLODLevelOnPosition(position)); } audioSource.outputAudioMixerGroup = _audioMixerGroups[audioGroupType]; audioSource.Play(); var (isCanceled, _) = await UniTask.WaitUntilValueChanged(audioSource, _ => !audioSource.isPlaying, cancellationToken: cancellationTokenSource.Token).SuppressCancellationThrow(); if (isCanceled) { cancellationTokenSource.Dispose(); _pool.ReleaseSource(audioSource); ReleaseCancellationSource(groupPartition, id); return; } PlayRepeatedly(groupPartition, id, AudioGroupType.Music, position, sourceType, audioSource, cancellationTokenSource) .Forget(); } public async UniTaskVoid PlayRepeatedly(AudioPartition groupPartition, int id, AudioGroupType audioGroupType, Transform parent, AudioSourceType sourceType = AudioSourceType.NoBlend, AudioSource audioSource = null, CancellationTokenSource cancellationTokenSource = null) { if (!CheckClip(groupPartition, id)) return; if (cancellationTokenSource == null) { cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSources[new AudioPartitionIdPair(groupPartition, id)] = cancellationTokenSource; } if (!audioSource) { audioSource = _pool.GetSource(sourceType); audioSource.GetComponent().FollowTarget = parent; } if (!audioSource.resource) { if (sourceType == AudioSourceType.NoBlend) audioSource.resource = GetSingleClip(groupPartition, id); if (sourceType == AudioSourceType.Full3D) audioSource.resource = GetLODClip(groupPartition, id, GetLODLevelOnPosition(parent.position)); } audioSource.outputAudioMixerGroup = _audioMixerGroups[audioGroupType]; audioSource.Play(); var (isCanceled, _) = await UniTask.WaitUntilValueChanged(audioSource, _ => !audioSource.isPlaying, cancellationToken: cancellationTokenSource.Token).SuppressCancellationThrow(); if (isCanceled) { cancellationTokenSource.Dispose(); _pool.ReleaseSource(audioSource); ReleaseCancellationSource(groupPartition, id); return; } PlayRepeatedly(groupPartition, id, AudioGroupType.Music, parent, sourceType, audioSource, cancellationTokenSource) .Forget(); } private AudioLODLevel GetLODLevelOnPosition(Vector3 position) { var distance = Vector3.Distance(position, _listener.transform.position); AudioLODLevel lodLevel; if (distance < 5f) lodLevel = AudioLODLevel.Interior; else if (distance < 20f) lodLevel = AudioLODLevel.Close; else if (distance < 50f) lodLevel = AudioLODLevel.Distant; else lodLevel = AudioLODLevel.Far; return lodLevel; } public AudioClip GetSingleClip(AudioPartition groupPartition, int id) { var reference = _audioFactory.ReferenceDictionary[groupPartition][id]; var asSingle = reference as AudioReferenceSingle; if(asSingle != null) return asSingle.Clip; return ((AudioReferenceRandom)reference).Clip; } public AudioClip GetLODClip(AudioPartition groupPartition, int id, AudioLODLevel lodLevel) { switch (lodLevel) { case AudioLODLevel.Interior: return ((AudioReferenceLODed)_audioFactory.ReferenceDictionary[groupPartition][id]).Interior; case AudioLODLevel.Close: return ((AudioReferenceLODed)_audioFactory.ReferenceDictionary[groupPartition][id]).Close; case AudioLODLevel.Distant: return ((AudioReferenceLODed)_audioFactory.ReferenceDictionary[groupPartition][id]).Distant; case AudioLODLevel.Far: return ((AudioReferenceLODed)_audioFactory.ReferenceDictionary[groupPartition][id]).Far; } return null; } public bool CheckClip(AudioPartition groupPartition, int id) { return _audioFactory.ReferenceDictionary.ContainsKey(groupPartition) && _audioFactory.ReferenceDictionary[groupPartition].ContainsKey(id); } public async UniTask ApplyParameter(string effectName, float value, float time, Action callback = null, CancellationTokenSource cts = null) { _mixer.GetFloat(effectName, out float startValue); if (time == 0f) _mixer.SetFloat(effectName, value); else { // TODO Проверить отмену if (cts != null) await LMotion.Create(startValue, value, time) .WithOnComplete(callback) .WithOnCancel(() => _mixer.SetFloat(effectName, value)) .Bind(x => _mixer.SetFloat(effectName, x)) .ToUniTask(cts.Token); else await LMotion.Create(startValue, value, time) .WithOnComplete(callback) .WithOnCancel(() => _mixer.SetFloat(effectName, value)) .Bind(x => _mixer.SetFloat(effectName, x)) .ToUniTask(); } } private Dictionary _volumeCancellationSources = new(); // TODO Очередь принятия параметров public async UniTask ChangeGroupVolume( AudioGroupType groupType, float value, float time, Action callback = null, CancellationTokenSource cts = null) { if (_volumeCancellationSources.ContainsKey(groupType)) _volumeCancellationSources[groupType].Cancel(); _volumeCancellationSources[groupType] = cts ?? new CancellationTokenSource(); await ApplyParameter(_audioMixerGroups[groupType].name, Mathf.Log10(value == 0f ? 0.001f : value) * 20, time, callback, cts); } } public struct AudioPartitionIdPair : IEquatable { public AudioPartition AudioPartition; public int ID; public AudioPartitionIdPair(AudioPartition audioPartition, int id) { AudioPartition = audioPartition; ID = id; } public bool Equals(AudioPartitionIdPair other) { return AudioPartition == other.AudioPartition && ID == other.ID; } public override bool Equals(object obj) { return obj is AudioPartitionIdPair other && Equals(other); } public override int GetHashCode() { return HashCode.Combine((int)AudioPartition, ID); } } }