using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using NVorbis; using OpenTK.Audio.OpenAL; namespace Barotrauma.Sounds { internal static class ALHelper { public static readonly XRamExtension XRam = new XRamExtension(); public static readonly EffectsExtension Efx = new EffectsExtension(); static ALHelper() { try { Debug.WriteLine("OpenAL Soft [" + (AL.Get(ALGetString.Version).Contains("SOFT") ? "X" : " ") + "], "); Debug.WriteLine("X-RAM [" + (XRam.IsInitialized ? "X" : " ") + "], "); Debug.WriteLine("Effect Extensions [" + (Efx.IsInitialized ? "X" : " ") + "]"); } catch (Exception e) { DebugConsole.ThrowError("OpenAL error!", e); } } [Conditional("TRACE")] public static void TraceMemoryUsage(Action logHandler) { var usedHeap = (double)GC.GetTotalMemory(true); string[] sizes = { "B", "KB", "MB", "GB" }; int order = 0; while (usedHeap >= 1024 && order + 1 < sizes.Length) { order++; usedHeap = usedHeap / 1024; } //logHandler(String.Format("Total memory : {0:0.###} {1} ", usedHeap, sizes[order]), 0, 6); } public static void Check() { ALError error; if ((error = AL.GetError()) != ALError.NoError) { #if DEBUG DebugConsole.ThrowError("OpenAL error: " + AL.GetErrorString(error)); #else DebugConsole.NewMessage("OpenAL error: " + AL.GetErrorString(error), Microsoft.Xna.Framework.Color.Red); #endif } } } public class OggStream : IDisposable { public const int DefaultBufferCount = 3; internal readonly object stopMutex = new object(); internal readonly object prepareMutex = new object(); internal readonly int alSourceId; internal readonly int[] alBufferIds; //readonly int alFilterId; readonly Stream underlyingStream; internal VorbisReader Reader { get; private set; } internal bool Ready { get; private set; } internal bool Preparing { get; private set; } public int BufferCount { get; private set; } #if TRACE public int logX, logY; public Action LogHandler; #endif public OggStream(string filename, int bufferCount = DefaultBufferCount) : this(File.OpenRead(filename), bufferCount) { } public OggStream(Stream stream, int bufferCount = DefaultBufferCount) { BufferCount = bufferCount; alBufferIds = AL.GenBuffers(bufferCount); alSourceId = AL.GenSource(); if (ALHelper.XRam.IsInitialized) { ALHelper.XRam.SetBufferMode(BufferCount, ref alBufferIds[0], XRamExtension.XRamStorage.Hardware); ALHelper.Check(); } if (ALHelper.Efx.IsInitialized) { //alFilterId = ALHelper.Efx.GenFilter(); //ALHelper.Efx.Filter(alFilterId, EfxFilteri.FilterType, (int)EfxFilterType.Lowpass); //ALHelper.Efx.Filter(alFilterId, EfxFilterf.LowpassGain, 1); //ALHelper.Efx.BindFilterToSource(alSourceId, alFilterId); //LowPassHFGain = 1; } underlyingStream = stream; IsLooped = true; } public void Prepare() { if (Preparing) return; var state = AL.GetSourceState(alSourceId); lock (stopMutex) { switch (state) { case ALSourceState.Playing: case ALSourceState.Paused: return; case ALSourceState.Stopped: lock (prepareMutex) { Reader.DecodedTime = TimeSpan.Zero; Ready = false; Empty(); } break; } if (!Ready) { lock (prepareMutex) { Preparing = true; Open(precache: true); } } } } public void Play(float volume) { var state = AL.GetSourceState(alSourceId); switch (state) { case ALSourceState.Playing: return; case ALSourceState.Paused: Resume(); return; } Prepare(); AL.SourcePlay(alSourceId); this.Volume = volume; ALHelper.Check(); Preparing = false; OggStreamer.Instance.AddStream(this); } public void Pause() { if (AL.GetSourceState(alSourceId) != ALSourceState.Playing) return; OggStreamer.Instance.RemoveStream(this); AL.SourcePause(alSourceId); ALHelper.Check(); } public void Resume() { if (AL.GetSourceState(alSourceId) != ALSourceState.Paused) return; OggStreamer.Instance.AddStream(this); AL.SourcePlay(alSourceId); ALHelper.Check(); } public void Stop() { var state = AL.GetSourceState(alSourceId); if (state == ALSourceState.Playing || state == ALSourceState.Paused) { StopPlayback(); } lock (stopMutex) { OggStreamer.Instance.RemoveStream(this); } } /*float lowPassHfGain; public float LowPassHFGain { get { return lowPassHfGain; } set { if (ALHelper.Efx.IsInitialized) { ALHelper.Efx.Filter(alFilterId, EfxFilterf.LowpassGainHF, lowPassHfGain = value); ALHelper.Efx.BindFilterToSource(alSourceId, alFilterId); ALHelper.Check(); } } }*/ float volume; public float Volume { get { return volume; } set { AL.Source(alSourceId, ALSourcef.Gain, volume = value); ALHelper.Check(); } } public bool IsLooped { get; set; } public void Dispose() { var state = AL.GetSourceState(alSourceId); if (state == ALSourceState.Playing || state == ALSourceState.Paused) StopPlayback(); lock (prepareMutex) { OggStreamer.Instance.RemoveStream(this); if (state != ALSourceState.Initial) Empty(); Close(); underlyingStream.Dispose(); } AL.DeleteSource(alSourceId); AL.DeleteBuffers(alBufferIds); /*if (ALHelper.Efx.IsInitialized) ALHelper.Efx.DeleteFilter(alFilterId);*/ ALHelper.Check(); } void StopPlayback() { AL.SourceStop(alSourceId); ALHelper.Check(); } void Empty() { int queued; AL.GetSource(alSourceId, ALGetSourcei.BuffersQueued, out queued); ALHelper.Check(); if (queued > 0) { try { AL.SourceUnqueueBuffers(alSourceId, queued); if (AL.GetError() != ALError.NoError) { throw new InvalidOperationException(); } } catch (InvalidOperationException) { // This is a bug in the OpenAL implementation // Salvage what we can int processed; AL.GetSource(alSourceId, ALGetSourcei.BuffersProcessed, out processed); var salvaged = new int[processed]; if (processed > 0) { AL.SourceUnqueueBuffers(alSourceId, processed, salvaged); ALHelper.Check(); } // Try turning it off again? AL.SourceStop(alSourceId); ALHelper.Check(); Empty(); } } } internal void Open(bool precache = false) { underlyingStream.Seek(0, SeekOrigin.Begin); Reader = new VorbisReader(underlyingStream, false); if (precache) { // Fill first buffer synchronously OggStreamer.Instance.FillBuffer(this, alBufferIds[0]); AL.SourceQueueBuffer(alSourceId, alBufferIds[0]); ALHelper.Check(); // Schedule the others asynchronously OggStreamer.Instance.AddStream(this); } Ready = true; } internal void Close() { if (Reader != null) { Reader.Dispose(); Reader = null; } Ready = false; } } public class OggStreamer : IDisposable { const float DefaultUpdateRate = 10; const int DefaultBufferSize = 44100; static readonly object singletonMutex = new object(); readonly object iterationMutex = new object(); readonly object readMutex = new object(); readonly float[] readSampleBuffer; readonly short[] castBuffer; readonly HashSet streams = new HashSet(); readonly List threadLocalStreams = new List(); readonly Thread underlyingThread; volatile bool cancelled; public float UpdateRate { get; private set; } public int BufferSize { get; private set; } static OggStreamer instance; public static OggStreamer Instance { get { lock (singletonMutex) { if (instance == null) throw new InvalidOperationException("No instance running"); return instance; } } private set { lock (singletonMutex) instance = value; } } public OggStreamer(int bufferSize = DefaultBufferSize, float updateRate = DefaultUpdateRate) { lock (singletonMutex) { if (instance != null) throw new InvalidOperationException("Already running"); Instance = this; underlyingThread = new Thread(EnsureBuffersFilled) { Priority = ThreadPriority.Lowest }; underlyingThread.Start(); } UpdateRate = updateRate; BufferSize = bufferSize; readSampleBuffer = new float[bufferSize]; castBuffer = new short[bufferSize]; } public void Dispose() { lock (singletonMutex) { Debug.Assert(Instance == this, "Two instances running, somehow...?"); cancelled = true; lock (iterationMutex) streams.Clear(); Instance = null; } } internal bool AddStream(OggStream stream) { lock (iterationMutex) return streams.Add(stream); } internal bool RemoveStream(OggStream stream) { lock (iterationMutex) return streams.Remove(stream); } public bool FillBuffer(OggStream stream, int bufferId) { int readSamples; lock (readMutex) { readSamples = stream.Reader.ReadSamples(readSampleBuffer, 0, BufferSize); CastBuffer(readSampleBuffer, castBuffer, readSamples); } AL.BufferData(bufferId, stream.Reader.Channels == 1 ? ALFormat.Mono16 : ALFormat.Stereo16, castBuffer, readSamples * sizeof(short), stream.Reader.SampleRate); ALHelper.Check(); return readSamples != BufferSize; } static void CastBuffer(float[] inBuffer, short[] outBuffer, int length) { for (int i = 0; i < length; i++) { var temp = (int)(32767f * inBuffer[i]); if (temp > short.MaxValue) temp = short.MaxValue; else if (temp < short.MinValue) temp = short.MinValue; outBuffer[i] = (short)temp; } } void EnsureBuffersFilled() { while (!cancelled) { Thread.Sleep((int)(1000 / UpdateRate)); if (cancelled) break; threadLocalStreams.Clear(); lock (iterationMutex) threadLocalStreams.AddRange(streams); foreach (var stream in threadLocalStreams) { lock (stream.prepareMutex) { lock (iterationMutex) if (!streams.Contains(stream)) continue; bool finished = false; int queued; AL.GetSource(stream.alSourceId, ALGetSourcei.BuffersQueued, out queued); ALHelper.Check(); int processed; AL.GetSource(stream.alSourceId, ALGetSourcei.BuffersProcessed, out processed); ALHelper.Check(); if (processed == 0 && queued == stream.BufferCount) continue; int[] tempBuffers; if (processed > 0) tempBuffers = AL.SourceUnqueueBuffers(stream.alSourceId, processed); else tempBuffers = stream.alBufferIds.Skip(queued).ToArray(); for (int i = 0; i < tempBuffers.Length; i++) { finished |= FillBuffer(stream, tempBuffers[i]); if (finished) { if (stream.IsLooped) stream.Reader.DecodedTime = TimeSpan.Zero; else { streams.Remove(stream); i = tempBuffers.Length; } } } AL.SourceQueueBuffers(stream.alSourceId, tempBuffers.Length, tempBuffers); ALHelper.Check(); if (finished && !stream.IsLooped) continue; } lock (stream.stopMutex) { if (stream.Preparing) continue; lock (iterationMutex) if (!streams.Contains(stream)) continue; var state = AL.GetSourceState(stream.alSourceId); if (state == ALSourceState.Stopped) { AL.SourcePlay(stream.alSourceId); ALHelper.Check(); } } } } } } }