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<T>()

Built-in `SizeOf<T>()` is 10x faster than our `TypeSize<T>` 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.
This commit is contained in:
jduncanator 2018-11-15 13:22:50 +11:00 committed by Ac_K
parent 85ffd76016
commit 8275bc3c08
34 changed files with 3399 additions and 21 deletions

View file

@ -0,0 +1,189 @@
using Ryujinx.Audio.SoundIo;
using SoundIOSharp;
using System.Collections.Generic;
namespace Ryujinx.Audio
{
/// <summary>
/// An audio renderer that uses libsoundio as the audio backend
/// </summary>
public class SoundIoAudioOut : IAalOutput
{
/// <summary>
/// The maximum amount of tracks we can issue simultaneously
/// </summary>
private const int MaximumTracks = 256;
/// <summary>
/// The <see cref="SoundIO"/> audio context
/// </summary>
private SoundIO m_AudioContext;
/// <summary>
/// The <see cref="SoundIODevice"/> audio device
/// </summary>
private SoundIODevice m_AudioDevice;
/// <summary>
/// An object pool containing <see cref="SoundIoAudioTrack"/> objects
/// </summary>
private SoundIoAudioTrackPool m_TrackPool;
/// <summary>
/// True if SoundIO is supported on the device.
/// </summary>
public static bool IsSupported => true;
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoAudioOut"/>
/// </summary>
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);
}
/// <summary>
/// Gets the current playback state of the specified track
/// </summary>
/// <param name="trackId">The track to retrieve the playback state for</param>
public PlaybackState GetState(int trackId)
{
if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track))
{
return track.State;
}
return PlaybackState.Stopped;
}
/// <summary>
/// Creates a new audio track with the specified parameters
/// </summary>
/// <param name="sampleRate">The requested sample rate</param>
/// <param name="channels">The requested channels</param>
/// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param>
/// <returns>The created track's Track ID</returns>
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;
}
/// <summary>
/// Stops playback and closes the track specified by <paramref name="trackId"/>
/// </summary>
/// <param name="trackId">The ID of the track to close</param>
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);
}
}
/// <summary>
/// Starts playback
/// </summary>
/// <param name="trackId">The ID of the track to start playback on</param>
public void Start(int trackId)
{
if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track))
{
track.Start();
}
}
/// <summary>
/// Stops playback
/// </summary>
/// <param name="trackId">The ID of the track to stop playback on</param>
public void Stop(int trackId)
{
if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track))
{
track.Stop();
}
}
/// <summary>
/// Appends an audio buffer to the specified track
/// </summary>
/// <typeparam name="T">The sample type of the buffer</typeparam>
/// <param name="trackId">The track to append the buffer to</param>
/// <param name="bufferTag">The internal tag of the buffer</param>
/// <param name="buffer">The buffer to append to the track</param>
public void AppendBuffer<T>(int trackId, long bufferTag, T[] buffer)
where T : struct
{
if(m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track))
{
track.AppendBuffer(bufferTag, buffer);
}
}
/// <summary>
/// Returns a value indicating whether the specified buffer is currently reserved by the specified track
/// </summary>
/// <param name="trackId">The track to check</param>
/// <param name="bufferTag">The buffer tag to check</param>
public bool ContainsBuffer(int trackId, long bufferTag)
{
if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track))
{
return track.ContainsBuffer(bufferTag);
}
return false;
}
/// <summary>
/// Gets a list of buffer tags the specified track is no longer reserving
/// </summary>
/// <param name="trackId">The track to retrieve buffer tags from</param>
/// <param name="maxCount">The maximum amount of buffer tags to retrieve</param>
/// <returns>Buffers released by the specified track</returns>
public long[] GetReleasedBuffers(int trackId, int maxCount)
{
if (m_TrackPool.TryGet(trackId, out SoundIoAudioTrack track))
{
List<long> bufferTags = new List<long>();
while(maxCount-- > 0 && track.ReleasedBuffers.TryDequeue(out long tag))
{
bufferTags.Add(tag);
}
return bufferTags.ToArray();
}
return new long[0];
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="SoundIoAudioOut" />
/// </summary>
public void Dispose()
{
m_TrackPool.Dispose();
m_AudioContext.Disconnect();
m_AudioContext.Dispose();
}
}
}

