diff options
Diffstat (limited to 'Ryujinx.Audio/Renderers/OpenAL')
| -rw-r--r-- | Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs b/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs new file mode 100644 index 00000000..93f92879 --- /dev/null +++ b/Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs @@ -0,0 +1,369 @@ +using OpenTK.Audio; +using OpenTK.Audio.OpenAL; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Ryujinx.Audio +{ + /// <summary> + /// An audio renderer that uses OpenAL as the audio backend + /// </summary> + public class OpenALAudioOut : IAalOutput, IDisposable + { + 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 ConcurrentDictionary<long, int> Buffers; + + private Queue<long> QueuedTagsQueue; + + private Queue<long> ReleasedTagsQueue; + + 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<long, int>(); + + QueuedTagsQueue = new Queue<long>(); + + ReleasedTagsQueue = new Queue<long>(); + } + + public bool ContainsBuffer(long Tag) + { + foreach (long QueuedTag in QueuedTagsQueue) + { + if (QueuedTag == Tag) + { + return true; + } + } + + return false; + } + + public long[] GetReleasedBuffers(int Count) + { + AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount); + + ReleasedCount += ReleasedTagsQueue.Count; + + if (Count > ReleasedCount) + { + Count = ReleasedCount; + } + + List<long> Tags = new List<long>(); + + while (Count-- > 0 && ReleasedTagsQueue.TryDequeue(out long Tag)) + { + Tags.Add(Tag); + } + + while (Count-- > 0 && QueuedTagsQueue.TryDequeue(out long Tag)) + { + AL.SourceUnqueueBuffers(SourceId, 1); + + 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 CallReleaseCallbackIfNeeded() + { + AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount); + + if (ReleasedCount > 0) + { + //If we signal, then we also need to have released buffers available + //to return when GetReleasedBuffers is called. + //If playback needs to be re-started due to all buffers being processed, + //then OpenAL zeros the counts (ReleasedCount), so we keep it on the queue. + while (ReleasedCount-- > 0 && QueuedTagsQueue.TryDequeue(out long Tag)) + { + AL.SourceUnqueueBuffers(SourceId, 1); + + ReleasedTagsQueue.Enqueue(Tag); + } + + Callback(); + } + } + + 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<int, Track> Tracks; + + private Thread AudioPollerThread; + + private bool KeepPolling; + + public OpenALAudioOut() + { + Context = new AudioContext(); + + Tracks = new ConcurrentDictionary<int, Track>(); + + KeepPolling = true; + + AudioPollerThread = new Thread(AudioPollerWork); + + AudioPollerThread.Start(); + } + + /// <summary> + /// True if OpenAL is supported on the device. + /// </summary> + public static bool IsSupported + { + get + { + try + { + return AudioContext.AvailableDevices.Count > 0; + } + catch + { + return false; + } + } + } + + private void AudioPollerWork() + { + do + { + foreach (Track Td in Tracks.Values) + { + lock (Td) + { + Td.CallReleaseCallbackIfNeeded(); + } + } + + //If it's not slept it will waste cycles. + Thread.Sleep(10); + } + while (KeepPolling); + + foreach (Track Td in Tracks.Values) + { + Td.Dispose(); + } + + Tracks.Clear(); + } + + public int OpenTrack(int SampleRate, int Channels, ReleaseCallback Callback) + { + Track Td = new Track(SampleRate, GetALFormat(Channels), Callback); + + for (int Id = 0; Id < MaxTracks; Id++) + { + if (Tracks.TryAdd(Id, Td)) + { + return Id; + } + } + + return -1; + } + + private ALFormat GetALFormat(int Channels) + { + switch (Channels) + { + case 1: return ALFormat.Mono16; + case 2: return ALFormat.Stereo16; + case 6: return ALFormat.Multi51Chn16Ext; + } + + throw new ArgumentOutOfRangeException(nameof(Channels)); + } + + public void CloseTrack(int Track) + { + if (Tracks.TryRemove(Track, out Track Td)) + { + lock (Td) + { + Td.Dispose(); + } + } + } + + public bool ContainsBuffer(int Track, long Tag) + { + if (Tracks.TryGetValue(Track, out Track Td)) + { + lock (Td) + { + return Td.ContainsBuffer(Tag); + } + } + + return false; + } + + public long[] GetReleasedBuffers(int Track, int MaxCount) + { + if (Tracks.TryGetValue(Track, out Track Td)) + { + lock (Td) + { + return Td.GetReleasedBuffers(MaxCount); + } + } + + return null; + } + + public void AppendBuffer<T>(int Track, long Tag, T[] Buffer) where T : struct + { + if (Tracks.TryGetValue(Track, out Track Td)) + { + lock (Td) + { + int BufferId = Td.AppendBuffer(Tag); + + int Size = Buffer.Length * Marshal.SizeOf<T>(); + + AL.BufferData<T>(BufferId, Td.Format, Buffer, Size, Td.SampleRate); + + AL.SourceQueueBuffer(Td.SourceId, BufferId); + + StartPlaybackIfNeeded(Td); + } + } + } + + public void Start(int Track) + { + if (Tracks.TryGetValue(Track, out Track Td)) + { + lock (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) + { + AL.SourcePlay(Td.SourceId); + } + } + + public void Stop(int Track) + { + if (Tracks.TryGetValue(Track, out Track Td)) + { + lock (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; + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool Disposing) + { + if (Disposing) + { + KeepPolling = false; + } + } + } +}
\ No newline at end of file |
