Files
Emberwild/Assets/GAME/Script/Audio/MusicManager.cs
T
2026-06-22 16:18:34 +02:00

481 lines
16 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Ashwild.Core;
namespace Ashwild.Audio
{
public class MusicManager : MonoBehaviour
{
public static MusicManager Instance { get; private set; }
[Header("Playlist")]
[SerializeField] private MusicPlaylist activePlaylist;
[SerializeField] private bool playOnAwake = true;
[Header("Ducking")]
[SerializeField] private bool enableDucking;
[Range(0f, 1f)] [SerializeField] private float duckVolume = 0.15f;
[Range(0f, 5f)] [SerializeField] private float duckFadeDuration = 0.5f;
[Header("Persistence")]
[SerializeField] private bool persistAcrossScenes = true;
[Header("Events")]
public UnityEvent<AudioClip> onTrackChanged;
public UnityEvent<MusicPlaylist> onPlaylistChanged;
private AudioSource sourceA;
private AudioSource sourceB;
private AudioSource activeSource;
private AudioSource inactiveSource;
private MusicPlaylist currentPlaylist;
private List<int> shuffleOrder;
private int shuffleIndex;
private int sequentialIndex;
private int lastRandomIndex = -1;
private Coroutine playbackRoutine;
private Coroutine duckRoutine;
private Coroutine oneShotRoutine;
private float masterVolume = 1f;
private bool isPaused;
private bool isDucked;
private float pausedTime;
private AudioClip pausedTrack;
private bool isPlayingOneShot;
public bool IsPlaying => activeSource != null && activeSource.isPlaying;
public bool IsPaused => isPaused;
public float MasterVolume => masterVolume;
public MusicPlaylist CurrentPlaylist => currentPlaylist;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
if (persistAcrossScenes)
Persistence.Persist(gameObject);
sourceA = gameObject.AddComponent<AudioSource>();
sourceB = gameObject.AddComponent<AudioSource>();
ConfigureSource(sourceA);
ConfigureSource(sourceB);
activeSource = sourceA;
inactiveSource = sourceB;
}
private void Start()
{
if (playOnAwake && activePlaylist != null)
SetPlaylist(activePlaylist);
}
private void ConfigureSource(AudioSource source)
{
source.playOnAwake = false;
source.loop = false;
source.spatialBlend = 0f;
}
// ── Public API ──────────────────────────────────────────────
public void SetPlaylist(MusicPlaylist playlist)
{
if (playlist == null || playlist.tracks == null || playlist.tracks.Length == 0)
return;
StopAllPlayback();
currentPlaylist = playlist;
sequentialIndex = 0;
lastRandomIndex = -1;
BuildShuffleOrder();
onPlaylistChanged?.Invoke(currentPlaylist);
playbackRoutine = StartCoroutine(PlaybackLoop());
}
public void TransitionToPlaylist(MusicPlaylist playlist)
{
if (playlist == null || playlist.tracks == null || playlist.tracks.Length == 0)
return;
if (isPlayingOneShot)
{
if (oneShotRoutine != null)
StopCoroutine(oneShotRoutine);
isPlayingOneShot = false;
}
StartCoroutine(TransitionRoutine(playlist));
}
public void PlayOneShot(AudioClip clip, float volume = 1f, float fadeIn = 1f, float fadeOut = 1f)
{
if (clip == null) return;
if (oneShotRoutine != null)
StopCoroutine(oneShotRoutine);
oneShotRoutine = StartCoroutine(OneShotRoutine(clip, volume, fadeIn, fadeOut));
}
public void Pause(bool resumeFromPosition = true)
{
if (isPaused || !IsPlaying) return;
isPaused = true;
if (resumeFromPosition)
pausedTime = activeSource.time;
StartCoroutine(FadeSource(activeSource, activeSource.volume, 0f, currentPlaylist != null ? currentPlaylist.fadeOutDuration : 1f, true));
}
public void Resume()
{
if (!isPaused) return;
isPaused = false;
if (pausedTime > 0f)
activeSource.time = pausedTime;
activeSource.UnPause();
float targetVol = GetTrackVolume(pausedTrack);
StartCoroutine(FadeSource(activeSource, 0f, targetVol, currentPlaylist != null ? currentPlaylist.fadeInDuration : 1f, false));
}
public void Stop()
{
StopAllPlayback();
}
public void SetMasterVolume(float volume)
{
float previous = masterVolume;
masterVolume = Mathf.Clamp01(volume);
if (activeSource.isPlaying && previous > 0f)
activeSource.volume = activeSource.volume / previous * masterVolume;
}
public void Duck()
{
if (isDucked || !IsPlaying) return;
isDucked = true;
if (duckRoutine != null)
StopCoroutine(duckRoutine);
duckRoutine = StartCoroutine(FadeSource(activeSource, activeSource.volume, duckVolume * masterVolume, duckFadeDuration, false));
}
public void Unduck()
{
if (!isDucked) return;
isDucked = false;
float targetVol = currentPlaylist != null ? currentPlaylist.volume * masterVolume : masterVolume;
if (duckRoutine != null)
StopCoroutine(duckRoutine);
duckRoutine = StartCoroutine(FadeSource(activeSource, activeSource.volume, targetVol, duckFadeDuration, false));
}
// ── Playback Loop ───────────────────────────────────────────
private IEnumerator PlaybackLoop()
{
while (currentPlaylist != null)
{
AudioClip track = GetNextTrack();
if (track == null)
yield break;
float targetVol = GetTrackVolume(track);
bool useCrossfade = currentPlaylist.crossfadeDuration > 0f && activeSource.isPlaying;
if (useCrossfade)
{
yield return StartCoroutine(CrossfadeToTrack(track, targetVol));
}
else
{
yield return StartCoroutine(PlayTrackWithFade(activeSource, track, targetVol));
}
if (isPaused)
{
pausedTrack = track;
yield return new WaitUntil(() => !isPaused);
}
float delay = Random.Range(currentPlaylist.delayMin, currentPlaylist.delayMax);
yield return new WaitForSeconds(delay);
}
}
private IEnumerator PlayTrackWithFade(AudioSource source, AudioClip track, float targetVol)
{
source.clip = track;
source.pitch = Random.Range(currentPlaylist.pitchMin, currentPlaylist.pitchMax);
source.volume = 0f;
source.Play();
pausedTrack = track;
onTrackChanged?.Invoke(track);
yield return StartCoroutine(FadeSource(source, 0f, targetVol, currentPlaylist.fadeInDuration, false));
float remainingTime = track.length / source.pitch - currentPlaylist.fadeOutDuration;
if (remainingTime > 0f)
yield return new WaitForSeconds(remainingTime);
if (!isPaused)
yield return StartCoroutine(FadeSource(source, source.volume, 0f, currentPlaylist.fadeOutDuration, false));
source.Stop();
}
private IEnumerator CrossfadeToTrack(AudioClip track, float targetVol)
{
AudioSource newSource = inactiveSource;
AudioSource oldSource = activeSource;
newSource.clip = track;
newSource.pitch = Random.Range(currentPlaylist.pitchMin, currentPlaylist.pitchMax);
newSource.volume = 0f;
newSource.Play();
pausedTrack = track;
onTrackChanged?.Invoke(track);
float duration = currentPlaylist.crossfadeDuration;
float elapsed = 0f;
float oldStartVol = oldSource.volume;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
newSource.volume = Mathf.Lerp(0f, targetVol, t);
oldSource.volume = Mathf.Lerp(oldStartVol, 0f, t);
yield return null;
}
oldSource.Stop();
oldSource.volume = 0f;
newSource.volume = targetVol;
activeSource = newSource;
inactiveSource = oldSource;
float remainingTime = track.length / newSource.pitch - duration - currentPlaylist.fadeOutDuration;
if (remainingTime > 0f)
yield return new WaitForSeconds(remainingTime);
if (!isPaused)
yield return StartCoroutine(FadeSource(activeSource, activeSource.volume, 0f, currentPlaylist.fadeOutDuration, false));
activeSource.Stop();
}
// ── Transitions ─────────────────────────────────────────────
private IEnumerator TransitionRoutine(MusicPlaylist newPlaylist)
{
if (playbackRoutine != null)
{
StopCoroutine(playbackRoutine);
playbackRoutine = null;
}
if (activeSource.isPlaying)
{
float fadeDuration = currentPlaylist != null ? currentPlaylist.fadeOutDuration : 1f;
yield return StartCoroutine(FadeSource(activeSource, activeSource.volume, 0f, fadeDuration, false));
activeSource.Stop();
}
currentPlaylist = newPlaylist;
sequentialIndex = 0;
lastRandomIndex = -1;
BuildShuffleOrder();
onPlaylistChanged?.Invoke(currentPlaylist);
playbackRoutine = StartCoroutine(PlaybackLoop());
}
// ── One-Shot ────────────────────────────────────────────────
private IEnumerator OneShotRoutine(AudioClip clip, float volume, float fadeIn, float fadeOut)
{
isPlayingOneShot = true;
bool wasPlaying = activeSource.isPlaying;
if (playbackRoutine != null)
{
StopCoroutine(playbackRoutine);
playbackRoutine = null;
}
if (wasPlaying)
{
float fadeDuration = currentPlaylist != null ? currentPlaylist.fadeOutDuration : 1f;
yield return StartCoroutine(FadeSource(activeSource, activeSource.volume, 0f, fadeDuration, false));
activeSource.Stop();
}
AudioSource shotSource = inactiveSource;
shotSource.clip = clip;
shotSource.pitch = 1f;
shotSource.volume = 0f;
shotSource.Play();
yield return StartCoroutine(FadeSource(shotSource, 0f, volume * masterVolume, fadeIn, false));
float remaining = clip.length - fadeIn - fadeOut;
if (remaining > 0f)
yield return new WaitForSeconds(remaining);
yield return StartCoroutine(FadeSource(shotSource, shotSource.volume, 0f, fadeOut, false));
shotSource.Stop();
isPlayingOneShot = false;
if (currentPlaylist != null && wasPlaying)
playbackRoutine = StartCoroutine(PlaybackLoop());
}
// ── Track Selection ─────────────────────────────────────────
private AudioClip GetNextTrack()
{
if (currentPlaylist == null || currentPlaylist.tracks.Length == 0)
return null;
int count = currentPlaylist.tracks.Length;
if (count == 1)
return currentPlaylist.tracks[0];
switch (currentPlaylist.playMode)
{
case PlayMode.Sequential:
int seqIdx = sequentialIndex % count;
sequentialIndex++;
return currentPlaylist.tracks[seqIdx];
case PlayMode.Random:
int rndIdx;
do
{
rndIdx = Random.Range(0, count);
} while (rndIdx == lastRandomIndex);
lastRandomIndex = rndIdx;
return currentPlaylist.tracks[rndIdx];
case PlayMode.Shuffle:
if (shuffleIndex >= shuffleOrder.Count)
{
int lastPlayed = shuffleOrder.Count > 0 ? shuffleOrder[shuffleOrder.Count - 1] : -1;
BuildShuffleOrder();
if (shuffleOrder.Count > 1 && shuffleOrder[0] == lastPlayed)
{
int swapIdx = Random.Range(1, shuffleOrder.Count);
(shuffleOrder[0], shuffleOrder[swapIdx]) = (shuffleOrder[swapIdx], shuffleOrder[0]);
}
}
int idx = shuffleOrder[shuffleIndex];
shuffleIndex++;
return currentPlaylist.tracks[idx];
default:
return currentPlaylist.tracks[0];
}
}
private void BuildShuffleOrder()
{
int count = currentPlaylist.tracks.Length;
shuffleOrder = new List<int>(count);
for (int i = 0; i < count; i++)
shuffleOrder.Add(i);
for (int i = count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(shuffleOrder[i], shuffleOrder[j]) = (shuffleOrder[j], shuffleOrder[i]);
}
shuffleIndex = 0;
}
// ── Utility ─────────────────────────────────────────────────
private float GetTrackVolume(AudioClip track)
{
if (currentPlaylist == null || track == null) return masterVolume;
if (isDucked) return duckVolume * masterVolume;
return currentPlaylist.volume * masterVolume;
}
private IEnumerator FadeSource(AudioSource source, float from, float to, float duration, bool pauseAtEnd)
{
if (duration <= 0f)
{
source.volume = to;
if (pauseAtEnd) source.Pause();
yield break;
}
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
source.volume = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
yield return null;
}
source.volume = to;
if (pauseAtEnd) source.Pause();
}
private void StopAllPlayback()
{
if (playbackRoutine != null)
{
StopCoroutine(playbackRoutine);
playbackRoutine = null;
}
if (oneShotRoutine != null)
{
StopCoroutine(oneShotRoutine);
oneShotRoutine = null;
}
if (duckRoutine != null)
{
StopCoroutine(duckRoutine);
duckRoutine = null;
}
sourceA.Stop();
sourceB.Stop();
sourceA.volume = 0f;
sourceB.volume = 0f;
isPaused = false;
isDucked = false;
isPlayingOneShot = false;
pausedTime = 0f;
}
}
}