View file

@ -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
{
/// <summary>
/// The audio track ring buffer
/// </summary>
private SoundIoRingBuffer m_Buffer;
/// <summary>
/// A list of buffers currently pending writeback to the audio backend
/// </summary>
private ConcurrentQueue<SoundIoBuffer> m_ReservedBuffers;
/// <summary>
/// Occurs when a buffer has been released by the audio backend
/// </summary>
private event ReleaseCallback BufferReleased;
/// <summary>
/// The track ID of this <see cref="SoundIoAudioTrack"/>
/// </summary>
public int TrackID { get; private set; }
/// <summary>
/// The current playback state
/// </summary>
public PlaybackState State { get; private set; }
/// <summary>
/// The <see cref="SoundIO"/> audio context this track belongs to
/// </summary>
public SoundIO AudioContext { get; private set; }
/// <summary>
/// The <see cref="SoundIODevice"/> this track belongs to
/// </summary>
public SoundIODevice AudioDevice { get; private set; }
/// <summary>
/// The audio output stream of this track
/// </summary>
public SoundIOOutStream AudioStream { get; private set; }
/// <summary>
/// Released buffers the track is no longer holding
/// </summary>
public ConcurrentQueue<long> ReleasedBuffers { get; private set; }
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoAudioTrack"/>
/// </summary>
/// <param name="trackId">The track ID</param>
/// <param name="audioContext">The SoundIO audio context</param>
/// <param name="audioDevice">The SoundIO audio device</param>
public SoundIoAudioTrack(int trackId, SoundIO audioContext, SoundIODevice audioDevice)
{
TrackID = trackId;
AudioContext = audioContext;
AudioDevice = audioDevice;
State = PlaybackState.Stopped;
ReleasedBuffers = new ConcurrentQueue<long>();
m_Buffer = new SoundIoRingBuffer();
m_ReservedBuffers = new ConcurrentQueue<SoundIoBuffer>();
}
/// <summary>
/// Opens the audio track with the specified parameters
/// </summary>
/// <param name="sampleRate">The requested sample rate of the track</param>
/// <param name="channelCount">The requested channel count of the track</param>
/// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param>
/// <param name="format">The requested sample format of the track</param>
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();
}
/// <summary>
/// This callback occurs when the sound device is ready to buffer more frames
/// </summary>
/// <param name="minFrameCount">The minimum amount of frames expected by the audio backend</param>
/// <param name="maxFrameCount">The maximum amount of frames that can be written to the audio backend</param>
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);
}
/// <summary>
/// Releases any buffers that have been fully written to the output device
/// </summary>
/// <param name="bytesRead">The amount of bytes written in the last device write</param>
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();
}
}
/// <summary>
/// Starts audio playback
/// </summary>
public void Start()
{
if (AudioStream == null)
{
return;
}
AudioStream.Start();
AudioStream.Pause(false);
AudioContext.FlushEvents();
State = PlaybackState.Playing;
}
/// <summary>
/// Stops audio playback
/// </summary>
public void Stop()
{
if (AudioStream == null)
{
return;
}
AudioStream.Pause(true);
AudioContext.FlushEvents();
State = PlaybackState.Stopped;
}
/// <summary>
/// Appends an audio buffer to the tracks internal ring buffer
/// </summary>
/// <typeparam name="T">The audio sample type</typeparam>
/// <param name="bufferTag">The unqiue tag of the buffer being appended</param>
/// <param name="buffer">The buffer to append</param>
public void AppendBuffer<T>(long bufferTag, T[] buffer)
{
if (AudioStream == null)
{
return;
}
// Calculate the size of the audio samples
int size = Unsafe.SizeOf<T>();
// 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));
}
/// <summary>
/// Returns a value indicating whether the specified buffer is currently reserved by the track
/// </summary>
/// <param name="bufferTag">The buffer tag to check</param>
public bool ContainsBuffer(long bufferTag)
{
return m_ReservedBuffers.Any(x => x.Tag == bufferTag);
}
/// <summary>
/// Closes the <see cref="SoundIoAudioTrack"/>
/// </summary>
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();
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="SoundIoAudioTrack" />
/// </summary>
public void Dispose()
{
Close();
}
~SoundIoAudioTrack()
{
Dispose();
}
}
}

