diff --git a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs index b6f45a3e5..fbdfe18f2 100644 --- a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs +++ b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceDriver.cs @@ -51,6 +51,40 @@ namespace Ryujinx.Audio.Backends.CompatLayer }; } + private SampleFormat SelectHardwareSampleFormat(SampleFormat targetSampleFormat) + { + if (_realDriver.SupportsSampleFormat(targetSampleFormat)) + { + return targetSampleFormat; + } + + // Attempt conversion from PCM16. + if (targetSampleFormat == SampleFormat.PcmInt16) + { + // Prefer PCM32 if we need to convert. + if (_realDriver.SupportsSampleFormat(SampleFormat.PcmInt32)) + { + return SampleFormat.PcmInt32; + } + + // If not supported, PCM float provides the best quality with a cost lower than PCM24. + if (_realDriver.SupportsSampleFormat(SampleFormat.PcmFloat)) + { + return SampleFormat.PcmFloat; + } + + // TODO: Implement PCM24 conversion. + + // If nothing is truly supported, attempt PCM8 at the cost of loosing quality. + if (_realDriver.SupportsSampleFormat(SampleFormat.PcmInt8)) + { + return SampleFormat.PcmInt8; + } + } + + throw new ArgumentException("No valid sample format configuration found!"); + } + public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume) { if (channelCount == 0) @@ -77,15 +111,26 @@ namespace Ryujinx.Audio.Backends.CompatLayer throw new NotImplementedException(); } + SampleFormat hardwareSampleFormat = SelectHardwareSampleFormat(sampleFormat); uint hardwareChannelCount = SelectHardwareChannelCount(channelCount); - IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, sampleFormat, sampleRate, hardwareChannelCount, volume); + IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount, volume); - if (hardwareChannelCount == channelCount) + if (hardwareChannelCount == channelCount && hardwareSampleFormat == sampleFormat) { return realSession; } + if (hardwareSampleFormat != sampleFormat) + { + Logger.Warning?.Print(LogClass.Audio, $"{sampleFormat} isn't supported by the audio device, conversion to {hardwareSampleFormat} will happen."); + + if (hardwareSampleFormat < sampleFormat) + { + Logger.Warning?.Print(LogClass.Audio, $"{hardwareSampleFormat} has lower quality than {sampleFormat}, expect some loss in audio fidelity."); + } + } + if (direction == Direction.Input) { Logger.Warning?.Print(LogClass.Audio, $"The selected audio backend doesn't support the requested audio input configuration, fallback to dummy..."); @@ -103,7 +148,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer } // If we need to do post processing before sending to the hardware device, wrap around it. - return new CompatLayerHardwareDeviceSession(realSessionOutputBase, channelCount); + return new CompatLayerHardwareDeviceSession(realSessionOutputBase, sampleFormat, channelCount); } public bool SupportsChannelCount(uint channelCount) diff --git a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs index ff3b0988c..ca6090fe9 100644 --- a/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs +++ b/Ryujinx.Audio/Backends/CompatLayer/CompatLayerHardwareDeviceSession.cs @@ -1,5 +1,6 @@ using Ryujinx.Audio.Backends.Common; using Ryujinx.Audio.Common; +using Ryujinx.Audio.Renderer.Dsp; using System; using System.Runtime.InteropServices; @@ -8,11 +9,13 @@ namespace Ryujinx.Audio.Backends.CompatLayer class CompatLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase { private HardwareDeviceSessionOutputBase _realSession; + private SampleFormat _userSampleFormat; private uint _userChannelCount; - public CompatLayerHardwareDeviceSession(HardwareDeviceSessionOutputBase realSession, uint userChannelCount) : base(realSession.MemoryManager, realSession.RequestedSampleFormat, realSession.RequestedSampleRate, userChannelCount) + public CompatLayerHardwareDeviceSession(HardwareDeviceSessionOutputBase realSession, SampleFormat userSampleFormat, uint userChannelCount) : base(realSession.MemoryManager, realSession.RequestedSampleFormat, realSession.RequestedSampleRate, userChannelCount) { _realSession = realSession; + _userSampleFormat = userSampleFormat; _userChannelCount = userChannelCount; } @@ -38,53 +41,86 @@ namespace Ryujinx.Audio.Backends.CompatLayer public override void QueueBuffer(AudioBuffer buffer) { + SampleFormat realSampleFormat = _realSession.RequestedSampleFormat; + + if (_userSampleFormat != realSampleFormat) + { + if (_userSampleFormat != SampleFormat.PcmInt16) + { + throw new NotImplementedException("Converting formats other than PCM16 is not supported."); + } + + int userSampleCount = buffer.Data.Length / BackendHelper.GetSampleSize(_userSampleFormat); + + ReadOnlySpan samples = MemoryMarshal.Cast(buffer.Data); + byte[] convertedSamples = new byte[BackendHelper.GetSampleSize(realSampleFormat) * userSampleCount]; + + switch (realSampleFormat) + { + case SampleFormat.PcmInt8: + PcmHelper.Convert(MemoryMarshal.Cast(convertedSamples), samples); + break; + case SampleFormat.PcmInt32: + PcmHelper.Convert(MemoryMarshal.Cast(convertedSamples), samples); + break; + case SampleFormat.PcmFloat: + PcmHelper.ConvertSampleToPcmFloat(MemoryMarshal.Cast(convertedSamples), samples); + break; + default: + throw new NotImplementedException($"Sample format conversion from {_userSampleFormat} to {realSampleFormat} not implemented."); + } + + buffer.Data = convertedSamples; + } + _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 samplesPCM16 = MemoryMarshal.Cast(samples); - - if (_userChannelCount == 6) + if (_userChannelCount != _realSession.RequestedChannelCount) { - downmixedBufferPCM16 = Downmixing.DownMixSurroundToStereo(samplesPCM16); - - if (_realSession.RequestedChannelCount == 1) + if (_userSampleFormat != SampleFormat.PcmInt16) { - downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(downmixedBufferPCM16); + throw new NotImplementedException("Downmixing formats other than PCM16 is not supported."); } - } - 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(downmixedBufferPCM16).ToArray(); + ReadOnlySpan samplesPCM16 = MemoryMarshal.Cast(samples); + + if (_userChannelCount == 6) + { + samplesPCM16 = Downmixing.DownMixSurroundToStereo(samplesPCM16); + + if (_realSession.RequestedChannelCount == 1) + { + samplesPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16); + } + } + else if (_userChannelCount == 2 && _realSession.RequestedChannelCount == 1) + { + samplesPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16); + } + else + { + throw new NotImplementedException($"Downmixing from {_userChannelCount} to {_realSession.RequestedChannelCount} not implemented."); + } + + samples = MemoryMarshal.Cast(samplesPCM16).ToArray(); + } AudioBuffer fakeBuffer = new AudioBuffer { BufferTag = buffer.BufferTag, DataPointer = buffer.DataPointer, - DataSize = (ulong)downmixedBuffer.Length + DataSize = (ulong)samples.Length }; - bool result = _realSession.RegisterBuffer(fakeBuffer, downmixedBuffer); + bool result = _realSession.RegisterBuffer(fakeBuffer, samples); if (result) { diff --git a/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs b/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs index 1eec3e415..1459e3a0f 100644 --- a/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs +++ b/Ryujinx.Audio/Renderer/Dsp/PcmHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using System.Runtime.CompilerServices; namespace Ryujinx.Audio.Renderer.Dsp @@ -23,6 +24,44 @@ namespace Ryujinx.Audio.Renderer.Dsp return GetCountToDecode(startSampleOffset, endSampleOffset, offset, count) * Unsafe.SizeOf(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ConvertSampleToPcmFloat(short sample) + { + return (float)sample / short.MaxValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short ConvertSampleToPcmInt16(float sample) + { + return Saturate(sample * short.MaxValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TOutput ConvertSample(TInput value) where TInput: INumber, IMinMaxValue where TOutput : INumber, IMinMaxValue + { + TInput conversionRate = TInput.CreateSaturating(TOutput.MaxValue / TOutput.CreateSaturating(TInput.MaxValue)); + + return TOutput.CreateSaturating(value * conversionRate); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convert(Span output, ReadOnlySpan input) where TInput : INumber, IMinMaxValue where TOutput : INumber, IMinMaxValue + { + for (int i = 0; i < input.Length; i++) + { + output[i] = ConvertSample(input[i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertSampleToPcmFloat(Span output, ReadOnlySpan input) + { + for (int i = 0; i < input.Length; i++) + { + output[i] = ConvertSampleToPcmFloat(input[i]); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Decode(Span output, ReadOnlySpan input, int startSampleOffset, int endSampleOffset, int channelIndex, int channelCount) { @@ -53,7 +92,7 @@ namespace Ryujinx.Audio.Renderer.Dsp for (int i = 0; i < decodedCount; i++) { - output[i] = (short)(input[i * channelCount + channelIndex] * short.MaxValue); + output[i] = ConvertSampleToPcmInt16(input[i * channelCount + channelIndex]); } return decodedCount;