You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Feel/MMTools/Tools/MMAudio/MMPlaylist/MMPlaylist.cs

774 lines
21 KiB
C#

3 months ago
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MoreMountains.Tools
{
/// <summary>
/// This class stores all the info related to items in a playlist
/// </summary>
public struct MMPlaylistPlayEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(int channel);
static public void Trigger(int channel)
{
OnEvent?.Invoke(channel);
}
}
public struct MMPlaylistStopEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(int channel);
static public void Trigger(int channel)
{
OnEvent?.Invoke(channel);
}
}
public struct MMPlaylistPauseEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(int channel);
static public void Trigger(int channel)
{
OnEvent?.Invoke(channel);
}
}
public struct MMPlaylistPlayNextEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(int channel);
static public void Trigger(int channel)
{
OnEvent?.Invoke(channel);
}
}
public struct MMPlaylistPlayPreviousEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(int channel);
static public void Trigger(int channel)
{
OnEvent?.Invoke(channel);
}
}
public struct MMPlaylistPlayIndexEvent
{
static private event Delegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RuntimeInitialization() { OnEvent = null; }
static public void Register(Delegate callback) { OnEvent += callback; }
static public void Unregister(Delegate callback) { OnEvent -= callback; }
public delegate void Delegate(int channel, int index);
static public void Trigger(int channel, int index)
{
OnEvent?.Invoke(channel, index);
}
}
[System.Serializable]
public class MMPlaylistSong
{
/// the audiosource that contains the audio clip we want to play
public AudioSource TargetAudioSource;
/// the min (when it's off) and max (when it's playing) volume for this source
[MMVector("Min", "Max")]
public Vector2 Volume = new Vector2(1f, 1f);
/// a random delay in seconds to apply, between its RMin and RMax
[MMVector("RMin", "RMax")]
public Vector2 InitialDelay = Vector2.zero;
/// a random crossfade duration (in seconds) to apply when transitioning to this song, between its RMin and RMax
[MMVector("RMin", "RMax")]
public Vector2 CrossFadeDuration = new Vector2(2f, 2f);
/// a random pitch to apply to this song, between its RMin and RMax
[MMVector("RMin", "RMax")]
public Vector2 Pitch = Vector2.one;
/// the stereo pan for this song
[Range(-1f, 1f)]
public float StereoPan = 0f;
/// the spatial blend for this song (0 is 2D, 1 is 3D)
[Range(0f, 1f)]
public float SpatialBlend = 0f;
/// whether this song should loop or not
public bool Loop = false;
/// whether this song is playing right now or not
[MMReadOnly]
public bool Playing = false;
/// whether this song is fading right now or not
[MMReadOnly]
public bool Fading = false;
[MMHidden]
public bool _initialized;
public virtual void Initialization()
{
if (_initialized)
{
return;
}
this.Volume = new Vector2(1f, 1f);
this.InitialDelay = Vector2.zero;
this.CrossFadeDuration = new Vector2(2f, 2f);
this.Pitch = Vector2.one;
this.StereoPan = 0f;
this.SpatialBlend = 0f;
this.Loop = false;
this._initialized = true;
}
}
/// <summary>
/// Use this class to play audiosources (usually background music but feel free to use that for anything) in sequence, with optional crossfade between songs
/// </summary>
[AddComponentMenu("More Mountains/Tools/Audio/MMPlaylist")]
[MMRequiresConstantRepaint]
public class MMPlaylist : MMMonoBehaviour
{
/// the possible states this playlist can be in
public enum PlaylistStates
{
Idle,
Playing,
Paused
}
[MMInspectorGroup("Playlist Songs", true, 18)]
/// the channel on which to broadcast orders for this playlist
[Tooltip("the channel on which to broadcast orders for this playlist")]
public int Channel = 0;
/// the songs that this playlist will play
[Tooltip("the songs that this playlist will play")]
public List<MMPlaylistSong> Songs;
[MMInspectorGroup("Settings", true, 13)]
/// whether this should play in random order or not
[Tooltip("whether this should play in random order or not")]
public bool RandomOrder = false;
/// if this is true, random seed will be randomized by the system clock
[Tooltip("if this is true, random seed will be randomized by the system clock")]
[MMCondition("RandomOrder", true)]
public bool RandomizeOrderSeed = true;
/// whether this playlist should play and loop as a whole forever or not
[Tooltip("whether this playlist should play and loop as a whole forever or not")]
public bool Endless = true;
/// whether this playlist should auto play on start or not
[Tooltip("whether this playlist should auto play on start or not")]
public bool PlayOnStart = true;
/// a global volume multiplier to apply when playing a song
[Tooltip("a global volume multiplier to apply when playing a song")]
public float VolumeMultiplier = 1f;
/// if this is true, this playlist will automatically pause/resume OnApplicationPause, useful if you've prevented your game from running in the background
[Tooltip("if this is true, this playlist will automatically pause/resume OnApplicationPause, useful if you've prevented your game from running in the background")]
public bool AutoHandleApplicationPause = true;
[MMInspectorGroup("Persistence", true, 32)]
/// if this is true, this playlist will persist from scene to scene
[Tooltip("if this is true, this playlist will persist from scene to scene")]
public bool Persistent = false;
/// if this is true, this singleton will auto detach if it finds itself parented on awake
[Tooltip("if this is true, this singleton will auto detach if it finds itself parented on awake")]
[MMCondition("Persistent", true)]
public bool AutomaticallyUnparentOnAwake = true;
[MMInspectorGroup("Status", true, 14)]
/// the current state of the playlist, debug display only
[Tooltip("the current state of the playlist, debug display only")]
[MMReadOnly]
public PlaylistStates DebugCurrentState = PlaylistStates.Idle;
/// the index we're currently playing
[Tooltip("the index we're currently playing")]
[MMReadOnly]
public int CurrentlyPlayingIndex = -1;
/// the name of the song that is currently playing
[Tooltip("the name of the song that is currently playing")]
[MMReadOnly]
public string CurrentSongName;
/// the current state of this playlist
[MMReadOnly]
public MMStateMachine<MMPlaylist.PlaylistStates> PlaylistState;
[MMInspectorGroup("Tests", true, 15)]
/// a play test button
[MMInspectorButton("Play")]
public bool PlayButton;
/// a pause test button
[MMInspectorButton("Pause")]
public bool PauseButton;
/// a stop test button
[MMInspectorButton("Stop")]
public bool StopButton;
/// a next song test button
[MMInspectorButton("PlayNextSong")]
public bool NextButton;
/// the index of the song to play when pressing the PlayTargetSong button
[Tooltip("the index of the song to play when pressing the PlayTargetSong button")]
public int TargetSongIndex = 0;
/// a next song test button
[MMInspectorButton("PlayTargetSong")]
public bool TargetSongButton;
protected int _songsPlayedSoFar = 0;
protected int _songsPlayedThisCycle = 0;
protected Coroutine _coroutine;
protected bool _shouldResumeOnApplicationPause = false;
public static bool HasInstance => _instance != null;
public static MMPlaylist Current => _instance;
protected static MMPlaylist _instance;
protected bool _enabled;
/// <summary>
/// Singleton design pattern
/// </summary>
/// <value>The instance.</value>
public static MMPlaylist Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<MMPlaylist> ();
if (_instance == null)
{
GameObject obj = new GameObject ();
obj.name = typeof(MMPlaylist).Name + "_AutoCreated";
_instance = obj.AddComponent<MMPlaylist> ();
}
}
return _instance;
}
}
/// <summary>
/// On awake, we check if there's already a copy of the object in the scene. If there's one, we destroy it.
/// </summary>
protected virtual void Awake ()
{
InitializeSingleton();
}
/// <summary>
/// Initializes the singleton.
/// </summary>
protected virtual void InitializeSingleton()
{
if (!Application.isPlaying)
{
return;
}
if (!Persistent)
{
return;
}
if (AutomaticallyUnparentOnAwake)
{
this.transform.SetParent(null);
}
if (_instance == null)
{
//If I am the first instance, make me the Singleton
_instance = this;
DontDestroyOnLoad (transform.gameObject);
_enabled = true;
}
else
{
//If a Singleton already exists and you find
//another reference in scene, destroy it!
if(this != _instance)
{
Destroy(this.gameObject);
}
}
}
/// <summary>
/// On Start we initialize our playlist
/// </summary>
protected virtual void Start()
{
Initialization();
}
/// <summary>
/// On init we initialize our state machine and start playing if needed
/// </summary>
protected virtual void Initialization()
{
if (RandomOrder && RandomizeOrderSeed)
{
Random.InitState(System.Environment.TickCount);
}
_songsPlayedSoFar = 0;
PlaylistState = new MMStateMachine<MMPlaylist.PlaylistStates>(this.gameObject, true);
ChangePlaylistState(PlaylistStates.Idle);
if (Songs.Count == 0)
{
return;
}
if (PlayOnStart)
{
PlayFirstSong();
}
}
protected virtual void ChangePlaylistState(PlaylistStates newState)
{
PlaylistState.ChangeState(newState);
DebugCurrentState = newState;
}
/// <summary>
/// Picks and plays the first song
/// </summary>
protected virtual void PlayFirstSong()
{
_songsPlayedThisCycle = 0;
CurrentlyPlayingIndex = -1;
int newIndex = PickNextIndex();
_coroutine = StartCoroutine(PlaySong(newIndex));
}
/// <summary>
/// Plays a new song in the playlist, and stops / fades the previous one
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
protected virtual IEnumerator PlaySong(int index)
{
// if we don't have a song, we stop
if (Songs.Count == 0)
{
yield break;
}
// if we've played all our songs, we stop
if (!Endless && (_songsPlayedThisCycle > Songs.Count))
{
yield break;
}
if (_coroutine != null)
{
StopCoroutine(_coroutine);
}
// we stop our current song
if ((PlaylistState.CurrentState == PlaylistStates.Playing)
&& (index >= 0 && index < Songs.Count)
&& !Songs[index].TargetAudioSource.isPlaying)
{
StartCoroutine(Fade(CurrentlyPlayingIndex,
Random.Range(Songs[index].CrossFadeDuration.x, Songs[index].CrossFadeDuration.y),
Songs[CurrentlyPlayingIndex].Volume.y * VolumeMultiplier,
Songs[CurrentlyPlayingIndex].Volume.x * VolumeMultiplier,
true));
}
// we stop all other coroutines
if ((CurrentlyPlayingIndex >= 0) && (Songs.Count > CurrentlyPlayingIndex))
{
foreach (MMPlaylistSong song in Songs)
{
if (song != Songs[CurrentlyPlayingIndex])
{
song.Fading = false;
}
}
}
if (index < 0 || index >= Songs.Count)
{
yield break;
}
// initial delay
yield return MMCoroutine.WaitFor(Random.Range(Songs[index].InitialDelay.x, Songs[index].InitialDelay.y));
if (Songs[index].TargetAudioSource == null)
{
Debug.LogError(this.name + " : the playlist song you're trying to play is null");
yield break;
}
Songs[index].TargetAudioSource.pitch = Random.Range(Songs[index].Pitch.x, Songs[index].Pitch.y);
Songs[index].TargetAudioSource.panStereo = Songs[index].StereoPan;
Songs[index].TargetAudioSource.spatialBlend = Songs[index].SpatialBlend;
Songs[index].TargetAudioSource.loop = Songs[index].Loop;
// fades the new song's volume
StartCoroutine(Fade(index,
Random.Range(Songs[index].CrossFadeDuration.x, Songs[index].CrossFadeDuration.y),
Songs[index].Volume.x * VolumeMultiplier,
Songs[index].Volume.y * VolumeMultiplier,
false));
// starts the new song
Songs[index].TargetAudioSource.Play();
// updates our state
CurrentSongName = Songs[index].TargetAudioSource.clip.name;
ChangePlaylistState(PlaylistStates.Playing);
Songs[index].Playing = true;
CurrentlyPlayingIndex = index;
_songsPlayedSoFar++;
_songsPlayedThisCycle++;
while (Songs[index].TargetAudioSource.isPlaying || (PlaylistState.CurrentState == PlaylistStates.Paused) || _shouldResumeOnApplicationPause)
{
yield return null;
}
if (PlaylistState.CurrentState != PlaylistStates.Playing)
{
yield break;
}
if (_songsPlayedSoFar < Songs.Count)
{
_coroutine = StartCoroutine(PlaySong(PickNextIndex()));
}
else
{
if (Endless)
{
_coroutine = StartCoroutine(PlaySong(PickNextIndex()));
}
else
{
ChangePlaylistState(PlaylistStates.Idle);
}
}
}
/// <summary>
/// Fades an audiosource in or out, optionnally stopping it at the end
/// </summary>
/// <param name="source"></param>
/// <param name="duration"></param>
/// <param name="initialVolume"></param>
/// <param name="endVolume"></param>
/// <param name="stopAtTheEnd"></param>
/// <returns></returns>
protected virtual IEnumerator Fade(int index, float duration, float initialVolume, float endVolume, bool stopAtTheEnd)
{
if (index < 0 || index >= Songs.Count)
{
yield break;
}
float startTimestamp = Time.time;
float progress = 0f;
Songs[index].Fading = true;
while ((Time.time - startTimestamp < duration) && (Songs[index].Fading))
{
progress = MMMaths.Remap(Time.time - startTimestamp, 0f, duration, 0f, 1f);
Songs[index].TargetAudioSource.volume = Mathf.Lerp(initialVolume, endVolume, progress);
yield return null;
}
Songs[index].TargetAudioSource.volume = endVolume;
if (stopAtTheEnd)
{
Songs[index].TargetAudioSource.Stop();
Songs[index].Playing = false;
Songs[index].Fading = false;
}
}
/// <summary>
/// Picks the next song to play
/// </summary>
/// <returns></returns>
protected virtual int PickNextIndex()
{
if (Songs.Count == 0)
{
return -1;
}
int newIndex = CurrentlyPlayingIndex;
if (RandomOrder)
{
while (newIndex == CurrentlyPlayingIndex)
{
newIndex = Random.Range(0, Songs.Count);
}
}
else
{
newIndex = (CurrentlyPlayingIndex + 1) % Songs.Count;
}
return newIndex;
}
/// <summary>
/// Picks the previous song to play
/// </summary>
/// <returns></returns>
protected virtual int PickPreviousIndex()
{
if (Songs.Count == 0)
{
return -1;
}
int newIndex = CurrentlyPlayingIndex;
if (RandomOrder)
{
while (newIndex == CurrentlyPlayingIndex)
{
newIndex = Random.Range(0, Songs.Count);
}
}
else
{
newIndex = (CurrentlyPlayingIndex - 1);
if (newIndex < 0)
{
newIndex = Songs.Count - 1;
}
}
return newIndex;
}
/// <summary>
/// Plays either the first song or resumes playing a paused one
/// </summary>
public virtual void Play()
{
switch (PlaylistState.CurrentState)
{
case PlaylistStates.Idle:
PlayFirstSong();
break;
case PlaylistStates.Paused:
Songs[CurrentlyPlayingIndex].TargetAudioSource.UnPause();
ChangePlaylistState(PlaylistStates.Playing);
break;
case PlaylistStates.Playing:
// do nothing
break;
}
}
public virtual void PlayAtIndex(int songIndex)
{
_coroutine = StartCoroutine(PlaySong(songIndex));
}
/// <summary>
/// Pauses the current song
/// </summary>
public virtual void Pause()
{
if (PlaylistState.CurrentState != PlaylistStates.Playing)
{
return;
}
Songs[CurrentlyPlayingIndex].TargetAudioSource.Pause();
ChangePlaylistState(PlaylistStates.Paused);
}
/// <summary>
/// Stops the playlist
/// </summary>
public virtual void Stop()
{
if (PlaylistState.CurrentState != PlaylistStates.Playing)
{
return;
}
Songs[CurrentlyPlayingIndex].TargetAudioSource.Stop();
Songs[CurrentlyPlayingIndex].Playing = false;
Songs[CurrentlyPlayingIndex].Fading = false;
CurrentlyPlayingIndex = -1;
ChangePlaylistState(PlaylistStates.Idle);
}
/// <summary>
/// Plays the next song in the playlist
/// </summary>
public virtual void PlayNextSong()
{
int newIndex = PickNextIndex();
_coroutine = StartCoroutine(PlaySong(newIndex));
}
/// <summary>
/// Plays the previous song in the playlist
/// </summary>
public virtual void PlayPreviousSong()
{
int newIndex = PickPreviousIndex();
_coroutine = StartCoroutine(PlaySong(newIndex));
}
protected virtual void PlayTargetSong()
{
int newIndex = Mathf.Clamp(TargetSongIndex, 0, Songs.Count - 1);
PlayAtIndex(newIndex);
}
protected virtual void OnPlayEvent(int channel)
{
if (channel != Channel) { return; }
Play();
}
protected virtual void OnPauseEvent(int channel)
{
if (channel != Channel) { return; }
Pause();
}
protected virtual void OnStopEvent(int channel)
{
if (channel != Channel) { return; }
Stop();
}
protected virtual void OnPlayNextEvent(int channel)
{
if (channel != Channel) { return; }
PlayNextSong();
}
protected virtual void OnPlayPreviousEvent(int channel)
{
if (channel != Channel) { return; }
PlayPreviousSong();
}
protected virtual void OnPlayIndexEvent(int channel, int index)
{
if (channel != Channel) { return; }
_coroutine = StartCoroutine(PlaySong(index));
}
/// <summary>
/// On enable, starts listening for playlist events
/// </summary>
protected virtual void OnEnable()
{
MMPlaylistPauseEvent.Register(OnPauseEvent);
MMPlaylistPlayEvent.Register(OnPlayEvent);
MMPlaylistPlayNextEvent.Register(OnPlayNextEvent);
MMPlaylistPlayPreviousEvent.Register(OnPlayPreviousEvent);
MMPlaylistStopEvent.Register(OnStopEvent);
MMPlaylistPlayIndexEvent.Register(OnPlayIndexEvent);
}
/// <summary>
/// On disable, stops listening for playlist events
/// </summary>
protected virtual void OnDisable()
{
MMPlaylistPauseEvent.Unregister(OnPauseEvent);
MMPlaylistPlayEvent.Unregister(OnPlayEvent);
MMPlaylistPlayNextEvent.Unregister(OnPlayNextEvent);
MMPlaylistPlayPreviousEvent.Unregister(OnPlayPreviousEvent);
MMPlaylistStopEvent.Unregister(OnStopEvent);
MMPlaylistPlayIndexEvent.Unregister(OnPlayIndexEvent);
}
protected bool _firstDeserialization = true;
protected int _listCount = 0;
/// <summary>
/// On Validate, we check if our array has changed and if yes we initialize our new elements
/// </summary>
protected virtual void OnValidate()
{
if (_firstDeserialization)
{
if (Songs == null)
{
_listCount = 0;
_firstDeserialization = false;
}
else
{
_listCount = Songs.Count;
_firstDeserialization = false;
}
}
else
{
if (Songs.Count != _listCount)
{
if (Songs.Count > _listCount)
{
foreach(MMPlaylistSong song in Songs)
{
song.Initialization();
}
}
_listCount = Songs.Count;
}
}
}
/// <summary>
/// On ApplicationPause, we pause the playlist and resume it afterwards
/// </summary>
/// <param name="pauseStatus"></param>
protected virtual void OnApplicationPause(bool pauseStatus)
{
if (!AutoHandleApplicationPause)
{
return;
}
if (pauseStatus && PlaylistState.CurrentState == PlaylistStates.Playing)
{
Pause();
_shouldResumeOnApplicationPause = true;
}
if (!pauseStatus && _shouldResumeOnApplicationPause)
{
_shouldResumeOnApplicationPause = false;
Play();
}
}
}
}