Haydn: Part 1 (#2007)

* Haydn: Part 1

Based on my reverse of audio 11.0.0.

As always, core implementation under LGPLv3 for the same reasons as for Amadeus.

This place the bases of a more flexible audio system while making audout & audin accurate.

This have the following improvements:
- Complete reimplementation of audout and audin.
- Audin currently only have a dummy backend.
- Dramatically reduce CPU usage by up to 50% in common cases (SoundIO and OpenAL).
- Audio Renderer now can output to 5.1 devices when supported.
- Audio Renderer init its backend on demand instead of keeping two up all the time.
- All backends implementation are now in their own project.
- Ryujinx.Audio.Renderer was renamed Ryujinx.Audio and was refactored because of this.

As a note, games having issues with OpenAL haven't improved and will not
because of OpenAL design (stopping when buffers finish playing causing
possible audio "pops" when buffers are very small).

* Update for latest hexkyz's edits on Switchbrew

* audren: Rollback channel configuration changes

* Address gdkchan's comments

* Fix typo in OpenAL backend driver

* Address last comments

* Fix a nit

* Address gdkchan's comments
This commit is contained in:
Mary 2021-02-26 01:11:56 +01:00 committed by GitHub
parent 1c49089ff0
commit f556c80d02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
249 changed files with 5614 additions and 2712 deletions

View file

@ -104,7 +104,7 @@ If you'd like to donate, please take a look at our [Patreon](https://www.patreon
## License ## License
This software is licensed under the terms of the MIT license. This software is licensed under the terms of the MIT license.
The Ryujinx.Audio.Renderer project is licensed under the terms of the LGPLv3 license. The Ryujinx.Audio project is licensed under the terms of the LGPLv3 license.
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3. This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](Ryujinx/THIRDPARTY.md) for more details. See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](Ryujinx/THIRDPARTY.md) for more details.

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Audio.Backends.OpenAL
{
class OpenALAudioBuffer
{
public int BufferId;
public ulong DriverIdentifier;
public ulong SampleCount;
}
}

View file

@ -0,0 +1,170 @@
using OpenTK.Audio;
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Threading;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.OpenAL
{
public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
{
private object _lock = new object();
private AudioContext _context;
private ManualResetEvent _updateRequiredEvent;
private List<OpenALHardwareDeviceSession> _sessions;
private bool _stillRunning;
private Thread _updaterThread;
public OpenALHardwareDeviceDriver()
{
_context = new AudioContext();
_updateRequiredEvent = new ManualResetEvent(false);
_sessions = new List<OpenALHardwareDeviceSession>();
_stillRunning = true;
_updaterThread = new Thread(Update)
{
Name = "HardwareDeviceDriver.OpenAL"
};
_updaterThread.Start();
}
public static bool IsSupported
{
get
{
try
{
return AudioContext.AvailableDevices.Count > 0;
}
catch
{
return false;
}
}
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
channelCount = 2;
}
if (sampleRate == 0)
{
sampleRate = Constants.TargetSampleRate;
}
if (direction != Direction.Output)
{
throw new ArgumentException($"{direction}");
}
else if (!SupportsChannelCount(channelCount))
{
throw new ArgumentException($"{channelCount}");
}
lock (_lock)
{
OpenALHardwareDeviceSession session = new OpenALHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.Add(session);
return session;
}
}
internal void Unregister(OpenALHardwareDeviceSession session)
{
lock (_lock)
{
_sessions.Remove(session);
}
}
public ManualResetEvent GetUpdateRequiredEvent()
{
return _updateRequiredEvent;
}
private void Update()
{
while (_stillRunning)
{
bool updateRequired = false;
lock (_lock)
{
foreach (OpenALHardwareDeviceSession session in _sessions)
{
if (session.Update())
{
updateRequired = true;
}
}
}
if (updateRequired)
{
_updateRequiredEvent.Set();
}
// If it's not slept it will waste cycles.
Thread.Sleep(10);
}
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_lock)
{
_stillRunning = false;
_updaterThread.Join();
// Loop against all sessions to dispose them (they will unregister themself)
while (_sessions.Count > 0)
{
OpenALHardwareDeviceSession session = _sessions[0];
session.Dispose();
}
}
_context.Dispose();
}
}
public bool SupportsSampleRate(uint sampleRate)
{
return true;
}
public bool SupportsSampleFormat(SampleFormat sampleFormat)
{
return true;
}
public bool SupportsChannelCount(uint channelCount)
{
return channelCount == 1 || channelCount == 2 || channelCount == 6;
}
public bool SupportsDirection(Direction direction)
{
return direction == Direction.Output;
}
}
}

View file

@ -0,0 +1,213 @@
using OpenTK.Audio.OpenAL;
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ryujinx.Audio.Backends.OpenAL
{
class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
{
private OpenALHardwareDeviceDriver _driver;
private int _sourceId;
private ALFormat _targetFormat;
private bool _isActive;
private Queue<OpenALAudioBuffer> _queuedBuffers;
private ulong _playedSampleCount;
private object _lock = new object();
public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_queuedBuffers = new Queue<OpenALAudioBuffer>();
_sourceId = AL.GenSource();
_targetFormat = GetALFormat();
_isActive = false;
_playedSampleCount = 0;
}
private ALFormat GetALFormat()
{
switch (RequestedSampleFormat)
{
case SampleFormat.PcmInt16:
switch (RequestedChannelCount)
{
case 1:
return ALFormat.Mono16;
case 2:
return ALFormat.Stereo16;
case 6:
return ALFormat.Multi51Chn16Ext;
default:
throw new NotImplementedException($"Unsupported channel config {RequestedChannelCount}");
}
default:
throw new NotImplementedException($"Unsupported sample format {RequestedSampleFormat}");
}
}
public override void PrepareToClose() { }
private void StartIfNotPlaying()
{
AL.GetSource(_sourceId, ALGetSourcei.SourceState, out int stateInt);
ALSourceState State = (ALSourceState)stateInt;
if (State != ALSourceState.Playing)
{
AL.SourcePlay(_sourceId);
}
}
public override void QueueBuffer(AudioBuffer buffer)
{
lock (_lock)
{
OpenALAudioBuffer driverBuffer = new OpenALAudioBuffer
{
DriverIdentifier = buffer.DataPointer,
BufferId = AL.GenBuffer(),
SampleCount = GetSampleCount(buffer)
};
AL.BufferData(driverBuffer.BufferId, _targetFormat, buffer.Data, (int)buffer.DataSize, (int)RequestedSampleRate);
_queuedBuffers.Enqueue(driverBuffer);
AL.SourceQueueBuffer(_sourceId, driverBuffer.BufferId);
if (_isActive)
{
StartIfNotPlaying();
}
}
}
public override void SetVolume(float volume)
{
lock (_lock)
{
AL.Source(_sourceId, ALSourcef.Gain, volume);
}
}
public override float GetVolume()
{
AL.GetSource(_sourceId, ALSourcef.Gain, out float volume);
return volume;
}
public override void Start()
{
lock (_lock)
{
_isActive = true;
StartIfNotPlaying();
}
}
public override void Stop()
{
lock (_lock)
{
SetVolume(0.0f);
AL.SourceStop(_sourceId);
_isActive = false;
}
}
public override void UnregisterBuffer(AudioBuffer buffer) {}
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{
lock (_lock)
{
if (!_queuedBuffers.TryPeek(out OpenALAudioBuffer driverBuffer))
{
return true;
}
return driverBuffer.DriverIdentifier != buffer.DataPointer;
}
}
public override ulong GetPlayedSampleCount()
{
lock (_lock)
{
return _playedSampleCount;
}
}
public bool Update()
{
lock (_lock)
{
if (_isActive)
{
AL.GetSource(_sourceId, ALGetSourcei.BuffersProcessed, out int releasedCount);
if (releasedCount > 0)
{
int[] bufferIds = new int[releasedCount];
AL.SourceUnqueueBuffers(_sourceId, releasedCount, bufferIds);
int i = 0;
while (_queuedBuffers.TryPeek(out OpenALAudioBuffer buffer) && i < bufferIds.Length)
{
if (buffer.BufferId == bufferIds[i])
{
_playedSampleCount += buffer.SampleCount;
_queuedBuffers.TryDequeue(out _);
i++;
}
}
Debug.Assert(i < bufferIds.Length, "Unknown buffer id");
AL.DeleteBuffers(bufferIds);
}
return releasedCount > 0;
}
return false;
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (_lock)
{
PrepareToClose();
Stop();
AL.DeleteSource(_sourceId);
_driver.Unregister(this);
}
}
}
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.32" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
</ItemGroup>
<ItemGroup>
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dll" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.dll</TargetPath>
</ContentWithTargetPath>
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.dylib" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'win-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.dylib</TargetPath>
</ContentWithTargetPath>
<ContentWithTargetPath Include="Native\libsoundio\libs\libsoundio.so" Condition="'$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.so</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
</Project>

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Audio.Backends.SoundIo
{
class SoundIoAudioBuffer
{
public ulong DriverIdentifier;
public ulong SampleCount;
public ulong SamplePlayed;
}
}

View file

@ -0,0 +1,251 @@
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Memory;
using SoundIOSharp;
using System;
using System.Collections.Generic;
using System.Threading;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.SoundIo
{
public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
{
private object _lock = new object();
private SoundIO _audioContext;
private SoundIODevice _audioDevice;
private ManualResetEvent _updateRequiredEvent;
private List<SoundIoHardwareDeviceSession> _sessions;
public SoundIoHardwareDeviceDriver()
{
_audioContext = new SoundIO();
_updateRequiredEvent = new ManualResetEvent(false);
_sessions = new List<SoundIoHardwareDeviceSession>();
_audioContext.Connect();
_audioContext.FlushEvents();
_audioDevice = FindNonRawDefaultAudioDevice(_audioContext, true);
}
public static bool IsSupported => IsSupportedInternal();
private static bool IsSupportedInternal()
{
SoundIO context = null;
SoundIODevice device = null;
SoundIOOutStream stream = null;
bool backendDisconnected = false;
try
{
context = new SoundIO();
context.OnBackendDisconnect = (i) =>
{
backendDisconnected = true;
};
context.Connect();
context.FlushEvents();
if (backendDisconnected)
{
return false;
}
if (context.OutputDeviceCount == 0)
{
return false;
}
device = FindNonRawDefaultAudioDevice(context);
if (device == null || backendDisconnected)
{
return false;
}
stream = device.CreateOutStream();
if (stream == null || backendDisconnected)
{
return false;
}
return true;
}
catch
{
return false;
}
finally
{
if (stream != null)
{
stream.Dispose();
}
if (context != null)
{
context.Dispose();
}
}
}
private static SoundIODevice FindNonRawDefaultAudioDevice(SoundIO audioContext, bool fallback = false)
{
SoundIODevice defaultAudioDevice = audioContext.GetOutputDevice(audioContext.DefaultOutputDeviceIndex);
if (!defaultAudioDevice.IsRaw)
{
return defaultAudioDevice;
}
for (int i = 0; i < audioContext.BackendCount; i++)
{
SoundIODevice audioDevice = audioContext.GetOutputDevice(i);
if (audioDevice.Id == defaultAudioDevice.Id && !audioDevice.IsRaw)
{
return audioDevice;
}
}
return fallback ? defaultAudioDevice : null;
}
public ManualResetEvent GetUpdateRequiredEvent()
{
return _updateRequiredEvent;
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
channelCount = 2;
}
if (sampleRate == 0)
{
sampleRate = Constants.TargetSampleRate;
}
if (direction != Direction.Output)
{
throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
}
lock (_lock)
{
SoundIoHardwareDeviceSession session = new SoundIoHardwareDeviceSession(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.Add(session);
return session;
}
}
internal void Unregister(SoundIoHardwareDeviceSession session)
{
lock (_lock)
{
_sessions.Remove(session);
}
}
public static SoundIOFormat GetSoundIoFormat(SampleFormat format)
{
return format switch
{
SampleFormat.PcmInt8 => SoundIOFormat.S8,
SampleFormat.PcmInt16 => SoundIOFormat.S16LE,
SampleFormat.PcmInt24 => SoundIOFormat.S24LE,
SampleFormat.PcmInt32 => SoundIOFormat.S32LE,
SampleFormat.PcmFloat => SoundIOFormat.Float32LE,
_ => throw new ArgumentException ($"Unsupported sample format {format}"),
};
}
internal SoundIOOutStream OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount)
{
SoundIOFormat driverSampleFormat = GetSoundIoFormat(requestedSampleFormat);
if (!_audioDevice.SupportsSampleRate((int)requestedSampleRate))
{
throw new ArgumentException($"This sound device does not support a sample rate of {requestedSampleRate}Hz");
}
if (!_audioDevice.SupportsFormat(driverSampleFormat))
{
throw new ArgumentException($"This sound device does not support {requestedSampleFormat}");
}
if (!_audioDevice.SupportsChannelCount((int)requestedChannelCount))
{
throw new ArgumentException($"This sound device does not support channel count {requestedChannelCount}");
}
SoundIOOutStream result = _audioDevice.CreateOutStream();
result.Name = "Ryujinx";
result.Layout = SoundIOChannelLayout.GetDefault((int)requestedChannelCount);
result.Format = driverSampleFormat;
result.SampleRate = (int)requestedSampleRate;
return result;
}
internal void FlushContextEvents()
{
_audioContext.FlushEvents();
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
while (_sessions.Count > 0)
{
SoundIoHardwareDeviceSession session = _sessions[_sessions.Count - 1];
session.Dispose();
}
_audioContext.Disconnect();
_audioContext.Dispose();
}
}
public bool SupportsSampleRate(uint sampleRate)
{
return _audioDevice.SupportsSampleRate((int)sampleRate);
}
public bool SupportsSampleFormat(SampleFormat sampleFormat)
{
return _audioDevice.SupportsFormat(GetSoundIoFormat(sampleFormat));
}
public bool SupportsChannelCount(uint channelCount)
{
return _audioDevice.SupportsChannelCount((int)channelCount);
}
public bool SupportsDirection(Direction direction)
{
// TODO: add direction input when supported.
return direction == Direction.Output;
}
}
}

