From 8275bc3c087830baf84ef7ac236fade24988b522 Mon Sep 17 00:00:00 2001 From: jduncanator Date: Thu, 15 Nov 2018 13:22:50 +1100 Subject: Implement libsoundio as an alternative audio backend (#406) * Audio: Implement libsoundio as an alternative audio backend libsoundio will be preferred over OpenAL if it is available on the machine. If neither are available, it will fallback to a dummy audio renderer that outputs no sound. * Audio: Fix SoundIoRingBuffer documentation * Audio: Unroll and optimize the audio write callback Copying one sample at a time is slow, this unrolls the most common audio channel layouts and manually copies the bytes between source and destination. This is over 2x faster than calling CopyBlockUnaligned every sample. * Audio: Optimize the write callback further This dramatically reduces the audio buffer copy time. When the sample size is one of handled sample sizes the buffer copy operation is almost 10x faster than CopyBlockAligned. This works by copying full samples at a time, rather than the individual bytes that make up the sample. This allows for 2x or 4x faster copy operations depending on sample size. * Audio: Fix typo in Stereo write callback * Audio: Fix Surround (5.1) audio write callback * Audio: Update Documentation * Audio: Use built-in Unsafe.SizeOf() Built-in `SizeOf()` is 10x faster than our `TypeSize` helper. This also helps reduce code surface area. * Audio: Keep fixed buffer style consistent * Audio: Address styling nits * Audio: More style nits * Audio: Add additional documentation * Audio: Move libsoundio bindings internal As per discussion, moving the libsoundio native bindings into Ryujinx.Audio * Audio: Bump Target Framework back up to .NET Core 2.1 * Audio: Remove voice mixing optimizations. Leaves Saturation optimizations in place. --- Ryujinx.Audio/Renderers/DummyAudioOut.cs | 63 +++ Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs | 369 ++++++++++++++ Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs | 189 +++++++ .../Renderers/SoundIo/SoundIoAudioTrack.cs | 560 +++++++++++++++++++++ .../Renderers/SoundIo/SoundIoAudioTrackPool.cs | 193 +++++++ Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs | 29 ++ .../Renderers/SoundIo/SoundIoRingBuffer.cs | 204 ++++++++ 7 files changed, 1607 insertions(+) create mode 100644 Ryujinx.Audio/Renderers/DummyAudioOut.cs create mode 100644 Ryujinx.Audio/Renderers/OpenAL/OpenALAudioOut.cs create mode 100644 Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs create mode 100644 Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs create mode 100644 Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs create mode 100644 Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs create mode 100644 Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs (limited to 'Ryujinx.Audio/Renderers') diff --git a/Ryujinx.Audio/Renderers/DummyAudioOut.cs b/Ryujinx.Audio/Renderers/DummyAudioOut.cs new file mode 100644 index 00000000..659734b6 --- /dev/null +++ b/Ryujinx.Audio/Renderers/DummyAudioOut.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Ryujinx.Audio +{ + /// + /// A Dummy audio renderer that does not output any audio + /// + public class DummyAudioOut : IAalOutput + { + private ConcurrentQueue m_Buffers; + + public DummyAudioOut() + { + m_Buffers = new ConcurrentQueue(); + } + + /// + /// Dummy audio output is always available, Baka! + /// + public static bool IsSupported => true; + + public PlaybackState GetState(int trackId) => PlaybackState.Stopped; + + public int OpenTrack(int sampleRate, int channels, ReleaseCallback callback) => 1; + + public void CloseTrack(int trackId) { } + + public void Start(int trackId) { } + + public void Stop(int trackId) { } + + public void AppendBuffer(int trackID, long bufferTag, T[] buffer) + where T : struct + { + m_Buffers.Enqueue(bufferTag); + } + + public long[] GetReleasedBuffers(int trackId, int maxCount) + { + List bufferTags = new List(); + + for (int i = 0; i < maxCount; i++) + { + if (!m_Buffers.TryDequeue(out long tag)) + { + break; + } + + bufferTags.Add(tag); + } + + return bufferTags.ToArray(); + } + + public bool ContainsBuffer(int trackID, long bufferTag) => false; + + public void Dispose() + { + m_Buffers.Clear(); + } + } +} 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 +{ + /// + /// An audio renderer that uses OpenAL as the audio backend + /// + 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 Buffers; + + private Queue QueuedTagsQueue; + + private Queue 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(); + + QueuedTagsQueue = new Queue(); + + ReleasedTagsQueue = new Queue(); + } + + 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 Tags = new List(); + + 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 Tracks; + + private Thread AudioPollerThread; + + private bool KeepPolling; + + public OpenALAudioOut() + { + Context = new AudioContext(); + + Tracks = new ConcurrentDictionary(); + + KeepPolling = true; + + AudioPollerThread = new Thread(AudioPollerWork); + + AudioPollerThread.Start(); + } + + /// + /// True if OpenAL is supported on the device. + /// + 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(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(); + + AL.BufferData(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 diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs new file mode 100644 index 00000000..76b1290d --- /dev/null +++ b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioOut.cs @@ -0,0 +1,189 @@ +using Ryujinx.Audio.SoundIo; +using SoundIOSharp; +using System.Collections.Generic; + +namespace Ryujinx.Audio +{ + /// + /// An audio renderer that uses libsoundio as the audio backend + /// + public class SoundIoAudioOut : IAalOutput + { + /// + /// The maximum amount of tracks we can issue simultaneously + /// + private const int MaximumTracks = 256; + + /// + /// The audio context + /// + private SoundIO m_AudioContext; + + /// + /// The audio device + /// + private SoundIODevice m_AudioDevice; + + /// + /// An object pool containing objects + /// + private SoundIoAudioTrackPool m_TrackPool; + + /// + /// True if SoundIO is supported on the device. + /// + public static bool IsSupported => true; + + /// + /// Constructs a new instance of a + /// + public SoundIoAudioOut() + { + m_AudioContext = new SoundIO(); + + m_AudioContext.Connect(); + m_AudioContext.FlushEvents(); + + m_AudioDevice = m_AudioContext.GetOutputDevice(m_AudioContext.DefaultOutputDeviceIndex); + m_TrackPool = new SoundIoAudioTrackPool(m_AudioContext, m_AudioDevice, MaximumTracks); + } + + /// + /// Gets the current playback state of the specified track + /// + /// The track to retrieve the playback state for + public PlaybackState GetState(int trackId) + { + if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + return track.State; + } + + return PlaybackState.Stopped; + } + + /// + /// Creates a new audio track with the specified parameters + /// + /// The requested sample rate + /// The requested channels + /// A that represents the delegate to invoke when a buffer has been released by the audio track + /// The created track's Track ID + public int OpenTrack(int sampleRate, int channels, ReleaseCallback callback) + { + if (!m_TrackPool.TryGet(out SoundIoAudioTrack track)) + { + return -1; + } + + // Open the output. We currently only support 16-bit signed LE + track.Open(sampleRate, channels, callback, SoundIOFormat.S16LE); + + return track.TrackID; + } + + /// + /// Stops playback and closes the track specified by + /// + /// The ID of the track to close + public void CloseTrack(int trackId) + { + if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + // Close and dispose of the track + track.Close(); + + // Recycle the track back into the pool + m_TrackPool.Put(track); + } + } + + /// + /// Starts playback + /// + /// The ID of the track to start playback on + public void Start(int trackId) + { + if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + track.Start(); + } + } + + /// + /// Stops playback + /// + /// The ID of the track to stop playback on + public void Stop(int trackId) + { + if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + track.Stop(); + } + } + + /// + /// Appends an audio buffer to the specified track + /// + /// The sample type of the buffer + /// The track to append the buffer to + /// The internal tag of the buffer + /// The buffer to append to the track + public void AppendBuffer(int trackId, long bufferTag, T[] buffer) + where T : struct + { + if(m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + track.AppendBuffer(bufferTag, buffer); + } + } + + /// + /// Returns a value indicating whether the specified buffer is currently reserved by the specified track + /// + /// The track to check + /// The buffer tag to check + public bool ContainsBuffer(int trackId, long bufferTag) + { + if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + return track.ContainsBuffer(bufferTag); + } + + return false; + } + + /// + /// Gets a list of buffer tags the specified track is no longer reserving + /// + /// The track to retrieve buffer tags from + /// The maximum amount of buffer tags to retrieve + /// Buffers released by the specified track + public long[] GetReleasedBuffers(int trackId, int maxCount) + { + if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track)) + { + List bufferTags = new List(); + + while(maxCount-- > 0 && track.ReleasedBuffers.TryDequeue(out long tag)) + { + bufferTags.Add(tag); + } + + return bufferTags.ToArray(); + } + + return new long[0]; + } + + /// + /// Releases the unmanaged resources used by the + /// + public void Dispose() + { + m_TrackPool.Dispose(); + m_AudioContext.Disconnect(); + m_AudioContext.Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs new file mode 100644 index 00000000..97ba11d5 --- /dev/null +++ b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrack.cs @@ -0,0 +1,560 @@ +using SoundIOSharp; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Audio.SoundIo +{ + internal class SoundIoAudioTrack : IDisposable + { + /// + /// The audio track ring buffer + /// + private SoundIoRingBuffer m_Buffer; + + /// + /// A list of buffers currently pending writeback to the audio backend + /// + private ConcurrentQueue m_ReservedBuffers; + + /// + /// Occurs when a buffer has been released by the audio backend + /// + private event ReleaseCallback BufferReleased; + + /// + /// The track ID of this + /// + public int TrackID { get; private set; } + + /// + /// The current playback state + /// + public PlaybackState State { get; private set; } + + /// + /// The audio context this track belongs to + /// + public SoundIO AudioContext { get; private set; } + + /// + /// The this track belongs to + /// + public SoundIODevice AudioDevice { get; private set; } + + /// + /// The audio output stream of this track + /// + public SoundIOOutStream AudioStream { get; private set; } + + /// + /// Released buffers the track is no longer holding + /// + public ConcurrentQueue ReleasedBuffers { get; private set; } + + /// + /// Constructs a new instance of a + /// + /// The track ID + /// The SoundIO audio context + /// The SoundIO audio device + public SoundIoAudioTrack(int trackId, SoundIO audioContext, SoundIODevice audioDevice) + { + TrackID = trackId; + AudioContext = audioContext; + AudioDevice = audioDevice; + State = PlaybackState.Stopped; + ReleasedBuffers = new ConcurrentQueue(); + + m_Buffer = new SoundIoRingBuffer(); + m_ReservedBuffers = new ConcurrentQueue(); + } + + /// + /// Opens the audio track with the specified parameters + /// + /// The requested sample rate of the track + /// The requested channel count of the track + /// A that represents the delegate to invoke when a buffer has been released by the audio track + /// The requested sample format of the track + public void Open( + int sampleRate, + int channelCount, + ReleaseCallback callback, + SoundIOFormat format = SoundIOFormat.S16LE) + { + // Close any existing audio streams + if (AudioStream != null) + { + Close(); + } + + if (!AudioDevice.SupportsSampleRate(sampleRate)) + { + throw new InvalidOperationException($"This sound device does not support a sample rate of {sampleRate}Hz"); + } + + if (!AudioDevice.SupportsFormat(format)) + { + throw new InvalidOperationException($"This sound device does not support SoundIOFormat.{Enum.GetName(typeof(SoundIOFormat), format)}"); + } + + AudioStream = AudioDevice.CreateOutStream(); + + AudioStream.Name = $"SwitchAudioTrack_{TrackID}"; + AudioStream.Layout = SoundIOChannelLayout.GetDefault(channelCount); + AudioStream.Format = format; + AudioStream.SampleRate = sampleRate; + + AudioStream.WriteCallback = WriteCallback; + + BufferReleased += callback; + + AudioStream.Open(); + } + + /// + /// This callback occurs when the sound device is ready to buffer more frames + /// + /// The minimum amount of frames expected by the audio backend + /// The maximum amount of frames that can be written to the audio backend + private unsafe void WriteCallback(int minFrameCount, int maxFrameCount) + { + int bytesPerFrame = AudioStream.BytesPerFrame; + uint bytesPerSample = (uint)AudioStream.BytesPerSample; + + int bufferedFrames = m_Buffer.Length / bytesPerFrame; + long bufferedSamples = m_Buffer.Length / bytesPerSample; + + int frameCount = Math.Min(bufferedFrames, maxFrameCount); + + if (frameCount == 0) + { + return; + } + + SoundIOChannelAreas areas = AudioStream.BeginWrite(ref frameCount); + int channelCount = areas.ChannelCount; + + byte[] samples = new byte[frameCount * bytesPerFrame]; + + m_Buffer.Read(samples, 0, samples.Length); + + // This is a huge ugly block of code, but we save + // a significant amount of time over the generic + // loop that handles other channel counts. + + // Mono + if (channelCount == 1) + { + SoundIOChannelArea area = areas.GetArea(0); + + fixed (byte* srcptr = samples) + { + if (bytesPerSample == 1) + { + for (int frame = 0; frame < frameCount; frame++) + { + ((byte*)area.Pointer)[0] = srcptr[frame * bytesPerFrame]; + + area.Pointer += area.Step; + } + } + else if (bytesPerSample == 2) + { + for (int frame = 0; frame < frameCount; frame++) + { + ((short*)area.Pointer)[0] = ((short*)srcptr)[frame * bytesPerFrame >> 1]; + + area.Pointer += area.Step; + } + } + else if (bytesPerSample == 4) + { + for (int frame = 0; frame < frameCount; frame++) + { + ((int*)area.Pointer)[0] = ((int*)srcptr)[frame * bytesPerFrame >> 2]; + + area.Pointer += area.Step; + } + } + else + { + for (int frame = 0; frame < frameCount; frame++) + { + Unsafe.CopyBlockUnaligned((byte*)area.Pointer, srcptr + (frame * bytesPerFrame), bytesPerSample); + + area.Pointer += area.Step; + } + } + } + } + // Stereo + else if (channelCount == 2) + { + SoundIOChannelArea area1 = areas.GetArea(0); + SoundIOChannelArea area2 = areas.GetArea(1); + + fixed (byte* srcptr = samples) + { + if (bytesPerSample == 1) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((byte*)area1.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 0]; + + // Channel 2 + ((byte*)area2.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 1]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + else if (bytesPerSample == 2) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((short*)area1.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 0]; + + // Channel 2 + ((short*)area2.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 1]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + else if (bytesPerSample == 4) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((int*)area1.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 0]; + + // Channel 2 + ((int*)area2.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 1]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + else + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + Unsafe.CopyBlockUnaligned((byte*)area1.Pointer, srcptr + (frame * bytesPerFrame) + (0 * bytesPerSample), bytesPerSample); + + // Channel 2 + Unsafe.CopyBlockUnaligned((byte*)area2.Pointer, srcptr + (frame * bytesPerFrame) + (1 * bytesPerSample), bytesPerSample); + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + } + } + } + } + // Surround + else if (channelCount == 6) + { + SoundIOChannelArea area1 = areas.GetArea(0); + SoundIOChannelArea area2 = areas.GetArea(1); + SoundIOChannelArea area3 = areas.GetArea(2); + SoundIOChannelArea area4 = areas.GetArea(3); + SoundIOChannelArea area5 = areas.GetArea(4); + SoundIOChannelArea area6 = areas.GetArea(5); + + fixed (byte* srcptr = samples) + { + if (bytesPerSample == 1) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((byte*)area1.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 0]; + + // Channel 2 + ((byte*)area2.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 1]; + + // Channel 3 + ((byte*)area3.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 2]; + + // Channel 4 + ((byte*)area4.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 3]; + + // Channel 5 + ((byte*)area5.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 4]; + + // Channel 6 + ((byte*)area6.Pointer)[0] = srcptr[(frame * bytesPerFrame) + 5]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + else if (bytesPerSample == 2) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((short*)area1.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 0]; + + // Channel 2 + ((short*)area2.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 1]; + + // Channel 3 + ((short*)area3.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 2]; + + // Channel 4 + ((short*)area4.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 3]; + + // Channel 5 + ((short*)area5.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 4]; + + // Channel 6 + ((short*)area6.Pointer)[0] = ((short*)srcptr)[(frame * bytesPerFrame >> 1) + 5]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + else if (bytesPerSample == 4) + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + ((int*)area1.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 0]; + + // Channel 2 + ((int*)area2.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 1]; + + // Channel 3 + ((int*)area3.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 2]; + + // Channel 4 + ((int*)area4.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 3]; + + // Channel 5 + ((int*)area5.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 4]; + + // Channel 6 + ((int*)area6.Pointer)[0] = ((int*)srcptr)[(frame * bytesPerFrame >> 2) + 5]; + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + else + { + for (int frame = 0; frame < frameCount; frame++) + { + // Channel 1 + Unsafe.CopyBlockUnaligned((byte*)area1.Pointer, srcptr + (frame * bytesPerFrame) + (0 * bytesPerSample), bytesPerSample); + + // Channel 2 + Unsafe.CopyBlockUnaligned((byte*)area2.Pointer, srcptr + (frame * bytesPerFrame) + (1 * bytesPerSample), bytesPerSample); + + // Channel 3 + Unsafe.CopyBlockUnaligned((byte*)area3.Pointer, srcptr + (frame * bytesPerFrame) + (2 * bytesPerSample), bytesPerSample); + + // Channel 4 + Unsafe.CopyBlockUnaligned((byte*)area4.Pointer, srcptr + (frame * bytesPerFrame) + (3 * bytesPerSample), bytesPerSample); + + // Channel 5 + Unsafe.CopyBlockUnaligned((byte*)area5.Pointer, srcptr + (frame * bytesPerFrame) + (4 * bytesPerSample), bytesPerSample); + + // Channel 6 + Unsafe.CopyBlockUnaligned((byte*)area6.Pointer, srcptr + (frame * bytesPerFrame) + (5 * bytesPerSample), bytesPerSample); + + area1.Pointer += area1.Step; + area2.Pointer += area2.Step; + area3.Pointer += area3.Step; + area4.Pointer += area4.Step; + area5.Pointer += area5.Step; + area6.Pointer += area6.Step; + } + } + } + } + // Every other channel count + else + { + SoundIOChannelArea[] channels = new SoundIOChannelArea[channelCount]; + + // Obtain the channel area for each channel + for (int i = 0; i < channelCount; i++) + { + channels[i] = areas.GetArea(i); + } + + fixed (byte* srcptr = samples) + { + for (int frame = 0; frame < frameCount; frame++) + for (int channel = 0; channel < areas.ChannelCount; channel++) + { + // Copy channel by channel, frame by frame. This is slow! + Unsafe.CopyBlockUnaligned((byte*)channels[channel].Pointer, srcptr + (frame * bytesPerFrame) + (channel * bytesPerSample), bytesPerSample); + + channels[channel].Pointer += channels[channel].Step; + } + } + } + + AudioStream.EndWrite(); + + UpdateReleasedBuffers(samples.Length); + } + + /// + /// Releases any buffers that have been fully written to the output device + /// + /// The amount of bytes written in the last device write + private void UpdateReleasedBuffers(int bytesRead) + { + bool bufferReleased = false; + + while (bytesRead > 0) + { + if (m_ReservedBuffers.TryPeek(out SoundIoBuffer buffer)) + { + if (buffer.Length > bytesRead) + { + buffer.Length -= bytesRead; + bytesRead = 0; + } + else + { + bufferReleased = true; + bytesRead -= buffer.Length; + + m_ReservedBuffers.TryDequeue(out buffer); + ReleasedBuffers.Enqueue(buffer.Tag); + } + } + } + + if (bufferReleased) + { + OnBufferReleased(); + } + } + + /// + /// Starts audio playback + /// + public void Start() + { + if (AudioStream == null) + { + return; + } + + AudioStream.Start(); + AudioStream.Pause(false); + AudioContext.FlushEvents(); + State = PlaybackState.Playing; + } + + /// + /// Stops audio playback + /// + public void Stop() + { + if (AudioStream == null) + { + return; + } + + AudioStream.Pause(true); + AudioContext.FlushEvents(); + State = PlaybackState.Stopped; + } + + /// + /// Appends an audio buffer to the tracks internal ring buffer + /// + /// The audio sample type + /// The unqiue tag of the buffer being appended + /// The buffer to append + public void AppendBuffer(long bufferTag, T[] buffer) + { + if (AudioStream == null) + { + return; + } + + // Calculate the size of the audio samples + int size = Unsafe.SizeOf(); + + // Calculate the amount of bytes to copy from the buffer + int bytesToCopy = size * buffer.Length; + + // Copy the memory to our ring buffer + m_Buffer.Write(buffer, 0, bytesToCopy); + + // Keep track of "buffered" buffers + m_ReservedBuffers.Enqueue(new SoundIoBuffer(bufferTag, bytesToCopy)); + } + + /// + /// Returns a value indicating whether the specified buffer is currently reserved by the track + /// + /// The buffer tag to check + public bool ContainsBuffer(long bufferTag) + { + return m_ReservedBuffers.Any(x => x.Tag == bufferTag); + } + + /// + /// Closes the + /// + public void Close() + { + if (AudioStream != null) + { + AudioStream.Pause(true); + AudioStream.Dispose(); + } + + m_Buffer.Clear(); + OnBufferReleased(); + ReleasedBuffers.Clear(); + + State = PlaybackState.Stopped; + AudioStream = null; + BufferReleased = null; + } + + private void OnBufferReleased() + { + BufferReleased?.Invoke(); + } + + /// + /// Releases the unmanaged resources used by the + /// + public void Dispose() + { + Close(); + } + + ~SoundIoAudioTrack() + { + Dispose(); + } + } +} diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs new file mode 100644 index 00000000..ec256e20 --- /dev/null +++ b/Ryujinx.Audio/Renderers/SoundIo/SoundIoAudioTrackPool.cs @@ -0,0 +1,193 @@ +using SoundIOSharp; +using System; +using System.Collections.Concurrent; +using System.Linq; + +namespace Ryujinx.Audio.SoundIo +{ + /// + /// An object pool containing a set of audio tracks + /// + internal class SoundIoAudioTrackPool : IDisposable + { + /// + /// The current size of the + /// + private int m_Size; + + /// + /// The maximum size of the + /// + private int m_MaxSize; + + /// + /// The audio context this track pool belongs to + /// + private SoundIO m_Context; + + /// + /// The audio device this track pool belongs to + /// + private SoundIODevice m_Device; + + /// + /// The queue that keeps track of the available in the pool. + /// + private ConcurrentQueue m_Queue; + + /// + /// The dictionary providing mapping between a TrackID and + /// + private ConcurrentDictionary m_TrackList; + + /// + /// Gets the current size of the + /// + public int Size { get => m_Size; } + + /// + /// Gets the maximum size of the + /// + public int MaxSize { get => m_MaxSize; } + + /// + /// Gets a value that indicates whether the is empty + /// + public bool IsEmpty { get => m_Queue.IsEmpty; } + + /// + /// Constructs a new instance of a that is empty + /// + /// The maximum amount of tracks that can be created + public SoundIoAudioTrackPool(SoundIO context, SoundIODevice device, int maxSize) + { + m_Size = 0; + m_Context = context; + m_Device = device; + m_MaxSize = maxSize; + + m_Queue = new ConcurrentQueue(); + m_TrackList = new ConcurrentDictionary(); + } + + /// + /// Constructs a new instance of a that contains + /// the specified amount of + /// + /// The maximum amount of tracks that can be created + /// The initial number of tracks that the pool contains + public SoundIoAudioTrackPool(SoundIO context, SoundIODevice device, int maxSize, int initialCapacity) + : this(context, device, maxSize) + { + var trackCollection = Enumerable.Range(0, initialCapacity) + .Select(TrackFactory); + + m_Size = initialCapacity; + m_Queue = new ConcurrentQueue(trackCollection); + } + + /// + /// Creates a new with the proper AudioContext and AudioDevice + /// and the specified + /// + /// The ID of the track to be created + /// A new AudioTrack with the specified ID + private SoundIoAudioTrack TrackFactory(int trackId) + { + // Create a new AudioTrack + SoundIoAudioTrack track = new SoundIoAudioTrack(trackId, m_Context, m_Device); + + // Keep track of issued tracks + m_TrackList[trackId] = track; + + return track; + } + + /// + /// Retrieves a from the pool + /// + /// An AudioTrack from the pool + public SoundIoAudioTrack Get() + { + // If we have a track available, reuse it + if (m_Queue.TryDequeue(out SoundIoAudioTrack track)) + { + return track; + } + + // Have we reached the maximum size of our pool? + if (m_Size >= m_MaxSize) + { + return null; + } + + // We don't have any pooled tracks, so create a new one + return TrackFactory(m_Size++); + } + + /// + /// Retrieves the associated with the specified from the pool + /// + /// The ID of the track to retrieve + public SoundIoAudioTrack Get(int trackId) + { + if (m_TrackList.TryGetValue(trackId, out SoundIoAudioTrack track)) + { + return track; + } + + return null; + } + + /// + /// Attempers to get a from the pool + /// + /// The track retrieved from the pool + /// True if retrieve was successful + public bool TryGet(out SoundIoAudioTrack track) + { + track = Get(); + + return track != null; + } + + /// + /// Attempts to get the associated with the specified from the pool + /// + /// The ID of the track to retrieve + /// The track retrieved from the pool + public bool TryGet(int trackId, out SoundIoAudioTrack track) + { + return m_TrackList.TryGetValue(trackId, out track); + } + + /// + /// Returns an back to the pool for reuse + /// + /// The track to be returned to the pool + public void Put(SoundIoAudioTrack track) + { + // Ensure the track is disposed and not playing audio + track.Close(); + + // Requeue the track for reuse later + m_Queue.Enqueue(track); + } + + /// + /// Releases the unmanaged resources used by the + /// + public void Dispose() + { + foreach (var track in m_TrackList) + { + track.Value.Close(); + track.Value.Dispose(); + } + + m_Size = 0; + m_Queue.Clear(); + m_TrackList.Clear(); + } + } +} diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs new file mode 100644 index 00000000..2a6190b5 --- /dev/null +++ b/Ryujinx.Audio/Renderers/SoundIo/SoundIoBuffer.cs @@ -0,0 +1,29 @@ +namespace Ryujinx.Audio.SoundIo +{ + /// + /// Represents the remaining bytes left buffered for a specific buffer tag + /// + internal class SoundIoBuffer + { + /// + /// The buffer tag this represents + /// + public long Tag { get; private set; } + + /// + /// The remaining bytes still to be released + /// + public int Length { get; set; } + + /// + /// Constructs a new instance of a + /// + /// The buffer tag + /// The size of the buffer + public SoundIoBuffer(long tag, int length) + { + Tag = tag; + Length = length; + } + } +} diff --git a/Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs b/Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs new file mode 100644 index 00000000..b2885021 --- /dev/null +++ b/Ryujinx.Audio/Renderers/SoundIo/SoundIoRingBuffer.cs @@ -0,0 +1,204 @@ +using System; + +namespace Ryujinx.Audio.SoundIo +{ + /// + /// A thread-safe variable-size circular buffer + /// + internal class SoundIoRingBuffer + { + private byte[] m_Buffer; + private int m_Size; + private int m_HeadOffset; + private int m_TailOffset; + + /// + /// Gets the available bytes in the ring buffer + /// + public int Length + { + get { return m_Size; } + } + + /// + /// Constructs a new instance of a + /// + public SoundIoRingBuffer() + { + m_Buffer = new byte[2048]; + } + + /// + /// Constructs a new instance of a with the specified capacity + /// + /// The number of entries that the can initially contain + public SoundIoRingBuffer(int capacity) + { + m_Buffer = new byte[capacity]; + } + + /// + /// Clears the ring buffer + /// + public void Clear() + { + m_Size = 0; + m_HeadOffset = 0; + m_TailOffset = 0; + } + + /// + /// Clears the specified amount of bytes from the ring buffer + /// + /// The amount of bytes to clear from the ring buffer + public void Clear(int size) + { + lock (this) + { + if (size > m_Size) + { + size = m_Size; + } + + if (size == 0) + { + return; + } + + m_HeadOffset = (m_HeadOffset + size) % m_Buffer.Length; + m_Size -= size; + + if (m_Size == 0) + { + m_HeadOffset = 0; + m_TailOffset = 0; + } + + return; + } + } + + /// + /// Extends the capacity of the ring buffer + /// + private void SetCapacity(int capacity) + { + byte[] buffer = new byte[capacity]; + + if (m_Size > 0) + { + if (m_HeadOffset < m_TailOffset) + { + Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, 0, m_Size); + } + else + { + Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, 0, m_Buffer.Length - m_HeadOffset); + Buffer.BlockCopy(m_Buffer, 0, buffer, m_Buffer.Length - m_HeadOffset, m_TailOffset); + } + } + + m_Buffer = buffer; + m_HeadOffset = 0; + m_TailOffset = m_Size; + } + + + /// + /// Writes a sequence of bytes to the ring buffer + /// + /// A byte array containing the data to write + /// The zero-based byte offset in from which to begin copying bytes to the ring buffer + /// The number of bytes to write + public void Write(T[] buffer, int index, int count) + { + if (count == 0) + { + return; + } + + lock (this) + { + if ((m_Size + count) > m_Buffer.Length) + { + SetCapacity((m_Size + count + 2047) & ~2047); + } + + if (m_HeadOffset < m_TailOffset) + { + int tailLength = m_Buffer.Length - m_TailOffset; + + if (tailLength >= count) + { + Buffer.BlockCopy(buffer, index, m_Buffer, m_TailOffset, count); + } + else + { + Buffer.BlockCopy(buffer, index, m_Buffer, m_TailOffset, tailLength); + Buffer.BlockCopy(buffer, index + tailLength, m_Buffer, 0, count - tailLength); + } + } + else + { + Buffer.BlockCopy(buffer, index, m_Buffer, m_TailOffset, count); + } + + m_Size += count; + m_TailOffset = (m_TailOffset + count) % m_Buffer.Length; + } + } + + /// + /// Reads a sequence of bytes from the ring buffer and advances the position within the ring buffer by the number of bytes read + /// + /// The buffer to write the data into + /// The zero-based byte offset in at which the read bytes will be placed + /// The maximum number of bytes to read + /// The total number of bytes read into the buffer. This might be less than the number of bytes requested if that number of bytes are not currently available, or zero if the ring buffer is empty + public int Read(T[] buffer, int index, int count) + { + lock (this) + { + if (count > m_Size) + { + count = m_Size; + } + + if (count == 0) + { + return 0; + } + + if (m_HeadOffset < m_TailOffset) + { + Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, index, count); + } + else + { + int tailLength = m_Buffer.Length - m_HeadOffset; + + if (tailLength >= count) + { + Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, index, count); + } + else + { + Buffer.BlockCopy(m_Buffer, m_HeadOffset, buffer, index, tailLength); + Buffer.BlockCopy(m_Buffer, 0, buffer, index + tailLength, count - tailLength); + } + } + + m_Size -= count; + m_HeadOffset = (m_HeadOffset + count) % m_Buffer.Length; + + if (m_Size == 0) + { + m_HeadOffset = 0; + m_TailOffset = 0; + } + + return count; + } + } + } +} -- cgit v1.2.3