/* Simple Sound Manager (c) 2016 Digital Ruby, LLC http://www.digitalruby.com Source code may no longer be redistributed in source format. Using this in apps and games is fine. */ using UnityEngine; using System.Collections; using System.Collections.Generic; namespace DigitalRuby.SoundManagerNamespace { /// /// Provides an easy wrapper to looping audio sources with nice transitions for volume when starting and stopping /// public class LoopingAudioSource { /// /// The audio source that is looping /// public AudioSource AudioSource { get; private set; } /// /// The target volume /// public float TargetVolume { get; set; } /// /// The original target volume - useful if the global sound volume changes you can still have the original target volume to multiply by. /// public float OriginalTargetVolume { get; private set; } /// /// Is this sound stopping? /// public bool Stopping { get; private set; } /// /// Whether the looping audio source persists in between scene changes /// public bool Persist { get; private set; } /// /// Tag for the looping audio source /// public int Tag { get; set; } private float startVolume; private float startMultiplier; private float stopMultiplier; private float currentMultiplier; private float timestamp; private bool paused; /// /// Constructor /// /// Audio source, will be looped automatically /// Start multiplier - seconds to reach peak sound /// Stop multiplier - seconds to fade sound back to 0 volume when stopped /// Whether to persist the looping audio source between scene changes public LoopingAudioSource(AudioSource audioSource, float startMultiplier, float stopMultiplier, bool persist) { AudioSource = audioSource; if (audioSource != null) { AudioSource.loop = true; AudioSource.volume = 0.0f; AudioSource.Stop(); } this.startMultiplier = currentMultiplier = startMultiplier; this.stopMultiplier = stopMultiplier; Persist = persist; } /// /// Play this looping audio source /// /// True if music, false if sound effect public void Play(bool isMusic) { Play(1.0f, isMusic); } /// /// Play this looping audio source /// /// Max volume /// True if music, false if sound effect /// True if played, false if already playing or error public bool Play(float targetVolume, bool isMusic) { if (AudioSource != null) { AudioSource.volume = startVolume = (AudioSource.isPlaying ? AudioSource.volume : 0.0f); AudioSource.loop = true; currentMultiplier = startMultiplier; OriginalTargetVolume = targetVolume; TargetVolume = targetVolume; Stopping = false; timestamp = 0.0f; if (!AudioSource.isPlaying) { AudioSource.Play(); return true; } } return false; } /// /// Stop this looping audio source. The sound will fade out smoothly. /// public void Stop() { if (AudioSource != null && AudioSource.isPlaying && !Stopping) { startVolume = AudioSource.volume; TargetVolume = 0.0f; currentMultiplier = stopMultiplier; Stopping = true; timestamp = 0.0f; } } /// /// Pauses the looping audio source /// public void Pause() { if (AudioSource != null && !paused && AudioSource.isPlaying) { paused = true; AudioSource.Pause(); } } /// /// Resumes the looping audio source /// public void Resume() { if (AudioSource != null && paused) { paused = false; AudioSource.UnPause(); } } /// /// Update this looping audio source /// /// True if finished playing, false otherwise public bool Update() { if (AudioSource != null && AudioSource.isPlaying) { if ((AudioSource.volume = Mathf.Lerp(startVolume, TargetVolume, (timestamp += Time.deltaTime) / currentMultiplier)) == 0.0f && Stopping) { AudioSource.Stop(); Stopping = false; return true; } else { return false; } } return !paused; } } /// /// Sound manager extension methods /// public static class SoundManagerExtensions { /// /// Play an audio clip once using the global sound volume as a multiplier /// /// AudioSource /// Clip public static void PlayOneShotSoundManaged(this AudioSource source, AudioClip clip) { SoundManager.PlayOneShotSound(source, clip, 1.0f); } /// /// Play an audio clip once using the global sound volume as a multiplier /// /// AudioSource /// Clip /// Additional volume scale public static void PlayOneShotSoundManaged(this AudioSource source, AudioClip clip, float volumeScale) { SoundManager.PlayOneShotSound(source, clip, volumeScale); } /// /// Play an audio clip once using the global music volume as a multiplier /// /// AudioSource /// Clip public static void PlayOneShotMusicManaged(this AudioSource source, AudioClip clip) { SoundManager.PlayOneShotMusic(source, clip, 1.0f); } /// /// Play an audio clip once using the global music volume as a multiplier /// /// AudioSource /// Clip /// Additional volume scale public static void PlayOneShotMusicManaged(this AudioSource source, AudioClip clip, float volumeScale) { SoundManager.PlayOneShotMusic(source, clip, volumeScale); } /// /// Play a sound and loop it until stopped, using the global sound volume as a modifier /// /// Audio source to play public static void PlayLoopingSoundManaged(this AudioSource source) { SoundManager.PlayLoopingSound(source, 1.0f, 1.0f); } /// /// Play a sound and loop it until stopped, using the global sound volume as a modifier /// /// Audio source to play /// Additional volume scale /// The number of seconds to fade in and out public static void PlayLoopingSoundManaged(this AudioSource source, float volumeScale, float fadeSeconds) { SoundManager.PlayLoopingSound(source, volumeScale, fadeSeconds); } /// /// Play a music track and loop it until stopped, using the global music volume as a modifier /// /// Audio source to play public static void PlayLoopingMusicManaged(this AudioSource source) { SoundManager.PlayLoopingMusic(source, 1.0f, 1.0f, false); } /// /// Play a music track and loop it until stopped, using the global music volume as a modifier /// /// Audio source to play /// Additional volume scale /// The number of seconds to fade in and out /// Whether to persist the looping music between scene changes public static void PlayLoopingMusicManaged(this AudioSource source, float volumeScale, float fadeSeconds, bool persist) { SoundManager.PlayLoopingMusic(source, volumeScale, fadeSeconds, persist); } /// /// Stop a looping sound /// /// AudioSource to stop public static void StopLoopingSoundManaged(this AudioSource source) { SoundManager.StopLoopingSound(source); } /// /// Stop a looping music track /// /// AudioSource to stop public static void StopLoopingMusicManaged(this AudioSource source) { SoundManager.StopLoopingMusic(source); } } /// /// Do not add this script in the inspector. Just call the static methods from your own scripts or use the AudioSource extension methods. /// public class SoundManager : MonoBehaviour { private static int persistTag = 0; private static bool needsInitialize = true; private static GameObject root; private static SoundManager instance; private static readonly List music = new List(); private static readonly List musicOneShot = new List(); private static readonly List sounds = new List(); private static readonly HashSet persistedSounds = new HashSet(); private static readonly Dictionary> soundsOneShot = new Dictionary>(); private static float soundVolume = 1.0f; private static float musicVolume = 1.0f; private static bool updated; private static bool pauseSoundsOnApplicationPause = true; /// /// Maximum number of the same audio clip to play at once /// public static int MaxDuplicateAudioClips = 4; /// /// Whether to stop sounds when a new level loads. Set to false for additive level loading. /// public static bool StopSoundsOnLevelLoad = true; private static void EnsureCreated() { if (needsInitialize) { needsInitialize = false; root = new GameObject(); root.name = "DigitalRubySoundManager"; root.hideFlags = HideFlags.HideAndDontSave; instance = root.AddComponent(); GameObject.DontDestroyOnLoad(root); } } private void StopLoopingListOnLevelLoad(IList list) { for (int i = list.Count - 1; i >= 0; i--) { if (!list[i].Persist || !list[i].AudioSource.isPlaying) { list.RemoveAt(i); } } } private void ClearPersistedSounds() { foreach (LoopingAudioSource s in persistedSounds) { if (!s.AudioSource.isPlaying) { GameObject.Destroy(s.AudioSource.gameObject); } } persistedSounds.Clear(); } private void SceneManagerSceneLoaded(UnityEngine.SceneManagement.Scene s, UnityEngine.SceneManagement.LoadSceneMode m) { // Just in case this is called a bunch of times, we put a check here if (updated && StopSoundsOnLevelLoad) { persistTag++; updated = false; Debug.LogWarningFormat("Reloaded level, new sound manager persist tag: {0}", persistTag); StopNonLoopingSounds(); StopLoopingListOnLevelLoad(sounds); StopLoopingListOnLevelLoad(music); soundsOneShot.Clear(); ClearPersistedSounds(); } } private void Start() { UnityEngine.SceneManagement.SceneManager.sceneLoaded += SceneManagerSceneLoaded; } private void Update() { updated = true; for (int i = sounds.Count - 1; i >= 0; i--) { if (sounds[i].Update()) { sounds.RemoveAt(i); } } for (int i = music.Count - 1; i >= 0; i--) { bool nullMusic = (music[i] == null || music[i].AudioSource == null); if (nullMusic || music[i].Update()) { if (!nullMusic && music[i].Tag != persistTag) { Debug.LogWarning("Destroying persisted audio from previous scene: " + music[i].AudioSource.gameObject.name); // cleanup persisted audio from previous scenes GameObject.Destroy(music[i].AudioSource.gameObject); } music.RemoveAt(i); } } for (int i = musicOneShot.Count - 1; i >= 0; i--) { if (!musicOneShot[i].isPlaying) { musicOneShot.RemoveAt(i); } } } private void OnApplicationFocus(bool paused) { if (SoundManager.PauseSoundsOnApplicationPause) { if (paused) { SoundManager.ResumeAll(); } else { SoundManager.PauseAll(); } } } private static void UpdateSounds() { foreach (LoopingAudioSource s in sounds) { s.TargetVolume = s.OriginalTargetVolume * soundVolume; } } private static void UpdateMusic() { foreach (LoopingAudioSource s in music) { if (!s.Stopping) { s.TargetVolume = s.OriginalTargetVolume * musicVolume; } } foreach (AudioSource s in musicOneShot) { s.volume = musicVolume; } } private static IEnumerator RemoveVolumeFromClip(AudioClip clip, float volume) { yield return new WaitForSeconds(clip.length); List volumes; if (soundsOneShot.TryGetValue(clip, out volumes)) { volumes.Remove(volume); } } private static void PlayLooping(AudioSource source, List sources, float volumeScale, float fadeSeconds, bool persist, bool stopAll) { EnsureCreated(); for (int i = sources.Count - 1; i >= 0; i--) { LoopingAudioSource s = sources[i]; if (s.AudioSource == source) { sources.RemoveAt(i); } if (stopAll) { s.Stop(); } } { source.gameObject.SetActive(true); LoopingAudioSource s = new LoopingAudioSource(source, fadeSeconds, fadeSeconds, persist); s.Play(volumeScale, true); s.Tag = persistTag; sources.Add(s); if (persist) { if (!source.gameObject.name.StartsWith("PersistedBySoundManager-")) { source.gameObject.name = "PersistedBySoundManager-" + source.gameObject.name + "-" + source.gameObject.GetInstanceID(); } source.gameObject.transform.parent = null; GameObject.DontDestroyOnLoad(source.gameObject); persistedSounds.Add(s); } } } private static void StopLooping(AudioSource source, List sources) { foreach (LoopingAudioSource s in sources) { if (s.AudioSource == source) { s.Stop(); source = null; break; } } if (source != null) { source.Stop(); } } /// /// Play a sound once - sound volume will be affected by global sound volume /// /// Audio source /// Clip public static void PlayOneShotSound(AudioSource source, AudioClip clip) { PlayOneShotSound(source, clip, 1.0f); } /// /// Play a sound once - sound volume will be affected by global sound volume /// /// Audio source /// Clip /// Additional volume scale public static void PlayOneShotSound(AudioSource source, AudioClip clip, float volumeScale) { EnsureCreated(); List volumes; if (!soundsOneShot.TryGetValue(clip, out volumes)) { volumes = new List(); soundsOneShot[clip] = volumes; } else if (volumes.Count == MaxDuplicateAudioClips) { return; } float minVolume = float.MaxValue; float maxVolume = float.MinValue; foreach (float volume in volumes) { minVolume = Mathf.Min(minVolume, volume); maxVolume = Mathf.Max(maxVolume, volume); } float requestedVolume = (volumeScale * soundVolume); if (maxVolume > 0.5f) { requestedVolume = (minVolume + maxVolume) / (float)(volumes.Count + 2); } // else requestedVolume can stay as is volumes.Add(requestedVolume); source.PlayOneShot(clip, requestedVolume); instance.StartCoroutine(RemoveVolumeFromClip(clip, requestedVolume)); } /// /// Play a looping sound - sound volume will be affected by global sound volume /// /// Audio source to play looping public static void PlayLoopingSound(AudioSource source) { PlayLoopingSound(source, 1.0f, 1.0f); } /// /// Play a looping sound - sound volume will be affected by global sound volume /// /// Audio source to play looping /// Additional volume scale /// Seconds to fade in and out public static void PlayLoopingSound(AudioSource source, float volumeScale, float fadeSeconds) { PlayLooping(source, sounds, volumeScale, fadeSeconds, false, false); UpdateSounds(); } /// /// Play a music track once - music volume will be affected by the global music volume /// /// /// public static void PlayOneShotMusic(AudioSource source, AudioClip clip) { PlayOneShotMusic(source, clip, 1.0f); } /// /// Play a music track once - music volume will be affected by the global music volume /// /// Audio source /// Clip /// Additional volume scale public static void PlayOneShotMusic(AudioSource source, AudioClip clip, float volumeScale) { EnsureCreated(); int index = musicOneShot.IndexOf(source); if (index >= 0) { musicOneShot.RemoveAt(index); } source.PlayOneShot(clip, volumeScale * musicVolume); musicOneShot.Add(source); } /// /// Play a looping music track - music volume will be affected by the global music volume /// /// Audio source public static void PlayLoopingMusic(AudioSource source) { PlayLoopingMusic(source, 1.0f, 1.0f, false); } /// /// Play a looping music track - music volume will be affected by the global music volume /// /// Audio source /// Additional volume scale /// Seconds to fade in and out /// Whether to persist the looping music between scene changes public static void PlayLoopingMusic(AudioSource source, float volumeScale, float fadeSeconds, bool persist) { PlayLooping(source, music, volumeScale, fadeSeconds, persist, true); UpdateMusic(); } /// /// Stop looping a sound for an audio source /// /// Audio source to stop looping sound for public static void StopLoopingSound(AudioSource source) { StopLooping(source, sounds); } /// /// Stop looping music for an audio source /// /// Audio source to stop looping music for public static void StopLoopingMusic(AudioSource source) { StopLooping(source, music); } /// /// Stop all looping sounds, music, and music one shots. Non-looping sounds are not stopped. /// public static void StopAll() { StopAllLoopingSounds(); StopNonLoopingSounds(); } /// /// Stop all looping sounds and music. Music one shots and non-looping sounds are not stopped. /// public static void StopAllLoopingSounds() { foreach (LoopingAudioSource s in sounds) { s.Stop(); } foreach (LoopingAudioSource s in music) { s.Stop(); } } /// /// Stop all non-looping sounds. Looping sounds and looping music are not stopped. /// public static void StopNonLoopingSounds() { foreach (AudioSource s in musicOneShot) { s.Stop(); } } /// /// Pause all sounds /// public static void PauseAll() { foreach (LoopingAudioSource s in sounds) { s.Pause(); } foreach (LoopingAudioSource s in music) { s.Pause(); } } /// /// Unpause and resume all sounds /// public static void ResumeAll() { foreach (LoopingAudioSource s in sounds) { s.Resume(); } foreach (LoopingAudioSource s in music) { s.Resume(); } } /// /// Global music volume multiplier /// public static float MusicVolume { get { return musicVolume; } set { if (value != musicVolume) { musicVolume = value; UpdateMusic(); } } } /// /// Global sound volume multiplier /// public static float SoundVolume { get { return soundVolume; } set { if (value != soundVolume) { soundVolume = value; UpdateSounds(); } } } /// /// Whether to pause sounds when the application is paused and resume them when the application is activated. /// Player option "Run In Background" must be selected to enable this. Default is true. /// public static bool PauseSoundsOnApplicationPause { get { return pauseSoundsOnApplicationPause; } set { pauseSoundsOnApplicationPause = value; } } } }