View file

@ -1,155 +1,118 @@
using SoundIOSharp; using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Memory;
using SoundIOSharp;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices; 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> private object _lock = new object();
/// The audio track ring buffer
/// </summary>
private SoundIoRingBuffer m_Buffer;
/// <summary> private SoundIoHardwareDeviceDriver _driver;
/// A list of buffers currently pending writeback to the audio backend private Queue<SoundIoAudioBuffer> _queuedBuffers;
/// </summary> private SoundIOOutStream _outputStream;
private ConcurrentQueue<SoundIoBuffer> m_ReservedBuffers; private DynamicRingBuffer _ringBuffer;
private ulong _playedSampleCount;
private ManualResetEvent _updateRequiredEvent;
/// <summary> public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
/// 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)
{ {
TrackID = trackId; _driver = driver;
AudioContext = audioContext; _updateRequiredEvent = _driver.GetUpdateRequiredEvent();
AudioDevice = audioDevice; _queuedBuffers = new Queue<SoundIoAudioBuffer>();
State = PlaybackState.Stopped; _ringBuffer = new DynamicRingBuffer();
ReleasedBuffers = new ConcurrentQueue<long>();
m_Buffer = new SoundIoRingBuffer(); SetupOutputStream();
m_ReservedBuffers = new ConcurrentQueue<SoundIoBuffer>();
} }
/// <summary> private void SetupOutputStream()
/// 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)
{ {
// Close any existing audio streams _outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
if (AudioStream != null) _outputStream.WriteCallback += Update;
{
Close(); // TODO: Setup other callbacks (errors, ect).
_outputStream.Open();
} }
if (!AudioDevice.SupportsSampleRate(sampleRate)) public override ulong GetPlayedSampleCount()
{ {
throw new InvalidOperationException($"This sound device does not support a sample rate of {sampleRate}Hz"); lock (_lock)
{
return _playedSampleCount;
}
} }
if (!AudioDevice.SupportsFormat(format)) public override float GetVolume()
{ {
throw new InvalidOperationException($"This sound device does not support SoundIOFormat.{Enum.GetName(typeof(SoundIOFormat), format)}"); return _outputStream.Volume;
} }
if (!AudioDevice.SupportsChannelCount(hardwareChannels)) public override void PrepareToClose() { }
public override void QueueBuffer(AudioBuffer buffer)
{ {
throw new InvalidOperationException($"This sound device does not support channel count {hardwareChannels}"); lock (_lock)
{
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; public override void SetVolume(float volume)
_virtualChannels = virtualChannels; {
_outputStream.SetVolume(volume);
AudioStream = AudioDevice.CreateOutStream();
AudioStream.Name = $"SwitchAudioTrack_{TrackID}";
AudioStream.Layout = SoundIOChannelLayout.GetDefault(hardwareChannels);
AudioStream.Format = format;
AudioStream.SampleRate = sampleRate;
AudioStream.WriteCallback = WriteCallback;
BufferReleased += callback;
AudioStream.Open();
} }
/// <summary> public override void Start()
/// 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; _outputStream.Start();
uint bytesPerSample = (uint)AudioStream.BytesPerSample; _outputStream.Pause(false);
int bufferedFrames = m_Buffer.Length / bytesPerFrame; _driver.FlushContextEvents();
long bufferedSamples = m_Buffer.Length / bytesPerSample; }
public override void Stop()
{
_outputStream.Pause(true);
_driver.FlushContextEvents();
}
public override void UnregisterBuffer(AudioBuffer buffer) {}
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{
lock (_lock)
{
if (!_queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer))
{
return true;
}
return driverBuffer.DriverIdentifier != buffer.DataPointer;
}
}
private unsafe void Update(int minFrameCount, int maxFrameCount)
{
int bytesPerFrame = _outputStream.BytesPerFrame;
uint bytesPerSample = (uint)_outputStream.BytesPerSample;
int bufferedFrames = _ringBuffer.Length / bytesPerFrame;
int frameCount = Math.Min(bufferedFrames, maxFrameCount); int frameCount = Math.Min(bufferedFrames, maxFrameCount);
@ -158,16 +121,18 @@ namespace Ryujinx.Audio.SoundIo
return; return;
} }
SoundIOChannelAreas areas = AudioStream.BeginWrite(ref frameCount); SoundIOChannelAreas areas = _outputStream.BeginWrite(ref frameCount);
int channelCount = areas.ChannelCount; int channelCount = areas.ChannelCount;
byte[] samples = new byte[frameCount * bytesPerFrame]; 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 // This is a huge ugly block of code, but we save
// a significant amount of time over the generic // a significant amount of time over the generic
// loop that handles other channel counts. // loop that handles other channel counts.
// TODO: Is this still right in 2021?
// Mono // Mono
if (channelCount == 1) 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); ulong availaibleSampleCount = sampleCount;
}
/// <summary> bool needUpdate = false;
/// Releases any buffers that have been fully written to the output device
/// </summary> while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer))
/// <param name="bytesRead">The amount of bytes written in the last device write</param>
private void UpdateReleasedBuffers(int bytesRead)
{ {
bool bufferReleased = false; ulong sampleStillNeeded = driverBuffer.SampleCount - driverBuffer.SamplePlayed;
ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount);
while (bytesRead > 0) driverBuffer.SamplePlayed += playedAudioBufferSampleCount;
availaibleSampleCount -= playedAudioBufferSampleCount;
if (driverBuffer.SamplePlayed == driverBuffer.SampleCount)
{ {
if (m_ReservedBuffers.TryPeek(out SoundIoBuffer buffer)) _queuedBuffers.TryDequeue(out _);
needUpdate = true;
}
_playedSampleCount += playedAudioBufferSampleCount;
}
// Notify the output if needed.
if (needUpdate)
{ {
if (buffer.Length > bytesRead) _updateRequiredEvent.Set();
}
}
protected virtual void Dispose(bool disposing)
{ {
buffer.Length -= bytesRead; if (disposing)
bytesRead = 0;
}
else
{ {
bufferReleased = true; lock (_lock)
bytesRead -= buffer.Length;
m_ReservedBuffers.TryDequeue(out buffer);
ReleasedBuffers.Enqueue(buffer.Tag);
}
}
}
if (bufferReleased)
{ {
OnBufferReleased(); PrepareToClose();
Stop();
_outputStream.Dispose();
_driver.Unregister(this);
}
} }
} }
/// <summary> public override void Dispose()
/// Starts audio playback
/// </summary>
public void Start()
{ {
if (AudioStream == null) Dispose(true);
{
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) 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))
{
throw new NotImplementedException("Downmixing formats other than PCM16 is not supported!");
}
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!");
}
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));
}
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));
}
}
/// <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>
/// Flush all track buffers
/// </summary>
public bool FlushBuffers()
{
m_Buffer.Clear();
if (m_ReservedBuffers.Count > 0)
{
foreach (var buffer in m_ReservedBuffers)
{
ReleasedBuffers.Enqueue(buffer.Tag);
}
OnBufferReleased();
return true;
}
return false;
}
/// <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,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.32" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
</ItemGroup>
<ItemGroup>
<ContentWithTargetPath Include="SoundIo\Native\libsoundio\libs\libsoundio.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.dll</TargetPath>
</ContentWithTargetPath>
<ContentWithTargetPath Include="SoundIo\Native\libsoundio\libs\libsoundio.dylib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.dylib</TargetPath>
</ContentWithTargetPath>
<ContentWithTargetPath Include="SoundIo\Native\libsoundio\libs\libsoundio.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>libsoundio.so</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
</Project>

View file

@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.Cpu\Ryujinx.Cpu.csproj" />
<ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,137 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using System;
using System.Threading;
namespace Ryujinx.Audio
{
/// <summary>
/// Manage audio input and output system.
/// </summary>
public class AudioManager : IDisposable
{
/// <summary>
/// Lock used to control the waiters registration.
/// </summary>
private object _lock = new object();
/// <summary>
/// Events signaled when the driver played audio buffers.
/// </summary>
private ManualResetEvent[] _updateRequiredEvents;
/// <summary>
/// Action to execute when the driver played audio buffers.
/// </summary>
private Action[] _actions;
/// <summary>
/// The worker thread in charge of handling sessions update.
/// </summary>
private Thread _workerThread;
/// <summary>
/// Create a new <see cref="AudioManager"/>.
/// </summary>
public AudioManager()
{
_updateRequiredEvents = new ManualResetEvent[2];
_actions = new Action[2];
// Termination event.
_updateRequiredEvents[1] = new ManualResetEvent(false);
_workerThread = new Thread(Update)
{
Name = "AudioManager.Worker"
};
}
/// <summary>
/// Start the <see cref="AudioManager"/>.
/// </summary>
public void Start()
{
if (_workerThread.IsAlive)
{
throw new InvalidOperationException();
}
_workerThread.Start();
}
/// <summary>
/// Initialize update handlers.
/// </summary>
/// <param name="updatedRequiredEvent ">The driver event that will get signaled by the device driver when an audio buffer finished playing/being captured</param>
/// <param name="outputCallback">The callback to call when an audio buffer finished playing</param>
/// <param name="inputCallback">The callback to call when an audio buffer was captured</param>
public void Initialize(ManualResetEvent updatedRequiredEvent, Action outputCallback, Action inputCallback)
{
lock (_lock)
{
_updateRequiredEvents[0] = updatedRequiredEvent;
_actions[0] = outputCallback;
_actions[1] = inputCallback;
}
}
/// <summary>
/// Entrypoint of the <see cref="_workerThread"/> in charge of updating the <see cref="AudioManager"/>.
/// </summary>
private void Update()
{
while (true)
{
int index = WaitHandle.WaitAny(_updateRequiredEvents);
// Last index is here to indicate thread termination.
if (index + 1 == _updateRequiredEvents.Length)
{
break;
}
lock (_lock)
{
foreach (Action action in _actions)
{
action?.Invoke();
}
_updateRequiredEvents[0].Reset();
}
}
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_updateRequiredEvents[1].Set();
_workerThread.Join();
_updateRequiredEvents[1].Dispose();
}
}
}
}

View file

@ -0,0 +1,43 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using System;
namespace Ryujinx.Audio.Backends.Common
{
public static class BackendHelper
{
public static int GetSampleSize(SampleFormat format)
{
return format switch
{
SampleFormat.PcmInt8 => sizeof(byte),
SampleFormat.PcmInt16 => sizeof(ushort),
SampleFormat.PcmInt24 => 3,
SampleFormat.PcmInt32 => sizeof(int),
SampleFormat.PcmFloat => sizeof(float),
_ => throw new ArgumentException($"{format}"),
};
}
public static int GetSampleCount(SampleFormat format, int channelCount, int bufferSize)
{
return bufferSize / GetSampleSize(format) / channelCount;
}
}
}

View file

@ -0,0 +1,183 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Common;
using System;
namespace Ryujinx.Audio.Backends.Common
{
/// <summary>
/// A ring buffer that grow if data written to it is too big to fit.
/// </summary>
public class DynamicRingBuffer
{
private const int RingBufferAlignment = 2048;
private object _lock = new object();
private byte[] _buffer;
private int _size;
private int _headOffset;
private int _tailOffset;
public int Length => _size;
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
{
_buffer = new byte[initialCapacity];
}
public void Clear()
{
_size = 0;
_headOffset = 0;
_tailOffset = 0;
}
public void Clear(int size)
{
lock (_lock)
{
if (size > _size)
{
size = _size;
}
if (size == 0)
{
return;
}
_headOffset = (_headOffset + size) % _buffer.Length;
_size -= size;
if (_size == 0)
{
_headOffset = 0;
_tailOffset = 0;
}
}
}
private void SetCapacityLocked(int capacity)
{
byte[] buffer = new byte[capacity];
if (_size > 0)
{
if (_headOffset < _tailOffset)
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size);
}
else
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset);
Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset);
}
}
_buffer = buffer;
_headOffset = 0;
_tailOffset = _size;
}
public void Write<T>(T[] buffer, int index, int count)
{
if (count == 0)
{
return;
}
lock (_lock)
{
if ((_size + count) > _buffer.Length)
{
SetCapacityLocked(BitUtils.AlignUp(_size + count, RingBufferAlignment));
}
if (_headOffset < _tailOffset)
{
int tailLength = _buffer.Length - _tailOffset;
if (tailLength >= count)
{
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
}
else
{
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength);
Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength);
}
}
else
{
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
}
_size += count;
_tailOffset = (_tailOffset + count) % _buffer.Length;
}
}
public int Read<T>(T[] buffer, int index, int count)
{
lock (_lock)
{
if (count > _size)
{
count = _size;
}
if (count == 0)
{
return 0;
}
if (_headOffset < _tailOffset)
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
}
else
{
int tailLength = _buffer.Length - _headOffset;
if (tailLength >= count)
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
}
else
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength);
Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength);
}
}
_size -= count;
_headOffset = (_headOffset + count) % _buffer.Length;
if (_size == 0)
{
_headOffset = 0;
_tailOffset = 0;
}
return count;
}
}
}
}

