using System; using System.Collections.Generic; using System.Linq; using OpenTK.Audio; using OpenTK.Audio.OpenAL; using BizHawk.Client.Common; namespace BizHawk.Client.EmuHawk { public class OpenALSoundOutput : ISoundOutput { private bool _disposed; private Sound _sound; private AudioContext _context; private int _sourceID; private BufferPool _bufferPool; private int _currentSamplesQueued; public OpenALSoundOutput(Sound sound) { _sound = sound; string deviceName = GetDeviceNames().FirstOrDefault(n => n == Global.Config.SoundDevice); _context = new AudioContext(deviceName, Sound.SampleRate); } public void Dispose() { if (_disposed) return; _context.Dispose(); _context = null; _disposed = true; } public static IEnumerable GetDeviceNames() { if (!Alc.IsExtensionPresent(IntPtr.Zero, "ALC_ENUMERATION_EXT")) return Enumerable.Empty(); return Alc.GetString(IntPtr.Zero, AlcGetStringList.AllDevicesSpecifier); } private int BufferSizeSamples { get; set; } public int MaxSamplesDeficit { get; private set; } public void ApplyVolumeSettings(double volume) { AL.Source(_sourceID, ALSourcef.Gain, (float)volume); } public void StartSound() { BufferSizeSamples = Sound.MillisecondsToSamples(Global.Config.SoundBufferSizeMs); MaxSamplesDeficit = BufferSizeSamples; _sourceID = AL.GenSource(); _bufferPool = new BufferPool(); _currentSamplesQueued = 0; } public void StopSound() { AL.SourceStop(_sourceID); AL.DeleteSource(_sourceID); _bufferPool.Dispose(); _bufferPool = null; BufferSizeSamples = 0; } public int CalculateSamplesNeeded() { int currentSamplesPlayed = GetSource(ALGetSourcei.SampleOffset); ALSourceState sourceState = AL.GetSourceState(_sourceID); bool isInitializing = sourceState == ALSourceState.Initial; bool detectedUnderrun = sourceState == ALSourceState.Stopped; if (detectedUnderrun) { _sound.OnUnderrun(); // SampleOffset should reset to 0 when stopped; update the queued sample count to match UnqueueProcessedBuffers(); currentSamplesPlayed = 0; } int samplesAwaitingPlayback = _currentSamplesQueued - currentSamplesPlayed; int samplesNeeded = (int)Math.Max(BufferSizeSamples - samplesAwaitingPlayback, 0); if (isInitializing || detectedUnderrun) { _sound.HandleInitializationOrUnderrun(detectedUnderrun, ref samplesNeeded); } return samplesNeeded; } public void WriteSamples(short[] samples, int sampleCount) { if (sampleCount == 0) return; UnqueueProcessedBuffers(); int byteCount = sampleCount * Sound.BlockAlign; var buffer = _bufferPool.Obtain(byteCount); AL.BufferData(buffer.BufferID, ALFormat.Stereo16, samples, byteCount, Sound.SampleRate); AL.SourceQueueBuffer(_sourceID, buffer.BufferID); _currentSamplesQueued += sampleCount; if (AL.GetSourceState(_sourceID) != ALSourceState.Playing) { AL.SourcePlay(_sourceID); } } private void UnqueueProcessedBuffers() { int releaseCount = GetSource(ALGetSourcei.BuffersProcessed); for (int i = 0; i < releaseCount; i++) { AL.SourceUnqueueBuffer(_sourceID); var releasedBuffer = _bufferPool.ReleaseOne(); _currentSamplesQueued -= releasedBuffer.Length / Sound.BlockAlign; } } private int GetSource(ALGetSourcei param) { int value; AL.GetSource(_sourceID, param, out value); return value; } private class BufferPool : IDisposable { private Stack _availableItems = new Stack(); private Queue _obtainedItems = new Queue(); public void Dispose() { foreach (BufferPoolItem item in _availableItems.Concat(_obtainedItems)) { AL.DeleteBuffer(item.BufferID); } _availableItems.Clear(); _obtainedItems.Clear(); } public BufferPoolItem Obtain(int length) { BufferPoolItem item = _availableItems.Count != 0 ? _availableItems.Pop() : new BufferPoolItem(); item.Length = length; _obtainedItems.Enqueue(item); return item; } public BufferPoolItem ReleaseOne() { BufferPoolItem item = _obtainedItems.Dequeue(); _availableItems.Push(item); return item; } public class BufferPoolItem { public int BufferID { get; private set; } public int Length { get; set; } public BufferPoolItem() { BufferID = AL.GenBuffer(); } } } } }