using OpenTK.Audio; using OpenTK.Audio.OpenAL; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; namespace Ryujinx.Audio.OpenAL { public class OpenALAudioOut : IAalOutput { private const int MaxTracks = 256; private const int MaxReleased = 32; private AudioContext Context; private class Track : IDisposable { public int SourceId { get; private set; } public int SampleRate { get; private set; } public ALFormat Format { get; private set; } private ReleaseCallback Callback; public PlaybackState State { get; set; } private bool ShouldCallReleaseCallback; private ConcurrentDictionary Buffers; private Queue QueuedTagsQueue; private Queue ReleasedTagsQueue; private int LastReleasedCount; private bool Disposed; public Track(int SampleRate, ALFormat Format, ReleaseCallback Callback) { this.SampleRate = SampleRate; this.Format = Format; this.Callback = Callback; State = PlaybackState.Stopped; SourceId = AL.GenSource(); Buffers = new ConcurrentDictionary(); QueuedTagsQueue = new Queue(); ReleasedTagsQueue = new Queue(); } public bool ContainsBuffer(long Tag) { SyncQueuedTags(); foreach (long QueuedTag in QueuedTagsQueue) { if (QueuedTag == Tag) { return true; } } return false; } public long[] GetReleasedBuffers(int MaxCount) { ClearReleased(); List Tags = new List(); HashSet Unique = new HashSet(); while (MaxCount-- > 0 && ReleasedTagsQueue.TryDequeue(out long Tag)) { if (Unique.Add(Tag)) { Tags.Add(Tag); } } return Tags.ToArray(); } public int AppendBuffer(long Tag) { if (Disposed) { throw new ObjectDisposedException(nameof(Track)); } int Id = AL.GenBuffer(); Buffers.AddOrUpdate(Tag, Id, (Key, OldId) => { AL.DeleteBuffer(OldId); return Id; }); QueuedTagsQueue.Enqueue(Tag); return Id; } public void ClearReleased() { SyncQueuedTags(); AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount); CheckReleaseChanges(ReleasedCount); if (ReleasedCount > 0) { AL.SourceUnqueueBuffers(SourceId, ReleasedCount); } } public void CallReleaseCallbackIfNeeded() { CheckReleaseChanges(); if (ShouldCallReleaseCallback) { ShouldCallReleaseCallback = false; Callback(); } } private void CheckReleaseChanges() { AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount); CheckReleaseChanges(ReleasedCount); } private void CheckReleaseChanges(int NewReleasedCount) { if (LastReleasedCount != NewReleasedCount) { LastReleasedCount = NewReleasedCount; ShouldCallReleaseCallback = true; } } private void SyncQueuedTags() { AL.GetSource(SourceId, ALGetSourcei.BuffersQueued, out int QueuedCount); AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount); QueuedCount -= ReleasedCount; while (QueuedTagsQueue.Count > QueuedCount) { ReleasedTagsQueue.Enqueue(QueuedTagsQueue.Dequeue()); } while (ReleasedTagsQueue.Count > MaxReleased) { ReleasedTagsQueue.Dequeue(); } } public void Dispose() { Dispose(true); } protected virtual void Dispose(bool Disposing) { if (Disposing && !Disposed) { Disposed = true; AL.DeleteSource(SourceId); foreach (int Id in Buffers.Values) { AL.DeleteBuffer(Id); } } } } private ConcurrentDictionary Tracks; private Thread AudioPollerThread; private bool KeepPolling; public OpenALAudioOut() { Context = new AudioContext(); Tracks = new ConcurrentDictionary(); KeepPolling = true; AudioPollerThread = new Thread(AudioPollerWork); AudioPollerThread.Start(); } private void AudioPollerWork() { do { foreach (Track Td in Tracks.Values) { Td.CallReleaseCallbackIfNeeded(); } Thread.Yield(); } while (KeepPolling); } public int OpenTrack( int SampleRate, int Channels, ReleaseCallback Callback, out AudioFormat Format) { Format = AudioFormat.PcmInt16; Track Td = new Track(SampleRate, GetALFormat(Channels, Format), Callback); for (int Id = 0; Id < MaxTracks; Id++) { if (Tracks.TryAdd(Id, Td)) { return Id; } } return -1; } private ALFormat GetALFormat(int Channels, AudioFormat Format) { if (Channels < 1 || Channels > 2) { throw new ArgumentOutOfRangeException(nameof(Channels)); } if (Channels == 1) { switch (Format) { case AudioFormat.PcmInt8: return ALFormat.Mono8; case AudioFormat.PcmInt16: return ALFormat.Mono16; } } else /* if (Channels == 2) */ { switch (Format) { case AudioFormat.PcmInt8: return ALFormat.Stereo8; case AudioFormat.PcmInt16: return ALFormat.Stereo16; } } throw new ArgumentException(nameof(Format)); } public void CloseTrack(int Track) { if (Tracks.TryRemove(Track, out Track Td)) { Td.Dispose(); } } public bool ContainsBuffer(int Track, long Tag) { if (Tracks.TryGetValue(Track, out Track Td)) { return Td.ContainsBuffer(Tag); } return false; } public long[] GetReleasedBuffers(int Track, int MaxCount) { if (Tracks.TryGetValue(Track, out Track Td)) { return Td.GetReleasedBuffers(MaxCount); } return null; } public void AppendBuffer(int Track, long Tag, byte[] Buffer) { if (Tracks.TryGetValue(Track, out Track Td)) { int BufferId = Td.AppendBuffer(Tag); AL.BufferData(BufferId, Td.Format, Buffer, Buffer.Length, Td.SampleRate); AL.SourceQueueBuffer(Td.SourceId, BufferId); StartPlaybackIfNeeded(Td); } } public void Start(int Track) { if (Tracks.TryGetValue(Track, out Track Td)) { Td.State = PlaybackState.Playing; StartPlaybackIfNeeded(Td); } } private void StartPlaybackIfNeeded(Track Td) { AL.GetSource(Td.SourceId, ALGetSourcei.SourceState, out int StateInt); ALSourceState State = (ALSourceState)StateInt; if (State != ALSourceState.Playing && Td.State == PlaybackState.Playing) { Td.ClearReleased(); AL.SourcePlay(Td.SourceId); } } public void Stop(int Track) { if (Tracks.TryGetValue(Track, out Track Td)) { Td.State = PlaybackState.Stopped; AL.SourceStop(Td.SourceId); } } public PlaybackState GetState(int Track) { if (Tracks.TryGetValue(Track, out Track Td)) { return Td.State; } return PlaybackState.Stopped; } } }