using Microsoft.Xna.Framework; using OpenAL; using System; using System.Threading; namespace Barotrauma.Sounds { class SoundSourcePool : IDisposable { public uint[] ALSources { get; private set; } public SoundSourcePool(int sourceCount = SoundManager.SOURCE_COUNT) { int alError; ALSources = new uint[sourceCount]; for (int i = 0; i < sourceCount; i++) { Al.GenSource(out ALSources[i]); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Error generating alSource[" + i.ToString() + "]: " + Al.GetErrorString(alError)); } if (!Al.IsSource(ALSources[i])) { throw new Exception("Generated alSource[" + i.ToString() + "] is invalid!"); } Al.SourceStop(ALSources[i]); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Error stopping newly generated alSource[" + i.ToString() + "]: " + Al.GetErrorString(alError)); } Al.Sourcef(ALSources[i], Al.MinGain, 0.0f); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Error setting min gain: " + Al.GetErrorString(alError)); } Al.Sourcef(ALSources[i], Al.MaxGain, 1.0f); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Error setting max gain: " + Al.GetErrorString(alError)); } Al.Sourcef(ALSources[i], Al.RolloffFactor, 1.0f); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Error setting rolloff factor: " + Al.GetErrorString(alError)); } } } public void Dispose() { if (ALSources == null) { return; } for (int i = 0; i < ALSources.Length; i++) { Al.DeleteSource(ALSources[i]); int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to delete ALSources[" + i.ToString() + "]: " + Al.GetErrorString(alError)); } } ALSources = null; } } class SoundChannel : IDisposable { private const int STREAM_BUFFER_SIZE = 8820; private readonly short[] streamShortBuffer; private string debugName = "SoundChannel"; private Vector3? position; public Vector3? Position { get { return position; } set { position = value; if (ALSourceIndex < 0) { return; } if (position != null) { if (float.IsNaN(position.Value.X)) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.X is NaN", appendStackTrace: true); return; } if (float.IsNaN(position.Value.Y)) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Y is NaN", appendStackTrace: true); return; } if (float.IsNaN(position.Value.Z)) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Z is NaN", appendStackTrace: true); return; } if (float.IsInfinity(position.Value.X)) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.X is Infinity", appendStackTrace: true); return; } if (float.IsInfinity(position.Value.Y)) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Y is Infinity", appendStackTrace: true); return; } if (float.IsInfinity(position.Value.Z)) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", position.Z is Infinity", appendStackTrace: true); return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcei(alSource, Al.SourceRelative, Al.False); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to enable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } Al.Source3f(alSource, Al.Position, position.Value.X, position.Value.Y, position.Value.Z); alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to set source's position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } } else { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcei(alSource, Al.SourceRelative, Al.True); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to disable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } Al.Source3f(alSource, Al.Position, 0.0f, 0.0f, 0.0f); alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to reset source's position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } } } } private float near; public float Near { get { return near; } set { near = value; if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcef(alSource, Al.ReferenceDistance, near); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to set source's reference distance: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } } } private float far; public float Far { get { return far; } set { far = value; if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcef(alSource, Al.MaxDistance, far); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to set source's max distance: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } } } private float gain; public float Gain { get { return gain; } set { if (!MathUtils.IsValid(value)) { return; } gain = Math.Max(value, 0.0f); if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); float effectiveGain = gain; if (category != null) { effectiveGain *= Sound.Owner.GetCategoryGainMultiplier(category); } Al.Sourcef(alSource, Al.Gain, effectiveGain); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError($"Failed to set source's gain to {gain} (effective gain {effectiveGain}): {debugName}, {Al.GetErrorString(alError)}", appendStackTrace: true); return; } } } private bool looping; public bool Looping { get { return looping; } set { looping = value; if (ALSourceIndex < 0) { return; } if (!IsStream) { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcei(alSource, Al.Looping, looping ? Al.True : Al.False); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to set source's looping state: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } } } } public const float MinFrequencyMultiplier = 0.25f; public const float MaxFrequencyMultiplier = 4.0f; public float frequencyMultiplier; public float FrequencyMultiplier { get { return frequencyMultiplier; } set { if (value is < MinFrequencyMultiplier or > MaxFrequencyMultiplier) { DebugConsole.ThrowError($"Frequency multiplier out of range: {value}" + Environment.StackTrace.CleanupStackTrace()); } frequencyMultiplier = Math.Clamp(value, MinFrequencyMultiplier, MaxFrequencyMultiplier); if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcef(alSource, Al.Pitch, frequencyMultiplier); int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to set source's frequency multiplier: " + debugName + ", " + Al.GetErrorString(alError)); } } } public bool FilledByNetwork { get; private set; } private int decayTimer; private bool muffled; public bool Muffled { get { return muffled; } set { if (muffled == value) { return; } muffled = value; if (ALSourceIndex < 0) { return; } if (!IsPlaying) { return; } if (IsStream) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } Al.SourceStop(alSource); alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } Sound.FillAlBuffers(); if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.Buffers.AlMuffledBuffer : (int)Sound.Buffers.AlBuffer); alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } Al.SourcePlay(alSource); alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } Al.Sourcei(alSource, Al.SampleOffset, playbackPos); alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return; } } } private float streamAmplitude; public float CurrentAmplitude { get { if (!IsPlaying) { return 0.0f; } uint alSource = Sound?.Owner?.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) ?? 0; if (alSource == 0) { return 0.0f; } if (!IsStream) { Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return 0.0f; } return Sound.GetAmplitudeAtPlaybackPos(playbackPos); } else { float retVal; Monitor.Enter(mutex); retVal = streamAmplitude; Monitor.Exit(mutex); return retVal; } } } private string category; public string Category { get { return category; } set { category = value; Gain = gain; } } public Sound Sound { get; private set; } public int ALSourceIndex { get; private set; } = -1; public bool IsStream { get; private set; } private int streamSeekPos; private int buffersToRequeue; private bool reachedEndSample; private int queueStartIndex; private readonly uint[] streamBuffers; private readonly uint[] unqueuedBuffers; private readonly float[] streamBufferAmplitudes; public int StreamSeekPos { get { return streamSeekPos; } set { if (!IsStream) { throw new InvalidOperationException("Cannot set StreamSeekPos on a non-streaming sound channel."); } streamSeekPos = Math.Max(value, 0); } } public long MaxStreamSeekPos { get { if (!IsStream || Sound is not OggSound oggSound) { return 0; } return oggSound.MaxStreamSamplePos; } } private readonly object mutex; public bool IsPlaying { get { if (ALSourceIndex < 0) { return false; } if (IsStream && !reachedEndSample) { return true; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); if (!Al.IsSource(alSource)) { return false; } Al.GetSourcei(alSource, Al.SourceState, out int state); int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Failed to determine playing state from source: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); return false; } bool playing = state == Al.Playing; return playing; } } public SoundChannel(Sound sound, float gain, Vector3? position, float freqMult, float near, float far, string category, bool muffle = false) { Sound = sound; debugName = sound == null ? "SoundChannel (null)" : $"SoundChannel ({(string.IsNullOrEmpty(sound.Filename) ? "filename empty" : sound.Filename) })"; IsStream = sound.Stream; FilledByNetwork = sound is VoipSound; decayTimer = 0; streamSeekPos = 0; reachedEndSample = false; buffersToRequeue = 4; muffled = muffle; if (IsStream) { mutex = new object(); } #if !DEBUG try { #endif if (mutex != null) { Monitor.Enter(mutex); } if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances) { ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this); } if (ALSourceIndex >= 0) { if (!IsStream) { Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0); int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } Sound.FillAlBuffers(); if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; } uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer; Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); } SetProperties(); Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError)); } } else { uint alBuffer = 0; Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer); int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError)); } streamShortBuffer = new short[STREAM_BUFFER_SIZE]; streamBuffers = new uint[4]; unqueuedBuffers = new uint[4]; streamBufferAmplitudes = new float[4]; for (int i = 0; i < 4; i++) { Al.GenBuffer(out streamBuffers[i]); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError)); } if (!Al.IsBuffer(streamBuffers[i])) { throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); } } Sound.Owner.InitUpdateChannelThread(); SetProperties(); } } #if !DEBUG } catch { throw; } finally { #endif if (mutex != null) { Monitor.Exit(mutex); } #if !DEBUG } #endif void SetProperties() { this.Position = position; this.Gain = gain; this.FrequencyMultiplier = freqMult; this.Looping = false; this.Near = near; this.Far = far; this.Category = category; } Sound.Owner.Update(); } public override string ToString() { return debugName; } public bool FadingOutAndDisposing { get; private set; } public void FadeOutAndDispose() { FadingOutAndDisposing = true; Sound.Owner.InitUpdateChannelThread(); } public void Dispose() { try { if (mutex != null) { Monitor.Enter(mutex); } if (ALSourceIndex >= 0) { Al.SourceStop(Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError)); } if (IsStream) { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.SourceStop(alSource); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to stop streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } int buffersToRequeue = 0; buffersToRequeue = 0; Al.GetSourcei(alSource, Al.BuffersProcessed, out buffersToRequeue); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to determine processed buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourceUnqueueBuffers(alSource, buffersToRequeue, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to unqueue buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Sourcei(alSource, Al.Buffer, 0); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to reset buffer for streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } for (int i = 0; i < 4; i++) { Al.DeleteBuffer(streamBuffers[i]); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to delete streamBuffers[" + i.ToString() + "] (" + streamBuffers[i].ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); } } reachedEndSample = true; } else { Al.Sourcei(Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to unbind buffer to non-streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } } ALSourceIndex = -1; debugName += " [DISPOSED]"; } } finally { if (mutex != null) { Monitor.Exit(mutex); } } } public void UpdateStream() { try { if (!IsStream) { throw new Exception("Called UpdateStream on a non-streamed sound channel!"); } Monitor.Enter(mutex); if (!reachedEndSample) { uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.GetSourcei(alSource, Al.SourceState, out int state); bool playing = state == Al.Playing; int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to determine playing state from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.GetSourcei(alSource, Al.BuffersProcessed, out int unqueuedBufferCount); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to determine processed buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourceUnqueueBuffers(alSource, unqueuedBufferCount, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to unqueue buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } buffersToRequeue += unqueuedBufferCount; int iterCount = buffersToRequeue; for (int k = 0; k < iterCount; k++) { int index = queueStartIndex; short[] buffer = streamShortBuffer; int readSamples = Sound.FillStreamBuffer(streamSeekPos, buffer); float readAmplitude = 0.0f; for (int i = 0; i < Math.Min(readSamples, buffer.Length); i++) { float sampleF = ((float)buffer[i]) / ((float)short.MaxValue); readAmplitude = Math.Max(readAmplitude, Math.Abs(sampleF)); } if (FilledByNetwork) { if (readSamples <= 0) { streamAmplitude *= 0.5f; decayTimer++; if (decayTimer > 120) //TODO: replace magic number { reachedEndSample = true; } } else { if (Sound is VoipSound voipSound) { voipSound.ApplyFilters(buffer, readSamples); } decayTimer = 0; } } else if (Sound.StreamsReliably) { streamSeekPos += readSamples * 2; if (readSamples * 2 < STREAM_BUFFER_SIZE) { if (looping) { streamSeekPos = 0; } else { reachedEndSample = true; } } } if (readSamples > 0) { streamBufferAmplitudes[index] = readAmplitude; Al.BufferData(streamBuffers[index], Sound.ALFormat, buffer, readSamples * 2, Sound.SampleRate); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to assign data to stream buffer: " + Al.GetErrorString(alError) + ": " + streamBuffers[index].ToString() + "/" + streamBuffers.Length + ", readSamples: " + readSamples + ", " + debugName); } Al.SourceQueueBuffer(alSource, streamBuffers[index]); queueStartIndex = (queueStartIndex + 1) % 4; alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to queue streamBuffer[" + index.ToString() + "] to stream: " + debugName + ", " + Al.GetErrorString(alError)); } } else { if (readSamples < 0) { reachedEndSample = true; } break; } buffersToRequeue--; } streamAmplitude = streamBufferAmplitudes[queueStartIndex]; Al.GetSourcei(alSource, Al.SourceState, out state); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to retrieve stream source state: " + debugName + ", " + Al.GetErrorString(alError)); } if (state != Al.Playing) { Al.SourcePlay(alSource); alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to start stream playback: " + debugName + ", " + Al.GetErrorString(alError)); } } } if (reachedEndSample) { streamAmplitude = 0.0f; } } catch (Exception e) { DebugConsole.ThrowError($"An exception was thrown when updating a sound stream ({debugName})", e); } finally { Monitor.Exit(mutex); } } } }