Files
A-Fairytale-Gone-Bad-Briar-…/Assets/Scripts/Framework/Managers/Audio/AudioManager.cs

669 lines
23 KiB
C#

using System;
using System.Collections.Generic;
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
/// - UI and Ambience route through SFX channel/group.
/// - SFX pool is transient — new channels are spawned on demand and
/// finished channels are reaped before each play.
/// </summary>
public class AudioManager : IDisposable, IManager
{
private const int INITIAL_SFX_SOURCES = 8;
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<SfxChannel> _sfxChannels = new();
private AudioSource _musicSourceA;
private AudioSource _musicSourceB;
private AudioSource _voiceSource;
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();
_sfxChannels.Clear();
_baseDb.Clear();
_musicSourceA = null;
_musicSourceB = null;
_voiceSource = null;
_currentMusicTrack = null;
_activeVoiceSubtitleId = null;
_voiceFinishedPublished = false;
Initialized = false;
}
// ── Source creation ───────────────────────────────────────────
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);
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 });
}
}
// ── Volume ────────────────────────────────────────────────────
private void PrimeMixerBaseValues()
{
PrimeBaseFromMixer(AudioMixerParameters.MASTER_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.MUSIC_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.SFX_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.VOICE_VOLUME);
}
public void SetVolume(string parameter, float value01)
{
if (!Initialized)
{
Debug.LogWarning($"[{nameof(AudioManager)}] SetVolume called before Initialize.");
return;
}
_baseDb[parameter] = Linear01ToDb(Mathf.Clamp01(value01));
ApplyEffectiveVolume(parameter);
}
private static float Linear01ToDb(float linear01)
{
return Mathf.Log10(Mathf.Max(linear01, 0.0001f)) * 20f;
}
private void PrimeBaseFromMixer(string parameter)
{
_baseDb[parameter] = _audioMixer != null && _audioMixer.GetFloat(parameter, out var db) ? db : 0f;
}
private void ApplyAllEffectiveVolumes()
{
ApplyEffectiveVolume(AudioMixerParameters.MASTER_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.VOICE_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)
{
effective += _pauseDuckDbCurrent;
}
if (parameter == AudioMixerParameters.MUSIC_VOLUME)
effective += _musicDuckDbCurrent;
_audioMixer.SetFloat(parameter, effective);
}
// ── UI stack / pause duck ─────────────────────────────────────
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;
}
var from = _pauseDuckDbCurrent;
_pauseDuckSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(
from,
targetDb,
Mathf.Max(0f, seconds),
v =>
{
_pauseDuckDbCurrent = v;
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.SFX_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();
}
// ── Play ──────────────────────────────────────────────────────
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:
case TrackType.UIFX:
case TrackType.Sfx:
PlaySfx(audioData);
break;
case TrackType.Voice:
PlayVoiceLine(audioData).Forget();
break;
}
if (audioData.DuckMusic)
DuckMusicAsync(audioData.Clip.length, audioData.FadeTime).Forget();
}
// ── Voice ─────────────────────────────────────────────────────
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;
}
public void StopVoice()
{
if (!Initialized) return;
StopAndDispose(ref _voiceCts);
if (_voiceSource != null && _voiceSource.isPlaying)
_voiceSource.Stop();
PublishVoiceFinishedIfNeeded();
}
// ── SFX (transient pool) ──────────────────────────────────────
private void PlaySfx(AudioFileSo audioData)
{
if (!Initialized || audioData == null || audioData.Clip == null)
return;
// Reap finished channels first so we don't accumulate stale entries
ReapFinishedSfxChannels();
// Try to find a free channel from the existing pool
AudioSource src = null;
var channelIndex = -1;
for (var i = 0; i < _sfxChannels.Count; i++)
{
var s = _sfxChannels[i].Source;
if (s != null && !s.isPlaying)
{
src = s;
channelIndex = i;
break;
}
}
// No free channel — spawn a transient one
if (src == null)
{
src = CreateAudioSource($"SFX_Source_Transient_{_sfxChannels.Count}", AudioMixerGroups.SFX_GROUP);
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
channelIndex = _sfxChannels.Count - 1;
Debug.Log($"[AudioManager] SFX pool expanded to {_sfxChannels.Count} channels.");
}
src.priority = audioData.Priority;
src.pitch = audioData.Pitch;
src.loop = audioData.Loopable;
src.PlayOneShot(audioData.Clip, audioData.Volume);
_sfxChannels[channelIndex] = new SfxChannel
{
Source = src,
StartedAtUnscaled = Time.unscaledTime
};
}
/// <summary>
/// Removes finished transient channels from the pool to prevent unbounded growth.
/// Preserves the initial pool channels even when idle.
/// </summary>
private void ReapFinishedSfxChannels()
{
for (var i = _sfxChannels.Count - 1; i >= INITIAL_SFX_SOURCES; i--)
{
var src = _sfxChannels[i].Source;
if (src == null || src.isPlaying)
continue;
// Destroy the transient GameObject and remove from pool
if (src.gameObject != null)
{
_createdAudioObjects.Remove(src.gameObject);
Object.Destroy(src.gameObject);
}
_sfxChannels.RemoveAt(i);
}
}
public void StopAllSfx()
{
if (!Initialized) return;
for (var i = _sfxChannels.Count - 1; i >= 0; i--)
{
var src = _sfxChannels[i].Source;
if (src == null) continue;
src.Stop();
// Destroy transient channels, reset initial ones
if (i >= INITIAL_SFX_SOURCES)
{
if (src.gameObject != null)
{
_createdAudioObjects.Remove(src.gameObject);
Object.Destroy(src.gameObject);
}
_sfxChannels.RemoveAt(i);
}
else
{
_sfxChannels[i] = new SfxChannel { Source = src, StartedAtUnscaled = -999f };
}
}
}
// ── Music ─────────────────────────────────────────────────────
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));
}
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;
}
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);
try
{
await TweenMusicDuckTo(DEFAULT_VOICE_DUCK_TARGET_DB, 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;
}
var from = _musicDuckDbCurrent;
_musicDuckSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(
from,
targetDb,
Mathf.Max(0f, seconds),
v =>
{
_musicDuckDbCurrent = v;
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
},
Ease.OutCubic,
useUnscaledTime: true));
await _musicDuckSequence.ToUniTask(cancellationToken: token);
_musicDuckSequence = default;
}
// ── Stop all ──────────────────────────────────────────────────
public void StopAllAudio()
{
if (!Initialized) return;
StopMusic();
StopVoice();
StopAllSfx();
}
// ── Helpers ───────────────────────────────────────────────────
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 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;
}
}
}