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:
parent
1c49089ff0
commit
f556c80d02
@ -104,7 +104,7 @@ If you'd like to donate, please take a look at our [Patreon](https://www.patreon
|
||||
## 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.
|
||||
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](Ryujinx/THIRDPARTY.md) for more details.
|
||||
|
||||
|
9
Ryujinx.Audio.Backends.OpenAL/OpenALAudioBuffer.cs
Normal file
9
Ryujinx.Audio.Backends.OpenAL/OpenALAudioBuffer.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Ryujinx.Audio.Backends.OpenAL
|
||||
{
|
||||
class OpenALAudioBuffer
|
||||
{
|
||||
public int BufferId;
|
||||
public ulong DriverIdentifier;
|
||||
public ulong SampleCount;
|
||||
}
|
||||
}
|
170
Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
Normal file
170
Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceDriver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
213
Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
Normal file
213
Ryujinx.Audio.Backends.OpenAL/OpenALHardwareDeviceSession.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
9
Ryujinx.Audio.Backends.SoundIo/SoundIoAudioBuffer.cs
Normal file
9
Ryujinx.Audio.Backends.SoundIo/SoundIoAudioBuffer.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Ryujinx.Audio.Backends.SoundIo
|
||||
{
|
||||
class SoundIoAudioBuffer
|
||||
{
|
||||
public ulong DriverIdentifier;
|
||||
public ulong SampleCount;
|
||||
public ulong SamplePlayed;
|
||||
}
|
||||
}
|
251
Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
Normal file
251
Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceDriver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,155 +1,118 @@
|
||||
using SoundIOSharp;
|
||||
using Ryujinx.Audio.Backends.Common;
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Memory;
|
||||
using SoundIOSharp;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Audio.SoundIo
|
||||
namespace Ryujinx.Audio.Backends.SoundIo
|
||||
{
|
||||
internal class SoundIoAudioTrack : IDisposable
|
||||
class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The audio track ring buffer
|
||||
/// </summary>
|
||||
private SoundIoRingBuffer m_Buffer;
|
||||
private object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// A list of buffers currently pending writeback to the audio backend
|
||||
/// </summary>
|
||||
private ConcurrentQueue<SoundIoBuffer> m_ReservedBuffers;
|
||||
private SoundIoHardwareDeviceDriver _driver;
|
||||
private Queue<SoundIoAudioBuffer> _queuedBuffers;
|
||||
private SoundIOOutStream _outputStream;
|
||||
private DynamicRingBuffer _ringBuffer;
|
||||
private ulong _playedSampleCount;
|
||||
private ManualResetEvent _updateRequiredEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a buffer has been released by the audio backend
|
||||
/// </summary>
|
||||
private event ReleaseCallback BufferReleased;
|
||||
|
||||
/// <summary>
|
||||
/// The track ID of this <see cref="SoundIoAudioTrack"/>
|
||||
/// </summary>
|
||||
public int TrackID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current playback state
|
||||
/// </summary>
|
||||
public PlaybackState State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SoundIO"/> audio context this track belongs to
|
||||
/// </summary>
|
||||
public SoundIO AudioContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SoundIODevice"/> this track belongs to
|
||||
/// </summary>
|
||||
public SoundIODevice AudioDevice { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The audio output stream of this track
|
||||
/// </summary>
|
||||
public SoundIOOutStream AudioStream { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Released buffers the track is no longer holding
|
||||
/// </summary>
|
||||
public ConcurrentQueue<long> ReleasedBuffers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer count of the track
|
||||
/// </summary>
|
||||
public uint BufferCount => (uint)m_ReservedBuffers.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Played sample count of the track
|
||||
/// </summary>
|
||||
public ulong PlayedSampleCount { get; private set; }
|
||||
|
||||
private int _hardwareChannels;
|
||||
private int _virtualChannels;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of a <see cref="SoundIoAudioTrack"/>
|
||||
/// </summary>
|
||||
/// <param name="trackId">The track ID</param>
|
||||
/// <param name="audioContext">The SoundIO audio context</param>
|
||||
/// <param name="audioDevice">The SoundIO audio device</param>
|
||||
public SoundIoAudioTrack(int trackId, SoundIO audioContext, SoundIODevice audioDevice)
|
||||
public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
TrackID = trackId;
|
||||
AudioContext = audioContext;
|
||||
AudioDevice = audioDevice;
|
||||
State = PlaybackState.Stopped;
|
||||
ReleasedBuffers = new ConcurrentQueue<long>();
|
||||
_driver = driver;
|
||||
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
|
||||
_queuedBuffers = new Queue<SoundIoAudioBuffer>();
|
||||
_ringBuffer = new DynamicRingBuffer();
|
||||
|
||||
m_Buffer = new SoundIoRingBuffer();
|
||||
m_ReservedBuffers = new ConcurrentQueue<SoundIoBuffer>();
|
||||
SetupOutputStream();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the audio track with the specified parameters
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">The requested sample rate of the track</param>
|
||||
/// <param name="hardwareChannels">The requested hardware channels</param>
|
||||
/// <param name="virtualChannels">The requested virtual channels</param>
|
||||
/// <param name="callback">A <see cref="ReleaseCallback" /> that represents the delegate to invoke when a buffer has been released by the audio track</param>
|
||||
/// <param name="format">The requested sample format of the track</param>
|
||||
public void Open(
|
||||
int sampleRate,
|
||||
int hardwareChannels,
|
||||
int virtualChannels,
|
||||
ReleaseCallback callback,
|
||||
SoundIOFormat format = SoundIOFormat.S16LE)
|
||||
private void SetupOutputStream()
|
||||
{
|
||||
// Close any existing audio streams
|
||||
if (AudioStream != null)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
|
||||
_outputStream.WriteCallback += Update;
|
||||
|
||||
if (!AudioDevice.SupportsSampleRate(sampleRate))
|
||||
{
|
||||
throw new InvalidOperationException($"This sound device does not support a sample rate of {sampleRate}Hz");
|
||||
}
|
||||
// TODO: Setup other callbacks (errors, ect).
|
||||
|
||||
if (!AudioDevice.SupportsFormat(format))
|
||||
{
|
||||
throw new InvalidOperationException($"This sound device does not support SoundIOFormat.{Enum.GetName(typeof(SoundIOFormat), format)}");
|
||||
}
|
||||
|
||||
if (!AudioDevice.SupportsChannelCount(hardwareChannels))
|
||||
{
|
||||
throw new InvalidOperationException($"This sound device does not support channel count {hardwareChannels}");
|
||||
}
|
||||
|
||||
_hardwareChannels = hardwareChannels;
|
||||
_virtualChannels = virtualChannels;
|
||||
|
||||
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();
|
||||
_outputStream.Open();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This callback occurs when the sound device is ready to buffer more frames
|
||||
/// </summary>
|
||||
/// <param name="minFrameCount">The minimum amount of frames expected by the audio backend</param>
|
||||
/// <param name="maxFrameCount">The maximum amount of frames that can be written to the audio backend</param>
|
||||
private unsafe void WriteCallback(int minFrameCount, int maxFrameCount)
|
||||
public override ulong GetPlayedSampleCount()
|
||||
{
|
||||
int bytesPerFrame = AudioStream.BytesPerFrame;
|
||||
uint bytesPerSample = (uint)AudioStream.BytesPerSample;
|
||||
lock (_lock)
|
||||
{
|
||||
return _playedSampleCount;
|
||||
}
|
||||
}
|
||||
|
||||
int bufferedFrames = m_Buffer.Length / bytesPerFrame;
|
||||
long bufferedSamples = m_Buffer.Length / bytesPerSample;
|
||||
public override float GetVolume()
|
||||
{
|
||||
return _outputStream.Volume;
|
||||
}
|
||||
|
||||
public override void PrepareToClose() { }
|
||||
|
||||
public override void QueueBuffer(AudioBuffer buffer)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetVolume(float volume)
|
||||
{
|
||||
_outputStream.SetVolume(volume);
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
_outputStream.Start();
|
||||
_outputStream.Pause(false);
|
||||
|
||||
_driver.FlushContextEvents();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@ -158,16 +121,18 @@ namespace Ryujinx.Audio.SoundIo
|
||||
return;
|
||||
}
|
||||
|
||||
SoundIOChannelAreas areas = AudioStream.BeginWrite(ref frameCount);
|
||||
SoundIOChannelAreas areas = _outputStream.BeginWrite(ref frameCount);
|
||||
|
||||
int channelCount = areas.ChannelCount;
|
||||
|
||||
byte[] samples = new byte[frameCount * bytesPerFrame];
|
||||
|
||||
m_Buffer.Read(samples, 0, samples.Length);
|
||||
_ringBuffer.Read(samples, 0, samples.Length);
|
||||
|
||||
// This is a huge ugly block of code, but we save
|
||||
// a significant amount of time over the generic
|
||||
// loop that handles other channel counts.
|
||||
// TODO: Is this still right in 2021?
|
||||
|
||||
// Mono
|
||||
if (channelCount == 1)
|
||||
@ -438,209 +403,58 @@ namespace Ryujinx.Audio.SoundIo
|
||||
}
|
||||
}
|
||||
|
||||
AudioStream.EndWrite();
|
||||
_outputStream.EndWrite();
|
||||
|
||||
PlayedSampleCount += (ulong)samples.Length;
|
||||
ulong sampleCount = (ulong)(samples.Length / bytesPerSample / channelCount);
|
||||
|
||||
UpdateReleasedBuffers(samples.Length);
|
||||
}
|
||||
ulong availaibleSampleCount = sampleCount;
|
||||
|
||||
/// <summary>
|
||||
/// Releases any buffers that have been fully written to the output device
|
||||
/// </summary>
|
||||
/// <param name="bytesRead">The amount of bytes written in the last device write</param>
|
||||
private void UpdateReleasedBuffers(int bytesRead)
|
||||
{
|
||||
bool bufferReleased = false;
|
||||
bool needUpdate = false;
|
||||
|
||||
while (bytesRead > 0)
|
||||
while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SoundIoAudioBuffer driverBuffer))
|
||||
{
|
||||
if (m_ReservedBuffers.TryPeek(out SoundIoBuffer buffer))
|
||||
ulong sampleStillNeeded = driverBuffer.SampleCount - driverBuffer.SamplePlayed;
|
||||
ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount);
|
||||
|
||||
driverBuffer.SamplePlayed += playedAudioBufferSampleCount;
|
||||
availaibleSampleCount -= playedAudioBufferSampleCount;
|
||||
|
||||
if (driverBuffer.SamplePlayed == driverBuffer.SampleCount)
|
||||
{
|
||||
if (buffer.Length > bytesRead)
|
||||
{
|
||||
buffer.Length -= bytesRead;
|
||||
bytesRead = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
bufferReleased = true;
|
||||
bytesRead -= buffer.Length;
|
||||
_queuedBuffers.TryDequeue(out _);
|
||||
|
||||
m_ReservedBuffers.TryDequeue(out buffer);
|
||||
ReleasedBuffers.Enqueue(buffer.Tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bufferReleased)
|
||||
{
|
||||
OnBufferReleased();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts audio playback
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (AudioStream == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AudioStream.Start();
|
||||
AudioStream.Pause(false);
|
||||
AudioContext.FlushEvents();
|
||||
State = PlaybackState.Playing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops audio playback
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (AudioStream == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AudioStream.Pause(true);
|
||||
AudioContext.FlushEvents();
|
||||
State = PlaybackState.Stopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends an audio buffer to the tracks internal ring buffer
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The audio sample type</typeparam>
|
||||
/// <param name="bufferTag">The unqiue tag of the buffer being appended</param>
|
||||
/// <param name="buffer">The buffer to append</param>
|
||||
public void AppendBuffer<T>(long bufferTag, T[] buffer) 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!");
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
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));
|
||||
_playedSampleCount += playedAudioBufferSampleCount;
|
||||
}
|
||||
else
|
||||
|
||||
// Notify the output if needed.
|
||||
if (needUpdate)
|
||||
{
|
||||
// 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));
|
||||
_updateRequiredEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the specified buffer is currently reserved by the track
|
||||
/// </summary>
|
||||
/// <param name="bufferTag">The buffer tag to check</param>
|
||||
public bool ContainsBuffer(long bufferTag)
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
return m_ReservedBuffers.Any(x => x.Tag == bufferTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush all track buffers
|
||||
/// </summary>
|
||||
public bool FlushBuffers()
|
||||
{
|
||||
m_Buffer.Clear();
|
||||
|
||||
if (m_ReservedBuffers.Count > 0)
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var buffer in m_ReservedBuffers)
|
||||
lock (_lock)
|
||||
{
|
||||
ReleasedBuffers.Enqueue(buffer.Tag);
|
||||
PrepareToClose();
|
||||
Stop();
|
||||
|
||||
_outputStream.Dispose();
|
||||
|
||||
_driver.Unregister(this);
|
||||
}
|
||||
|
||||
OnBufferReleased();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the <see cref="SoundIoAudioTrack"/>
|
||||
/// </summary>
|
||||
public void Close()
|
||||
public override void Dispose()
|
||||
{
|
||||
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();
|
||||
Dispose(true);
|
||||
}
|
||||
}
|
||||
}
|
32
Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj
Normal file
32
Ryujinx.Audio.Backends/Ryujinx.Audio.Backends.csproj
Normal 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>
|
@ -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>
|
137
Ryujinx.Audio/AudioManager.cs
Normal file
137
Ryujinx.Audio/AudioManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
Ryujinx.Audio/Backends/Common/BackendHelper.cs
Normal file
43
Ryujinx.Audio/Backends/Common/BackendHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
183
Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
Normal file
183
Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) { }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.InteropServices;
|
||||
|
||||
namespace Ryujinx.Audio
|
||||
namespace Ryujinx.Audio.Backends.CompatLayer
|
||||
{
|
||||
public static class Downmixing
|
||||
{
|
96
Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
Normal file
96
Ryujinx.Audio/Backends/Dummy/DummyHardwareDeviceDriver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
53
Ryujinx.Audio/Common/AudioBuffer.cs
Normal file
53
Ryujinx.Audio/Common/AudioBuffer.cs
Normal 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;
|
||||
}
|
||||
}
|
532
Ryujinx.Audio/Common/AudioDeviceSession.cs
Normal file
532
Ryujinx.Audio/Common/AudioDeviceSession.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
Ryujinx.Audio/Common/AudioDeviceState.cs
Normal file
35
Ryujinx.Audio/Common/AudioDeviceState.cs
Normal 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
|
||||
}
|
||||
}
|
46
Ryujinx.Audio/Common/AudioInputConfiguration.cs
Normal file
46
Ryujinx.Audio/Common/AudioInputConfiguration.cs
Normal 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;
|
||||
}
|
||||
}
|
54
Ryujinx.Audio/Common/AudioOutputConfiguration.cs
Normal file
54
Ryujinx.Audio/Common/AudioOutputConfiguration.cs
Normal 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;
|
||||
}
|
||||
}
|
53
Ryujinx.Audio/Common/AudioUserBuffer.cs
Normal file
53
Ryujinx.Audio/Common/AudioUserBuffer.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Common
|
||||
namespace Ryujinx.Audio.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Sample format definition.
|
@ -15,13 +15,23 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
namespace Ryujinx.Audio.Renderer
|
||||
namespace Ryujinx.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Define constants used by the Audio Renderer.
|
||||
/// Define constants used by the audio system.
|
||||
/// </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>
|
||||
/// The maximum number of channels supported. (6 channels for 5.1 surround)
|
||||
/// </summary>
|
||||
@ -33,17 +43,17 @@ namespace Ryujinx.Audio.Renderer
|
||||
public const int VoiceChannelCountMax = ChannelCountMax;
|
||||
|
||||
/// <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>
|
||||
public const int MixBufferCountMax = 24;
|
||||
|
||||
/// <summary>
|
||||
/// The max count of wavebuffer per voice.
|
||||
/// The maximum count of wavebuffer per voice.
|
||||
/// </summary>
|
||||
public const int VoiceWaveBufferCount = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The max count of biquad filter per voice.
|
||||
/// The maximum count of biquad filter per voice.
|
||||
/// </summary>
|
||||
public const int VoiceBiquadFilterCount = 2;
|
||||
|
||||
@ -59,7 +69,7 @@ namespace Ryujinx.Audio.Renderer
|
||||
public const int VoiceHighestPriority = 0;
|
||||
|
||||
/// <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>
|
||||
public const int MaxErrorInfos = 10;
|
||||
|
||||
@ -109,10 +119,25 @@ namespace Ryujinx.Audio.Renderer
|
||||
public const int InvalidProcessingOrder = -1;
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
/// The target sample rate of the audio renderer. (48kHz)
|
||||
/// </summary>
|
||||
@ -139,12 +164,12 @@ namespace Ryujinx.Audio.Renderer
|
||||
public const int AudioProcessorMaxUpdateTimeTarget = 1000000000 / ((int)TargetSampleRate / TargetSampleCount); // 5.00 ms
|
||||
|
||||
/// <summary>
|
||||
/// The max update time of the DSP on original hardware.
|
||||
/// The maximum update time of the DSP on original hardware.
|
||||
/// </summary>
|
||||
public const int AudioProcessorMaxUpdateTime = 5760000; // 5.76 ms
|
||||
|
||||
/// <summary>
|
||||
/// The max update time per audio renderer session.
|
||||
/// The maximum update time per audio renderer session.
|
||||
/// </summary>
|
||||
public const int AudioProcessorMaxUpdateTimePerSessions = AudioProcessorMaxUpdateTime / AudioRendererSessionCountMax;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
262
Ryujinx.Audio/Input/AudioInputManager.cs
Normal file
262
Ryujinx.Audio/Input/AudioInputManager.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
400
Ryujinx.Audio/Input/AudioInputSystem.cs
Normal file
400
Ryujinx.Audio/Input/AudioInputSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
82
Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
Normal file
82
Ryujinx.Audio/Integration/HardwareDeviceImpl.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,12 +18,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Integration
|
||||
namespace Ryujinx.Audio.Integration
|
||||
{
|
||||
/// <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>
|
||||
public interface HardwareDevice : IDisposable
|
||||
public interface IHardwareDevice : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the supported sample rate of this device.
|
||||
@ -52,9 +52,9 @@ namespace Ryujinx.Audio.Renderer.Integration
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
50
Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
Normal file
50
Ryujinx.Audio/Integration/IHardwareDeviceDriver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2019-2021 Ryujinx
|
||||
//
|
||||
// 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/>.
|
||||
//
|
||||
|
||||
namespace Ryujinx.Audio.Renderer
|
||||
using Ryujinx.Audio.Common;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Audio.Integration
|
||||
{
|
||||
public enum ResultCode
|
||||
public interface IHardwareDeviceSession : IDisposable
|
||||
{
|
||||
ModuleId = 153,
|
||||
ErrorCodeShift = 9,
|
||||
bool RegisterBuffer(AudioBuffer buffer);
|
||||
|
||||
Success = 0,
|
||||
void UnregisterBuffer(AudioBuffer buffer);
|
||||
|
||||
OperationFailed = (2 << ErrorCodeShift) | ModuleId,
|
||||
WorkBufferTooSmall = (4 << ErrorCodeShift) | ModuleId,
|
||||
InvalidUpdateInfo = (41 << ErrorCodeShift) | ModuleId,
|
||||
InvalidAddressInfo = (42 << ErrorCodeShift) | ModuleId,
|
||||
InvalidMixSorting = (43 << ErrorCodeShift) | ModuleId,
|
||||
UnsupportedOperation = (513 << ErrorCodeShift) | ModuleId,
|
||||
void QueueBuffer(AudioBuffer buffer);
|
||||
|
||||
bool WasBufferFullyConsumed(AudioBuffer buffer);
|
||||
|
||||
void SetVolume(float volume);
|
||||
|
||||
float GetVolume();
|
||||
|
||||
ulong GetPlayedSampleCount();
|
||||
|
||||
void Start();
|
||||
|
||||
void Stop();
|
||||
|
||||
void PrepareToClose();
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
namespace Ryujinx.Audio.Renderer.Integration
|
||||
namespace Ryujinx.Audio.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a writable event with manual clear.
|
256
Ryujinx.Audio/Output/AudioOutputManager.cs
Normal file
256
Ryujinx.Audio/Output/AudioOutputManager.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
373
Ryujinx.Audio/Output/AudioOutputSystem.cs
Normal file
373
Ryujinx.Audio/Output/AudioOutputSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
namespace Ryujinx.Audio
|
||||
{
|
||||
public delegate void ReleaseCallback();
|
||||
}
|
@ -46,7 +46,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
/// <returns>The size required for the given <paramref name="nodeCount"/>.</returns>
|
||||
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>();
|
||||
}
|
@ -86,7 +86,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
/// </summary>
|
||||
public int LoopCount;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 1 * RendererConstants.VoiceWaveBufferCount, Pack = 1)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 1 * Constants.VoiceWaveBufferCount, Pack = 1)]
|
||||
private struct WaveBufferValidArray { }
|
||||
|
||||
/// <summary>
|
||||
@ -107,7 +107,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
LoopCount = 0;
|
||||
waveBufferConsumed++;
|
||||
|
||||
if (waveBufferIndex >= RendererConstants.VoiceWaveBufferCount)
|
||||
if (waveBufferIndex >= Constants.VoiceWaveBufferCount)
|
||||
{
|
||||
waveBufferIndex = 0;
|
||||
}
|
@ -15,8 +15,8 @@
|
||||
// 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.Integration;
|
||||
using Ryujinx.Audio.Renderer.Utils;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
@ -48,7 +48,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
private Mailbox<MailboxMessage> _mailbox;
|
||||
private RendererSession[] _sessionCommandList;
|
||||
private Thread _workerThread;
|
||||
private HardwareDevice[] _outputDevices;
|
||||
|
||||
public IHardwareDevice[] OutputDevices { get; private set; }
|
||||
|
||||
private long _lastTime;
|
||||
private long _playbackEnds;
|
||||
@ -59,15 +60,38 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
_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>();
|
||||
_sessionCommandList = new RendererSession[RendererConstants.AudioRendererSessionCountMax];
|
||||
_sessionCommandList = new RendererSession[Constants.AudioRendererSessionCountMax];
|
||||
_event.Reset();
|
||||
_lastTime = PerformanceCounter.ElapsedNanoseconds;
|
||||
|
||||
@ -89,6 +113,11 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
{
|
||||
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)
|
||||
@ -113,7 +142,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
throw new InvalidOperationException("Audio Processor Wait response was invalid!");
|
||||
}
|
||||
|
||||
long increment = RendererConstants.AudioProcessorMaxUpdateTimeTarget;
|
||||
long increment = Constants.AudioProcessorMaxUpdateTimeTarget;
|
||||
|
||||
long timeNow = PerformanceCounter.ElapsedNanoseconds;
|
||||
|
||||
@ -188,7 +217,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
{
|
||||
if (_sessionCommandList[i] != null)
|
||||
{
|
||||
_sessionCommandList[i].CommandList.Process(_outputDevices[i]);
|
||||
_sessionCommandList[i].CommandList.Process(OutputDevices[i]);
|
||||
_sessionCommandList[i] = null;
|
||||
}
|
||||
}
|
||||
@ -197,9 +226,9 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||
|
||||
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);
|
@ -15,6 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Audio.Renderer.Common;
|
||||
using System;
|
||||
using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter;
|
||||
@ -54,7 +55,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
SampleRate = serverState.SampleRate;
|
||||
Pitch = serverState.Pitch;
|
||||
|
||||
WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount];
|
||||
WaveBuffers = new WaveBuffer[Constants.VoiceWaveBufferCount];
|
||||
|
||||
for (int i = 0; i < WaveBuffers.Length; i++)
|
||||
{
|
@ -44,7 +44,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
Enabled = true;
|
||||
NodeId = nodeId;
|
||||
|
||||
Input = new ushort[RendererConstants.ChannelCountMax];
|
||||
Input = new ushort[Constants.ChannelCountMax];
|
||||
InputCount = parameter.InputCount;
|
||||
|
||||
for (int i = 0; i < InputCount; i++)
|
@ -15,7 +15,7 @@
|
||||
// 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.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
@ -39,7 +39,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
public IVirtualMemoryManager MemoryManager { get; }
|
||||
|
||||
public HardwareDevice OutputDevice { get; private set; }
|
||||
public IHardwareDevice OutputDevice { get; private set; }
|
||||
|
||||
public CommandList(AudioRenderSystem renderSystem) : this(renderSystem.MemoryManager,
|
||||
renderSystem.GetMixBuffer(),
|
||||
@ -85,7 +85,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
return (ulong)PerformanceCounter.ElapsedNanoseconds - StartTime;
|
||||
}
|
||||
|
||||
public void Process(HardwareDevice outputDevice)
|
||||
public void Process(IHardwareDevice outputDevice)
|
||||
{
|
||||
OutputDevice = outputDevice;
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Audio.Renderer.Common;
|
||||
using System;
|
||||
using static Ryujinx.Audio.Renderer.Parameter.VoiceInParameter;
|
||||
@ -67,7 +68,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
SampleRate = serverState.SampleRate;
|
||||
Pitch = serverState.Pitch;
|
||||
|
||||
WaveBuffers = new WaveBuffer[RendererConstants.VoiceWaveBufferCount];
|
||||
WaveBuffers = new WaveBuffer[Constants.VoiceWaveBufferCount];
|
||||
|
||||
for (int i = 0; i < WaveBuffers.Length; i++)
|
||||
{
|
@ -54,8 +54,8 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
IsEffectEnabled = isEnabled;
|
||||
|
||||
InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax];
|
||||
OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax];
|
||||
InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
|
||||
OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
|
||||
|
||||
for (int i = 0; i < Parameter.ChannelCount; i++)
|
||||
{
|
@ -43,9 +43,9 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
NodeId = nodeId;
|
||||
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);
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
// 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 System;
|
||||
using System.Runtime.CompilerServices;
|
||||
@ -75,14 +75,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
|
||||
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();
|
||||
uint bufferCount = Math.Min(device.GetChannelCount(), InputCount);
|
||||
|
||||
const int sampleCount = RendererConstants.TargetSampleCount;
|
||||
const int sampleCount = Constants.TargetSampleCount;
|
||||
|
||||
short[] outputBuffer = new short[bufferCount * sampleCount];
|
||||
|
@ -40,10 +40,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
|
||||
Enabled = true;
|
||||
NodeId = nodeId;
|
||||
|
||||
InputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax];
|
||||
OutputBufferIndices = new ushort[RendererConstants.VoiceChannelCountMax];
|
||||
InputBufferIndices = new ushort[Constants.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]);
|
||||
OutputBufferIndices[i] = (ushort)(bufferOffset + outputBufferOffset[i]);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user