View file

@ -0,0 +1,89 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Memory;
namespace Ryujinx.Audio.Backends.Common
{
public abstract class HardwareDeviceSessionOutputBase : IHardwareDeviceSession
{
public IVirtualMemoryManager MemoryManager { get; }
public SampleFormat RequestedSampleFormat { get; }
public uint RequestedSampleRate { get; }
public uint RequestedChannelCount { get; }
public HardwareDeviceSessionOutputBase(IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount)
{
MemoryManager = memoryManager;
RequestedSampleFormat = requestedSampleFormat;
RequestedSampleRate = requestedSampleRate;
RequestedChannelCount = requestedChannelCount;
}
private byte[] GetBufferSamples(AudioBuffer buffer)
{
if (buffer.DataPointer == 0)
{
return null;
}
byte[] data = new byte[buffer.DataSize];
MemoryManager.Read(buffer.DataPointer, data);
return data;
}
protected ulong GetSampleCount(AudioBuffer buffer)
{
return (ulong)BackendHelper.GetSampleCount(RequestedSampleFormat, (int)RequestedChannelCount, (int)buffer.DataSize);
}
public abstract void Dispose();
public abstract void PrepareToClose();
public abstract void QueueBuffer(AudioBuffer buffer);
public abstract void SetVolume(float volume);
public abstract float GetVolume();
public abstract void Start();
public abstract void Stop();
public abstract ulong GetPlayedSampleCount();
public abstract bool WasBufferFullyConsumed(AudioBuffer buffer);
public virtual bool RegisterBuffer(AudioBuffer buffer)
{
return RegisterBuffer(buffer, GetBufferSamples(buffer));
}
public virtual bool RegisterBuffer(AudioBuffer buffer, byte[] samples)
{
if (samples == null)
{
return false;
}
if (buffer.Data == null)
{
buffer.Data = samples;
}
return true;
}
public virtual void UnregisterBuffer(AudioBuffer buffer) { }
}
}

View file

@ -0,0 +1,146 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Backends.Dummy;
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Threading;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.CompatLayer
{
public class CompatLayerHardwareDeviceDriver : IHardwareDeviceDriver
{
private IHardwareDeviceDriver _realDriver;
public CompatLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice)
{
_realDriver = realDevice;
}
public void Dispose()
{
_realDriver.Dispose();
}
public ManualResetEvent GetUpdateRequiredEvent()
{
return _realDriver.GetUpdateRequiredEvent();
}
private uint SelectHardwareChannelCount(uint targetChannelCount)
{
if (_realDriver.SupportsChannelCount(targetChannelCount))
{
return targetChannelCount;
}
return targetChannelCount switch
{
6 => SelectHardwareChannelCount(2),
2 => SelectHardwareChannelCount(1),
1 => throw new ArgumentException("No valid channel configuration found!"),
_ => throw new ArgumentException($"Invalid targetChannelCount {targetChannelCount}")
};
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
channelCount = 2;
}
if (sampleRate == 0)
{
sampleRate = Constants.TargetSampleRate;
}
if (!_realDriver.SupportsDirection(direction))
{
if (direction == Direction.Input)
{
Logger.Warning?.Print(LogClass.Audio, "The selected audio backend doesn't support audio input, fallback to dummy...");
return new DummyHardwareDeviceSessionInput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
throw new NotImplementedException();
}
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, hardwareChannelCount);
if (hardwareChannelCount == channelCount)
{
return realSession;
}
if (direction == Direction.Input)
{
Logger.Warning?.Print(LogClass.Audio, $"The selected audio backend doesn't support the requested audio input configuration, fallback to dummy...");
// TODO: We currently don't support audio input upsampling/downsampling, implement this.
realSession.Dispose();
return new DummyHardwareDeviceSessionInput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
// It must be a HardwareDeviceSessionOutputBase.
if (realSession is not HardwareDeviceSessionOutputBase realSessionOutputBase)
{
throw new InvalidOperationException($"Real driver session class type isn't based on {typeof(HardwareDeviceSessionOutputBase).Name}.");
}
// If we need to do post processing before sending to the hardware device, wrap around it.
return new CompatLayerHardwareDeviceSession(realSessionOutputBase, channelCount);
}
public bool SupportsChannelCount(uint channelCount)
{
return channelCount == 1 || channelCount == 2 || channelCount == 6;
}
public bool SupportsSampleFormat(SampleFormat sampleFormat)
{
// TODO: More formats.
return sampleFormat == SampleFormat.PcmInt16;
}
public bool SupportsSampleRate(uint sampleRate)
{
// TODO: More sample rates.
return sampleRate == Constants.TargetSampleRate;
}
public IHardwareDeviceDriver GetRealDeviceDriver()
{
return _realDriver;
}
public bool SupportsDirection(Direction direction)
{
return direction == Direction.Input || direction == Direction.Output;
}
}
}

View file

@ -0,0 +1,140 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Backends.CompatLayer
{
class CompatLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase
{
private HardwareDeviceSessionOutputBase _realSession;
private uint _userChannelCount;
public CompatLayerHardwareDeviceSession(HardwareDeviceSessionOutputBase realSession, uint userChannelCount) : base(realSession.MemoryManager, realSession.RequestedSampleFormat, realSession.RequestedSampleRate, userChannelCount)
{
_realSession = realSession;
_userChannelCount = userChannelCount;
}
public override void Dispose()
{
_realSession.Dispose();
}
public override ulong GetPlayedSampleCount()
{
return _realSession.GetPlayedSampleCount();
}
public override float GetVolume()
{
return _realSession.GetVolume();
}
public override void PrepareToClose()
{
_realSession.PrepareToClose();
}
public override void QueueBuffer(AudioBuffer buffer)
{
_realSession.QueueBuffer(buffer);
}
public override bool RegisterBuffer(AudioBuffer buffer, byte[] samples)
{
if (RequestedSampleFormat != SampleFormat.PcmInt16)
{
throw new NotImplementedException("Downmixing formats other than PCM16 is not supported.");
}
if (samples == null)
{
return false;
}
short[] downmixedBufferPCM16;
ReadOnlySpan<short> samplesPCM16 = MemoryMarshal.Cast<byte, short>(samples);
if (_userChannelCount == 6)
{
downmixedBufferPCM16 = Downmixing.DownMixSurroundToStereo(samplesPCM16);
if (_realSession.RequestedChannelCount == 1)
{
downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(downmixedBufferPCM16);
}
}
else if (_userChannelCount == 2 && _realSession.RequestedChannelCount == 1)
{
downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16);
}
else
{
throw new NotImplementedException($"Downmixing from {_userChannelCount} to {_realSession.RequestedChannelCount} not implemented.");
}
byte[] downmixedBuffer = MemoryMarshal.Cast<short, byte>(downmixedBufferPCM16).ToArray();
AudioBuffer fakeBuffer = new AudioBuffer
{
BufferTag = buffer.BufferTag,
DataPointer = buffer.DataPointer,
DataSize = (ulong)downmixedBuffer.Length
};
bool result = _realSession.RegisterBuffer(fakeBuffer, downmixedBuffer);
if (result)
{
buffer.Data = fakeBuffer.Data;
buffer.DataSize = fakeBuffer.DataSize;
}
return result;
}
public override void SetVolume(float volume)
{
_realSession.SetVolume(volume);
}
public override void Start()
{
_realSession.Start();
}
public override void Stop()
{
_realSession.Stop();
}
public override void UnregisterBuffer(AudioBuffer buffer)
{
_realSession.UnregisterBuffer(buffer);
}
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{
return _realSession.WasBufferFullyConsumed(buffer);
}
}
}

View file

