779 lines
24 KiB
C#
779 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using BriarQueen.Data.Identifiers;
|
|
using BriarQueen.Framework.Coordinators.Events;
|
|
using BriarQueen.Framework.Events.Audio;
|
|
using BriarQueen.Framework.Events.UI;
|
|
using BriarQueen.Framework.Managers.Audio.Data;
|
|
using BriarQueen.Framework.Registries;
|
|
using Cysharp.Threading.Tasks;
|
|
using PrimeTween;
|
|
using UnityEngine;
|
|
using UnityEngine.Audio;
|
|
using VContainer;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace BriarQueen.Framework.Managers.Audio
|
|
{
|
|
/// <summary>
|
|
/// AudioManager (modifier-based mixer control)
|
|
/// Key idea:
|
|
/// - Settings set "base" volumes (in dB) per mixer parameter.
|
|
/// - Runtime states (Pause duck, Voice duck) apply "modifiers" (extra dB offsets).
|
|
/// - Effective mixer value is always: effectiveDb = baseDb + modifiersDb
|
|
/// </summary>
|
|
public class AudioManager : IDisposable, IManager
|
|
{
|
|
private const int INITIAL_AMBIENCE_SOURCES = 3;
|
|
private const int INITIAL_SFX_SOURCES = 6;
|
|
|
|
private const float PAUSE_DUCK_TARGET_DB = -18f;
|
|
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
|
|
private const float DEFAULT_VOICE_DUCK_TARGET_DB = -20f;
|
|
|
|
private readonly AudioMixer _audioMixer;
|
|
private readonly AudioRegistry _audioRegistry;
|
|
private readonly EventCoordinator _eventCoordinator;
|
|
|
|
private readonly Dictionary<string, float> _baseDb = new();
|
|
private readonly List<GameObject> _createdAudioObjects = new();
|
|
private readonly List<AudioSource> _ambienceSources = new();
|
|
private readonly List<AudioFileSo> _currentAmbienceTracks = new();
|
|
private readonly List<SfxChannel> _sfxChannels = new();
|
|
|
|
private AudioSource _musicSourceA;
|
|
private AudioSource _musicSourceB;
|
|
private AudioSource _voiceSource;
|
|
private AudioSource _uiSource;
|
|
|
|
private string _activeVoiceSubtitleId;
|
|
private AudioFileSo _currentMusicTrack;
|
|
|
|
private CancellationTokenSource _musicDuckCts;
|
|
private CancellationTokenSource _musicFadeCts;
|
|
private CancellationTokenSource _voiceCts;
|
|
|
|
private float _musicDuckDbCurrent;
|
|
private float _pauseDuckDbCurrent;
|
|
|
|
private Sequence _musicDuckSequence;
|
|
private Sequence _pauseDuckSequence;
|
|
|
|
private bool _voiceFinishedPublished;
|
|
private bool _disposed;
|
|
|
|
public bool Initialized { get; private set; }
|
|
|
|
[Inject]
|
|
public AudioManager(AudioMixer mainMixer, AudioRegistry audioRegistry, EventCoordinator eventCoordinator)
|
|
{
|
|
_audioMixer = mainMixer;
|
|
_audioRegistry = audioRegistry;
|
|
_eventCoordinator = eventCoordinator;
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
Debug.LogWarning($"[{nameof(AudioManager)}] Initialize called after Dispose.");
|
|
return;
|
|
}
|
|
|
|
if (Initialized)
|
|
{
|
|
Debug.LogWarning($"[{nameof(AudioManager)}] Initialize called more than once.");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[{nameof(AudioManager)}] Initializing...");
|
|
|
|
CreateSources();
|
|
PrimeMixerBaseValues();
|
|
|
|
_pauseDuckDbCurrent = 0f;
|
|
_musicDuckDbCurrent = 0f;
|
|
|
|
ApplyAllEffectiveVolumes();
|
|
|
|
_eventCoordinator.Subscribe<UIStackChangedEvent>(OnUIStackChanged);
|
|
|
|
Initialized = true;
|
|
Debug.Log($"[{nameof(AudioManager)}] Initialized.");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposed = true;
|
|
|
|
if (Initialized)
|
|
_eventCoordinator.Unsubscribe<UIStackChangedEvent>(OnUIStackChanged);
|
|
|
|
StopAndDispose(ref _musicDuckCts);
|
|
StopAndDispose(ref _musicFadeCts);
|
|
StopAndDispose(ref _voiceCts);
|
|
|
|
if (_pauseDuckSequence.isAlive)
|
|
{
|
|
_pauseDuckSequence.Stop();
|
|
_pauseDuckSequence = default;
|
|
}
|
|
|
|
if (_musicDuckSequence.isAlive)
|
|
{
|
|
_musicDuckSequence.Stop();
|
|
_musicDuckSequence = default;
|
|
}
|
|
|
|
foreach (var go in _createdAudioObjects)
|
|
{
|
|
if (go != null)
|
|
Object.Destroy(go);
|
|
}
|
|
|
|
_createdAudioObjects.Clear();
|
|
_ambienceSources.Clear();
|
|
_sfxChannels.Clear();
|
|
_currentAmbienceTracks.Clear();
|
|
_baseDb.Clear();
|
|
|
|
_musicSourceA = null;
|
|
_musicSourceB = null;
|
|
_voiceSource = null;
|
|
_uiSource = null;
|
|
_currentMusicTrack = null;
|
|
_activeVoiceSubtitleId = null;
|
|
_voiceFinishedPublished = false;
|
|
Initialized = false;
|
|
}
|
|
|
|
private void CreateSources()
|
|
{
|
|
_musicSourceA = CreateAudioSource("Music_Source_A", AudioMixerGroups.MUSIC_GROUP);
|
|
_musicSourceB = CreateAudioSource("Music_Source_B", AudioMixerGroups.MUSIC_GROUP);
|
|
|
|
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
|
|
_uiSource = CreateAudioSource("UI_Source", AudioMixerGroups.UI_GROUP);
|
|
|
|
for (var i = 0; i < INITIAL_SFX_SOURCES; i++)
|
|
{
|
|
var src = CreateAudioSource($"SFX_Source_{i}", AudioMixerGroups.SFX_GROUP);
|
|
_sfxChannels.Add(new SfxChannel
|
|
{
|
|
Source = src,
|
|
StartedAtUnscaled = -999f
|
|
});
|
|
}
|
|
|
|
for (var i = 0; i < INITIAL_AMBIENCE_SOURCES; i++)
|
|
{
|
|
_ambienceSources.Add(CreateAudioSource($"Ambience_Source_{i}", AudioMixerGroups.AMBIENCE_GROUP));
|
|
}
|
|
}
|
|
|
|
private void PrimeMixerBaseValues()
|
|
{
|
|
PrimeBaseFromMixer(AudioMixerParameters.MASTER_VOLUME);
|
|
PrimeBaseFromMixer(AudioMixerParameters.MUSIC_VOLUME);
|
|
PrimeBaseFromMixer(AudioMixerParameters.SFX_VOLUME);
|
|
PrimeBaseFromMixer(AudioMixerParameters.AMBIENCE_VOLUME);
|
|
PrimeBaseFromMixer(AudioMixerParameters.VOICE_VOLUME);
|
|
PrimeBaseFromMixer(AudioMixerParameters.UI_VOLUME);
|
|
}
|
|
|
|
public void SetVolume(string parameter, float value01)
|
|
{
|
|
if (!Initialized)
|
|
{
|
|
Debug.LogWarning($"[{nameof(AudioManager)}] SetVolume called before Initialize.");
|
|
return;
|
|
}
|
|
|
|
var linear = Mathf.Clamp01(value01);
|
|
var db = Linear01ToDb(linear);
|
|
|
|
_baseDb[parameter] = db;
|
|
ApplyEffectiveVolume(parameter);
|
|
}
|
|
|
|
private static float Linear01ToDb(float linear01)
|
|
{
|
|
var lin = Mathf.Max(linear01, 0.0001f);
|
|
return Mathf.Log10(lin) * 20f;
|
|
}
|
|
|
|
private void PrimeBaseFromMixer(string parameter)
|
|
{
|
|
if (_audioMixer != null && _audioMixer.GetFloat(parameter, out var db))
|
|
_baseDb[parameter] = db;
|
|
else
|
|
_baseDb[parameter] = 0f;
|
|
}
|
|
|
|
private void ApplyAllEffectiveVolumes()
|
|
{
|
|
ApplyEffectiveVolume(AudioMixerParameters.MASTER_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.VOICE_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.UI_VOLUME);
|
|
}
|
|
|
|
private void ApplyEffectiveVolume(string parameter)
|
|
{
|
|
if (_audioMixer == null || string.IsNullOrWhiteSpace(parameter))
|
|
return;
|
|
|
|
if (!_baseDb.TryGetValue(parameter, out var baseDb))
|
|
baseDb = 0f;
|
|
|
|
var effective = baseDb;
|
|
|
|
if (parameter == AudioMixerParameters.MUSIC_VOLUME ||
|
|
parameter == AudioMixerParameters.SFX_VOLUME ||
|
|
parameter == AudioMixerParameters.AMBIENCE_VOLUME)
|
|
{
|
|
effective += _pauseDuckDbCurrent;
|
|
}
|
|
|
|
if (parameter == AudioMixerParameters.MUSIC_VOLUME)
|
|
effective += _musicDuckDbCurrent;
|
|
|
|
_audioMixer.SetFloat(parameter, effective);
|
|
}
|
|
|
|
private void OnUIStackChanged(UIStackChangedEvent e)
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
if (e.AnyUIOpen)
|
|
OnGamePausedInternal().Forget();
|
|
else
|
|
OnGameUnpausedInternal().Forget();
|
|
}
|
|
|
|
private async UniTask OnGamePausedInternal()
|
|
{
|
|
PauseVoiceSource(true);
|
|
await TweenPauseDuckTo(PAUSE_DUCK_TARGET_DB, PAUSE_DUCK_FADE_SECONDS);
|
|
}
|
|
|
|
private async UniTask OnGameUnpausedInternal()
|
|
{
|
|
await TweenPauseDuckTo(0f, PAUSE_DUCK_FADE_SECONDS);
|
|
PauseVoiceSource(false);
|
|
}
|
|
|
|
private async UniTask TweenPauseDuckTo(float targetDb, float seconds)
|
|
{
|
|
if (_pauseDuckSequence.isAlive)
|
|
{
|
|
_pauseDuckSequence.Stop();
|
|
_pauseDuckSequence = default;
|
|
}
|
|
|
|
seconds = Mathf.Max(0f, seconds);
|
|
|
|
var from = _pauseDuckDbCurrent;
|
|
_pauseDuckSequence = Sequence.Create(useUnscaledTime: true)
|
|
.Group(Tween.Custom(
|
|
from,
|
|
targetDb,
|
|
seconds,
|
|
v =>
|
|
{
|
|
_pauseDuckDbCurrent = v;
|
|
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
|
|
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
|
|
},
|
|
Ease.OutCubic,
|
|
useUnscaledTime: true));
|
|
|
|
await _pauseDuckSequence.ToUniTask();
|
|
_pauseDuckSequence = default;
|
|
}
|
|
|
|
public void PauseVoiceSource(bool paused)
|
|
{
|
|
if (!Initialized || _voiceSource == null)
|
|
return;
|
|
|
|
if (paused)
|
|
_voiceSource.Pause();
|
|
else
|
|
_voiceSource.UnPause();
|
|
}
|
|
|
|
public void Play(string audioName)
|
|
{
|
|
if (!Initialized)
|
|
{
|
|
Debug.LogWarning($"[{nameof(AudioManager)}] Play called before Initialize.");
|
|
return;
|
|
}
|
|
|
|
if (!_audioRegistry.TryGetAudio(audioName, out var audioFile))
|
|
{
|
|
Debug.LogWarning($"[AudioManager] Audio '{audioName}' not found in registry!");
|
|
return;
|
|
}
|
|
|
|
if (audioFile is AudioFileSo audio)
|
|
Play(audio);
|
|
else
|
|
Debug.LogWarning($"[AudioManager] Audio '{audioName}' is not a valid AudioSO!");
|
|
}
|
|
|
|
public void Play(AudioFileSo audioData)
|
|
{
|
|
if (!Initialized || !audioData || !audioData.Clip)
|
|
return;
|
|
|
|
switch (audioData.Type)
|
|
{
|
|
case TrackType.Music:
|
|
CrossfadeMusic(audioData, audioData.FadeTime > 0f ? audioData.FadeTime : 1.0f).Forget();
|
|
break;
|
|
|
|
case TrackType.Ambience:
|
|
if (!_currentAmbienceTracks.Contains(audioData))
|
|
{
|
|
_currentAmbienceTracks.Add(audioData);
|
|
PlayOnAvailableAmbienceSource(audioData);
|
|
}
|
|
break;
|
|
|
|
case TrackType.Sfx:
|
|
PlaySfx(audioData);
|
|
break;
|
|
|
|
case TrackType.UIFX:
|
|
PlayOneShotAsync(_uiSource, audioData).Forget();
|
|
break;
|
|
|
|
case TrackType.Voice:
|
|
PlayVoiceLine(audioData).Forget();
|
|
break;
|
|
}
|
|
|
|
if (audioData.DuckMusic)
|
|
DuckMusicAsync(audioData.Clip.length, audioData.FadeTime).Forget();
|
|
}
|
|
|
|
private async UniTaskVoid PlayVoiceLine(AudioFileSo audioData)
|
|
{
|
|
if (!Initialized || _voiceSource == null || audioData?.Clip == null)
|
|
return;
|
|
|
|
StopAndDispose(ref _voiceCts);
|
|
_voiceCts = new CancellationTokenSource();
|
|
var token = _voiceCts.Token;
|
|
|
|
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
|
|
_voiceFinishedPublished = false;
|
|
|
|
_eventCoordinator.Publish(new VoiceLineStartedEvent(_activeVoiceSubtitleId));
|
|
|
|
_voiceSource.clip = audioData.Clip;
|
|
_voiceSource.pitch = audioData.Pitch;
|
|
_voiceSource.volume = audioData.Volume;
|
|
_voiceSource.loop = false;
|
|
_voiceSource.priority = audioData.Priority;
|
|
_voiceSource.Play();
|
|
|
|
try
|
|
{
|
|
await UniTask.WaitUntil(() => !_voiceSource.isPlaying, cancellationToken: token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
PublishVoiceFinishedIfNeeded();
|
|
}
|
|
}
|
|
|
|
private void PublishVoiceFinishedIfNeeded()
|
|
{
|
|
if (_voiceFinishedPublished)
|
|
return;
|
|
|
|
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
|
|
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
|
|
|
|
_voiceFinishedPublished = true;
|
|
_activeVoiceSubtitleId = null;
|
|
}
|
|
|
|
private async UniTask DuckMusicAsync(float clipLengthSeconds, float fadeTimeSeconds)
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
StopAndDispose(ref _musicDuckCts);
|
|
_musicDuckCts = new CancellationTokenSource();
|
|
var token = _musicDuckCts.Token;
|
|
|
|
fadeTimeSeconds = Mathf.Max(0.0001f, fadeTimeSeconds);
|
|
var duckTarget = DEFAULT_VOICE_DUCK_TARGET_DB;
|
|
|
|
try
|
|
{
|
|
await TweenMusicDuckTo(duckTarget, fadeTimeSeconds, token);
|
|
|
|
var hold = clipLengthSeconds - fadeTimeSeconds * 2f;
|
|
if (hold > 0.01f)
|
|
await UniTask.Delay(TimeSpan.FromSeconds(hold), cancellationToken: token);
|
|
|
|
await TweenMusicDuckTo(0f, fadeTimeSeconds, token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
await TweenMusicDuckTo(0f, 0.35f, CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
private async UniTask TweenMusicDuckTo(float targetDb, float seconds, CancellationToken token)
|
|
{
|
|
if (_musicDuckSequence.isAlive)
|
|
{
|
|
_musicDuckSequence.Stop();
|
|
_musicDuckSequence = default;
|
|
}
|
|
|
|
seconds = Mathf.Max(0f, seconds);
|
|
|
|
var from = _musicDuckDbCurrent;
|
|
|
|
_musicDuckSequence = Sequence.Create(useUnscaledTime: true)
|
|
.Group(Tween.Custom(
|
|
from,
|
|
targetDb,
|
|
seconds,
|
|
v =>
|
|
{
|
|
_musicDuckDbCurrent = v;
|
|
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
|
|
},
|
|
Ease.OutCubic,
|
|
useUnscaledTime: true));
|
|
|
|
await _musicDuckSequence.ToUniTask(cancellationToken: token);
|
|
_musicDuckSequence = default;
|
|
}
|
|
|
|
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
|
|
{
|
|
if (!Initialized || !newTrack || !newTrack.Clip)
|
|
return;
|
|
|
|
if (_currentMusicTrack == newTrack)
|
|
return;
|
|
|
|
StopAndDispose(ref _musicFadeCts);
|
|
_musicFadeCts = new CancellationTokenSource();
|
|
var token = _musicFadeCts.Token;
|
|
|
|
var activeSource = _musicSourceA.isPlaying
|
|
? _musicSourceA
|
|
: _musicSourceB.isPlaying
|
|
? _musicSourceB
|
|
: null;
|
|
|
|
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
|
|
|
|
PlayOnSource(inactiveSource, newTrack);
|
|
|
|
if (activeSource == null)
|
|
{
|
|
inactiveSource.volume = newTrack.Volume;
|
|
_currentMusicTrack = newTrack;
|
|
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
|
return;
|
|
}
|
|
|
|
duration = Mathf.Max(0.0001f, duration);
|
|
|
|
var elapsed = 0f;
|
|
var startVolume = activeSource.volume;
|
|
|
|
try
|
|
{
|
|
while (elapsed < duration)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
var t = elapsed / duration;
|
|
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
|
|
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
|
|
|
|
elapsed += Time.unscaledDeltaTime;
|
|
await UniTask.Yield(PlayerLoopTiming.Update, token);
|
|
}
|
|
|
|
activeSource.volume = 0f;
|
|
inactiveSource.volume = newTrack.Volume;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
activeSource.Stop();
|
|
activeSource.volume = startVolume;
|
|
|
|
_currentMusicTrack = newTrack;
|
|
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
|
|
}
|
|
|
|
private void PlaySfx(AudioFileSo audioData)
|
|
{
|
|
if (!Initialized || audioData == null || audioData.Clip == null)
|
|
return;
|
|
|
|
var channelIndex = GetBestSfxChannelIndex(audioData.Priority);
|
|
if (channelIndex < 0 || channelIndex >= _sfxChannels.Count)
|
|
return;
|
|
|
|
var src = _sfxChannels[channelIndex].Source;
|
|
if (src == null)
|
|
return;
|
|
|
|
if (src.isPlaying)
|
|
src.Stop();
|
|
|
|
src.priority = audioData.Priority;
|
|
src.pitch = audioData.Pitch;
|
|
src.PlayOneShot(audioData.Clip, audioData.Volume);
|
|
|
|
_sfxChannels[channelIndex] = new SfxChannel
|
|
{
|
|
Source = src,
|
|
StartedAtUnscaled = Time.unscaledTime
|
|
};
|
|
}
|
|
|
|
private int GetBestSfxChannelIndex(int incomingPriority)
|
|
{
|
|
for (var i = 0; i < _sfxChannels.Count; i++)
|
|
{
|
|
var src = _sfxChannels[i].Source;
|
|
if (src == null)
|
|
continue;
|
|
|
|
if (!src.isPlaying)
|
|
return i;
|
|
}
|
|
|
|
var bestIndex = -1;
|
|
var worstPriority = int.MinValue;
|
|
var oldestStart = float.MaxValue;
|
|
|
|
for (var i = 0; i < _sfxChannels.Count; i++)
|
|
{
|
|
var src = _sfxChannels[i].Source;
|
|
if (src == null)
|
|
continue;
|
|
|
|
var p = src.priority;
|
|
var started = _sfxChannels[i].StartedAtUnscaled;
|
|
|
|
if (p > worstPriority || (p == worstPriority && started < oldestStart))
|
|
{
|
|
worstPriority = p;
|
|
oldestStart = started;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
|
|
return bestIndex;
|
|
}
|
|
|
|
public void StopAmbience(AudioFileSo audioData)
|
|
{
|
|
if (!Initialized || !audioData || !audioData.Clip || audioData.Type != TrackType.Ambience)
|
|
return;
|
|
|
|
if (_currentAmbienceTracks.Remove(audioData))
|
|
{
|
|
foreach (var source in _ambienceSources.Where(s => s != null && s.clip == audioData.Clip))
|
|
source.Stop();
|
|
}
|
|
}
|
|
|
|
public void StopAllAmbience()
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
foreach (var s in _ambienceSources)
|
|
{
|
|
if (s != null)
|
|
s.Stop();
|
|
}
|
|
|
|
_currentAmbienceTracks.Clear();
|
|
}
|
|
|
|
private void PlayOnAvailableAmbienceSource(AudioFileSo audioData)
|
|
{
|
|
var source = _ambienceSources.FirstOrDefault(s => s != null && !s.isPlaying);
|
|
if (source == null)
|
|
{
|
|
source = CreateAudioSource(
|
|
$"Ambience_Source_{_ambienceSources.Count}",
|
|
AudioMixerGroups.AMBIENCE_GROUP);
|
|
|
|
_ambienceSources.Add(source);
|
|
}
|
|
|
|
PlayOnSource(source, audioData);
|
|
}
|
|
|
|
public void StopMusic()
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
StopAndDispose(ref _musicFadeCts);
|
|
|
|
if (_musicSourceA != null)
|
|
{
|
|
_musicSourceA.Stop();
|
|
_musicSourceA.clip = null;
|
|
_musicSourceA.volume = 0f;
|
|
}
|
|
|
|
if (_musicSourceB != null)
|
|
{
|
|
_musicSourceB.Stop();
|
|
_musicSourceB.clip = null;
|
|
_musicSourceB.volume = 0f;
|
|
}
|
|
|
|
_currentMusicTrack = null;
|
|
}
|
|
|
|
public void StopVoice()
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
StopAndDispose(ref _voiceCts);
|
|
|
|
if (_voiceSource != null && _voiceSource.isPlaying)
|
|
_voiceSource.Stop();
|
|
|
|
PublishVoiceFinishedIfNeeded();
|
|
}
|
|
|
|
public void StopAllSfx()
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
for (var i = 0; i < _sfxChannels.Count; i++)
|
|
{
|
|
var src = _sfxChannels[i].Source;
|
|
if (src == null)
|
|
continue;
|
|
|
|
src.Stop();
|
|
_sfxChannels[i] = new SfxChannel
|
|
{
|
|
Source = src,
|
|
StartedAtUnscaled = -999f
|
|
};
|
|
}
|
|
}
|
|
|
|
public void StopAllAudio()
|
|
{
|
|
if (!Initialized)
|
|
return;
|
|
|
|
StopMusic();
|
|
StopVoice();
|
|
StopAllSfx();
|
|
StopAllAmbience();
|
|
|
|
if (_uiSource != null)
|
|
_uiSource.Stop();
|
|
}
|
|
|
|
private static void StopAndDispose(ref CancellationTokenSource cts)
|
|
{
|
|
if (cts == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
cts.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
cts.Dispose();
|
|
cts = null;
|
|
}
|
|
|
|
private AudioSource CreateAudioSource(string name, string groupName)
|
|
{
|
|
var obj = new GameObject(name);
|
|
Object.DontDestroyOnLoad(obj);
|
|
_createdAudioObjects.Add(obj);
|
|
|
|
var src = obj.AddComponent<AudioSource>();
|
|
var group = _audioMixer.FindMatchingGroups(groupName);
|
|
|
|
if (group != null && group.Length > 0)
|
|
src.outputAudioMixerGroup = group[0];
|
|
else
|
|
Debug.LogWarning($"[AudioManager] Mixer Group '{groupName}' not found for {name}");
|
|
|
|
return src;
|
|
}
|
|
|
|
private async UniTaskVoid PlayOneShotAsync(AudioSource source, AudioFileSo audioData)
|
|
{
|
|
if (!Initialized || source == null || audioData == null || audioData.Clip == null)
|
|
return;
|
|
|
|
source.priority = audioData.Priority;
|
|
source.pitch = audioData.Pitch;
|
|
source.PlayOneShot(audioData.Clip, audioData.Volume);
|
|
|
|
var seconds = audioData.Clip.length / Mathf.Max(audioData.Pitch, 0.0001f);
|
|
await UniTask.Delay(TimeSpan.FromSeconds(seconds));
|
|
}
|
|
|
|
private void PlayOnSource(AudioSource source, AudioFileSo audioData)
|
|
{
|
|
if (!Initialized || source == null || audioData == null)
|
|
return;
|
|
|
|
source.clip = audioData.Clip;
|
|
source.loop = audioData.Loopable;
|
|
source.volume = audioData.Volume;
|
|
source.pitch = audioData.Pitch;
|
|
source.priority = audioData.Priority;
|
|
source.Play();
|
|
}
|
|
|
|
private struct SfxChannel
|
|
{
|
|
public AudioSource Source;
|
|
public float StartedAtUnscaled;
|
|
}
|
|
}
|
|
} |