View file

@ -0,0 +1,193 @@
using SoundIOSharp;
using System;
using System.Collections.Concurrent;
using System.Linq;
namespace Ryujinx.Audio.SoundIo
{
/// <summary>
/// An object pool containing a set of audio tracks
/// </summary>
internal class SoundIoAudioTrackPool : IDisposable
{
/// <summary>
/// The current size of the <see cref="SoundIoAudioTrackPool"/>
/// </summary>
private int m_Size;
/// <summary>
/// The maximum size of the <see cref="SoundIoAudioTrackPool"/>
/// </summary>
private int m_MaxSize;
/// <summary>
/// The <see cref="SoundIO"/> audio context this track pool belongs to
/// </summary>
private SoundIO m_Context;
/// <summary>
/// The <see cref="SoundIODevice"/> audio device this track pool belongs to
/// </summary>
private SoundIODevice m_Device;
/// <summary>
/// The queue that keeps track of the available <see cref="SoundIoAudioTrack"/> in the pool.
/// </summary>
private ConcurrentQueue<SoundIoAudioTrack> m_Queue;
/// <summary>
/// The dictionary providing mapping between a TrackID and <see cref="SoundIoAudioTrack"/>
/// </summary>
private ConcurrentDictionary<int, SoundIoAudioTrack> m_TrackList;
/// <summary>
/// Gets the current size of the <see cref="SoundIoAudioTrackPool"/>
/// </summary>
public int Size { get => m_Size; }
/// <summary>
/// Gets the maximum size of the <see cref="SoundIoAudioTrackPool"/>
/// </summary>
public int MaxSize { get => m_MaxSize; }
/// <summary>
/// Gets a value that indicates whether the <see cref="SoundIoAudioTrackPool"/> is empty
/// </summary>
public bool IsEmpty { get => m_Queue.IsEmpty; }
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoAudioTrackPool"/> that is empty
/// </summary>
/// <param name="maxSize">The maximum amount of tracks that can be created</param>
public SoundIoAudioTrackPool(SoundIO context, SoundIODevice device, int maxSize)
{
m_Size = 0;
m_Context = context;
m_Device = device;
m_MaxSize = maxSize;
m_Queue = new ConcurrentQueue<SoundIoAudioTrack>();
m_TrackList = new ConcurrentDictionary<int, SoundIoAudioTrack>();
}
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoAudioTrackPool"/> that contains
/// the specified amount of <see cref="SoundIoAudioTrack"/>
/// </summary>
/// <param name="maxSize">The maximum amount of tracks that can be created</param>
/// <param name="initialCapacity">The initial number of tracks that the pool contains</param>
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<SoundIoAudioTrack>(trackCollection);
}
/// <summary>
/// Creates a new <see cref="SoundIoAudioTrack"/> with the proper AudioContext and AudioDevice
/// and the specified <paramref name="trackId" />
/// </summary>
/// <param name="trackId">The ID of the track to be created</param>
/// <returns>A new AudioTrack with the specified ID</returns>
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;
}
/// <summary>
/// Retrieves a <see cref="SoundIoAudioTrack"/> from the pool
/// </summary>
/// <returns>An AudioTrack from the pool</returns>
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++);
}
/// <summary>
/// Retrieves the <see cref="SoundIoAudioTrack"/> associated with the specified <paramref name="trackId"/> from the pool
/// </summary>
/// <param name="trackId">The ID of the track to retrieve</param>
public SoundIoAudioTrack Get(int trackId)
{
if (m_TrackList.TryGetValue(trackId, out SoundIoAudioTrack track))
{
return track;
}
return null;
}
/// <summary>
/// Attempers to get a <see cref="SoundIoAudioTrack"/> from the pool
/// </summary>
/// <param name="track">The track retrieved from the pool</param>
/// <returns>True if retrieve was successful</returns>
public bool TryGet(out SoundIoAudioTrack track)
{
track = Get();
return track != null;
}
/// <summary>
/// Attempts to get the <see cref="SoundIoAudioTrack" /> associated with the specified <paramref name="trackId"/> from the pool
/// </summary>
/// <param name="trackId">The ID of the track to retrieve</param>
/// <param name="track">The track retrieved from the pool</param>
public bool TryGet(int trackId, out SoundIoAudioTrack track)
{
return m_TrackList.TryGetValue(trackId, out track);
}
/// <summary>
/// Returns an <see cref="SoundIoAudioTrack"/> back to the pool for reuse
/// </summary>
/// <param name="track">The track to be returned to the pool</param>
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);
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="SoundIoAudioTrackPool" />
/// </summary>
public void Dispose()
{
foreach (var track in m_TrackList)
{
track.Value.Close();
track.Value.Dispose();
}
m_Size = 0;
m_Queue.Clear();
m_TrackList.Clear();
}
}
}