@ -1,8 +1,25 @@
using System; //
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ryujinx.Audio namespace Ryujinx.Audio.Backends.CompatLayer
{ {
public static class Downmixing public static class Downmixing
{ {

View file

@ -0,0 +1,96 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Memory;
using System.Threading;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.Dummy
{
public class DummyHardwareDeviceDriver : IHardwareDeviceDriver
{
private ManualResetEvent _updateRequiredEvent;
public DummyHardwareDeviceDriver()
{
_updateRequiredEvent = new ManualResetEvent(false);
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (sampleRate == 0)
{
sampleRate = Constants.TargetSampleRate;
}
if (channelCount == 0)
{
channelCount = 2;
}
if (direction == Direction.Output)
{
return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
else
{
return new DummyHardwareDeviceSessionInput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
}
public ManualResetEvent GetUpdateRequiredEvent()
{
return _updateRequiredEvent;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// NOTE: The _updateRequiredEvent will be disposed somewhere else.
}
}
public bool SupportsSampleRate(uint sampleRate)
{
return true;
}
public bool SupportsSampleFormat(SampleFormat sampleFormat)
{
return true;
}
public bool SupportsDirection(Direction direction)
{
return direction == Direction.Output || direction == Direction.Input;
}
public bool SupportsChannelCount(uint channelCount)
{
return channelCount == 1 || channelCount == 2 || channelCount == 6;
}
}
}

View file

@ -0,0 +1,84 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Memory;
using System;
namespace Ryujinx.Audio.Backends.Dummy
{
class DummyHardwareDeviceSessionInput : IHardwareDeviceSession
{
private float _volume;
private IHardwareDeviceDriver _manager;
private IVirtualMemoryManager _memoryManager;
public DummyHardwareDeviceSessionInput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount)
{
_volume = 1.0f;
_manager = manager;
_memoryManager = memoryManager;
}
public void Dispose()
{
// Nothing to do.
}
public ulong GetPlayedSampleCount()
{
// Not implemented for input.
throw new NotSupportedException();
}
public float GetVolume()
{
return _volume;
}
public void PrepareToClose() { }
public void QueueBuffer(AudioBuffer buffer)
{
_memoryManager.Fill(buffer.DataPointer, buffer.DataSize, 0);
_manager.GetUpdateRequiredEvent().Set();
}
public bool RegisterBuffer(AudioBuffer buffer)
{
return buffer.DataPointer != 0;
}
public void SetVolume(float volume)
{
_volume = volume;
}
public void Start() { }
public void Stop() { }
public void UnregisterBuffer(AudioBuffer buffer) { }
public bool WasBufferFullyConsumed(AudioBuffer buffer)
{
return true;
}
}
}

View file

@ -0,0 +1,79 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Memory;
using System.Threading;
namespace Ryujinx.Audio.Backends.Dummy
{
internal class DummyHardwareDeviceSessionOutput : HardwareDeviceSessionOutputBase
{
private float _volume;
private IHardwareDeviceDriver _manager;
private ulong _playedSampleCount;
public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_volume = 1.0f;
_manager = manager;
}
public override void Dispose()
{
// Nothing to do.
}
public override ulong GetPlayedSampleCount()
{
return Interlocked.Read(ref _playedSampleCount);
}
public override float GetVolume()
{
return _volume;
}
public override void PrepareToClose() { }
public override void QueueBuffer(AudioBuffer buffer)
{
Interlocked.Add(ref _playedSampleCount, GetSampleCount(buffer));
_manager.GetUpdateRequiredEvent().Set();
}
public override void SetVolume(float volume)
{
_volume = volume;
}
public override void Start() { }
public override void Stop() { }
public override void UnregisterBuffer(AudioBuffer buffer) { }
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{
return true;
}
}
}

View file

@ -0,0 +1,53 @@
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Integration;
namespace Ryujinx.Audio.Common
{
/// <summary>
/// Represent an audio buffer that will be used by an <see cref="IHardwareDeviceSession"/>.
/// </summary>
public class AudioBuffer
{
/// <summary>
/// Unique tag of this buffer.
/// </summary>
/// <remarks>Unique per session</remarks>
public ulong BufferTag;
/// <summary>
/// Pointer to the user samples.
/// </summary>
public ulong DataPointer;
/// <summary>
/// Size of the user samples region.
/// </summary>
public ulong DataSize;
/// <summary>
/// The timestamp at which the buffer was played.
/// </summary>
/// <remarks>Not used but useful for debugging</remarks>
public ulong PlayedTimestamp;
/// <summary>
/// The user samples.
/// </summary>
public byte[] Data;
}
}

View file

@ -0,0 +1,532 @@
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Integration;
using Ryujinx.Common;
using System;
using System.Diagnostics;
namespace Ryujinx.Audio.Common
{
/// <summary>
/// An audio device session.
/// </summary>
class AudioDeviceSession : IDisposable
{
/// <summary>
/// The volume of the <see cref="AudioDeviceSession"/>.
/// </summary>
private float _volume;
/// <summary>
/// The state of the <see cref="AudioDeviceSession"/>.
/// </summary>
private AudioDeviceState _state;
/// <summary>
/// Array of all buffers currently used or released.
/// </summary>
private AudioBuffer[] _buffers;
/// <summary>
/// The server index inside <see cref="_buffers"/> (appended but not queued to device driver).
/// </summary>
private uint _serverBufferIndex;
/// <summary>
/// The hardware index inside <see cref="_buffers"/> (queued to device driver).
/// </summary>
private uint _hardwareBufferIndex;
/// <summary>
/// The released index inside <see cref="_buffers"/> (released by the device driver).
/// </summary>
private uint _releasedBufferIndex;
/// <summary>
/// The count of buffer appended (server side).
/// </summary>
private uint _bufferAppendedCount;
/// <summary>
/// The count of buffer registered (driver side).
/// </summary>
private uint _bufferRegisteredCount;
/// <summary>
/// The count of buffer released (released by the driver side).
/// </summary>
private uint _bufferReleasedCount;
/// <summary>
/// The released buffer event.
/// </summary>
private IWritableEvent _bufferEvent;
/// <summary>
/// The session on the device driver.
/// </summary>
private IHardwareDeviceSession _hardwareDeviceSession;
/// <summary>
/// Max number of buffers that can be registered to the device driver at a time.
/// </summary>
private uint _bufferRegisteredLimit;
/// <summary>
/// Create a new <see cref="AudioDeviceSession"/>.
/// </summary>
/// <param name="deviceSession">The device driver session associated</param>
/// <param name="bufferEvent">The release buffer event</param>
/// <param name="bufferRegisteredLimit">The max number of buffers that can be registered to the device driver at a time</param>
public AudioDeviceSession(IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent, uint bufferRegisteredLimit = 4)
{
_bufferEvent = bufferEvent;
_hardwareDeviceSession = deviceSession;
_bufferRegisteredLimit = bufferRegisteredLimit;
_buffers = new AudioBuffer[Constants.AudioDeviceBufferCountMax];
_serverBufferIndex = 0;
_hardwareBufferIndex = 0;
_releasedBufferIndex = 0;
_bufferAppendedCount = 0;
_bufferRegisteredCount = 0;
_bufferReleasedCount = 0;
_volume = 1.0f;
_state = AudioDeviceState.Stopped;
}
/// <summary>
/// Get the released buffer event.
/// </summary>
/// <returns>The released buffer event</returns>
public IWritableEvent GetBufferEvent()
{
return _bufferEvent;
}
/// <summary>
/// Get the state of the session.
/// </summary>
/// <returns>The state of the session</returns>
public AudioDeviceState GetState()
{
Debug.Assert(_state == AudioDeviceState.Started || _state == AudioDeviceState.Stopped);
return _state;
}
/// <summary>
/// Get the total buffer count (server + driver + released).
/// </summary>
/// <returns>Return the total buffer count</returns>
private uint GetTotalBufferCount()
{
uint bufferCount = _bufferAppendedCount + _bufferRegisteredCount + _bufferReleasedCount;
Debug.Assert(bufferCount <= Constants.AudioDeviceBufferCountMax);
return bufferCount;
}
/// <summary>
/// Register a new <see cref="AudioBuffer"/> on the server side.
/// </summary>
/// <param name="buffer">The <see cref="AudioBuffer"/> to register</param>
/// <returns>True if the operation succeeded</returns>
private bool RegisterBuffer(AudioBuffer buffer)
{
if (GetTotalBufferCount() == Constants.AudioDeviceBufferCountMax)
{
return false;
}
_buffers[_serverBufferIndex] = buffer;
_serverBufferIndex = (_serverBufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
_bufferAppendedCount++;
return true;
}
/// <summary>
/// Flush server buffers to hardware.
/// </summary>
private void FlushToHardware()
{
uint bufferToFlushCount = Math.Min(Math.Min(_bufferAppendedCount, 4), _bufferRegisteredLimit - _bufferRegisteredCount);
AudioBuffer[] buffersToFlush = new AudioBuffer[bufferToFlushCount];
uint hardwareBufferIndex = _hardwareBufferIndex;
for (int i = 0; i < buffersToFlush.Length; i++)
{
buffersToFlush[i] = _buffers[_hardwareBufferIndex];
_bufferAppendedCount--;
_bufferRegisteredCount++;
hardwareBufferIndex = (hardwareBufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
}
_hardwareBufferIndex = hardwareBufferIndex;
for (int i = 0; i < buffersToFlush.Length; i++)
{
_hardwareDeviceSession.QueueBuffer(buffersToFlush[i]);
}
}
/// <summary>
/// Get the current index of the <see cref="AudioBuffer"/> playing on the driver side.
/// </summary>
/// <param name="playingIndex">The output index of the <see cref="AudioBuffer"/> playing on the driver side</param>
/// <returns>True if any buffer is playing</returns>
private bool TryGetPlayingBufferIndex(out uint playingIndex)
{
if (_bufferRegisteredCount > 0)
{
playingIndex = (_hardwareBufferIndex - _bufferRegisteredCount) % Constants.AudioDeviceBufferCountMax;
return true;
}
playingIndex = 0;
return false;
}
/// <summary>
/// Try to pop the <see cref="AudioBuffer"/> playing on the driver side.
/// </summary>
/// <param name="buffer">The output <see cref="AudioBuffer"/> playing on the driver side</param>
/// <returns>True if any buffer is playing</returns>
private bool TryPopPlayingBuffer(out AudioBuffer buffer)
{
if (_bufferRegisteredCount > 0)
{
uint bufferIndex = (_hardwareBufferIndex - _bufferRegisteredCount) % Constants.AudioDeviceBufferCountMax;
buffer = _buffers[bufferIndex];
_buffers[bufferIndex] = null;
_bufferRegisteredCount--;
return true;
}
buffer = null;
return false;
}
/// <summary>
/// Try to pop a <see cref="AudioBuffer"/> released by the driver side.
/// </summary>
/// <param name="buffer">The output <see cref="AudioBuffer"/> released by the driver side</param>
/// <returns>True if any buffer has been released</returns>
public bool TryPopReleasedBuffer(out AudioBuffer buffer)
{
if (_bufferReleasedCount > 0)
{
uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax;
buffer = _buffers[bufferIndex];
_buffers[bufferIndex] = null;
_bufferReleasedCount--;
return true;
}
buffer = null;
return false;
}
/// <summary>
/// Release a <see cref="AudioBuffer"/>.
/// </summary>
/// <param name="buffer">The <see cref="AudioBuffer"/> to release</param>
private void ReleaseBuffer(AudioBuffer buffer)
{
buffer.PlayedTimestamp = (ulong)PerformanceCounter.ElapsedNanoseconds;
_bufferRegisteredCount--;
_bufferReleasedCount++;
_releasedBufferIndex = (_releasedBufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
}
/// <summary>
/// Update the released buffers.
/// </summary>
/// <param name="updateForStop">True if the session is currently stopping</param>
private void UpdateReleaseBuffers(bool updateForStop = false)
{
bool wasAnyBuffersReleased = false;
while (TryGetPlayingBufferIndex(out uint playingIndex))
{
if (!updateForStop && !_hardwareDeviceSession.WasBufferFullyConsumed(_buffers[playingIndex]))
{
break;
}
if (updateForStop)
{
_hardwareDeviceSession.UnregisterBuffer(_buffers[playingIndex]);
}
ReleaseBuffer(_buffers[playingIndex]);
wasAnyBuffersReleased = true;
}
if (wasAnyBuffersReleased)
{
_bufferEvent.Signal();
}
}
/// <summary>
/// Append a new <see cref="AudioBuffer"/>.
/// </summary>
/// <param name="buffer">The <see cref="AudioBuffer"/> to append</param>
/// <returns>True if the buffer was appended</returns>
public bool AppendBuffer(AudioBuffer buffer)
{
if (_hardwareDeviceSession.RegisterBuffer(buffer))
{
if (RegisterBuffer(buffer))
{
FlushToHardware();
return true;
}
_hardwareDeviceSession.UnregisterBuffer(buffer);
}
return false;
}
public bool AppendUacBuffer(AudioBuffer buffer, uint handle)
{
// NOTE: On hardware, there is another RegisterBuffer method taking an handle.
// This variant of the call always return false (stubbed?) as a result this logic will never succeed.
return false;
}
/// <summary>
/// Start the audio session.
/// </summary>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode Start()
{
if (_state == AudioDeviceState.Started)
{
return ResultCode.OperationFailed;
}
_hardwareDeviceSession.Start();
_state = AudioDeviceState.Started;
FlushToHardware();
_hardwareDeviceSession.SetVolume(_volume);
return ResultCode.Success;
}
/// <summary>
/// Stop the audio session.
/// </summary>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode Stop()
{
if (_state == AudioDeviceState.Started)
{
_hardwareDeviceSession.Stop();
UpdateReleaseBuffers(true);
_state = AudioDeviceState.Stopped;
}
return ResultCode.Success;
}
/// <summary>
/// Get the volume of the session.
/// </summary>
/// <returns>The volume of the session</returns>
public float GetVolume()
{
return _hardwareDeviceSession.GetVolume();
}
/// <summary>
/// Set the volume of the session.
/// </summary>
/// <param name="volume">The new volume to set</param>
public void SetVolume(float volume)
{
_volume = volume;
if (_state == AudioDeviceState.Started)
{
_hardwareDeviceSession.SetVolume(volume);
}
}
/// <summary>
/// Get the count of buffer currently in use (server + driver side).
/// </summary>
/// <returns>The count of buffer currently in use</returns>
public uint GetBufferCount()
{
return _bufferAppendedCount + _bufferRegisteredCount;
}
/// <summary>
/// Check if a buffer is present.
/// </summary>
/// <param name="bufferTag">The unique tag of the buffer</param>
/// <returns>Return true if a buffer is present</returns>
public bool ContainsBuffer(ulong bufferTag)
{
uint bufferIndex = (_releasedBufferIndex - _bufferReleasedCount) % Constants.AudioDeviceBufferCountMax;
for (int i = 0; i < GetTotalBufferCount(); i++)
{
if (_buffers[bufferIndex].BufferTag == bufferTag)
{
return true;
}
bufferIndex = (bufferIndex + 1) % Constants.AudioDeviceBufferCountMax;
}
return false;
}
/// <summary>
/// Get the count of sample played in this session.
/// </summary>
/// <returns>The count of sample played in this session</returns>
public ulong GetPlayedSampleCount()
{
if (_state == AudioDeviceState.Stopped)
{
return 0;
}
else
{
return _hardwareDeviceSession.GetPlayedSampleCount();
}
}
/// <summary>
/// Flush all buffers to the initial state.
/// </summary>
/// <returns>True if any buffer was flushed</returns>
public bool FlushBuffers()
{
if (_state == AudioDeviceState.Stopped)
{
return false;
}
uint bufferCount = GetBufferCount();
while (TryPopReleasedBuffer(out AudioBuffer buffer))
{
_hardwareDeviceSession.UnregisterBuffer(buffer);
}
while (TryPopPlayingBuffer(out AudioBuffer buffer))
{
_hardwareDeviceSession.UnregisterBuffer(buffer);
}
if (_bufferRegisteredCount == 0 || (_bufferReleasedCount + _bufferAppendedCount) > Constants.AudioDeviceBufferCountMax)
{
return false;
}
_bufferReleasedCount += _bufferAppendedCount;
_releasedBufferIndex = (_releasedBufferIndex + _bufferAppendedCount) % Constants.AudioDeviceBufferCountMax;
_bufferAppendedCount = 0;
_hardwareBufferIndex = _serverBufferIndex;
if (bufferCount > 0)
{
_bufferEvent.Signal();
}
return true;
}
/// <summary>
/// Update the session.
/// </summary>
public void Update()
{
if (_state == AudioDeviceState.Started)
{
UpdateReleaseBuffers();
FlushToHardware();
}
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Tell the hardware session that we are ending.
_hardwareDeviceSession.PrepareToClose();
// Unregister all buffers
while (TryPopReleasedBuffer(out AudioBuffer buffer))
{
_hardwareDeviceSession.UnregisterBuffer(buffer);
}
while (TryPopPlayingBuffer(out AudioBuffer buffer))
{
_hardwareDeviceSession.UnregisterBuffer(buffer);
}
// Finally dispose hardware session.
_hardwareDeviceSession.Dispose();
_bufferEvent.Signal();
}
}
}
}

View file

@ -0,0 +1,35 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
namespace Ryujinx.Audio.Common
{
/// <summary>
/// Audio device state.
/// </summary>
public enum AudioDeviceState : uint
{
/// <summary>
/// The audio device is started.
/// </summary>
Started,
/// <summary>
/// The audio device is stopped.
/// </summary>
Stopped
}
}

View file

@ -0,0 +1,46 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Common
{
/// <summary>
/// Audio user input configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct AudioInputConfiguration
{
/// <summary>
/// The target sample rate of the user.
/// </summary>
/// <remarks>Only 48000Hz is considered valid, other sample rates will be refused.</remarks>
public uint SampleRate;
/// <summary>
/// The target channel count of the user.
/// </summary>
/// <remarks>Only Stereo and Surround are considered valid, other configurations will be refused.</remarks>
/// <remarks>Not used in audin.</remarks>
public ushort ChannelCount;
/// <summary>
/// Reserved/unused.
/// </summary>
private ushort _reserved;
}
}

View file

@ -0,0 +1,54 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Common
{
/// <summary>
/// Audio system output configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct AudioOutputConfiguration
{
/// <summary>
/// The target sample rate of the system.
/// </summary>
public uint SampleRate;
/// <summary>
/// The target channel count of the system.
/// </summary>
public uint ChannelCount;
/// <summary>
/// Reserved/unused
/// </summary>
public SampleFormat SampleFormat;
/// <summary>
/// Reserved/unused.
/// </summary>
private Array3<byte> _padding;
/// <summary>
/// The initial audio system state.
/// </summary>
public AudioDeviceState AudioOutState;
}
}

View file

@ -0,0 +1,53 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Common
{
/// <summary>
/// Audio user buffer.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct AudioUserBuffer
{
/// <summary>
/// Pointer to the next buffer (ignored).
/// </summary>
public ulong NextBuffer;
/// <summary>
/// Pointer to the user samples.
/// </summary>
public ulong Data;
/// <summary>
/// Capacity of the buffer (unused).
/// </summary>
public ulong Capacity;
/// <summary>
/// Size of the user samples region.
/// </summary>
public ulong DataSize;
/// <summary>
/// Offset in the user samples region (unused).
/// </summary>
public ulong DataOffset;
}
}

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
namespace Ryujinx.Audio.Renderer.Common namespace Ryujinx.Audio.Common
{ {
/// <summary> /// <summary>
/// Sample format definition. /// Sample format definition.

View file

@ -15,13 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
namespace Ryujinx.Audio.Renderer namespace Ryujinx.Audio
{ {
/// <summary> /// <summary>
/// Define constants used by the Audio Renderer. /// Define constants used by the audio system.
/// </summary> /// </summary>
public static class RendererConstants public static class Constants
{ {
/// <summary>
/// The default device output name.
/// </summary>
public const string DefaultDeviceOutputName = "DeviceOut";
/// <summary>
/// The default device input name.
/// </summary>
public const string DefaultDeviceInputName = "BuiltInHeadset";
/// <summary> /// <summary>
/// The maximum number of channels supported. (6 channels for 5.1 surround) /// The maximum number of channels supported. (6 channels for 5.1 surround)
/// </summary> /// </summary>
@ -33,17 +43,17 @@ namespace Ryujinx.Audio.Renderer
public const int VoiceChannelCountMax = ChannelCountMax; public const int VoiceChannelCountMax = ChannelCountMax;
/// <summary> /// <summary>
/// The max count of mix buffer supported per operations (volumes, mix effect, ...) /// The maximum count of mix buffer supported per operations (volumes, mix effect, ...)
/// </summary> /// </summary>
public const int MixBufferCountMax = 24; public const int MixBufferCountMax = 24;
/// <summary> /// <summary>
/// The max count of wavebuffer per voice. /// The maximum count of wavebuffer per voice.
/// </summary> /// </summary>
public const int VoiceWaveBufferCount = 4; public const int VoiceWaveBufferCount = 4;
/// <summary> /// <summary>
/// The max count of biquad filter per voice. /// The maximum count of biquad filter per voice.
/// </summary> /// </summary>
public const int VoiceBiquadFilterCount = 2; public const int VoiceBiquadFilterCount = 2;
@ -59,7 +69,7 @@ namespace Ryujinx.Audio.Renderer
public const int VoiceHighestPriority = 0; public const int VoiceHighestPriority = 0;
/// <summary> /// <summary>
/// Max <see cref="Common.BehaviourParameter.ErrorInfo"/> that can be returned by <see cref="Parameter.BehaviourErrorInfoOutStatus"/>. /// Maximum <see cref="Common.BehaviourParameter.ErrorInfo"/> that can be returned by <see cref="Parameter.BehaviourErrorInfoOutStatus"/>.
/// </summary> /// </summary>
public const int MaxErrorInfos = 10; public const int MaxErrorInfos = 10;
@ -109,10 +119,25 @@ namespace Ryujinx.Audio.Renderer
public const int InvalidProcessingOrder = -1; public const int InvalidProcessingOrder = -1;
/// <summary> /// <summary>
/// The max number of audio renderer sessions allowed to be created system wide. /// The maximum number of audio renderer sessions allowed to be created system wide.
/// </summary> /// </summary>
public const int AudioRendererSessionCountMax = 2; public const int AudioRendererSessionCountMax = 2;
/// <summary>
/// The maximum number of audio output sessions allowed to be created system wide.
/// </summary>
public const int AudioOutSessionCountMax = 12;
/// <summary>
/// The maximum number of audio input sessions allowed to be created system wide.
/// </summary>
public const int AudioInSessionCountMax = 4;
/// <summary>
/// Maximum buffers supported by one audio device session.
/// </summary>
public const int AudioDeviceBufferCountMax = 32;
/// <summary> /// <summary>
/// The target sample rate of the audio renderer. (48kHz) /// The target sample rate of the audio renderer. (48kHz)
/// </summary> /// </summary>
@ -139,12 +164,12 @@ namespace Ryujinx.Audio.Renderer
public const int AudioProcessorMaxUpdateTimeTarget = 1000000000 / ((int)TargetSampleRate / TargetSampleCount); // 5.00 ms public const int AudioProcessorMaxUpdateTimeTarget = 1000000000 / ((int)TargetSampleRate / TargetSampleCount); // 5.00 ms
/// <summary> /// <summary>
/// The max update time of the DSP on original hardware. /// The maximum update time of the DSP on original hardware.
/// </summary> /// </summary>
public const int AudioProcessorMaxUpdateTime = 5760000; // 5.76 ms public const int AudioProcessorMaxUpdateTime = 5760000; // 5.76 ms
/// <summary> /// <summary>
/// The max update time per audio renderer session. /// The maximum update time per audio renderer session.
/// </summary> /// </summary>
public const int AudioProcessorMaxUpdateTimePerSessions = AudioProcessorMaxUpdateTime / AudioRendererSessionCountMax; public const int AudioProcessorMaxUpdateTimePerSessions = AudioProcessorMaxUpdateTime / AudioRendererSessionCountMax;

View file

@ -1,56 +0,0 @@
using System;
namespace Ryujinx.Audio
{
public interface IAalOutput : IDisposable
{
bool SupportsChannelCount(int channels);
private int SelectHardwareChannelCount(int targetChannelCount)
{
if (SupportsChannelCount(targetChannelCount))
{
return targetChannelCount;
}
return targetChannelCount switch
{
6 => SelectHardwareChannelCount(2),
2 => SelectHardwareChannelCount(1),
1 => throw new ArgumentException("No valid channel configuration found!"),
_ => throw new ArgumentException($"Invalid targetChannelCount {targetChannelCount}"),
};
}
int OpenTrack(int sampleRate, int channels, ReleaseCallback callback)
{
return OpenHardwareTrack(sampleRate, SelectHardwareChannelCount(channels), channels, callback);
}
int OpenHardwareTrack(int sampleRate, int hardwareChannels, int virtualChannels, ReleaseCallback callback);
void CloseTrack(int trackId);
bool ContainsBuffer(int trackId, long bufferTag);
long[] GetReleasedBuffers(int trackId, int maxCount);
void AppendBuffer<T>(int trackId, long bufferTag, T[] buffer) where T : struct;
void Start(int trackId);
void Stop(int trackId);
uint GetBufferCount(int trackId);
ulong GetPlayedSampleCount(int trackId);
bool FlushBuffers(int trackId);
float GetVolume(int trackId);
void SetVolume(int trackId, float volume);
PlaybackState GetState(int trackId);
}
}

View file

@ -0,0 +1,262 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Diagnostics;
namespace Ryujinx.Audio.Input
{
/// <summary>
/// The audio input manager.
/// </summary>
public class AudioInputManager : IDisposable
{
private object _lock = new object();
/// <summary>
/// Lock used for session allocation.
/// </summary>
private object _sessionLock = new object();
/// <summary>
/// The session ids allocation table.
/// </summary>
private int[] _sessionIds;
/// <summary>
/// The device driver.
/// </summary>
private IHardwareDeviceDriver _deviceDriver;
/// <summary>
/// The events linked to each session.
/// </summary>
private IWritableEvent[] _sessionsBufferEvents;
/// <summary>
/// The <see cref="AudioInputSystem"/> session instances.
/// </summary>
private AudioInputSystem[] _sessions;
/// <summary>
/// The count of active sessions.
/// </summary>
private int _activeSessionCount;
/// <summary>
/// Create a new <see cref="AudioInputManager"/>.
/// </summary>
public AudioInputManager()
{
_sessionIds = new int[Constants.AudioInSessionCountMax];
_sessions = new AudioInputSystem[Constants.AudioInSessionCountMax];
_activeSessionCount = 0;
for (int i = 0; i < _sessionIds.Length; i++)
{
_sessionIds[i] = i;
}
}
/// <summary>
/// Initialize the <see cref="AudioInputManager"/>.
/// </summary>
/// <param name="deviceDriver">The device driver.</param>
/// <param name="sessionRegisterEvents">The events associated to each session.</param>
public void Initialize(IHardwareDeviceDriver deviceDriver, IWritableEvent[] sessionRegisterEvents)
{
_deviceDriver = deviceDriver;
_sessionsBufferEvents = sessionRegisterEvents;
}
/// <summary>
/// Acquire a new session id.
/// </summary>
/// <returns>A new session id.</returns>
private int AcquireSessionId()
{
lock (_sessionLock)
{
int index = _activeSessionCount;
Debug.Assert(index < _sessionIds.Length);
int sessionId = _sessionIds[index];
_sessionIds[index] = -1;
_activeSessionCount++;
Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new input ({sessionId})");
return sessionId;
}
}
/// <summary>
/// Release a given <paramref name="sessionId"/>.
/// </summary>
/// <param name="sessionId">The session id to release.</param>
private void ReleaseSessionId(int sessionId)
{
lock (_sessionLock)
{
Debug.Assert(_activeSessionCount > 0);
int newIndex = --_activeSessionCount;
_sessionIds[newIndex] = sessionId;
}
Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered input ({sessionId})");
}
/// <summary>
/// Used to update audio input system.
/// </summary>
public void Update()
{
lock (_sessionLock)
{
foreach (AudioInputSystem input in _sessions)
{
input?.Update();
}
}
}
/// <summary>
/// Register a new <see cref="AudioInputSystem"/>.
/// </summary>
/// <param name="input">The <see cref="AudioInputSystem"/> to register.</param>
private void Register(AudioInputSystem input)
{
lock (_sessionLock)
{
_sessions[input.GetSessionId()] = input;
}
}
/// <summary>
/// Unregister a new <see cref="AudioInputSystem"/>.
/// </summary>
/// <param name="input">The <see cref="AudioInputSystem"/> to unregister.</param>
internal void Unregister(AudioInputSystem input)
{
lock (_sessionLock)
{
int sessionId = input.GetSessionId();
_sessions[input.GetSessionId()] = null;
ReleaseSessionId(sessionId);
}
}
/// <summary>
/// Get the list of all audio inputs names.
/// </summary>
/// <param name="filtered">If true, filter disconnected devices</param>
/// <returns>The list of all audio inputs name</returns>
public string[] ListAudioIns(bool filtered)
{
if (filtered)
{
// TODO: Detect if the driver supports audio input
}
return new string[] { Constants.DefaultDeviceInputName };
}
/// <summary>
/// Open a new <see cref="AudioInputSystem"/>.
/// </summary>
/// <param name="outputDeviceName">The output device name selected by the <see cref="AudioInputSystem"/></param>
/// <param name="outputConfiguration">The output audio configuration selected by the <see cref="AudioInputSystem"/></param>
/// <param name="obj">The new <see cref="AudioInputSystem"/></param>
/// <param name="memoryManager">The memory manager that will be used for all guest memory operations</param>
/// <param name="inputDeviceName">The input device name wanted by the user</param>
/// <param name="sampleFormat">The sample format to use</param>
/// <param name="parameter">The user configuration</param>
/// <param name="appletResourceUserId">The applet resource user id of the application</param>
/// <param name="processHandle">The process handle of the application</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode OpenAudioIn(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
out AudioInputSystem obj,
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
ref AudioInputConfiguration parameter,
ulong appletResourceUserId,
uint processHandle)
{
int sessionId = AcquireSessionId();
_sessionsBufferEvents[sessionId].Clear();
IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Input, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
AudioInputSystem audioIn = new AudioInputSystem(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
ResultCode result = audioIn.Initialize(inputDeviceName, sampleFormat, ref parameter, sessionId);
if (result == ResultCode.Success)
{
outputDeviceName = audioIn.DeviceName;
outputConfiguration = new AudioOutputConfiguration
{
ChannelCount = audioIn.ChannelCount,
SampleFormat = audioIn.SampleFormat,
SampleRate = audioIn.SampleRate,
AudioOutState = audioIn.GetState(),
};
obj = audioIn;
Register(audioIn);
}
else
{
ReleaseSessionId(sessionId);
obj = null;
outputDeviceName = null;
outputConfiguration = default;
}
return result;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Nothing to do here.
}
}
}
}

View file

@ -0,0 +1,400 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using System;
namespace Ryujinx.Audio.Input
{
/// <summary>
/// Audio input system.
/// </summary>
public class AudioInputSystem : IDisposable
{
/// <summary>
/// The session id associated to the <see cref="AudioInputSystem"/>.
/// </summary>
private int _sessionId;
/// <summary>
/// The session the <see cref="AudioInputSystem"/>.
/// </summary>
private AudioDeviceSession _session;
/// <summary>
/// The target device name of the <see cref="AudioInputSystem"/>.
/// </summary>
public string DeviceName { get; private set; }
/// <summary>
/// The target sample rate of the <see cref="AudioInputSystem"/>.
/// </summary>
public uint SampleRate { get; private set; }
/// <summary>
/// The target channel count of the <see cref="AudioInputSystem"/>.
/// </summary>
public uint ChannelCount { get; private set; }
/// <summary>
/// The target sample format of the <see cref="AudioInputSystem"/>.
/// </summary>
public SampleFormat SampleFormat { get; private set; }
/// <summary>
/// The <see cref="AudioInputManager"/> owning this.
/// </summary>
private AudioInputManager _manager;
/// <summary>
/// THe lock of the parent.
/// </summary>
private object _parentLock;
/// <summary>
/// Create a new <see cref="AudioInputSystem"/>.
/// </summary>
/// <param name="manager">The manager instance</param>
/// <param name="parentLock">The lock of the manager</param>
/// <param name="deviceSession">The hardware device session</param>
/// <param name="bufferEvent">The buffer release event of the audio input</param>
public AudioInputSystem(AudioInputManager manager, object parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent)
{
_manager = manager;
_parentLock = parentLock;
_session = new AudioDeviceSession(deviceSession, bufferEvent);
}
/// <summary>
/// Get the default device name on the system.
/// </summary>
/// <returns>The default device name on the system.</returns>
private static string GetDeviceDefaultName()
{
return Constants.DefaultDeviceInputName;
}
/// <summary>
/// Check if a given configuration and device name is valid on the system.
/// </summary>
/// <param name="configuration">The configuration to check.</param>
/// <param name="deviceName">The device name to check.</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
private static ResultCode IsConfigurationValid(ref AudioInputConfiguration configuration, string deviceName)
{
if (deviceName.Length != 0 && !deviceName.Equals(GetDeviceDefaultName()))
{
return ResultCode.DeviceNotFound;
}
else if (configuration.SampleRate != 0 && configuration.SampleRate != Constants.TargetSampleRate)
{
return ResultCode.UnsupportedSampleRate;
}
else if (configuration.ChannelCount != 0 && configuration.ChannelCount != 1 && configuration.ChannelCount != 2 && configuration.ChannelCount != 6)
{
return ResultCode.UnsupportedChannelConfiguration;
}
return ResultCode.Success;
}
/// <summary>
/// Get the released buffer event.
/// </summary>
/// <returns>The released buffer event</returns>
public IWritableEvent RegisterBufferEvent()
{
lock (_parentLock)
{
return _session.GetBufferEvent();
}
}
/// <summary>
/// Update the <see cref="AudioInputSystem"/>.
/// </summary>
public void Update()
{
lock (_parentLock)
{
_session.Update();
}
}
/// <summary>
/// Get the id of this session.
/// </summary>
/// <returns>The id of this session</returns>
public int GetSessionId()
{
return _sessionId;
}
/// <summary>
/// Initialize the <see cref="AudioInputSystem"/>.
/// </summary>
/// <param name="inputDeviceName">The input device name wanted by the user</param>
/// <param name="sampleFormat">The sample format to use</param>
/// <param name="parameter">The user configuration</param>
/// <param name="sessionId">The session id associated to this <see cref="AudioInputSystem"/></param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode Initialize(string inputDeviceName, SampleFormat sampleFormat, ref AudioInputConfiguration parameter, int sessionId)
{
_sessionId = sessionId;
ResultCode result = IsConfigurationValid(ref parameter, inputDeviceName);
if (result == ResultCode.Success)
{
if (inputDeviceName.Length == 0)
{
DeviceName = GetDeviceDefaultName();
}
else
{
DeviceName = inputDeviceName;
}
if (parameter.ChannelCount == 6)
{
ChannelCount = 6;
}
else
{
ChannelCount = 2;
}
SampleFormat = sampleFormat;
SampleRate = Constants.TargetSampleRate;
}
return result;
}
/// <summary>
/// Append a new audio buffer to the audio input.
/// </summary>
/// <param name="bufferTag">The unique tag of this buffer.</param>
/// <param name="userBuffer">The buffer informations.</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer userBuffer)
{
lock (_parentLock)
{
AudioBuffer buffer = new AudioBuffer
{
BufferTag = bufferTag,
DataPointer = userBuffer.Data,
DataSize = userBuffer.DataSize
};
if (_session.AppendBuffer(buffer))
{
return ResultCode.Success;
}
return ResultCode.BufferRingFull;
}
}
/// <summary>
/// Append a new audio buffer to the audio input.
/// </summary>
/// <remarks>This is broken by design, only added for completness.</remarks>
/// <param name="bufferTag">The unique tag of this buffer.</param>
/// <param name="userBuffer">The buffer informations.</param>
/// <param name="handle">Some unknown handle.</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode AppendUacBuffer(ulong bufferTag, ref AudioUserBuffer userBuffer, uint handle)
{
lock (_parentLock)
{
AudioBuffer buffer = new AudioBuffer
{
BufferTag = bufferTag,
DataPointer = userBuffer.Data,
DataSize = userBuffer.DataSize
};
if (_session.AppendUacBuffer(buffer, handle))
{
return ResultCode.Success;
}
return ResultCode.BufferRingFull;
}
}
/// <summary>
/// Get the release buffers.
/// </summary>
/// <param name="releasedBuffers">The buffer to write the release buffers</param>
/// <param name="releasedCount">The count of released buffers</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode GetReleasedBuffers(Span<ulong> releasedBuffers, out uint releasedCount)
{
releasedCount = 0;
// Ensure that the first entry is set to zero if no entries are returned.
if (releasedBuffers.Length > 0)
{
releasedBuffers[0] = 0;
}
lock (_parentLock)
{
for (int i = 0; i < releasedBuffers.Length; i++)
{
if (!_session.TryPopReleasedBuffer(out AudioBuffer buffer))
{
break;
}
releasedBuffers[i] = buffer.BufferTag;
releasedCount++;
}
}
return ResultCode.Success;
}
/// <summary>
/// Get the current state of the <see cref="AudioInputSystem"/>.
/// </summary>
/// <returns>Return the curent sta\te of the <see cref="AudioInputSystem"/></returns>
public AudioDeviceState GetState()
{
lock (_parentLock)
{
return _session.GetState();
}
}
/// <summary>
/// Start the audio session.
/// </summary>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode Start()
{
lock (_parentLock)
{
return _session.Start();
}
}
/// <summary>
/// Stop the audio session.
/// </summary>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode Stop()
{
lock (_parentLock)
{
return _session.Stop();
}
}
/// <summary>
/// Get the volume of the session.
/// </summary>
/// <returns>The volume of the session</returns>
public float GetVolume()
{
lock (_parentLock)
{
return _session.GetVolume();
}
}
/// <summary>
/// Set the volume of the session.
/// </summary>
/// <param name="volume">The new volume to set</param>
public void SetVolume(float volume)
{
lock (_parentLock)
{
_session.SetVolume(volume);
}
}
/// <summary>
/// Get the count of buffer currently in use (server + driver side).
/// </summary>
/// <returns>The count of buffer currently in use</returns>
public uint GetBufferCount()
{
lock (_parentLock)
{
return _session.GetBufferCount();
}
}
/// <summary>
/// Check if a buffer is present.
/// </summary>
/// <param name="bufferTag">The unique tag of the buffer</param>
/// <returns>Return true if a buffer is present</returns>
public bool ContainsBuffer(ulong bufferTag)
{
lock (_parentLock)
{
return _session.ContainsBuffer(bufferTag);
}
}
/// <summary>
/// Get the count of sample played in this session.
/// </summary>
/// <returns>The count of sample played in this session</returns>
public ulong GetPlayedSampleCount()
{
lock (_parentLock)
{
return _session.GetPlayedSampleCount();
}
}
/// <summary>
/// Flush all buffers to the initial state.
/// </summary>
/// <returns>True if any buffers was flushed</returns>
public bool FlushBuffers()
{
lock (_parentLock)
{
return _session.FlushBuffers();
}
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_session.Dispose();
_manager.Unregister(this);
}
}
}
}

View file

@ -0,0 +1,82 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Integration
{
public class HardwareDeviceImpl : IHardwareDevice
{
private IHardwareDeviceSession _session;
private uint _channelCount;
private uint _sampleRate;
private uint _currentBufferTag;
private byte[] _buffer;
public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate)
{
_session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount);
_channelCount = channelCount;
_sampleRate = sampleRate;
_currentBufferTag = 0;
_buffer = new byte[Constants.TargetSampleCount * channelCount * sizeof(ushort)];
_session.Start();
}
public void AppendBuffer(ReadOnlySpan<short> data, uint channelCount)
{
data.CopyTo(MemoryMarshal.Cast<byte, short>(_buffer));
_session.QueueBuffer(new AudioBuffer
{
DataPointer = _currentBufferTag++,
Data = _buffer,
DataSize = (ulong)_buffer.Length,
});
_currentBufferTag = _currentBufferTag % 4;
}
public uint GetChannelCount()
{
return _channelCount;
}
public uint GetSampleRate()
{
return _sampleRate;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_session.Dispose();
}
}
}
}

View file

@ -18,12 +18,12 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
namespace Ryujinx.Audio.Renderer.Integration namespace Ryujinx.Audio.Integration
{ {
/// <summary> /// <summary>
/// Represent an hardware device used in <see cref="Dsp.Command.DeviceSinkCommand"/> /// Represent an hardware device used in <see cref="Renderer.Dsp.Command.DeviceSinkCommand"/>
/// </summary> /// </summary>
public interface HardwareDevice : IDisposable public interface IHardwareDevice : IDisposable
{ {
/// <summary> /// <summary>
/// Get the supported sample rate of this device. /// Get the supported sample rate of this device.
@ -52,9 +52,9 @@ namespace Ryujinx.Audio.Renderer.Integration
{ {
uint channelCount = GetChannelCount(); uint channelCount = GetChannelCount();
Debug.Assert(channelCount > 0 && channelCount <= RendererConstants.ChannelCountMax); Debug.Assert(channelCount > 0 && channelCount <= Constants.ChannelCountMax);
return channelCount != RendererConstants.ChannelCountMax; return channelCount != Constants.ChannelCountMax;
} }
} }
} }

View file

@ -0,0 +1,50 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Memory;
using System;
using System.Threading;
namespace Ryujinx.Audio.Integration
{
/// <summary>
/// Represent an hardware device driver used in <see cref="Output.AudioOutputSystem"/>.
/// </summary>
public interface IHardwareDeviceDriver : IDisposable
{
public enum Direction
{
Input,
Output
}
IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount);
ManualResetEvent GetUpdateRequiredEvent();
bool SupportsDirection(Direction direction);
bool SupportsSampleRate(uint sampleRate);
bool SupportsSampleFormat(SampleFormat sampleFormat);
bool SupportsChannelCount(uint channelCount);
IHardwareDeviceDriver GetRealDeviceDriver()
{
return this;
}
}
}

View file

@ -1,4 +1,4 @@
// //
// Copyright (c) 2019-2021 Ryujinx // Copyright (c) 2019-2021 Ryujinx
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
@ -15,20 +15,31 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
namespace Ryujinx.Audio.Renderer using Ryujinx.Audio.Common;
{ using System;
public enum ResultCode
{
ModuleId = 153,
ErrorCodeShift = 9,
Success = 0, namespace Ryujinx.Audio.Integration
{
public interface IHardwareDeviceSession : IDisposable
{
bool RegisterBuffer(AudioBuffer buffer);
OperationFailed = (2 << ErrorCodeShift) | ModuleId, void UnregisterBuffer(AudioBuffer buffer);
WorkBufferTooSmall = (4 << ErrorCodeShift) | ModuleId,
InvalidUpdateInfo = (41 << ErrorCodeShift) | ModuleId, void QueueBuffer(AudioBuffer buffer);
InvalidAddressInfo = (42 << ErrorCodeShift) | ModuleId,
InvalidMixSorting = (43 << ErrorCodeShift) | ModuleId, bool WasBufferFullyConsumed(AudioBuffer buffer);
UnsupportedOperation = (513 << ErrorCodeShift) | ModuleId,
void SetVolume(float volume);
float GetVolume();
ulong GetPlayedSampleCount();
void Start();
void Stop();
void PrepareToClose();
} }
} }

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
namespace Ryujinx.Audio.Renderer.Integration namespace Ryujinx.Audio.Integration
{ {
/// <summary> /// <summary>
/// Represent a writable event with manual clear. /// Represent a writable event with manual clear.

View file

@ -0,0 +1,256 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Diagnostics;
namespace Ryujinx.Audio.Output
{
/// <summary>
/// The audio output manager.
/// </summary>
public class AudioOutputManager : IDisposable
{
private object _lock = new object();
/// <summary>
/// Lock used for session allocation.
/// </summary>
private object _sessionLock = new object();
/// <summary>
/// The session ids allocation table.
/// </summary>
private int[] _sessionIds;
/// <summary>
/// The device driver.
/// </summary>
private IHardwareDeviceDriver _deviceDriver;
/// <summary>
/// The events linked to each session.
/// </summary>
private IWritableEvent[] _sessionsBufferEvents;
/// <summary>
/// The <see cref="AudioOutputSystem"/> session instances.
/// </summary>
private AudioOutputSystem[] _sessions;
/// <summary>
/// The count of active sessions.
/// </summary>
private int _activeSessionCount;
/// <summary>
/// Create a new <see cref="AudioOutputManager"/>.
/// </summary>
public AudioOutputManager()
{
_sessionIds = new int[Constants.AudioOutSessionCountMax];
_sessions = new AudioOutputSystem[Constants.AudioOutSessionCountMax];
_activeSessionCount = 0;
for (int i = 0; i < _sessionIds.Length; i++)
{
_sessionIds[i] = i;
}
}
/// <summary>
/// Initialize the <see cref="AudioOutputManager"/>.
/// </summary>
/// <param name="deviceDriver">The device driver.</param>
/// <param name="sessionRegisterEvents">The events associated to each session.</param>
public void Initialize(IHardwareDeviceDriver deviceDriver, IWritableEvent[] sessionRegisterEvents)
{
_deviceDriver = deviceDriver;
_sessionsBufferEvents = sessionRegisterEvents;
}
/// <summary>
/// Acquire a new session id.
/// </summary>
/// <returns>A new session id.</returns>
private int AcquireSessionId()
{
lock (_sessionLock)
{
int index = _activeSessionCount;
Debug.Assert(index < _sessionIds.Length);
int sessionId = _sessionIds[index];
_sessionIds[index] = -1;
_activeSessionCount++;
Logger.Info?.Print(LogClass.AudioRenderer, $"Registered new output ({sessionId})");
return sessionId;
}
}
/// <summary>
/// Release a given <paramref name="sessionId"/>.
/// </summary>
/// <param name="sessionId">The session id to release.</param>
private void ReleaseSessionId(int sessionId)
{
lock (_sessionLock)
{
Debug.Assert(_activeSessionCount > 0);
int newIndex = --_activeSessionCount;
_sessionIds[newIndex] = sessionId;
}
Logger.Info?.Print(LogClass.AudioRenderer, $"Unregistered output ({sessionId})");
}
/// <summary>
/// Used to update audio output system.
/// </summary>
public void Update()
{
lock (_sessionLock)
{
foreach (AudioOutputSystem output in _sessions)
{
output?.Update();
}
}
}
/// <summary>
/// Register a new <see cref="AudioOutputSystem"/>.
/// </summary>
/// <param name="output">The <see cref="AudioOutputSystem"/> to register.</param>
private void Register(AudioOutputSystem output)
{
lock (_sessionLock)
{
_sessions[output.GetSessionId()] = output;
}
}
/// <summary>
/// Unregister a new <see cref="AudioOutputSystem"/>.
/// </summary>
/// <param name="output">The <see cref="AudioOutputSystem"/> to unregister.</param>
internal void Unregister(AudioOutputSystem output)
{
lock (_sessionLock)
{
int sessionId = output.GetSessionId();
_sessions[output.GetSessionId()] = null;
ReleaseSessionId(sessionId);
}
}
/// <summary>
/// Get the list of all audio outputs name.
/// </summary>
/// <returns>The list of all audio outputs name</returns>
public string[] ListAudioOuts()
{
return new string[] { Constants.DefaultDeviceOutputName };
}
/// <summary>
/// Open a new <see cref="AudioOutputSystem"/>.
/// </summary>
/// <param name="outputDeviceName">The output device name selected by the <see cref="AudioOutputSystem"/></param>
/// <param name="outputConfiguration">The output audio configuration selected by the <see cref="AudioOutputSystem"/></param>
/// <param name="obj">The new <see cref="AudioOutputSystem"/></param>
/// <param name="memoryManager">The memory manager that will be used for all guest memory operations</param>
/// <param name="inputDeviceName">The input device name wanted by the user</param>
/// <param name="sampleFormat">The sample format to use</param>
/// <param name="parameter">The user configuration</param>
/// <param name="appletResourceUserId">The applet resource user id of the application</param>
/// <param name="processHandle">The process handle of the application</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode OpenAudioOut(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
out AudioOutputSystem obj,
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
ref AudioInputConfiguration parameter,
ulong appletResourceUserId,
uint processHandle)
{
int sessionId = AcquireSessionId();
_sessionsBufferEvents[sessionId].Clear();
IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
AudioOutputSystem audioOut = new AudioOutputSystem(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
ResultCode result = audioOut.Initialize(inputDeviceName, sampleFormat, ref parameter, sessionId);
if (result == ResultCode.Success)
{
outputDeviceName = audioOut.DeviceName;
outputConfiguration = new AudioOutputConfiguration
{
ChannelCount = audioOut.ChannelCount,
SampleFormat = audioOut.SampleFormat,
SampleRate = audioOut.SampleRate,
AudioOutState = audioOut.GetState(),
};
obj = audioOut;
Register(audioOut);
}
else
{
ReleaseSessionId(sessionId);
obj = null;
outputDeviceName = null;
outputConfiguration = default;
}
return result;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Nothing to do here.
}
}
}
}

View file

@ -0,0 +1,373 @@
//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using System;
namespace Ryujinx.Audio.Output
{
/// <summary>
/// Audio output system.
/// </summary>
public class AudioOutputSystem : IDisposable
{
/// <summary>
/// The session id associated to the <see cref="AudioOutputSystem"/>.
/// </summary>
private int _sessionId;
/// <summary>
/// The session the <see cref="AudioOutputSystem"/>.
/// </summary>
private AudioDeviceSession _session;
/// <summary>
/// The target device name of the <see cref="AudioOutputSystem"/>.
/// </summary>
public string DeviceName { get; private set; }
/// <summary>
/// The target sample rate of the <see cref="AudioOutputSystem"/>.
/// </summary>
public uint SampleRate { get; private set; }
/// <summary>
/// The target channel count of the <see cref="AudioOutputSystem"/>.
/// </summary>
public uint ChannelCount { get; private set; }
/// <summary>
/// The target sample format of the <see cref="AudioOutputSystem"/>.
/// </summary>
public SampleFormat SampleFormat { get; private set; }
/// <summary>
/// The <see cref="AudioOutputManager"/> owning this.
/// </summary>
private AudioOutputManager _manager;
/// <summary>
/// THe lock of the parent.
/// </summary>
private object _parentLock;
/// <summary>
/// Create a new <see cref="AudioOutputSystem"/>.
/// </summary>
/// <param name="manager">The manager instance</param>
/// <param name="parentLock">The lock of the manager</param>
/// <param name="deviceSession">The hardware device session</param>
/// <param name="bufferEvent">The buffer release event of the audio output</param>
public AudioOutputSystem(AudioOutputManager manager, object parentLock, IHardwareDeviceSession deviceSession, IWritableEvent bufferEvent)
{
_manager = manager;
_parentLock = parentLock;
_session = new AudioDeviceSession(deviceSession, bufferEvent);
}
/// <summary>
/// Get the default device name on the system.
/// </summary>
/// <returns>The default device name on the system.</returns>
private static string GetDeviceDefaultName()
{
return Constants.DefaultDeviceOutputName;
}
/// <summary>
/// Check if a given configuration and device name is valid on the system.
/// </summary>
/// <param name="configuration">The configuration to check.</param>
/// <param name="deviceName">The device name to check.</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
private static ResultCode IsConfigurationValid(ref AudioInputConfiguration configuration, string deviceName)
{
if (deviceName.Length != 0 && !deviceName.Equals(GetDeviceDefaultName()))
{
return ResultCode.DeviceNotFound;
}
else if (configuration.SampleRate != 0 && configuration.SampleRate != Constants.TargetSampleRate)
{
return ResultCode.UnsupportedSampleRate;
}
else if (configuration.ChannelCount != 0 && configuration.ChannelCount != 1 && configuration.ChannelCount != 2 && configuration.ChannelCount != 6)
{
return ResultCode.UnsupportedChannelConfiguration;
}
return ResultCode.Success;
}
/// <summary>
/// Get the released buffer event.
/// </summary>
/// <returns>The released buffer event</returns>
public IWritableEvent RegisterBufferEvent()
{
lock (_parentLock)
{
return _session.GetBufferEvent();
}
}
/// <summary>
/// Update the <see cref="AudioOutputSystem"/>.
/// </summary>
public void Update()
{
lock (_parentLock)
{
_session.Update();
}
}
/// <summary>
/// Get the id of this session.
/// </summary>
/// <returns>The id of this session</returns>
public int GetSessionId()
{
return _sessionId;
}
/// <summary>
/// Initialize the <see cref="AudioOutputSystem"/>.
/// </summary>
/// <param name="inputDeviceName">The input device name wanted by the user</param>
/// <param name="sampleFormat">The sample format to use</param>
/// <param name="parameter">The user configuration</param>
/// <param name="sessionId">The session id associated to this <see cref="AudioOutputSystem"/></param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode Initialize(string inputDeviceName, SampleFormat sampleFormat, ref AudioInputConfiguration parameter, int sessionId)
{
_sessionId = sessionId;
ResultCode result = IsConfigurationValid(ref parameter, inputDeviceName);
if (result == ResultCode.Success)
{
if (inputDeviceName.Length == 0)
{
DeviceName = GetDeviceDefaultName();
}
else
{
DeviceName = inputDeviceName;
}
if (parameter.ChannelCount == 6)
{
ChannelCount = 6;
}
else
{
ChannelCount = 2;
}
SampleFormat = sampleFormat;
SampleRate = Constants.TargetSampleRate;
}
return result;
}
/// <summary>
/// Append a new audio buffer to the audio output.
/// </summary>
/// <param name="bufferTag">The unique tag of this buffer.</param>
/// <param name="userBuffer">The buffer informations.</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode AppendBuffer(ulong bufferTag, ref AudioUserBuffer userBuffer)
{
lock (_parentLock)
{
AudioBuffer buffer = new AudioBuffer
{
BufferTag = bufferTag,
DataPointer = userBuffer.Data,
DataSize = userBuffer.DataSize
};
if (_session.AppendBuffer(buffer))
{
return ResultCode.Success;
}
return ResultCode.BufferRingFull;
}
}
/// <summary>
/// Get the release buffers.
/// </summary>
/// <param name="releasedBuffers">The buffer to write the release buffers</param>
/// <param name="releasedCount">The count of released buffers</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success.</returns>
public ResultCode GetReleasedBuffer(Span<ulong> releasedBuffers, out uint releasedCount)
{
releasedCount = 0;
// Ensure that the first entry is set to zero if no entries are returned.
if (releasedBuffers.Length > 0)
{
releasedBuffers[0] = 0;
}
lock (_parentLock)
{
for (int i = 0; i < releasedBuffers.Length; i++)
{
if (!_session.TryPopReleasedBuffer(out AudioBuffer buffer))
{
break;
}
releasedBuffers[i] = buffer.BufferTag;
releasedCount++;
}
}
return ResultCode.Success;
}
/// <summary>
/// Get the current state of the <see cref="AudioOutputSystem"/>.
/// </summary>
/// <returns>Return the curent sta\te of the <see cref="AudioOutputSystem"/></returns>
/// <returns></returns>
public AudioDeviceState GetState()
{
lock (_parentLock)
{
return _session.GetState();
}
}
/// <summary>
/// Start the audio session.
/// </summary>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode Start()
{
lock (_parentLock)
{
return _session.Start();
}
}
/// <summary>
/// Stop the audio session.
/// </summary>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode Stop()
{
lock (_parentLock)
{
return _session.Stop();
}
}
/// <summary>
/// Get the volume of the session.
/// </summary>
/// <returns>The volume of the session</returns>
public float GetVolume()
{
lock (_parentLock)
{
return _session.GetVolume();
}
}
/// <summary>
/// Set the volume of the session.
/// </summary>
/// <param name="volume">The new volume to set</param>
public void SetVolume(float volume)
{
lock (_parentLock)
{
_session.SetVolume(volume);
}
}
/// <summary>
/// Get the count of buffer currently in use (server + driver side).
/// </summary>
/// <returns>The count of buffer currently in use</returns>
public uint GetBufferCount()
{
lock (_parentLock)
{
return _session.GetBufferCount();
}
}
/// <summary>
/// Check if a buffer is present.
/// </summary>
/// <param name="bufferTag">The unique tag of the buffer</param>
/// <returns>Return true if a buffer is present</returns>
public bool ContainsBuffer(ulong bufferTag)
{
lock (_parentLock)
{
return _session.ContainsBuffer(bufferTag);
}
}
/// <summary>
/// Get the count of sample played in this session.
/// </summary>
/// <returns>The count of sample played in this session</returns>
public ulong GetPlayedSampleCount()
{
lock (_parentLock)
{
return _session.GetPlayedSampleCount();
}
}
/// <summary>
/// Flush all buffers to the initial state.
/// </summary>
/// <returns>True if any buffers was flushed</returns>
public bool FlushBuffers()
{
lock (_parentLock)
{
return _session.FlushBuffers();
}
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_session.Dispose();
_manager.Unregister(this);
}
}
}
}

View file

@ -1,17 +0,0 @@
namespace Ryujinx.Audio
{
/// <summary>
/// The playback state of a track
/// </summary>
public enum PlaybackState
{
/// <summary>
/// The track is currently playing
/// </summary>
Playing = 0,
/// <summary>
/// The track is currently stopped
/// </summary>
Stopped = 1
}
}

View file

@ -1,4 +0,0 @@
namespace Ryujinx.Audio
{
public delegate void ReleaseCallback();
}

View file

@ -46,7 +46,7 @@ namespace Ryujinx.Audio.Renderer.Common
/// <returns>The size required for the given <paramref name="nodeCount"/>.</returns> /// <returns>The size required for the given <paramref name="nodeCount"/>.</returns>
public static int GetWorkBufferSize(int nodeCount) public static int GetWorkBufferSize(int nodeCount)
{ {
int size = BitUtils.AlignUp(nodeCount * nodeCount, RendererConstants.BufferAlignment); int size = BitUtils.AlignUp(nodeCount * nodeCount, Constants.BufferAlignment);
return size / Unsafe.SizeOf<byte>(); return size / Unsafe.SizeOf<byte>();
} }

View file

@ -86,7 +86,7 @@ namespace Ryujinx.Audio.Renderer.Common
/// </summary> /// </summary>
public int LoopCount; public int LoopCount;
[StructLayout(LayoutKind.Sequential, Size = 1 * RendererConstants.VoiceWaveBufferCount, Pack = 1)] [StructLayout(LayoutKind.Sequential, Size = 1 * Constants.VoiceWaveBufferCount, Pack = 1)]
private struct WaveBufferValidArray { } private struct WaveBufferValidArray { }
/// <summary> /// <summary>
@ -107,7 +107,7 @@ namespace Ryujinx.Audio.Renderer.Common
LoopCount = 0; LoopCount = 0;
waveBufferConsumed++; waveBufferConsumed++;
if (waveBufferIndex >= RendererConstants.VoiceWaveBufferCount) if (waveBufferIndex >= Constants.VoiceWaveBufferCount)
{ {
waveBufferIndex = 0; waveBufferIndex = 0;
} }

View file

@ -15,8 +15,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Dsp.Command; using Ryujinx.Audio.Renderer.Dsp.Command;
using Ryujinx.Audio.Renderer.Integration;
using Ryujinx.Audio.Renderer.Utils; using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@ -48,7 +48,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
private Mailbox<MailboxMessage> _mailbox; private Mailbox<MailboxMessage> _mailbox;
private RendererSession[] _sessionCommandList; private RendererSession[] _sessionCommandList;
private Thread _workerThread; private Thread _workerThread;
private HardwareDevice[] _outputDevices;
public IHardwareDevice[] OutputDevices { get; private set; }
private long _lastTime; private long _lastTime;
private long _playbackEnds; private long _playbackEnds;
@ -59,15 +60,38 @@ namespace Ryujinx.Audio.Renderer.Dsp
_event = new ManualResetEvent(false); _event = new ManualResetEvent(false);
} }
public void SetOutputDevices(HardwareDevice[] outputDevices) private static uint GetHardwareChannelCount(IHardwareDeviceDriver deviceDriver)
{ {
_outputDevices = outputDevices; // Get the real device driver (In case the compat layer is on top of it).
deviceDriver = deviceDriver.GetRealDeviceDriver();
if (deviceDriver.SupportsChannelCount(6))
{
return 6;
}
else
{
// NOTE: We default to stereo as this will get downmixed to mono by the compat layer if it's not compatible.
return 2;
}
} }
public void Start() public void Start(IHardwareDeviceDriver deviceDriver)
{ {
OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
// TODO: Before enabling this, we need up-mixing from stereo to 5.1.
// uint channelCount = GetHardwareChannelCount(deviceDriver);
uint channelCount = 2;
for (int i = 0; i < OutputDevices.Length; i++)
{
// TODO: Don't hardcode sample rate.
OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate);
}
_mailbox = new Mailbox<MailboxMessage>(); _mailbox = new Mailbox<MailboxMessage>();
_sessionCommandList = new RendererSession[RendererConstants.AudioRendererSessionCountMax]; _sessionCommandList = new RendererSession[Constants.AudioRendererSessionCountMax];
_event.Reset(); _event.Reset();
_lastTime = PerformanceCounter.ElapsedNanoseconds; _lastTime = PerformanceCounter.ElapsedNanoseconds;
@ -89,6 +113,11 @@ namespace Ryujinx.Audio.Renderer.Dsp
{ {
throw new InvalidOperationException("Audio Processor Stop response was invalid!"); throw new InvalidOperationException("Audio Processor Stop response was invalid!");
} }
foreach (IHardwareDevice device in OutputDevices)
{
device.Dispose();
}
} }
public void Send(int sessionId, CommandList commands, int renderingLimit, ulong appletResourceId) public void Send(int sessionId, CommandList commands, int renderingLimit, ulong appletResourceId)
@ -113,7 +142,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
throw new InvalidOperationException("Audio Processor Wait response was invalid!"); throw new InvalidOperationException("Audio Processor Wait response was invalid!");
} }
long increment = RendererConstants.AudioProcessorMaxUpdateTimeTarget; long increment = Constants.AudioProcessorMaxUpdateTimeTarget;
long timeNow = PerformanceCounter.ElapsedNanoseconds; long timeNow = PerformanceCounter.ElapsedNanoseconds;
@ -188,7 +217,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
{ {
if (_sessionCommandList[i] != null) if (_sessionCommandList[i] != null)
{ {
_sessionCommandList[i].CommandList.Process(_outputDevices[i]); _sessionCommandList[i].CommandList.Process(OutputDevices[i]);
_sessionCommandList[i] = null; _sessionCommandList[i] = null;
} }
} }
@ -197,9 +226,9 @@ namespace Ryujinx.Audio.Renderer.Dsp
long elapsedTime = endTicks - startTicks; long elapsedTime = endTicks - startTicks;
if (RendererConstants.AudioProcessorMaxUpdateTime < elapsedTime) if (Constants.AudioProcessorMaxUpdateTime < elapsedTime)
{ {
Logger.Debug?.Print(LogClass.AudioRenderer, $"DSP too slow (exceeded by {elapsedTime - RendererConstants.AudioProcessorMaxUpdateTime}ns)"); Logger.Debug?.Print(LogClass.AudioRenderer, $"DSP too slow (exceeded by {elapsedTime - Constants.AudioProcessorMaxUpdateTime}ns)");
} }
_mailbox.SendResponse(MailboxMessage.RenderEnd); _mailbox.SendResponse(MailboxMessage.RenderEnd);

