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 onTrackChanged; public UnityEvent onPlaylistChanged; private AudioSource sourceA; private AudioSource sourceB; private AudioSource activeSource; private AudioSource inactiveSource; private MusicPlaylist currentPlaylist; private List 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(); sourceB = gameObject.AddComponent(); 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(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; } } }