8aa6ba71b6
The errors seem to be caused by adding messages to the debug console from another thread, which may happen if an OpenAL error occurs during sound streaming. Now the debug console queues the messages and creates the necessary UI elements in the main thread, which should(?) make it thread-safe. (TODO: figure out what's causing the OpenAL errors)
525 lines
16 KiB
C#
525 lines
16 KiB
C#
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<string, int, int> 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) + "\n" + Environment.StackTrace);
|
|
#else
|
|
DebugConsole.NewMessage("OpenAL error: " + AL.GetErrorString(error) + "\n" + Environment.StackTrace, 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<string, int, int> 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<OggStream> streams = new HashSet<OggStream>();
|
|
readonly List<OggStream> threadLocalStreams = new List<OggStream>();
|
|
|
|
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 };
|
|
|
|
//background threads are automatically stopped when all foreground threads have been stopped
|
|
// -> the streaming thread won't stay running in the background if the main thread crashes
|
|
underlyingThread.IsBackground = true;
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|