View file

@ -15,6 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Renderer.Common; using Ryujinx.Audio.Renderer.Common;
using System; using System;
using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter;
@ -54,7 +55,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
SampleRate = serverState.SampleRate; SampleRate = serverState.SampleRate;
Pitch = serverState.Pitch; Pitch = serverState.Pitch;
WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount]; WaveBuffers = new WaveBuffer[Constants.VoiceWaveBufferCount];
for (int i = 0; i < WaveBuffers.Length; i++) for (int i = 0; i < WaveBuffers.Length; i++)
{ {

View file

@ -44,7 +44,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
Enabled = true; Enabled = true;
NodeId = nodeId; NodeId = nodeId;
Input = new ushort[RendererConstants.ChannelCountMax]; Input = new ushort[Constants.ChannelCountMax];
InputCount = parameter.InputCount; InputCount = parameter.InputCount;
for (int i = 0; i < InputCount; i++) for (int i = 0; i < InputCount; i++)

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
using Ryujinx.Audio.Renderer.Integration; using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Server; using Ryujinx.Audio.Renderer.Server;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
@ -39,7 +39,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public IVirtualMemoryManager MemoryManager { get; } public IVirtualMemoryManager MemoryManager { get; }
public HardwareDevice OutputDevice { get; private set; } public IHardwareDevice OutputDevice { get; private set; }
public CommandList(AudioRenderSystem renderSystem) : this(renderSystem.MemoryManager, public CommandList(AudioRenderSystem renderSystem) : this(renderSystem.MemoryManager,
renderSystem.GetMixBuffer(), renderSystem.GetMixBuffer(),
@ -85,7 +85,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
return (ulong)PerformanceCounter.ElapsedNanoseconds - StartTime; return (ulong)PerformanceCounter.ElapsedNanoseconds - StartTime;
} }
public void Process(HardwareDevice outputDevice) public void Process(IHardwareDevice outputDevice)
{ {
OutputDevice = outputDevice; OutputDevice = outputDevice;

View file

@ -15,6 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Renderer.Common; using Ryujinx.Audio.Renderer.Common;
using System; using System;
using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter; using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter;
@ -67,7 +68,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
SampleRate = serverState.SampleRate; SampleRate = serverState.SampleRate;
Pitch = serverState.Pitch; Pitch = serverState.Pitch;
WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount]; WaveBuffers = new WaveBuffer[Constants.VoiceWaveBufferCount];
for (int i = 0; i < WaveBuffers.Length; i++) for (int i = 0; i < WaveBuffers.Length; i++)
{ {

View file

@ -54,8 +54,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
IsEffectEnabled = isEnabled; IsEffectEnabled = isEnabled;
InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
for (int i = 0; i < Parameter.ChannelCount; i++) for (int i = 0; i < Parameter.ChannelCount; i++)
{ {

View file

@ -43,9 +43,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
NodeId = nodeId; NodeId = nodeId;
MixBufferCount = mixBufferCount; MixBufferCount = mixBufferCount;
OutputBufferIndices = new ushort[RendererConstants.MixBufferCountMax]; OutputBufferIndices = new ushort[Constants.MixBufferCountMax];
for (int i = 0; i < RendererConstants.MixBufferCountMax; i++) for (int i = 0; i < Constants.MixBufferCountMax; i++)
{ {
OutputBufferIndices[i] = (ushort)(bufferOffset + i); OutputBufferIndices[i] = (ushort)(bufferOffset + i);
} }

View file

@ -15,7 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// //
using Ryujinx.Audio.Renderer.Integration; using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Server.Sink; using Ryujinx.Audio.Renderer.Server.Sink;
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -75,14 +75,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public void Process(CommandList context) public void Process(CommandList context)
{ {
HardwareDevice device = context.OutputDevice; IHardwareDevice device = context.OutputDevice;
if (device.GetSampleRate() == RendererConstants.TargetSampleRate) if (device.GetSampleRate() == Constants.TargetSampleRate)
{ {
int channelCount = (int)device.GetChannelCount(); int channelCount = (int)device.GetChannelCount();
uint bufferCount = Math.Min(device.GetChannelCount(), InputCount); uint bufferCount = Math.Min(device.GetChannelCount(), InputCount);
const int sampleCount = RendererConstants.TargetSampleCount; const int sampleCount = Constants.TargetSampleCount;
short[] outputBuffer = new short[bufferCount * sampleCount]; short[] outputBuffer = new short[bufferCount * sampleCount];

View file

@ -40,10 +40,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
Enabled = true; Enabled = true;
NodeId = nodeId; NodeId = nodeId;
InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax]; OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
for (int i = 0; i < RendererConstants.VoiceChannelCountMax; i++) for (int i = 0; i < Constants.VoiceChannelCountMax; i++)
{ {
InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]); InputBufferIndices[i] = (ushort)(bufferOffset + inputBufferOffset[i]);
OutputBufferIndices[i] = (ushort)(bufferOffset + outputBufferOffset[i]); OutputBufferIndices[i] = (ushort)(bufferOffset + outputBufferOffset[i]);

Some files were not shown because too many files have changed in this diff Show more