|
|
@@ -1,155 +1,118 @@
|
|
|
-using SoundIOSharp;
|
|
|
+using Ryujinx.Audio.Backends.Common;
|
|
|
+using Ryujinx.Audio.Common;
|
|
|
+using Ryujinx.Memory;
|
|
|
+using SoundIOSharp;
|
|
|
using System;
|
|
|
-using System.Collections.Concurrent;
|
|
|
-using System.Linq;
|
|
|
+using System.Collections.Generic;
|
|
|
using System.Runtime.CompilerServices;
|
|
|
-using System.Runtime.InteropServices;
|
|
|
+using System.Threading;
|
|
|
|
|
|
-namespace Ryujinx.Audio.SoundIo
|
|
|
+namespace Ryujinx.Audio.Backends.SoundIo
|
|
|
{
|
|
|
- internal class SoundIoAudioTrack : IDisposable
|
|
|
+ class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
|
|
{
|
|
|
- /// <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>
|
|
|
- /// Buffer count of the track
|
|
|
- /// </summary>
|
|
|
- public uint BufferCount => (uint)m_ReservedBuffers.Count;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Played sample count of the track
|
|
|
- /// </summary>
|
|
|
- public ulong PlayedSampleCount { get; private set; }
|
|
|
-
|
|
|
- private int _hardwareChannels;
|
|
|
- private int _virtualChannels;
|
|
|
-
|
|
|
- /// <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)
|
|
|
+ private object _lock = new object();
|
|
|
+
|
|
|
+ private SoundIoHardwareDeviceDriver _driver;
|
|
|
+ private Queue<SoundIoAudioBuffer> _queuedBuffers;
|
|
|
+ private SoundIOOutStream _outputStream;
|
|
|
+ private DynamicRingBuffer _ringBuffer;
|
|
|
+ private ulong _playedSampleCount;
|
|
|
+ private ManualResetEvent _updateRequiredEvent;
|
|
|
+
|
|
|
+ public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
|
|
{
|
|
|
- TrackID = trackId;
|
|
|
- AudioContext = audioContext;
|
|
|
- AudioDevice = audioDevice;
|
|
|
- State = PlaybackState.Stopped;
|
|
|
- ReleasedBuffers = new ConcurrentQueue<long>();
|
|
|
-
|
|
|
- m_Buffer = new SoundIoRingBuffer();
|
|
|
- m_ReservedBuffers = new ConcurrentQueue<SoundIoBuffer>();
|
|
|
+ _driver = driver;
|
|
|
+ _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
|
|
|
+ _queuedBuffers = new Queue<SoundIoAudioBuffer>();
|
|
|
+ _ringBuffer = new DynamicRingBuffer();
|
|
|
+
|
|
|
+ SetupOutputStream();
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Opens the audio track with the specified parameters
|
|
|
- /// </summary>
|
|
|
- /// <param name="sampleRate">The requested sample rate of the track</param>
|
|
|
- /// <param name="hardwareChannels">The requested hardware channels</param>
|
|
|
- /// <param name="virtualChannels">The requested virtual 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>
|
|
|
- /// <param name="format">The requested sample format of the track</param>
|
|
|
- public void Open(
|
|
|
- int sampleRate,
|
|
|
- int hardwareChannels,
|
|
|
- int virtualChannels,
|
|
|
- ReleaseCallback callback,
|
|
|
- SoundIOFormat format = SoundIOFormat.S16LE)
|
|
|
+ private void SetupOutputStream()
|
|
|
{
|
|
|
- // Close any existing audio streams
|
|
|
- if (AudioStream != null)
|
|
|
- {
|
|
|
- Close();
|
|
|
- }
|
|
|
+ _outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
|
|
|
+ _outputStream.WriteCallback += Update;
|
|
|
|
|
|
- if (!AudioDevice.SupportsSampleRate(sampleRate))
|
|
|
- {
|
|
|
- throw new InvalidOperationException($"This sound device does not support a sample rate of {sampleRate}Hz");
|
|
|
- }
|
|
|
+ // TODO: Setup other callbacks (errors, ect).
|
|
|
|
|
|
- if (!AudioDevice.SupportsFormat(format))
|
|
|
+ _outputStream.Open();
|
|
|
+ }
|
|
|
+
|
|
|
+ public override ulong GetPlayedSampleCount()
|
|
|
+ {
|
|
|
+ lock (_lock)
|
|
|
{
|
|
|
- throw new InvalidOperationException($"This sound device does not support SoundIOFormat.{Enum.GetName(typeof(SoundIOFormat), format)}");
|
|
|
+ return _playedSampleCount;
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ public override float GetVolume()
|
|
|
+ {
|
|
|
+ return _outputStream.Volume;
|
|
|
+ }
|
|
|
|
|
|
- if (!AudioDevice.SupportsChannelCount(hardwareChannels))
|
|
|
+ public override void PrepareToClose() { }
|
|
|
+
|
|
|
+ public override void QueueBuffer(AudioBuffer buffer)
|
|
|
+ {
|
|
|
+ lock (_lock)
|
|
|
{
|
|
|
- throw new InvalidOperationException($"This sound device does not support channel count {hardwareChannels}");
|
|
|
+ SoundIoAudioBuffer driverBuffer = new SoundIoAudioBuffer
|
|
|
+ {
|
|
|
+ DriverIdentifier = buffer.DataPointer,
|
|
|
+ SampleCount = GetSampleCount(buffer),
|
|
|
+ SamplePlayed = 0,
|
|
|
+ };
|
|
|
+
|
|
|
+ _ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
|
|
|
+
|
|
|
+ _queuedBuffers.Enqueue(driverBuffer);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- _hardwareChannels = hardwareChannels;
|
|
|
- _virtualChannels = virtualChannels;
|
|
|
+ public override void SetVolume(float volume)
|
|
|
+ {
|
|
|
+ _outputStream.SetVolume(volume);
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Start()
|
|
|
+ {
|
|
|
+ _outputStream.Start();
|
|
|
+ _outputStream.Pause(false);
|
|
|
|
|
|
- AudioStream = AudioDevice.CreateOutStream();
|
|
|
+ _driver.FlushContextEvents();
|
|
|
+ }
|
|
|
|
|
|
- AudioStream.Name = $"SwitchAudioTrack_{TrackID}";
|
|
|
- AudioStream.Layout = SoundIOChannelLayout.GetDefault(hardwareChannels);
|
|
|
- AudioStream.Format = format;
|
|
|
- AudioStream.SampleRate = sampleRate;
|
|
|
+ public override void Stop()
|
|
|
+ {
|
|
|
+ _outputStream.Pause(true);
|
|
|
|
|
|
- AudioStream.WriteCallback = WriteCallback;
|
|
|
+ _driver.FlushContextEvents();
|
|
|
+ }
|
|
|
|
|
|
- BufferReleased += callback;
|
|
|
+ public override void UnregisterBuffer(AudioBuffer buffer) {}
|
|
|
|
|
|
- AudioStream.Open();
|
|
|
+ public override bool WasBufferFullyConsumed(AudioBuffer buffer)
|
|
|
+ {
|
|
|
+ lock (_lock)
|
|
|
+ {
|
|
|
+ if (!_queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return driverBuffer.DriverIdentifier != buffer.DataPointer;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /// <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)
|
|
|
+ private unsafe void Update(int minFrameCount, int maxFrameCount)
|
|
|
{
|
|
|
- int bytesPerFrame = AudioStream.BytesPerFrame;
|
|
|
- uint bytesPerSample = (uint)AudioStream.BytesPerSample;
|
|
|
+ int bytesPerFrame = _outputStream.BytesPerFrame;
|
|
|
+ uint bytesPerSample = (uint)_outputStream.BytesPerSample;
|
|
|
|
|
|
- int bufferedFrames = m_Buffer.Length / bytesPerFrame;
|
|
|
- long bufferedSamples = m_Buffer.Length / bytesPerSample;
|
|
|
+ int bufferedFrames = _ringBuffer.Length / bytesPerFrame;
|
|
|
|
|
|
int frameCount = Math.Min(bufferedFrames, maxFrameCount);
|
|
|
|
|
|
@@ -158,16 +121,18 @@ namespace Ryujinx.Audio.SoundIo
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- SoundIOChannelAreas areas = AudioStream.BeginWrite(ref frameCount);
|
|
|
+ SoundIOChannelAreas areas = _outputStream.BeginWrite(ref frameCount);
|
|
|
+
|
|
|
int channelCount = areas.ChannelCount;
|
|
|
|
|
|
byte[] samples = new byte[frameCount * bytesPerFrame];
|
|
|
|
|
|
- m_Buffer.Read(samples, 0, samples.Length);
|
|
|
+ _ringBuffer.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.
|
|
|
+ // TODO: Is this still right in 2021?
|
|
|
|
|
|
// Mono
|
|
|
if (channelCount == 1)
|
|
|
@@ -438,209 +403,58 @@ namespace Ryujinx.Audio.SoundIo
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- AudioStream.EndWrite();
|
|
|
+ _outputStream.EndWrite();
|
|
|
|
|
|
- PlayedSampleCount += (ulong)samples.Length;
|
|
|
+ ulong sampleCount = (ulong)(samples.Length / bytesPerSample / channelCount);
|
|
|
|
|
|
- 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;
|
|
|
+ ulong availaibleSampleCount = sampleCount;
|
|
|
|
|
|
- m_ReservedBuffers.TryDequeue(out buffer);
|
|
|
- ReleasedBuffers.Enqueue(buffer.Tag);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ bool needUpdate = false;
|
|
|
|
|
|
- if (bufferReleased)
|
|
|
+ while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer))
|
|
|
{
|
|
|
- OnBufferReleased();
|
|
|
- }
|
|
|
- }
|
|
|
+ ulong sampleStillNeeded = driverBuffer.SampleCount - driverBuffer.SamplePlayed;
|
|
|
+ ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount);
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Starts audio playback
|
|
|
- /// </summary>
|
|
|
- public void Start()
|
|
|
- {
|
|
|
- if (AudioStream == null)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
+ driverBuffer.SamplePlayed += playedAudioBufferSampleCount;
|
|
|
+ availaibleSampleCount -= playedAudioBufferSampleCount;
|
|
|
|
|
|
- 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) where T: struct
|
|
|
- {
|
|
|
- if (AudioStream == null)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- int sampleSize = Unsafe.SizeOf<T>();
|
|
|
- int targetSize = sampleSize * buffer.Length;
|
|
|
-
|
|
|
- // Do we need to downmix?
|
|
|
- if (_hardwareChannels != _virtualChannels)
|
|
|
- {
|
|
|
- if (sampleSize != sizeof(short))
|
|
|
+ if (driverBuffer.SamplePlayed == driverBuffer.SampleCount)
|
|
|
{
|
|
|
- throw new NotImplementedException("Downmixing formats other than PCM16 is not supported!");
|
|
|
- }
|
|
|
+ _queuedBuffers.TryDequeue(out _);
|
|
|
|
|
|
- short[] downmixedBuffer;
|
|
|
-
|
|
|
- ReadOnlySpan<short> bufferPCM16 = MemoryMarshal.Cast<T, short>(buffer);
|
|
|
-
|
|
|
- if (_virtualChannels == 6)
|
|
|
- {
|
|
|
- downmixedBuffer = Downmixing.DownMixSurroundToStereo(bufferPCM16);
|
|
|
-
|
|
|
- if (_hardwareChannels == 1)
|
|
|
- {
|
|
|
- downmixedBuffer = Downmixing.DownMixStereoToMono(downmixedBuffer);
|
|
|
- }
|
|
|
- }
|
|
|
- else if (_virtualChannels == 2)
|
|
|
- {
|
|
|
- downmixedBuffer = Downmixing.DownMixStereoToMono(bufferPCM16);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- throw new NotImplementedException($"Downmixing from {_virtualChannels} to {_hardwareChannels} not implemented!");
|
|
|
+ needUpdate = true;
|
|
|
}
|
|
|
|
|
|
- targetSize = sampleSize * downmixedBuffer.Length;
|
|
|
-
|
|
|
- // Copy the memory to our ring buffer
|
|
|
- m_Buffer.Write(downmixedBuffer, 0, targetSize);
|
|
|
-
|
|
|
- // Keep track of "buffered" buffers
|
|
|
- m_ReservedBuffers.Enqueue(new SoundIoBuffer(bufferTag, targetSize));
|
|
|
+ _playedSampleCount += playedAudioBufferSampleCount;
|
|
|
}
|
|
|
- else
|
|
|
- {
|
|
|
- // Copy the memory to our ring buffer
|
|
|
- m_Buffer.Write(buffer, 0, targetSize);
|
|
|
|
|
|
- // Keep track of "buffered" buffers
|
|
|
- m_ReservedBuffers.Enqueue(new SoundIoBuffer(bufferTag, targetSize));
|
|
|
+ // Notify the output if needed.
|
|
|
+ if (needUpdate)
|
|
|
+ {
|
|
|
+ _updateRequiredEvent.Set();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <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)
|
|
|
+ protected virtual void Dispose(bool disposing)
|
|
|
{
|
|
|
- return m_ReservedBuffers.Any(x => x.Tag == bufferTag);
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Flush all track buffers
|
|
|
- /// </summary>
|
|
|
- public bool FlushBuffers()
|
|
|
- {
|
|
|
- m_Buffer.Clear();
|
|
|
-
|
|
|
- if (m_ReservedBuffers.Count > 0)
|
|
|
+ if (disposing)
|
|
|
{
|
|
|
- foreach (var buffer in m_ReservedBuffers)
|
|
|
+ lock (_lock)
|
|
|
{
|
|
|
- ReleasedBuffers.Enqueue(buffer.Tag);
|
|
|
- }
|
|
|
-
|
|
|
- OnBufferReleased();
|
|
|
-
|
|
|
- return true;
|
|
|
- }
|
|
|
+ PrepareToClose();
|
|
|
+ Stop();
|
|
|
|
|
|
- return false;
|
|
|
- }
|
|
|
+ _outputStream.Dispose();
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Closes the <see cref="SoundIoAudioTrack"/>
|
|
|
- /// </summary>
|
|
|
- public void Close()
|
|
|
- {
|
|
|
- if (AudioStream != null)
|
|
|
- {
|
|
|
- AudioStream.Pause(true);
|
|
|
- AudioStream.Dispose();
|
|
|
+ _driver.Unregister(this);
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- 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()
|
|
|
+ public override void Dispose()
|
|
|
{
|
|
|
- Dispose();
|
|
|
+ Dispose(true);
|
|
|
}
|
|
|
}
|
|
|
}
|