using System; using System.Collections; using System.Collections.Generic; using Unity.DebugDisplay; using Unity.Sample.Core; using UnityEngine; using UnityEngine.Audio; using UnityEngine.Profiling; using Random = UnityEngine.Random; public static class SoundSystem { [ConfigVar(Name = "sound.debug", DefaultValue = "0", Description = "Enable sound debug overlay")] public static ConfigVar soundDebug; [ConfigVar(Name = "sound.numemitters", DefaultValue = "48", Description = "Number of sound emitters")] public static ConfigVar soundNumEmitters; [ConfigVar(Name = "sound.spatialize", DefaultValue = "0", Description = "Use spatializer")] public static ConfigVar soundSpatialize; [ConfigVar(Name = "sound.mute", DefaultValue = "-1", Description = "Is audio enabled. -1 causes default behavior (on when window has focus)", Flags = ConfigVar.Flags.None)] public static ConfigVar soundMute; // Debugging only [ConfigVar(Name = "sound.mastervol", DefaultValue = "1", Description = "Master volume", Flags = ConfigVar.Flags.None)] public static ConfigVar soundMasterVol; // Exposed in options menu [ConfigVar(Name = "sound.menuvol", DefaultValue = "1", Description = "Menu volume", Flags = ConfigVar.Flags.None)] public static ConfigVar soundMenuVol; [ConfigVar(Name = "sound.sfxvol", DefaultValue = "1", Description = "SFX volume", Flags = ConfigVar.Flags.None)] public static ConfigVar soundSFXVol; [ConfigVar(Name = "sound.musicvol", DefaultValue = "1", Description = "Music volume", Flags = ConfigVar.Flags.None)] public static ConfigVar soundMusicVol; // These are passed to the game code public struct SoundHandle { //public SoundEmitter emitter; public int emitter_idx; public int seq; public bool IsNull() { return emitter_idx == 0 && seq == 0; } public SoundHandle(int emitter_idx, int seq) { this.emitter_idx = emitter_idx; this.seq = seq; } } public static ISoundSystem Instance { get { return m_instance; } } public static void Initialize(ISoundSystem soundSystem) { m_instance = soundSystem; } private static ISoundSystem m_instance; } public class SoundSystemNull : ISoundSystem { public void Init(AudioMixer mixer) {} public bool IsValid(ref SoundSystem.SoundHandle handle) { return false; } public void MountBank(SoundBank bank) {} public SoundSystem.SoundHandle Play(SoundDef soundDef) { return default(SoundSystem.SoundHandle); } public SoundSystem.SoundHandle Play(SoundDef soundDef, Transform parent) { return default(SoundSystem.SoundHandle); } public SoundSystem.SoundHandle Play(SoundDef soundDef, Vector3 position) { return default(SoundSystem.SoundHandle); } public SoundSystem.SoundHandle Play(WeakAssetReference weakSoundDef, Vector3 position) { return default(SoundSystem.SoundHandle); } public void SetCurrentListener(AudioListener audioListener) {} public void SetRegistry(SoundRegistry registry) { } public void Stop(SoundSystem.SoundHandle sh, float fadeOutTime = 0) {} public void UnmountBank(SoundBank bank) {} public void Update() {} public void UpdatePosition(ref SoundSystem.SoundHandle handle, Vector3 position) { } } public class SoundSystemBase : ISoundSystem { public bool IsValid(ref SoundSystem.SoundHandle handle) { return m_Emitters[handle.emitter_idx] != null && m_Emitters[handle.emitter_idx].seqId == handle.seq; } // These are internal to the SoundSystem public class SoundEmitter { public AudioSource source; public SoundDef soundDef; public bool playing; public int repeatCount; public Interpolator fadeToKill; public int seqId; internal void Kill() { source.Stop(); repeatCount = 0; } }; AudioSource MakeAudioSource() { var go = new GameObject("SoundSystemSource"); go.transform.parent = m_SourceHolder.transform; return go.AddComponent(); } static AudioMixerGroup[] s_MixerGroups; public void Init(AudioMixer mixer) { m_SourceHolder = new GameObject("SoundSystemSources"); GameObject.DontDestroyOnLoad(m_SourceHolder); m_AudioMixer = mixer; GameDebug.Log("SoundSystem using mixer: " + m_AudioMixer.name); m_SequenceId = 1; // Create pool of emitters m_Emitters = new SoundEmitter[SoundSystem.soundNumEmitters.IntValue]; for (var i = 0; i < SoundSystem.soundNumEmitters.IntValue; i++) { var emitter = new SoundEmitter(); emitter.source = MakeAudioSource(); emitter.fadeToKill = new Interpolator(1.0f, Interpolator.CurveType.Linear); m_Emitters[i] = emitter; } // Set up mixer groups s_MixerGroups = new AudioMixerGroup[(int)SoundMixerGroup._Count]; s_MixerGroups[(int)SoundMixerGroup.Menu] = m_AudioMixer.FindMatchingGroups("Menu")[0]; s_MixerGroups[(int)SoundMixerGroup.Music] = m_AudioMixer.FindMatchingGroups("Music")[0]; s_MixerGroups[(int)SoundMixerGroup.SFX] = m_AudioMixer.FindMatchingGroups("SFX")[0]; } public struct SoundReq { public SoundDef def; public bool usePos; public Vector3 pos; public Transform transform; } int AllocEmitter() { // Look for unused emitter for(int i = 0; i < m_Emitters.Length; ++i) { var e = m_Emitters[i]; if (!e.playing) { e.seqId = m_SequenceId++; return i; } } // Hunt down one emitter to kill int emitter_idx = -1; float distance = float.MinValue; var listenerPos = m_CurrentListener != null ? m_CurrentListener.transform.position : Vector3.zero; for(var i = 0; i < m_Emitters.Length; ++i) //foreach (var e in m_Emitters) { var e = m_Emitters[i]; var s = e.source; if (s == null) { // Could happen if parent was killed. Not good, but fixable: GameDebug.LogWarning("Soundemitter had its audiosource destroyed. Making a new."); e.source = MakeAudioSource(); e.repeatCount = 0; s = e.source; } // Skip destroyed sources and looping sources if (s.loop) continue; // Pick closest; assuming 2d sounds very close! var dist = 0.0f; if (s.spatialBlend > 0.0f) { dist = (s.transform.position - listenerPos).magnitude; // if tracking another object assume closer var t = s.transform; if (t.parent != m_SourceHolder.transform) dist *= 0.5f; } if (dist > distance) { distance = dist; emitter_idx = i; } } if (emitter_idx != -1) { var e = m_Emitters[emitter_idx]; e.Kill(); e.seqId = m_SequenceId++; return emitter_idx; } GameDebug.Log("Unable to allocate sound emitter!"); return -1; } public SoundSystem.SoundHandle Play(SoundDef soundDef) { if(soundDef == null) { GameDebug.LogWarning("Trying to play null soundDef"); return new SoundSystem.SoundHandle(); } var emitter_idx = AllocEmitter(); if (emitter_idx < 0) return new SoundSystem.SoundHandle(); var e = m_Emitters[emitter_idx]; if (soundDef.spatialBlend > 0.0f) GameDebug.LogWarning(string.Format("Playing 3d {0} sound at 0,0,0", soundDef.name)); e.source.transform.position = new Vector3(0, 0, 0); e.repeatCount = Random.Range(soundDef.repeatMin, soundDef.repeatMax); e.playing = true; e.soundDef = soundDef; StartEmitter(e); return new SoundSystem.SoundHandle(emitter_idx, e.seqId); } public SoundSystem.SoundHandle Play(SoundDef soundDef, Vector3 position) { var emitter_idx = AllocEmitter(); if (emitter_idx < 0) return new SoundSystem.SoundHandle(); var e = m_Emitters[emitter_idx]; e.source.transform.position = position; e.repeatCount = Random.Range(soundDef.repeatMin, soundDef.repeatMax); e.playing = true; e.soundDef = soundDef; StartEmitter(e); return new SoundSystem.SoundHandle(emitter_idx, e.seqId); } public void UpdatePosition(ref SoundSystem.SoundHandle sh, Vector3 position) { if(!IsValid(ref sh)) { GameDebug.LogWarning("Trying to reposition invalid soundhandle"); return; } m_Emitters[sh.emitter_idx].source.transform.position = position; } public SoundSystem.SoundHandle Play(SoundDef soundDef, Transform parent) { Profiler.BeginSample("SoundSystem.AllocEmitter"); var emitter_idx = AllocEmitter(); Profiler.EndSample(); if (emitter_idx < 0) return new SoundSystem.SoundHandle(); var e = m_Emitters[emitter_idx]; e.source.transform.parent = parent; e.source.transform.localPosition = Vector3.zero; e.repeatCount = Random.Range(soundDef.repeatMin, soundDef.repeatMax); e.playing = true; e.soundDef = soundDef; Profiler.BeginSample("SoundSystem.StartEmitter"); StartEmitter(e); Profiler.EndSample(); return new SoundSystem.SoundHandle(emitter_idx, e.seqId); } public void Stop(SoundSystem.SoundHandle sh, float fadeOutTime = 0.0f) { if (!IsValid(ref sh)) { GameDebug.LogWarning("SoundSystem.Stop(): invalid SoundHandle"); return; } var emitter = m_Emitters[sh.emitter_idx]; if (fadeOutTime == 0.0f) emitter.fadeToKill.SetValue(0.0f); else { emitter.fadeToKill.SetValue(1.0f); emitter.fadeToKill.MoveTo(0.0f, fadeOutTime); } } bool focus = false; public void Update() { if (focus != Application.isFocused) { focus = Application.isFocused; if (SoundSystem.soundMute.IntValue == -1) m_MasterVolume.MoveTo(focus ? 1.0f : 0.0f, 0.5f); } var masterVolume = m_MasterVolume.GetValue(); if (SoundSystem.soundMute.IntValue == 0) { masterVolume = 0.0f; Overlay.Managed.Write(Overlay.Color.Red, Overlay.Managed.CellsWide - 10, 2, "{0}", "AUDIO MUTED"); } else if (SoundSystem.soundMute.IntValue == 1) { masterVolume = 1.0f; Overlay.Managed.Write(Overlay.Color.Green, Overlay.Managed.CellsWide - 10, 2, "{0}", "AUDIO PLAYING"); } m_AudioMixer.SetFloat("MasterVolume", DecibelFromAmplitude(Mathf.Clamp(SoundSystem.soundMasterVol.FloatValue, 0.0f, 1.0f) * masterVolume)); m_AudioMixer.SetFloat("MusicVolume", DecibelFromAmplitude(Mathf.Clamp(SoundSystem.soundMusicVol.FloatValue, 0.0f, 1.0f))); m_AudioMixer.SetFloat("SFXVolume", DecibelFromAmplitude(Mathf.Clamp(SoundSystem.soundSFXVol.FloatValue, 0.0f, 1.0f))); m_AudioMixer.SetFloat("MenuVolume", DecibelFromAmplitude(Mathf.Clamp(SoundSystem.soundMenuVol.FloatValue, 0.0f, 1.0f))); // Update running sounds int count = 0; foreach (var e in m_Emitters) { if (!e.playing) continue; if (e.source == null) { // Could happen if parent was killed. Not good, but fixable: GameDebug.LogWarning("Soundemitter had its audiosource destroyed. Making a new."); e.source = MakeAudioSource(); e.repeatCount = 0; } if (e.fadeToKill.IsMoving()) { e.source.volume = AmplitudeFromDecibel(e.soundDef.volume) * e.fadeToKill.GetValue(); } else if (e.fadeToKill.GetValue() == 0.0f) { // kill no matter what e.Kill(); } if (e.source.isPlaying) { count++; continue; } if (e.repeatCount > 1) { e.repeatCount--; StartEmitter(e); continue; } // Reset for reuse e.playing = false; e.seqId = -1; // make handles invalid e.source.transform.parent = m_SourceHolder.transform; e.source.enabled = true; e.source.gameObject.SetActive(true); e.source.transform.position = Vector3.zero; e.fadeToKill.SetValue(1.0f); } if (SoundSystem.soundDebug.IntValue > 0) { // Overlay.Managed.Write(30, 1, "Mixer: {0} {1}", m_AudioMixer.GetInstanceID(), Game.game.audioMixer.GetInstanceID()); int ii = 4; foreach (var o in GameObject.FindObjectsOfType()) { Overlay.Managed.Write(30, ii++, "group: {0} {1}", o.name, o.GetInstanceID()); } Overlay.Managed.Write(1, 1, "Num audios {0}", count); for (int i = 0, c = m_Emitters.Length; i < c; ++i) { var e = m_Emitters[i]; Overlay.Managed.Write(1, 3 + i, "Emitter {0:##} {1} {2} {3}", i, e.playing ? e.soundDef.name : "", e.source.gameObject.activeInHierarchy ? "act" : "nact", e.playing ? "Mixer: " + e.source.outputAudioMixerGroup.audioMixer.GetInstanceID() : ""); } if (m_CurrentListener == null) { Overlay.Managed.Write(Overlay.Managed.CellsWide / 2 - 5, Overlay.Managed.CellsTall, "No AudioListener?"); return; } for (int i = 0, c = m_Emitters.Length; i < c; ++i) { var e = m_Emitters[i]; if (!e.playing) continue; var s = e.source.spatialBlend; Vector3 locpos = m_CurrentListener.transform.InverseTransformPoint(e.source.transform.position); int x = (int)Mathf.Lerp(e.source.panStereo * 10.0f, Mathf.Clamp(locpos.x, -10, 10), s);; int z = (int)Mathf.Lerp(-10.0f, Mathf.Clamp(locpos.z, -10, 10), s); Overlay.Managed.Write(s < 0.5 ? Overlay.Color.Green : Overlay.Color.Blue, Overlay.Managed.CellsWide / 2 + x, Overlay.Managed.CellsTall / 2 - z, "{0} ({1:##.#})", e.soundDef.name, locpos.magnitude); } } } void StartEmitter(SoundEmitter emitter) { var soundDef = emitter.soundDef; var source = emitter.source; StartSource(source, soundDef); } #if UNITY_EDITOR public static void StartSource(AudioSource source, SoundDef soundDef) #else static void StartSource(AudioSource source, SoundDef soundDef) #endif { Profiler.BeginSample(".Set source clip"); source.clip = soundDef.clips[Random.Range(0, soundDef.clips.Count)]; Profiler.EndSample(); Profiler.BeginSample(".Setup source"); // Map from halftone space to linear playback multiplier source.pitch = Mathf.Pow(2.0f, Random.Range(soundDef.pitchMin, soundDef.pitchMax) / 12.0f); source.minDistance = soundDef.distMin; source.maxDistance = soundDef.distMax; source.volume = AmplitudeFromDecibel(soundDef.volume); source.loop = soundDef.loopCount < 1 ? true : false; source.rolloffMode = soundDef.rolloffMode; float delay = Random.Range(soundDef.delayMin, soundDef.delayMax); if (s_MixerGroups != null) source.outputAudioMixerGroup = s_MixerGroups[(int)soundDef.soundGroup]; source.spatialBlend = soundDef.spatialBlend; source.panStereo = Random.Range(soundDef.panMin, soundDef.panMax); Profiler.EndSample(); // soundSpatialize can be null as this is run from editor too Profiler.BeginSample(".Setup spatializer"); if (SoundSystem.soundSpatialize != null && SoundSystem.soundSpatialize.IntValue > 0 && soundDef.spatialBlend > 0.5f) { source.spatialize = true; source.SetSpatializerFloat(0, 8.0f); source.SetSpatializerFloat(1, 0.0f); //source.SetSpatializerFloat(2, soundDef.distMin); //source.SetSpatializerFloat(3, soundDef.distMax); source.SetSpatializerFloat(4, 0.0f); source.SetSpatializerFloat(5, 0.0f); source.spatializePostEffects = false; //source.rolloffMode = source.spatialize ? AudioRolloffMode.Linear : source.rolloffMode; } else { source.spatialize = false; } Profiler.EndSample(); // TODO (petera) can we remove this? -- should never be needed due to re-enabling code in main update loop if (!source.enabled) { GameDebug.Log("Fixing disabled soundsource"); source.enabled = true; } Profiler.BeginSample("AudioSource.Play"); if (delay > 0.0f) source.PlayDelayed(delay); else source.Play(); Profiler.EndSample(); } public void SetCurrentListener(AudioListener audioListener) { m_CurrentListener = audioListener; } public static float SOUND_VOL_CUTOFF = -60.0f; public static float SOUND_AMP_CUTOFF = Mathf.Pow(2.0f, SOUND_VOL_CUTOFF / 6.0f); public static float DecibelFromAmplitude(float amplitude) { if (amplitude < SOUND_AMP_CUTOFF) return -60.0f; return 6.0f * Mathf.Log(amplitude) / Mathf.Log(2.0f); } public static float AmplitudeFromDecibel(float decibel) { if (decibel <= SOUND_VOL_CUTOFF) { return 0; } return Mathf.Pow(2.0f, decibel / 6.0f); } public void SetRegistry(SoundRegistry registry) { m_SoundRegistry = registry; } public SoundSystem.SoundHandle Play(WeakAssetReference weakSoundDef, Vector3 position) { var soundDef = m_SoundRegistry.GetSoundDef(weakSoundDef); if (soundDef == null) { GameDebug.LogWarning("Trying to play sound with asset ref " + weakSoundDef.ToGuidStr() + " but it is not in the registry"); return new SoundSystem.SoundHandle(); } return Play(soundDef, position); } AudioMixer m_AudioMixer; int m_SequenceId; SoundEmitter[] m_Emitters; GameObject m_SourceHolder; AudioListener m_CurrentListener; Interpolator m_MasterVolume = new Interpolator(1.0f, Interpolator.CurveType.SmoothStep); private SoundRegistry m_SoundRegistry; }