haydn: Add support for PCMFloat, PCM32 and PCM8 conversions (#4100)
* haydn: Add support for PCMFloat, PCM32 and PCM8 conversions This adds support in the compatibility layer for other sample format than PCM16. This should help extends compatibility with soundio on devices that doesn't expose PCM16. I ommited PCM24 conversion for now as it's not simplest of all. * Address TSRBerry's comment * Address comments * Fix conversion issue and clean up saturation usage * Revert saturation changes * Address gdkchan's comment
This commit is contained in:
parent
0d3b82477e
commit
c20f3fbebd
3 changed files with 151 additions and 31 deletions
|
@ -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)
|
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
|
||||||
{
|
{
|
||||||
if (channelCount == 0)
|
if (channelCount == 0)
|
||||||
|
@ -77,15 +111,26 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SampleFormat hardwareSampleFormat = SelectHardwareSampleFormat(sampleFormat);
|
||||||
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
|
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;
|
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)
|
if (direction == Direction.Input)
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Audio, $"The selected audio backend doesn't support the requested audio input configuration, fallback to dummy...");
|
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.
|
// 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)
|
public bool SupportsChannelCount(uint channelCount)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Ryujinx.Audio.Backends.Common;
|
using Ryujinx.Audio.Backends.Common;
|
||||||
using Ryujinx.Audio.Common;
|
using Ryujinx.Audio.Common;
|
||||||
|
using Ryujinx.Audio.Renderer.Dsp;
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
@ -8,11 +9,13 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||||
class CompatLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
class CompatLayerHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||||
{
|
{
|
||||||
private HardwareDeviceSessionOutputBase _realSession;
|
private HardwareDeviceSessionOutputBase _realSession;
|
||||||
|
private SampleFormat _userSampleFormat;
|
||||||
private uint _userChannelCount;
|
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;
|
_realSession = realSession;
|
||||||
|
_userSampleFormat = userSampleFormat;
|
||||||
_userChannelCount = userChannelCount;
|
_userChannelCount = userChannelCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,53 +41,86 @@ namespace Ryujinx.Audio.Backends.CompatLayer
|
||||||
|
|
||||||
public override void QueueBuffer(AudioBuffer buffer)
|
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<short> samples = MemoryMarshal.Cast<byte, short>(buffer.Data);
|
||||||
|
byte[] convertedSamples = new byte[BackendHelper.GetSampleSize(realSampleFormat) * userSampleCount];
|
||||||
|
|
||||||
|
switch (realSampleFormat)
|
||||||
|
{
|
||||||
|
case SampleFormat.PcmInt8:
|
||||||
|
PcmHelper.Convert(MemoryMarshal.Cast<byte, sbyte>(convertedSamples), samples);
|
||||||
|
break;
|
||||||
|
case SampleFormat.PcmInt32:
|
||||||
|
PcmHelper.Convert(MemoryMarshal.Cast<byte, int>(convertedSamples), samples);
|
||||||
|
break;
|
||||||
|
case SampleFormat.PcmFloat:
|
||||||
|
PcmHelper.ConvertSampleToPcmFloat(MemoryMarshal.Cast<byte, float>(convertedSamples), samples);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"Sample format conversion from {_userSampleFormat} to {realSampleFormat} not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.Data = convertedSamples;
|
||||||
|
}
|
||||||
|
|
||||||
_realSession.QueueBuffer(buffer);
|
_realSession.QueueBuffer(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool RegisterBuffer(AudioBuffer buffer, byte[] samples)
|
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)
|
if (samples == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
short[] downmixedBufferPCM16;
|
if (_userChannelCount != _realSession.RequestedChannelCount)
|
||||||
|
{
|
||||||
|
if (_userSampleFormat != SampleFormat.PcmInt16)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Downmixing formats other than PCM16 is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
ReadOnlySpan<short> samplesPCM16 = MemoryMarshal.Cast<byte, short>(samples);
|
ReadOnlySpan<short> samplesPCM16 = MemoryMarshal.Cast<byte, short>(samples);
|
||||||
|
|
||||||
if (_userChannelCount == 6)
|
if (_userChannelCount == 6)
|
||||||
{
|
{
|
||||||
downmixedBufferPCM16 = Downmixing.DownMixSurroundToStereo(samplesPCM16);
|
samplesPCM16 = Downmixing.DownMixSurroundToStereo(samplesPCM16);
|
||||||
|
|
||||||
if (_realSession.RequestedChannelCount == 1)
|
if (_realSession.RequestedChannelCount == 1)
|
||||||
{
|
{
|
||||||
downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(downmixedBufferPCM16);
|
samplesPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_userChannelCount == 2 && _realSession.RequestedChannelCount == 1)
|
else if (_userChannelCount == 2 && _realSession.RequestedChannelCount == 1)
|
||||||
{
|
{
|
||||||
downmixedBufferPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16);
|
samplesPCM16 = Downmixing.DownMixStereoToMono(samplesPCM16);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotImplementedException($"Downmixing from {_userChannelCount} to {_realSession.RequestedChannelCount} not implemented.");
|
throw new NotImplementedException($"Downmixing from {_userChannelCount} to {_realSession.RequestedChannelCount} not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] downmixedBuffer = MemoryMarshal.Cast<short, byte>(downmixedBufferPCM16).ToArray();
|
samples = MemoryMarshal.Cast<short, byte>(samplesPCM16).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
AudioBuffer fakeBuffer = new AudioBuffer
|
AudioBuffer fakeBuffer = new AudioBuffer
|
||||||
{
|
{
|
||||||
BufferTag = buffer.BufferTag,
|
BufferTag = buffer.BufferTag,
|
||||||
DataPointer = buffer.DataPointer,
|
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)
|
if (result)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Ryujinx.Audio.Renderer.Dsp
|
namespace Ryujinx.Audio.Renderer.Dsp
|
||||||
|
@ -23,6 +24,44 @@ namespace Ryujinx.Audio.Renderer.Dsp
|
||||||
return GetCountToDecode(startSampleOffset, endSampleOffset, offset, count) * Unsafe.SizeOf<T>();
|
return GetCountToDecode(startSampleOffset, endSampleOffset, offset, count) * Unsafe.SizeOf<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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, TOutput>(TInput value) where TInput: INumber<TInput>, IMinMaxValue<TInput> where TOutput : INumber<TOutput>, IMinMaxValue<TOutput>
|
||||||
|
{
|
||||||
|
TInput conversionRate = TInput.CreateSaturating(TOutput.MaxValue / TOutput.CreateSaturating(TInput.MaxValue));
|
||||||
|
|
||||||
|
return TOutput.CreateSaturating(value * conversionRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static void Convert<TInput, TOutput>(Span<TOutput> output, ReadOnlySpan<TInput> input) where TInput : INumber<TInput>, IMinMaxValue<TInput> where TOutput : INumber<TOutput>, IMinMaxValue<TOutput>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
output[i] = ConvertSample<TInput, TOutput>(input[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static void ConvertSampleToPcmFloat(Span<float> output, ReadOnlySpan<short> input)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
output[i] = ConvertSampleToPcmFloat(input[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static int Decode(Span<short> output, ReadOnlySpan<short> input, int startSampleOffset, int endSampleOffset, int channelIndex, int channelCount)
|
public static int Decode(Span<short> output, ReadOnlySpan<short> 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++)
|
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;
|
return decodedCount;
|
||||||
|
|
Reference in a new issue