using System; using System.Threading; using System.Collections.Generic; using System.Xml.Linq; using OpenTK.Audio.OpenAL; using Microsoft.Xna.Framework; using System.Linq; using System.IO; namespace Barotrauma.Sounds { public class SoundManager : IDisposable { public const int SOURCE_COUNT = 32; public bool Disabled { get; private set; } private IntPtr alcDevice; private OpenTK.ContextHandle alcContext; public enum SourcePoolIndex { Default = 0, Voice = 1 } private readonly SoundSourcePool[] sourcePools; private List loadedSounds; private readonly SoundChannel[][] playingChannels = new SoundChannel[2][]; private Thread streamingThread; private Vector3 listenerPosition; public Vector3 ListenerPosition { get { return listenerPosition; } set { if (Disabled) { return; } listenerPosition = value; AL.Listener(ALListener3f.Position,value.X,value.Y,value.Z); ALError alError = AL.GetError(); if (alError != ALError.NoError) { throw new Exception("Failed to set listener position: " + AL.GetErrorString(alError)); } } } private float[] listenerOrientation = new float[6]; public Vector3 ListenerTargetVector { get { return new Vector3(listenerOrientation[0], listenerOrientation[1], listenerOrientation[2]); } set { if (Disabled) { return; } listenerOrientation[0] = value.X; listenerOrientation[1] = value.Y; listenerOrientation[2] = value.Z; AL.Listener(ALListenerfv.Orientation, ref listenerOrientation); ALError alError = AL.GetError(); if (alError != ALError.NoError) { throw new Exception("Failed to set listener target vector: " + AL.GetErrorString(alError)); } } } public Vector3 ListenerUpVector { get { return new Vector3(listenerOrientation[3], listenerOrientation[4], listenerOrientation[5]); } set { if (Disabled) { return; } listenerOrientation[3] = value.X; listenerOrientation[4] = value.Y; listenerOrientation[5] = value.Z; AL.Listener(ALListenerfv.Orientation, ref listenerOrientation); ALError alError = AL.GetError(); if (alError != ALError.NoError) { throw new Exception("Failed to set listener up vector: " + AL.GetErrorString(alError)); } } } private float listenerGain; public float ListenerGain { get { return listenerGain; } set { if (Disabled) { return; } if (Math.Abs(ListenerGain - value) < 0.001f) { return; } listenerGain = value; AL.Listener(ALListenerf.Gain, listenerGain); ALError alError = AL.GetError(); if (alError != ALError.NoError) { throw new Exception("Failed to set listener gain: " + AL.GetErrorString(alError)); } } } public int LoadedSoundCount { get { return loadedSounds.Count; } } public int UniqueLoadedSoundCount { get { return loadedSounds.Select(s => s.Filename).Distinct().Count(); } } private Dictionary> categoryModifiers; public SoundManager() { loadedSounds = new List(); streamingThread = null; categoryModifiers = null; alcDevice = Alc.OpenDevice(null); if (alcDevice == null) { DebugConsole.ThrowError("Failed to open an ALC device! Disabling audio playback..."); Disabled = true; return; } AlcError alcError = Alc.GetError(alcDevice); if (alcError != AlcError.NoError) { //The audio device probably wasn't ready, this happens quite often //Just wait a while and try again Thread.Sleep(100); alcDevice = Alc.OpenDevice(null); alcError = Alc.GetError(alcDevice); if (alcError != AlcError.NoError) { DebugConsole.ThrowError("Error initializing ALC device: " + alcError.ToString() + ". Disabling audio playback..."); Disabled = true; return; } } int[] alcContextAttrs = new int[] { }; alcContext = Alc.CreateContext(alcDevice, alcContextAttrs); if (alcContext == null) { DebugConsole.ThrowError("Failed to create an ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + "). Disabling audio playback..."); Disabled = true; return; } if (!Alc.MakeContextCurrent(alcContext)) { DebugConsole.ThrowError("Failed to assign the current ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + "). Disabling audio playback..."); Disabled = true; return; } alcError = Alc.GetError(alcDevice); if (alcError != AlcError.NoError) { DebugConsole.ThrowError("Error after assigning ALC context: " + alcError.ToString() + ". Disabling audio playback..."); Disabled = true; return; } ALError alError = ALError.NoError; sourcePools = new SoundSourcePool[2]; sourcePools[(int)SourcePoolIndex.Default] = new SoundSourcePool(SOURCE_COUNT); playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; sourcePools[(int)SourcePoolIndex.Voice] = new SoundSourcePool(8); playingChannels[(int)SourcePoolIndex.Voice] = new SoundChannel[8]; AL.DistanceModel(ALDistanceModel.LinearDistanceClamped); alError = AL.GetError(); if (alError != ALError.NoError) { DebugConsole.ThrowError("Error setting distance model: " + AL.GetErrorString(alError) + ". Disabling audio playback..."); Disabled = true; return; } ListenerPosition = Vector3.Zero; ListenerTargetVector = new Vector3(0.0f, 0.0f, 1.0f); ListenerUpVector = new Vector3(0.0f, -1.0f, 0.0f); } public Sound LoadSound(string filename, bool stream = false) { if (Disabled) { return null; } if (!File.Exists(filename)) { throw new FileNotFoundException("Sound file \"" + filename + "\" doesn't exist!"); } Sound newSound = new OggSound(this, filename, stream); loadedSounds.Add(newSound); return newSound; } public Sound LoadSound(XElement element, bool stream = false) { if (Disabled) { return null; } string filePath = element.GetAttributeString("file", ""); if (!File.Exists(filePath)) { throw new FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); } var newSound = new OggSound(this, filePath, stream); if (newSound != null) { newSound.BaseGain = element.GetAttributeFloat("volume", 1.0f); float range = element.GetAttributeFloat("range", 1000.0f); newSound.BaseNear = range * 0.4f; newSound.BaseFar = range; } loadedSounds.Add(newSound); return newSound; } public SoundChannel GetSoundChannelFromIndex(SourcePoolIndex poolIndex, int ind) { if (Disabled || ind < 0 || ind >= playingChannels[(int)poolIndex].Length) return null; return playingChannels[(int)poolIndex][ind]; } public uint GetSourceFromIndex(SourcePoolIndex poolIndex, int srcInd) { if (Disabled || srcInd < 0 || srcInd >= sourcePools[(int)poolIndex].ALSources.Length) return 0; if (!AL.IsSource(sourcePools[(int)poolIndex].ALSources[srcInd])) { throw new Exception("alSources[" + srcInd.ToString() + "] is invalid!"); } return sourcePools[(int)poolIndex].ALSources[srcInd]; } public int AssignFreeSourceToChannel(SoundChannel newChannel) { if (Disabled) { return -1; } lock (playingChannels) { //remove a channel that has stopped //or hasn't even been assigned int poolIndex = (int)newChannel.Sound.SourcePoolIndex; for (int i = 0; i < playingChannels[poolIndex].Length; i++) { if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying) { if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); } playingChannels[poolIndex][i] = newChannel; return i; } } //we couldn't get a free source to assign to this channel! return -1; } } #if DEBUG public void DebugSource(int ind) { for (int i = 0; i < sourcePools[0].ALSources.Length; i++) { AL.Source(sourcePools[0].ALSources[i], ALSourcef.MaxGain, i == ind ? 1.0f : 0.0f); AL.Source(sourcePools[0].ALSources[i], ALSourcef.MinGain, 0.0f); } } #endif public bool IsPlaying(Sound sound) { if (Disabled) { return false; } lock (playingChannels) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { if (playingChannels[(int)sound.SourcePoolIndex][i] != null && playingChannels[(int)sound.SourcePoolIndex][i].Sound == sound) { if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) return true; } } } return false; } public int CountPlayingInstances(Sound sound) { if (Disabled) { return 0; } int count = 0; lock (playingChannels) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { if (playingChannels[(int)sound.SourcePoolIndex][i] != null && playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename) { if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; }; } } } return count; } public SoundChannel GetChannelFromSound(Sound sound) { if (Disabled) { return null; } lock (playingChannels) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { if (playingChannels[(int)sound.SourcePoolIndex][i] != null && playingChannels[(int)sound.SourcePoolIndex][i].Sound == sound) { if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) return playingChannels[(int)sound.SourcePoolIndex][i]; } } } return null; } public void KillChannels(Sound sound) { if (Disabled) { return; } lock (playingChannels) { for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++) { if (playingChannels[(int)sound.SourcePoolIndex][i]!=null && playingChannels[(int)sound.SourcePoolIndex][i].Sound == sound) { playingChannels[(int)sound.SourcePoolIndex][i]?.Dispose(); playingChannels[(int)sound.SourcePoolIndex][i] = null; } } } } public void RemoveSound(Sound sound) { for (int i = 0; i < loadedSounds.Count; i++) { if (loadedSounds[i] == sound) { loadedSounds.RemoveAt(i); return; } } } public void SetCategoryGainMultiplier(string category, float gain) { if (Disabled) { return; } category = category.ToLower(); if (categoryModifiers == null) categoryModifiers = new Dictionary>(); if (!categoryModifiers.ContainsKey(category)) { categoryModifiers.Add(category, new Pair(gain, false)); } else { categoryModifiers[category].First = gain; } lock (playingChannels) { for (int i = 0; i < playingChannels.Length; i++) { for (int j = 0; j < playingChannels[i].Length; j++) { if (playingChannels[i][j] != null && playingChannels[i][j].IsPlaying) { playingChannels[i][j].Gain = playingChannels[i][j].Gain; //force all channels to recalculate their gain } } } } } public float GetCategoryGainMultiplier(string category) { if (Disabled) { return 0.0f; } category = category.ToLower(); if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) return 1.0f; return categoryModifiers[category].First; } public void SetCategoryMuffle(string category,bool muffle) { if (Disabled) { return; } category = category.ToLower(); if (categoryModifiers == null) categoryModifiers = new Dictionary>(); if (!categoryModifiers.ContainsKey(category)) { categoryModifiers.Add(category, new Pair(1.0f, muffle)); } else { categoryModifiers[category].Second = muffle; } for (int i = 0; i < playingChannels.Length; i++) { for (int j = 0; j < playingChannels[i].Length; j++) { if (playingChannels[i][j] != null && playingChannels[i][j].IsPlaying) { if (playingChannels[i][j].Category.ToLower() == category) playingChannels[i][j].Muffled = muffle; } } } } public bool GetCategoryMuffle(string category) { if (Disabled) { return false; } category = category.ToLower(); if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) return false; return categoryModifiers[category].Second; } public void InitStreamThread() { if (Disabled) { return; } if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped)) { streamingThread = new Thread(UpdateStreaming) { Name = "SoundManager Streaming Thread", IsBackground = true //this should kill the thread if the game crashes }; streamingThread.Start(); } } void UpdateStreaming() { bool areStreamsPlaying = true; while (areStreamsPlaying) { areStreamsPlaying = false; lock (playingChannels) { for (int i = 0; i < playingChannels.Length; i++) { for (int j = 0; j < playingChannels[i].Length; j++) { if (playingChannels[i][j] == null) { continue; } if (playingChannels[i][j].IsStream) { if (playingChannels[i][j].IsPlaying) { areStreamsPlaying = true; playingChannels[i][j].UpdateStream(); } else { playingChannels[i][j].Dispose(); } } else if (playingChannels[i][j].FadingOutAndDisposing) { playingChannels[i][j].Gain -= 0.1f; if (playingChannels[i][j].Gain <= 0.0f) { playingChannels[i][j].Dispose(); } } } } } Thread.Sleep(10); //TODO: use a separate thread for network audio? } } public void Dispose() { if (Disabled) { return; } lock (playingChannels) { for (int i = 0; i < playingChannels.Length; i++) { for (int j = 0; j < playingChannels[i].Length; j++) { if (playingChannels[i][j] != null) playingChannels[i][j].Dispose(); } } } if (streamingThread != null && !streamingThread.ThreadState.HasFlag(ThreadState.Stopped)) { streamingThread.Join(); } for (int i = loadedSounds.Count - 1; i >= 0; i--) { loadedSounds[i].Dispose(); } sourcePools[(int)SourcePoolIndex.Default]?.Dispose(); sourcePools[(int)SourcePoolIndex.Voice]?.Dispose(); if (!Alc.MakeContextCurrent(OpenTK.ContextHandle.Zero)) { throw new Exception("Failed to detach the current ALC context! (error code: " + Alc.GetError(alcDevice).ToString() + ")"); } Alc.DestroyContext(alcContext); if (!Alc.CloseDevice(alcDevice)) { throw new Exception("Failed to close ALC device!"); } } } }