View file

@ -0,0 +1,29 @@
namespace Ryujinx.Audio.SoundIo
{
/// <summary>
/// Represents the remaining bytes left buffered for a specific buffer tag
/// </summary>
internal class SoundIoBuffer
{
/// <summary>
/// The buffer tag this <see cref="SoundIoBuffer"/> represents
/// </summary>
public long Tag { get; private set; }
/// <summary>
/// The remaining bytes still to be released
/// </summary>
public int Length { get; set; }
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoBuffer"/>
/// </summary>
/// <param name="tag">The buffer tag</param>
/// <param name="length">The size of the buffer</param>
public SoundIoBuffer(long tag, int length)
{
Tag = tag;
Length = length;
}
}
}

View file

@ -0,0 +1,204 @@
using System;
namespace Ryujinx.Audio.SoundIo
{
/// <summary>
/// A thread-safe variable-size circular buffer
/// </summary>
internal class SoundIoRingBuffer
{
private byte[] m_Buffer;
private int m_Size;
private int m_HeadOffset;
private int m_TailOffset;
/// <summary>
/// Gets the available bytes in the ring buffer
/// </summary>
public int Length
{
get { return m_Size; }
}
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoRingBuffer"/>
/// </summary>
public SoundIoRingBuffer()
{
m_Buffer = new byte[2048];
}
/// <summary>
/// Constructs a new instance of a <see cref="SoundIoRingBuffer"/> with the specified capacity
/// </summary>
/// <param name="capacity">The number of entries that the <see cref="SoundIoRingBuffer"/> can initially contain</param>
public SoundIoRingBuffer(int capacity)
{
m_Buffer = new byte[capacity];
}
/// <summary>
/// Clears the ring buffer
/// </summary>
public void Clear()
{
m_Size = 0;
m_HeadOffset = 0;
m_TailOffset = 0;
}
/// <summary>
/// Clears the specified amount of bytes from the ring buffer
/// </summary>
/// <param name="size">The amount of bytes to clear from the ring buffer</param>
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;
}
}
/// <summary>
/// Extends the capacity of the ring buffer
/// </summary>
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;
}
/// <summary>
/// Writes a sequence of bytes to the ring buffer
/// </summary>
/// <param name="buffer">A byte array containing the data to write</param>
/// <param name="index">The zero-based byte offset in <paramref name="buffer" /> from which to begin copying bytes to the ring buffer</param>
/// <param name="count">The number of bytes to write</param>
public void Write<T>(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;
}
}
/// <summary>
/// Reads a sequence of bytes from the ring buffer and advances the position within the ring buffer by the number of bytes read
/// </summary>
/// <param name="buffer">The buffer to write the data into</param>
/// <param name="index">The zero-based byte offset in <paramref name="buffer" /> at which the read bytes will be placed</param>
/// <param name="count">The maximum number of bytes to read</param>
/// <returns>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</returns>
public int Read<T>(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;
}
}
}
}