First commit for private source control. Older commits available on Github.
This commit is contained in:
BIN
Assets/Scripts/Framework/Managers/Audio/.DS_Store
vendored
Normal file
BIN
Assets/Scripts/Framework/Managers/Audio/.DS_Store
vendored
Normal file
Binary file not shown.
779
Assets/Scripts/Framework/Managers/Audio/AudioManager.cs
Normal file
779
Assets/Scripts/Framework/Managers/Audio/AudioManager.cs
Normal file
@@ -0,0 +1,779 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a9a5c68541441fead61270b45705435
|
||||
timeCreated: 1769802028
|
||||
3
Assets/Scripts/Framework/Managers/Audio/Data.meta
Normal file
3
Assets/Scripts/Framework/Managers/Audio/Data.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2419f0c722d945eb8899b181c7f37012
|
||||
timeCreated: 1773830009
|
||||
129
Assets/Scripts/Framework/Managers/Audio/Data/AudioFileSO.cs
Normal file
129
Assets/Scripts/Framework/Managers/Audio/Data/AudioFileSO.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using BriarQueen.Data.Identifiers;
|
||||
using NaughtyAttributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BriarQueen.Framework.Managers.Audio.Data
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Briar Queen/Audio/New Audio File", fileName = "New Audio File")]
|
||||
public class AudioFileSo : ScriptableObject
|
||||
{
|
||||
[Header("Audio Type")]
|
||||
[SerializeField]
|
||||
private TrackType _type;
|
||||
|
||||
[Header("Audio ID")]
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsMusic))]
|
||||
private MusicKey _musicKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsSfx))]
|
||||
private SFXKey _sfxKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsUiFx))]
|
||||
private UIFXKey _uiFxKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsAmbience))]
|
||||
private AmbienceKey _ambienceKey;
|
||||
|
||||
[SerializeField]
|
||||
[ShowIf(nameof(IsVoice))]
|
||||
private VoiceKey _voiceKey;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("A globally unique identifier used for linking with subtitles or dialogue events.")]
|
||||
private SubtitleKey _matchingSubtitleID;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("A readable display name (optional, for debugging or editor UI).")]
|
||||
private string _displayName;
|
||||
|
||||
[Header("Audio Settings")]
|
||||
[SerializeField]
|
||||
private AudioClip _clip;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float _volume = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0.5f, 2f)]
|
||||
private float _pitch = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Range(0, 256)]
|
||||
private int _priority = 128;
|
||||
|
||||
[SerializeField]
|
||||
private bool _loopable;
|
||||
|
||||
[Header("Mixing & Behavior")]
|
||||
[SerializeField]
|
||||
[Tooltip("If true, music volume will duck during playback.")]
|
||||
private bool _duckMusic;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Seconds to fade music back in after ducking.")]
|
||||
[Min(0.1f)]
|
||||
private float _fadeTime = 0.5f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Optional narrative event name (for triggers, debugging, or cutscenes).")]
|
||||
private string _narrativeEvent;
|
||||
|
||||
public TrackType Type => _type;
|
||||
|
||||
public bool IsMusic => _type == TrackType.Music;
|
||||
public bool IsSfx => _type == TrackType.Sfx;
|
||||
public bool IsUiFx => _type == TrackType.UIFX;
|
||||
public bool IsAmbience => _type == TrackType.Ambience;
|
||||
public bool IsVoice => _type == TrackType.Voice;
|
||||
|
||||
public MusicKey MusicKey => _musicKey;
|
||||
public SFXKey SfxKey => _sfxKey;
|
||||
public UIFXKey UiFxKey => _uiFxKey;
|
||||
public AmbienceKey AmbienceKey => _ambienceKey;
|
||||
public VoiceKey VoiceKey => _voiceKey;
|
||||
|
||||
public SubtitleKey MatchingSubtitleID => _matchingSubtitleID;
|
||||
public string DisplayName => _displayName;
|
||||
|
||||
public AudioClip Clip => _clip;
|
||||
public float Volume => _volume;
|
||||
public float Pitch => _pitch;
|
||||
public int Priority => _priority;
|
||||
public bool Loopable => _loopable;
|
||||
|
||||
public bool DuckMusic => _duckMusic;
|
||||
public float FadeTime => _fadeTime;
|
||||
public string NarrativeEvent => _narrativeEvent;
|
||||
|
||||
public string UniqueID
|
||||
{
|
||||
get
|
||||
{
|
||||
return _type switch
|
||||
{
|
||||
TrackType.Music when _musicKey != MusicKey.None =>
|
||||
AudioNameIdentifiers.Get(_musicKey),
|
||||
|
||||
TrackType.Sfx when _sfxKey != SFXKey.None =>
|
||||
AudioNameIdentifiers.Get(_sfxKey),
|
||||
|
||||
TrackType.UIFX when _uiFxKey != UIFXKey.None =>
|
||||
AudioNameIdentifiers.Get(_uiFxKey),
|
||||
|
||||
TrackType.Ambience when _ambienceKey != AmbienceKey.None =>
|
||||
AudioNameIdentifiers.Get(_ambienceKey),
|
||||
|
||||
TrackType.Voice when _voiceKey != VoiceKey.None =>
|
||||
AudioNameIdentifiers.Get(_voiceKey),
|
||||
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e10f120272164594b27c0055a5d34c1b
|
||||
timeCreated: 1769802228
|
||||
Reference in New Issue
Block a user