mirror of
https://git.ryujinx.app/projects/Kenji-NX.git
synced 2026-10-04 21:48:54 +02:00
Add macOS native Audio Backend
Adds Ryujinx.Audio.Backends.Apple, which is a AudioToolbox based audio backend for macOS (and by extension iOS). Also added in MeloNX. * Expose AudioToolkit in UI Co-authored-by: GreemDev <greemdev@ryujinx.app>
This commit is contained in:
committed by
KeatonTheBot
co-authored by
GreemDev
parent
061bfb4074
commit
3cdae775b1
@@ -6,6 +6,7 @@
|
|||||||
<File Path="nuget.config" />
|
<File Path="nuget.config" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Project Path="src/ARMeilleure/ARMeilleure.csproj" />
|
<Project Path="src/ARMeilleure/ARMeilleure.csproj" />
|
||||||
|
<Project Path="src/Ryujinx.Audio.Backends.Apple/Ryujinx.Audio.Backends.Apple.csproj" />
|
||||||
<Project Path="src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj" />
|
<Project Path="src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj" />
|
||||||
<Project Path="src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj" />
|
<Project Path="src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj" />
|
||||||
<Project Path="src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj" />
|
<Project Path="src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj" />
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Ryujinx.Audio.Backends.Apple
|
||||||
|
{
|
||||||
|
class AppleAudioBuffer
|
||||||
|
{
|
||||||
|
public readonly ulong DriverIdentifier;
|
||||||
|
public readonly ulong SampleCount;
|
||||||
|
public ulong SamplePlayed;
|
||||||
|
|
||||||
|
public AppleAudioBuffer(ulong driverIdentifier, ulong sampleCount)
|
||||||
|
{
|
||||||
|
DriverIdentifier = driverIdentifier;
|
||||||
|
SampleCount = sampleCount;
|
||||||
|
SamplePlayed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using Ryujinx.Audio.Common;
|
||||||
|
using Ryujinx.Audio.Integration;
|
||||||
|
using Ryujinx.Memory;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading;
|
||||||
|
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
|
||||||
|
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||||
|
|
||||||
|
namespace Ryujinx.Audio.Backends.Apple
|
||||||
|
{
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
|
public class AppleHardwareDeviceDriver : IHardwareDeviceDriver
|
||||||
|
{
|
||||||
|
private readonly ManualResetEvent _updateRequiredEvent;
|
||||||
|
private readonly ManualResetEvent _pauseEvent;
|
||||||
|
private readonly ConcurrentDictionary<AppleHardwareDeviceSession, byte> _sessions;
|
||||||
|
private readonly bool _supportSurroundConfiguration;
|
||||||
|
|
||||||
|
public float Volume { get; set; }
|
||||||
|
|
||||||
|
public AppleHardwareDeviceDriver()
|
||||||
|
{
|
||||||
|
_updateRequiredEvent = new ManualResetEvent(false);
|
||||||
|
_pauseEvent = new ManualResetEvent(true);
|
||||||
|
_sessions = new ConcurrentDictionary<AppleHardwareDeviceSession, byte>();
|
||||||
|
|
||||||
|
_supportSurroundConfiguration = TestSurroundSupport();
|
||||||
|
|
||||||
|
Volume = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TestSurroundSupport()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var format = GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6);
|
||||||
|
|
||||||
|
int result = AudioQueueNewOutput(
|
||||||
|
ref format,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero,
|
||||||
|
0,
|
||||||
|
out IntPtr testQueue);
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
AudioChannelLayout layout = new AudioChannelLayout
|
||||||
|
{
|
||||||
|
AudioChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A,
|
||||||
|
AudioChannelBitmap = 0,
|
||||||
|
NumberChannelDescriptions = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
int layoutResult = AudioQueueSetProperty(
|
||||||
|
testQueue,
|
||||||
|
kAudioQueueProperty_ChannelLayout,
|
||||||
|
ref layout,
|
||||||
|
(uint)Marshal.SizeOf<AudioChannelLayout>());
|
||||||
|
|
||||||
|
if (layoutResult == 0)
|
||||||
|
{
|
||||||
|
AudioQueueDispose(testQueue, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioQueueDispose(testQueue, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsSupported => OperatingSystem.IsMacOSVersionAtLeast(10, 5);
|
||||||
|
|
||||||
|
public ManualResetEvent GetUpdateRequiredEvent()
|
||||||
|
{
|
||||||
|
return _updateRequiredEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManualResetEvent GetPauseEvent()
|
||||||
|
{
|
||||||
|
return _pauseEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Apple backend!");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppleHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||||
|
|
||||||
|
_sessions.TryAdd(session, 0);
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Unregister(AppleHardwareDeviceSession session)
|
||||||
|
{
|
||||||
|
return _sessions.TryRemove(session, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||||
|
{
|
||||||
|
uint formatFlags;
|
||||||
|
uint bitsPerChannel;
|
||||||
|
|
||||||
|
switch (sampleFormat)
|
||||||
|
{
|
||||||
|
case SampleFormat.PcmInt8:
|
||||||
|
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||||
|
bitsPerChannel = 8;
|
||||||
|
break;
|
||||||
|
case SampleFormat.PcmInt16:
|
||||||
|
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||||
|
bitsPerChannel = 16;
|
||||||
|
break;
|
||||||
|
case SampleFormat.PcmInt32:
|
||||||
|
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||||
|
bitsPerChannel = 32;
|
||||||
|
break;
|
||||||
|
case SampleFormat.PcmFloat:
|
||||||
|
formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
|
||||||
|
bitsPerChannel = 32;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"Unsupported sample format {sampleFormat}");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint bytesPerFrame = (bitsPerChannel / 8) * channelCount;
|
||||||
|
|
||||||
|
return new AudioStreamBasicDescription
|
||||||
|
{
|
||||||
|
SampleRate = sampleRate,
|
||||||
|
FormatID = kAudioFormatLinearPCM,
|
||||||
|
FormatFlags = formatFlags,
|
||||||
|
BytesPerPacket = bytesPerFrame,
|
||||||
|
FramesPerPacket = 1,
|
||||||
|
BytesPerFrame = bytesPerFrame,
|
||||||
|
ChannelsPerFrame = channelCount,
|
||||||
|
BitsPerChannel = bitsPerChannel,
|
||||||
|
Reserved = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
Dispose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
foreach (AppleHardwareDeviceSession session in _sessions.Keys)
|
||||||
|
{
|
||||||
|
session.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pauseEvent.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsSampleRate(uint sampleRate)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsSampleFormat(SampleFormat sampleFormat)
|
||||||
|
{
|
||||||
|
return sampleFormat != SampleFormat.PcmInt24;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsChannelCount(uint channelCount)
|
||||||
|
{
|
||||||
|
if (channelCount == 6)
|
||||||
|
{
|
||||||
|
return _supportSurroundConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsDirection(Direction direction)
|
||||||
|
{
|
||||||
|
return direction != Direction.Input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
using Ryujinx.Audio.Backends.Common;
|
||||||
|
using Ryujinx.Audio.Common;
|
||||||
|
using Ryujinx.Memory;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
|
||||||
|
|
||||||
|
namespace Ryujinx.Audio.Backends.Apple
|
||||||
|
{
|
||||||
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
|
class AppleHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||||
|
{
|
||||||
|
private const int NumBuffers = 3;
|
||||||
|
|
||||||
|
private readonly AppleHardwareDeviceDriver _driver;
|
||||||
|
private readonly ConcurrentQueue<AppleAudioBuffer> _queuedBuffers = new();
|
||||||
|
private readonly DynamicRingBuffer _ringBuffer = new();
|
||||||
|
private readonly ManualResetEvent _updateRequiredEvent;
|
||||||
|
|
||||||
|
private readonly AudioQueueOutputCallback _callbackDelegate;
|
||||||
|
private readonly GCHandle _gcHandle;
|
||||||
|
|
||||||
|
private IntPtr _audioQueue;
|
||||||
|
private readonly IntPtr[] _audioQueueBuffers = new IntPtr[NumBuffers];
|
||||||
|
private readonly int[] _bufferBytesFilled = new int[NumBuffers];
|
||||||
|
|
||||||
|
private readonly int _bytesPerFrame;
|
||||||
|
|
||||||
|
private ulong _playedSampleCount;
|
||||||
|
private bool _started;
|
||||||
|
private float _volume = 1f;
|
||||||
|
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
private delegate void AudioQueueOutputCallback(
|
||||||
|
IntPtr userData,
|
||||||
|
IntPtr audioQueue,
|
||||||
|
IntPtr buffer);
|
||||||
|
|
||||||
|
public AppleHardwareDeviceSession(
|
||||||
|
AppleHardwareDeviceDriver driver,
|
||||||
|
IVirtualMemoryManager memoryManager,
|
||||||
|
SampleFormat requestedSampleFormat,
|
||||||
|
uint requestedSampleRate,
|
||||||
|
uint requestedChannelCount)
|
||||||
|
: base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||||
|
{
|
||||||
|
_driver = driver;
|
||||||
|
_updateRequiredEvent = driver.GetUpdateRequiredEvent();
|
||||||
|
_callbackDelegate = OutputCallback;
|
||||||
|
_bytesPerFrame = BackendHelper.GetSampleSize(requestedSampleFormat) * (int)requestedChannelCount;
|
||||||
|
|
||||||
|
_gcHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||||
|
|
||||||
|
SetupAudioQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupAudioQueue()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var format = AppleHardwareDeviceDriver.GetAudioFormat(
|
||||||
|
RequestedSampleFormat,
|
||||||
|
RequestedSampleRate,
|
||||||
|
RequestedChannelCount);
|
||||||
|
|
||||||
|
IntPtr callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate);
|
||||||
|
IntPtr userData = GCHandle.ToIntPtr(_gcHandle);
|
||||||
|
|
||||||
|
int result = AudioQueueNewOutput(
|
||||||
|
ref format,
|
||||||
|
callbackPtr,
|
||||||
|
userData,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero,
|
||||||
|
0,
|
||||||
|
out _audioQueue);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"AudioQueueNewOutput failed: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint framesPerBuffer = RequestedSampleRate / 100;
|
||||||
|
uint bufferSize = framesPerBuffer * (uint)_bytesPerFrame;
|
||||||
|
|
||||||
|
for (int i = 0; i < NumBuffers; i++)
|
||||||
|
{
|
||||||
|
AudioQueueAllocateBuffer(_audioQueue, bufferSize, out _audioQueueBuffers[i]);
|
||||||
|
_bufferBytesFilled[i] = 0;
|
||||||
|
|
||||||
|
PrimeBuffer(_audioQueueBuffers[i], i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void PrimeBuffer(IntPtr bufferPtr, int bufferIndex)
|
||||||
|
{
|
||||||
|
AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr;
|
||||||
|
|
||||||
|
int capacityBytes = (int)buffer->AudioDataBytesCapacity;
|
||||||
|
int framesPerBuffer = capacityBytes / _bytesPerFrame;
|
||||||
|
|
||||||
|
int availableFrames = _ringBuffer.Length / _bytesPerFrame;
|
||||||
|
int framesToRead = Math.Min(availableFrames, framesPerBuffer);
|
||||||
|
int bytesToRead = framesToRead * _bytesPerFrame;
|
||||||
|
|
||||||
|
Span<byte> dst = new((void*)buffer->AudioData, capacityBytes);
|
||||||
|
dst.Clear();
|
||||||
|
|
||||||
|
if (bytesToRead > 0)
|
||||||
|
{
|
||||||
|
Span<byte> audio = dst.Slice(0, bytesToRead);
|
||||||
|
_ringBuffer.Read(audio, 0, bytesToRead);
|
||||||
|
ApplyVolume(buffer->AudioData, bytesToRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->AudioDataByteSize = (uint)capacityBytes;
|
||||||
|
_bufferBytesFilled[bufferIndex] = bytesToRead;
|
||||||
|
|
||||||
|
AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, IntPtr.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OutputCallback(IntPtr userData, IntPtr audioQueue, IntPtr bufferPtr)
|
||||||
|
{
|
||||||
|
if (!_started || bufferPtr == IntPtr.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr);
|
||||||
|
if (bufferIndex < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int bytesPlayed = _bufferBytesFilled[bufferIndex];
|
||||||
|
if (bytesPlayed > 0)
|
||||||
|
{
|
||||||
|
ProcessPlayedSamples(bytesPlayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimeBuffer(bufferPtr, bufferIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessPlayedSamples(int bytesPlayed)
|
||||||
|
{
|
||||||
|
ulong samplesPlayed = GetSampleCount(bytesPlayed);
|
||||||
|
ulong remaining = samplesPlayed;
|
||||||
|
bool needUpdate = false;
|
||||||
|
|
||||||
|
while (remaining > 0 && _queuedBuffers.TryPeek(out AppleAudioBuffer buffer))
|
||||||
|
{
|
||||||
|
ulong needed = buffer.SampleCount - Interlocked.Read(ref buffer.SamplePlayed);
|
||||||
|
ulong take = Math.Min(needed, remaining);
|
||||||
|
|
||||||
|
ulong played = Interlocked.Add(ref buffer.SamplePlayed, take);
|
||||||
|
remaining -= take;
|
||||||
|
|
||||||
|
if (played == buffer.SampleCount)
|
||||||
|
{
|
||||||
|
_queuedBuffers.TryDequeue(out _);
|
||||||
|
needUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Add(ref _playedSampleCount, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needUpdate)
|
||||||
|
{
|
||||||
|
_updateRequiredEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void ApplyVolume(IntPtr dataPtr, int byteSize)
|
||||||
|
{
|
||||||
|
float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f);
|
||||||
|
if (volume >= 0.999f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int sampleCount = byteSize / BackendHelper.GetSampleSize(RequestedSampleFormat);
|
||||||
|
|
||||||
|
switch (RequestedSampleFormat)
|
||||||
|
{
|
||||||
|
case SampleFormat.PcmInt16:
|
||||||
|
short* s16 = (short*)dataPtr;
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
s16[i] = (short)(s16[i] * volume);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleFormat.PcmFloat:
|
||||||
|
float* f32 = (float*)dataPtr;
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
f32[i] *= volume;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleFormat.PcmInt32:
|
||||||
|
int* s32 = (int*)dataPtr;
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
s32[i] = (int)(s32[i] * volume);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SampleFormat.PcmInt8:
|
||||||
|
sbyte* s8 = (sbyte*)dataPtr;
|
||||||
|
for (int i = 0; i < sampleCount; i++)
|
||||||
|
s8[i] = (sbyte)(s8[i] * volume);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void QueueBuffer(AudioBuffer buffer)
|
||||||
|
{
|
||||||
|
_ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
|
||||||
|
_queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.DataPointer, GetSampleCount(buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Start()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_started)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_started = true;
|
||||||
|
AudioQueueStart(_audioQueue, IntPtr.Zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Stop()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!_started)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_started = false;
|
||||||
|
AudioQueuePause(_audioQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ulong GetPlayedSampleCount()
|
||||||
|
=> Interlocked.Read(ref _playedSampleCount);
|
||||||
|
|
||||||
|
public override float GetVolume() => _volume;
|
||||||
|
public override void SetVolume(float volume) => _volume = volume;
|
||||||
|
|
||||||
|
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
|
||||||
|
{
|
||||||
|
if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return driverBuffer.DriverIdentifier != buffer.DataPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PrepareToClose() { }
|
||||||
|
public override void UnregisterBuffer(AudioBuffer buffer) { }
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
if (_audioQueue != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
AudioQueueStop(_audioQueue, true);
|
||||||
|
AudioQueueDispose(_audioQueue, true);
|
||||||
|
_audioQueue = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_gcHandle.IsAllocated)
|
||||||
|
{
|
||||||
|
_gcHandle.Free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.Audio.Backends.Apple.Native
|
||||||
|
{
|
||||||
|
public static partial class AudioToolbox
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct AudioStreamBasicDescription
|
||||||
|
{
|
||||||
|
public double SampleRate;
|
||||||
|
public uint FormatID;
|
||||||
|
public uint FormatFlags;
|
||||||
|
public uint BytesPerPacket;
|
||||||
|
public uint FramesPerPacket;
|
||||||
|
public uint BytesPerFrame;
|
||||||
|
public uint ChannelsPerFrame;
|
||||||
|
public uint BitsPerChannel;
|
||||||
|
public uint Reserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct AudioChannelLayout
|
||||||
|
{
|
||||||
|
public uint AudioChannelLayoutTag;
|
||||||
|
public uint AudioChannelBitmap;
|
||||||
|
public uint NumberChannelDescriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const uint kAudioFormatLinearPCM = 0x6C70636D;
|
||||||
|
internal const uint kAudioQueueProperty_ChannelLayout = 0x6171636c;
|
||||||
|
internal const uint kAudioChannelLayoutTag_MPEG_5_1_A = 0x650006;
|
||||||
|
internal const uint kAudioFormatFlagIsFloat = (1 << 0);
|
||||||
|
internal const uint kAudioFormatFlagIsSignedInteger = (1 << 2);
|
||||||
|
internal const uint kAudioFormatFlagIsPacked = (1 << 3);
|
||||||
|
internal const uint kAudioFormatFlagIsBigEndian = (1 << 1);
|
||||||
|
internal const uint kAudioFormatFlagIsAlignedHigh = (1 << 4);
|
||||||
|
internal const uint kAudioFormatFlagIsNonInterleaved = (1 << 5);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueNewOutput(
|
||||||
|
ref AudioStreamBasicDescription format,
|
||||||
|
nint callback,
|
||||||
|
nint userData,
|
||||||
|
nint callbackRunLoop,
|
||||||
|
nint callbackRunLoopMode,
|
||||||
|
uint flags,
|
||||||
|
out nint audioQueue);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueSetProperty(
|
||||||
|
nint audioQueue,
|
||||||
|
uint propertyID,
|
||||||
|
ref AudioChannelLayout layout,
|
||||||
|
uint layoutSize);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueDispose(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueAllocateBuffer(
|
||||||
|
nint audioQueue,
|
||||||
|
uint bufferByteSize,
|
||||||
|
out nint buffer);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueStart(nint audioQueue, nint startTime);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueuePause(nint audioQueue);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueStop(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueSetParameter(
|
||||||
|
nint audioQueue,
|
||||||
|
uint parameterID,
|
||||||
|
float value);
|
||||||
|
|
||||||
|
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||||
|
internal static partial int AudioQueueEnqueueBuffer(
|
||||||
|
nint audioQueue,
|
||||||
|
nint buffer,
|
||||||
|
uint numPacketDescs,
|
||||||
|
nint packetDescs);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct AudioQueueBuffer
|
||||||
|
{
|
||||||
|
public uint AudioDataBytesCapacity;
|
||||||
|
public nint AudioData;
|
||||||
|
public uint AudioDataByteSize;
|
||||||
|
public nint UserData;
|
||||||
|
public uint PacketDescriptionCapacity;
|
||||||
|
public nint PacketDescriptions;
|
||||||
|
public uint PacketDescriptionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const uint kAudioQueueParam_Volume = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
||||||
|
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -9,5 +9,6 @@ namespace Ryujinx.UI.Common.Configuration
|
|||||||
OpenAl,
|
OpenAl,
|
||||||
SoundIo,
|
SoundIo,
|
||||||
SDL2,
|
SDL2,
|
||||||
|
AudioToolbox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
|
using Ryujinx.Audio.Backends.Apple;
|
||||||
using Ryujinx.Audio.Backends.Dummy;
|
using Ryujinx.Audio.Backends.Dummy;
|
||||||
using Ryujinx.Audio.Backends.OpenAL;
|
using Ryujinx.Audio.Backends.OpenAL;
|
||||||
using Ryujinx.Audio.Backends.SDL2;
|
using Ryujinx.Audio.Backends.SDL2;
|
||||||
@@ -968,6 +969,9 @@ namespace Ryujinx.Ava
|
|||||||
AudioBackend.Dummy,
|
AudioBackend.Dummy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
availableBackends.Insert(0, AudioBackend.AudioToolbox);
|
||||||
|
|
||||||
AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
|
AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
|
||||||
|
|
||||||
for (int i = 0; i < availableBackends.Count; i++)
|
for (int i = 0; i < availableBackends.Count; i++)
|
||||||
@@ -1001,6 +1005,9 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
deviceDriver = currentBackend switch
|
deviceDriver = currentBackend switch
|
||||||
{
|
{
|
||||||
|
#pragma warning disable CA1416 // Platform compatibility is enforced in AppleHardwareDeviceDriver.IsSupported, before any potentially platform-sensitive code can run.
|
||||||
|
AudioBackend.AudioToolbox => InitializeAudioBackend<AppleHardwareDeviceDriver>(AudioBackend.AudioToolbox, nextBackend),
|
||||||
|
#pragma warning restore CA1416
|
||||||
AudioBackend.SDL2 => InitializeAudioBackend<SDL2HardwareDeviceDriver>(AudioBackend.SDL2, nextBackend),
|
AudioBackend.SDL2 => InitializeAudioBackend<SDL2HardwareDeviceDriver>(AudioBackend.SDL2, nextBackend),
|
||||||
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
|
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
|
||||||
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
|
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
|
||||||
|
|||||||
@@ -174,6 +174,7 @@
|
|||||||
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
|
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
|
||||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||||
|
"SettingsTabSystemAudioBackendAudioToolbox": "Apple Audio (macOS only)",
|
||||||
"SettingsTabSystemCustomVSyncInterval": "Interval",
|
"SettingsTabSystemCustomVSyncInterval": "Interval",
|
||||||
"SettingsTabSystemHacks": "Hacks",
|
"SettingsTabSystemHacks": "Hacks",
|
||||||
"SettingsTabSystemHacksNote": "May cause instability",
|
"SettingsTabSystemHacksNote": "May cause instability",
|
||||||
|
|||||||
@@ -50,11 +50,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
|
|
||||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
|
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
|
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
|
||||||
|
<ProjectReference Include="..\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj" />
|
||||||
|
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
|
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
|
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
|
||||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
|
using Ryujinx.Audio.Backends.Apple;
|
||||||
using Ryujinx.Audio.Backends.OpenAL;
|
using Ryujinx.Audio.Backends.OpenAL;
|
||||||
using Ryujinx.Audio.Backends.SDL2;
|
using Ryujinx.Audio.Backends.SDL2;
|
||||||
using Ryujinx.Audio.Backends.SoundIo;
|
using Ryujinx.Audio.Backends.SoundIo;
|
||||||
@@ -255,6 +256,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public bool IsOpenAlEnabled { get; set; }
|
public bool IsOpenAlEnabled { get; set; }
|
||||||
public bool IsSoundIoEnabled { get; set; }
|
public bool IsSoundIoEnabled { get; set; }
|
||||||
public bool IsSDL2Enabled { get; set; }
|
public bool IsSDL2Enabled { get; set; }
|
||||||
|
public bool IsAudioToolboxEnabled { get; set; }
|
||||||
public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
|
public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
|
||||||
public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr;
|
public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr;
|
||||||
|
|
||||||
@@ -459,12 +461,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
|
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
|
||||||
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
|
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
|
||||||
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
|
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
|
||||||
|
IsAudioToolboxEnabled = OperatingSystem.IsMacOS() && AppleHardwareDeviceDriver.IsSupported;
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsOpenAlEnabled));
|
OnPropertyChanged(nameof(IsOpenAlEnabled));
|
||||||
OnPropertyChanged(nameof(IsSoundIoEnabled));
|
OnPropertyChanged(nameof(IsSoundIoEnabled));
|
||||||
OnPropertyChanged(nameof(IsSDL2Enabled));
|
OnPropertyChanged(nameof(IsSDL2Enabled));
|
||||||
|
OnPropertyChanged(nameof(IsAudioToolboxEnabled));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
<ComboBoxItem
|
<ComboBoxItem
|
||||||
IsEnabled="{Binding IsSDL2Enabled}"
|
IsEnabled="{Binding IsSDL2Enabled}"
|
||||||
Content="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
|
Content="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
|
||||||
|
<ComboBoxItem
|
||||||
|
IsEnabled="{Binding IsAudioToolboxEnabled}"
|
||||||
|
Content="{locale:Locale SettingsTabSystemAudioBackendAudioToolbox}" />
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||||
|
|||||||
Reference in New Issue
Block a user