diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/AudioCallback.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/AudioCallback.cs
index ea4c8da..f5ddf68 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/AudioCallback.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/AudioCallback.cs
@@ -6,7 +6,7 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform;
/// Callback delegate invoked by the audio backend when it needs audio data.
/// Reference: SDL_AudioCallback from SDL_audio.h
///
-/// Buffer to fill with audio samples (interleaved float stereo).
+/// Buffer to fill with interleaved float samples in the obtained channel layout.
///
/// This callback is called from the audio thread. Implementations must be thread-safe
/// and should not block. Fill the buffer with silence (zeros) if no audio is available.
@@ -21,9 +21,12 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform;
public delegate void AudioPostmixCallback(Span buffer);
///
-/// Audio format specification matching SDL_AudioSpec.
+/// Audio format specification matching the managed subset of SDL_AudioSpec.
+/// The callback-facing mix format is always interleaved float samples even when
+/// a backend negotiates a different native device representation internally.
///
-public sealed class AudioSpec {
+public sealed class AudioSpec
+{
///
/// Sample rate in Hz (e.g., 48000).
///
@@ -61,7 +64,7 @@ public sealed class AudioSpec {
public int BufferSamples => BufferFrames * Channels;
///
- /// Gets the buffer size in bytes (for float samples).
+ /// Gets the buffer size in bytes for the managed float callback representation.
///
public int BufferBytes => BufferSamples * sizeof(float);
}
@@ -69,7 +72,8 @@ public sealed class AudioSpec {
///
/// Audio device state.
///
-public enum AudioDeviceState {
+public enum AudioDeviceState
+{
///
/// Device is stopped/paused.
///
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/ISdlAudioDriver.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/ISdlAudioDriver.cs
index 292650f..f84c70c 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/ISdlAudioDriver.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/ISdlAudioDriver.cs
@@ -7,7 +7,15 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl;
/// Reference: SDL_AudioDriverImpl from SDL_sysaudio.h
/// Each platform (WASAPI, ALSA, CoreAudio) implements this interface.
///
-internal interface ISdlAudioDriver {
+internal interface ISdlAudioDriver
+{
+ ///
+ /// Gets a value indicating whether the backend owns the callback thread.
+ /// Drivers such as CoreAudio run their own callback loop and should not use
+ /// the shared SDL-style playback thread in .
+ ///
+ bool ProvidesOwnCallbackThread { get; }
+
///
/// Opens the audio device with the desired spec.
/// Reference: SDL_AudioDriverImpl.OpenDevice
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Linux/Alsa/SdlAlsaDriver.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Linux/Alsa/SdlAlsaDriver.cs
index 93a99ad..286d06f 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Linux/Alsa/SdlAlsaDriver.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Linux/Alsa/SdlAlsaDriver.cs
@@ -17,7 +17,8 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Linux.Alsa;
/// - ALSA_GetDeviceBuf (line 415): returns the mix buffer pointer
///
[SupportedOSPlatform("linux")]
-internal sealed class SdlAlsaDriver : ISdlAudioDriver {
+internal sealed class SdlAlsaDriver : ISdlAudioDriver
+{
private IntPtr _pcmHandle;
private IntPtr _mixBuffer;
private int _mixBufferBytes;
@@ -25,6 +26,8 @@ internal sealed class SdlAlsaDriver : ISdlAudioDriver {
private int _channels;
private int _frameSize;
+ public bool ProvidesOwnCallbackThread => false;
+
///
/// Opens the ALSA PCM device.
/// Reference: ALSA_OpenDevice (SDL_alsa_audio.c line 593)
@@ -37,7 +40,8 @@ internal sealed class SdlAlsaDriver : ISdlAudioDriver {
/// 5. Set to blocking mode for playback
/// 6. Allocate mix buffer
///
- public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error) {
+ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error)
+ {
obtainedSpec = desiredSpec;
sampleFrames = 0;
error = null;
@@ -53,7 +57,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
AlsaConstants.SndPcmStreamPlayback,
AlsaConstants.SndPcmNonblock);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't open audio device: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -62,16 +67,19 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Reference: ALSA_OpenDevice line 630-631
IntPtr hwparams = IntPtr.Zero;
status = AlsaNativeMethods.snd_pcm_hw_params_malloc(out hwparams);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't allocate hw params: {AlsaNativeMethods.GetErrorString(status)}";
CleanupPcm();
return false;
}
- try {
+ try
+ {
// Reference: ALSA_OpenDevice line 632-634
status = AlsaNativeMethods.snd_pcm_hw_params_any(_pcmHandle, hwparams);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't get hardware config: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -80,7 +88,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// SDL only uses interleaved sample output
status = AlsaNativeMethods.snd_pcm_hw_params_set_access(
_pcmHandle, hwparams, AlsaConstants.SndPcmAccessRwInterleaved);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set interleaved access: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -90,7 +99,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// This corresponds to the AUDIO_F32LSB / SND_PCM_FORMAT_FLOAT_LE path in SDL
status = AlsaNativeMethods.snd_pcm_hw_params_set_format(
_pcmHandle, hwparams, AlsaConstants.SndPcmFormatFloatLe);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set float format: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -99,10 +109,12 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Set the number of channels
uint channels = (uint)desiredSpec.Channels;
status = AlsaNativeMethods.snd_pcm_hw_params_set_channels(_pcmHandle, hwparams, channels);
- if (status < 0) {
+ if (status < 0)
+ {
// Try to get whatever channels the hardware supports
status = AlsaNativeMethods.snd_pcm_hw_params_get_channels(hwparams, out channels);
- if (status < 0) {
+ if (status < 0)
+ {
error = "ALSA: Couldn't set audio channels";
return false;
}
@@ -112,7 +124,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Set the audio rate
uint rate = (uint)desiredSpec.SampleRate;
status = AlsaNativeMethods.snd_pcm_hw_params_set_rate_near(_pcmHandle, hwparams, ref rate, IntPtr.Zero);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set audio frequency: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -120,7 +133,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Reference: ALSA_OpenDevice line 721-724 -> ALSA_set_buffer_size
// Set the buffer size (period size + periods)
ulong persize = (ulong)desiredSpec.BufferFrames;
- if (!SetBufferSize(_pcmHandle, hwparams, ref persize, out error)) {
+ if (!SetBufferSize(_pcmHandle, hwparams, ref persize, out error))
+ {
return false;
}
@@ -128,38 +142,46 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Set the software parameters
IntPtr swparams = IntPtr.Zero;
status = AlsaNativeMethods.snd_pcm_sw_params_malloc(out swparams);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't allocate sw params: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
- try {
+ try
+ {
status = AlsaNativeMethods.snd_pcm_sw_params_current(_pcmHandle, swparams);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't get software config: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
// Reference: ALSA_OpenDevice line 733-736
status = AlsaNativeMethods.snd_pcm_sw_params_set_avail_min(_pcmHandle, swparams, persize);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set minimum available samples: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
// Reference: ALSA_OpenDevice line 737-741
status = AlsaNativeMethods.snd_pcm_sw_params_set_start_threshold(_pcmHandle, swparams, 1);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set start threshold: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
status = AlsaNativeMethods.snd_pcm_sw_params(_pcmHandle, swparams);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set software audio parameters: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
- } finally {
+ }
+ finally
+ {
AlsaNativeMethods.snd_pcm_sw_params_free(swparams);
}
@@ -169,7 +191,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// If the device suggests a different sample size and we don't allow negotiation,
// keep the desired buffer size instead of adopting the device's negotiated size.
ulong finalBufferFrames = persize;
- if (!desiredSpec.AllowNegotiate && persize != (ulong)desiredSpec.BufferFrames) {
+ if (!desiredSpec.AllowNegotiate && persize != (ulong)desiredSpec.BufferFrames)
+ {
finalBufferFrames = (ulong)desiredSpec.BufferFrames;
}
@@ -181,7 +204,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Allocate mixing buffer
// Reference: ALSA_OpenDevice line 758-762
_mixBuffer = Marshal.AllocHGlobal(_mixBufferBytes);
- unsafe {
+ unsafe
+ {
NativeMemory.Clear(_mixBuffer.ToPointer(), (nuint)_mixBufferBytes);
}
@@ -189,7 +213,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Set to blocking mode for playback (SDL_ALSA_NON_BLOCKING is 0 by default)
AlsaNativeMethods.snd_pcm_nonblock(_pcmHandle, 0);
- obtainedSpec = new AudioSpec {
+ obtainedSpec = new AudioSpec
+ {
SampleRate = (int)rate,
Channels = (int)channels,
BufferFrames = _sampleFrames,
@@ -200,7 +225,9 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
sampleFrames = _sampleFrames;
return true;
- } finally {
+ }
+ finally
+ {
AlsaNativeMethods.snd_pcm_hw_params_free(hwparams);
}
}
@@ -214,13 +241,16 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
/// 2. snd_pcm_close
/// 3. Free mix buffer
///
- public void CloseDevice(SdlAudioDevice device) {
- if (_pcmHandle != IntPtr.Zero) {
+ public void CloseDevice(SdlAudioDevice device)
+ {
+ if (_pcmHandle != IntPtr.Zero)
+ {
// Reference: ALSA_CloseDevice line 471-478
// Wait for the submitted audio to drain
// ALSA_snd_pcm_drop() can hang, so don't use that.
int delay = ((device.SampleFrames * 1000) / device.ObtainedSpec.SampleRate) * 2;
- if (delay > 100) {
+ if (delay > 100)
+ {
delay = 100;
}
Thread.Sleep(delay);
@@ -229,7 +259,8 @@ public void CloseDevice(SdlAudioDevice device) {
_pcmHandle = IntPtr.Zero;
}
- if (_mixBuffer != IntPtr.Zero) {
+ if (_mixBuffer != IntPtr.Zero)
+ {
Marshal.FreeHGlobal(_mixBuffer);
_mixBuffer = IntPtr.Zero;
}
@@ -243,7 +274,8 @@ public void CloseDevice(SdlAudioDevice device) {
/// snd_pcm_writei in blocking mode already waits. SDL's default ALSA path
/// uses blocking writes.
///
- public void WaitDevice(SdlAudioDevice device) {
+ public void WaitDevice(SdlAudioDevice device)
+ {
// Reference: ALSA_WaitDevice
// When SDL_ALSA_NON_BLOCKING is 0 (default), this function is empty.
// The blocking snd_pcm_writei in PlayDevice handles the wait.
@@ -254,7 +286,8 @@ public void WaitDevice(SdlAudioDevice device) {
/// Reference: ALSA_GetDeviceBuf (SDL_alsa_audio.c line 415)
/// Returns this->hidden->mixbuf
///
- public IntPtr GetDeviceBuf(SdlAudioDevice device) {
+ public IntPtr GetDeviceBuf(SdlAudioDevice device)
+ {
// Reference: ALSA_GetDeviceBuf line 417
// return this->hidden->mixbuf
return _mixBuffer;
@@ -271,18 +304,22 @@ public IntPtr GetDeviceBuf(SdlAudioDevice device) {
/// 4. On unrecoverable error: return false (disconnected)
/// 5. On status==0: delay half the remaining time, retry
///
- public void PlayDevice(SdlAudioDevice device) {
+ public void PlayDevice(SdlAudioDevice device)
+ {
// Reference: ALSA_PlayDevice (SDL_alsa_audio.c line 373-413)
IntPtr sampleBuf = _mixBuffer;
int frameSize = _frameSize;
long framesLeft = _sampleFrames;
- while (framesLeft > 0 && device.Enabled) {
+ while (framesLeft > 0 && device.Enabled)
+ {
// Reference: ALSA_PlayDevice line 383-384
long status = AlsaNativeMethods.snd_pcm_writei(_pcmHandle, sampleBuf, (ulong)framesLeft);
- if (status < 0) {
- if (status == -AlsaConstants.Eagain) {
+ if (status < 0)
+ {
+ if (status == -AlsaConstants.Eagain)
+ {
// Reference: ALSA_PlayDevice line 387-391
Thread.Sleep(1);
continue;
@@ -290,7 +327,8 @@ public void PlayDevice(SdlAudioDevice device) {
// Reference: ALSA_PlayDevice line 392-401
int recoverStatus = AlsaNativeMethods.snd_pcm_recover(_pcmHandle, (int)status, 0);
- if (recoverStatus < 0) {
+ if (recoverStatus < 0)
+ {
// Hmm, not much we can do - abort
// Reference: ALSA_PlayDevice line 394-400
// SDL_OpenedAudioDeviceDisconnected(this)
@@ -298,10 +336,13 @@ public void PlayDevice(SdlAudioDevice device) {
return;
}
continue;
- } else if (status == 0) {
+ }
+ else if (status == 0)
+ {
// Reference: ALSA_PlayDevice line 402-406
int delay = (int)((framesLeft / 2 * 1000) / device.ObtainedSpec.SampleRate);
- if (delay > 0) {
+ if (delay > 0)
+ {
Thread.Sleep(delay);
}
}
@@ -316,25 +357,31 @@ public void PlayDevice(SdlAudioDevice device) {
/// Called at the start of the audio thread.
/// ALSA has no ThreadInit callback in SDL.
///
- public void ThreadInit(SdlAudioDevice device) {
+ public void ThreadInit(SdlAudioDevice device)
+ {
}
///
/// Called at the end of the audio thread.
/// ALSA has no ThreadDeinit callback in SDL.
///
- public void ThreadDeinit(SdlAudioDevice device) {
+ public void ThreadDeinit(SdlAudioDevice device)
+ {
}
///
/// Gets the ALSA device name based on channel count.
/// Reference: get_audio_device() in SDL_alsa_audio.c line 229-242
///
- private static string GetAudioDevice(int channels) {
+ private static string GetAudioDevice(int channels)
+ {
// Reference: get_audio_device lines 237-241
- if (channels == 6) {
+ if (channels == 6)
+ {
return "plug:surround51";
- } else if (channels == 4) {
+ }
+ else if (channels == 4)
+ {
return "plug:surround40";
}
return "default";
@@ -351,7 +398,8 @@ private static string GetAudioDevice(int channels) {
/// 4. Set periods to first available
/// 5. Apply hw params
///
- private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize, out string? error) {
+ private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize, out string? error)
+ {
error = null;
int status;
@@ -359,19 +407,22 @@ private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize,
// Copy the hardware parameters for this setup
IntPtr hwparamsCopy = IntPtr.Zero;
status = AlsaNativeMethods.snd_pcm_hw_params_malloc(out hwparamsCopy);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't allocate hw params copy: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
- try {
+ try
+ {
AlsaNativeMethods.snd_pcm_hw_params_copy(hwparamsCopy, hwparams);
// Reference: ALSA_set_buffer_size line 548-551
// Attempt to match the period size to the requested buffer size
status = AlsaNativeMethods.snd_pcm_hw_params_set_period_size_near(
pcmHandle, hwparamsCopy, ref persize, IntPtr.Zero);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set period size: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -381,7 +432,8 @@ private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize,
uint periods = 2;
status = AlsaNativeMethods.snd_pcm_hw_params_set_periods_min(
pcmHandle, hwparamsCopy, ref periods, IntPtr.Zero);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set periods min: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -389,7 +441,8 @@ private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize,
// Reference: ALSA_set_buffer_size line 560-564
status = AlsaNativeMethods.snd_pcm_hw_params_set_periods_first(
pcmHandle, hwparamsCopy, ref periods, IntPtr.Zero);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set periods first: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -397,7 +450,8 @@ private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize,
// Reference: ALSA_set_buffer_size line 567-570
// "set" the hardware with the desired parameters
status = AlsaNativeMethods.snd_pcm_hw_params(pcmHandle, hwparamsCopy);
- if (status < 0) {
+ if (status < 0)
+ {
error = $"ALSA: Couldn't set hardware params: {AlsaNativeMethods.GetErrorString(status)}";
return false;
}
@@ -407,13 +461,17 @@ private bool SetBufferSize(IntPtr pcmHandle, IntPtr hwparams, ref ulong persize,
// persize was updated by set_period_size_near
return true;
- } finally {
+ }
+ finally
+ {
AlsaNativeMethods.snd_pcm_hw_params_free(hwparamsCopy);
}
}
- private void CleanupPcm() {
- if (_pcmHandle != IntPtr.Zero) {
+ private void CleanupPcm()
+ {
+ if (_pcmHandle != IntPtr.Zero)
+ {
AlsaNativeMethods.snd_pcm_close(_pcmHandle);
_pcmHandle = IntPtr.Zero;
}
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/CoreAudioNativeMethods.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/CoreAudioNativeMethods.cs
index a1ce8ec..51b4cc2 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/CoreAudioNativeMethods.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/CoreAudioNativeMethods.cs
@@ -4,106 +4,83 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Mac.CoreAudio;
using System.Runtime.InteropServices;
///
-/// P/Invoke bindings for Apple AudioToolbox (AudioQueue API).
-/// Reference: SDL_coreaudio.m uses AudioQueueNewOutput, AudioQueueAllocateBuffer,
-/// AudioQueueEnqueueBuffer, AudioQueueStart, AudioQueueStop, AudioQueueFlush,
-/// AudioQueueDispose, AudioQueueSetProperty, and CoreFoundation's CFRunLoop.
+/// P/Invoke bindings for the subset of CoreAudio, AudioToolbox, and CoreFoundation
+/// used by the managed macOS audio backend.
+/// References:
+/// - SDL/src/audio/coreaudio/SDL_coreaudio.m
+/// - SDL/src/audio/coreaudio/SDL_coreaudio.h
+/// - Apple AudioQueue and AudioObject property APIs
///
-/// These are macOS-only APIs from AudioToolbox.framework and CoreFoundation.framework.
+/// These bindings model the default-device playback flow actually used by
+/// Spice86.Audio rather than the full SDL3 device-enumeration surface.
///
-internal static class CoreAudioNativeMethods {
+internal static class CoreAudioNativeMethods
+{
private const string AudioToolboxLib = "/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox";
+ private const string CoreAudioLib = "/System/Library/Frameworks/CoreAudio.framework/CoreAudio";
private const string CoreFoundationLib = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
- // OSStatus is Int32
- // noErr = 0
-
///
/// AudioStreamBasicDescription structure.
/// Reference: CoreAudioTypes.h
///
[StructLayout(LayoutKind.Sequential)]
- internal struct AudioStreamBasicDescription {
- /// Sample rate in Hz.
+ internal struct AudioStreamBasicDescription
+ {
public double SampleRate;
-
- /// Audio format ID (e.g., kAudioFormatLinearPCM).
public uint FormatId;
-
- /// Format flags (e.g., kLinearPCMFormatFlagIsFloat | kLinearPCMFormatFlagIsPacked).
public uint FormatFlags;
-
- /// Bytes per packet.
public uint BytesPerPacket;
-
- /// Frames per packet (1 for PCM).
public uint FramesPerPacket;
-
- /// Bytes per frame.
public uint BytesPerFrame;
-
- /// Channels per frame.
public uint ChannelsPerFrame;
-
- /// Bits per channel.
public uint BitsPerChannel;
-
- /// Reserved, must be 0.
public uint Reserved;
}
///
/// AudioQueueBuffer structure.
/// Reference: AudioQueue.h
- /// The native struct has more fields but we only need AudioData and the byte sizes.
///
[StructLayout(LayoutKind.Sequential)]
- internal struct AudioQueueBuffer {
- /// The size in bytes of the allocated buffer data.
+ internal struct AudioQueueBuffer
+ {
public uint AudioDataBytesCapacity;
-
- /// Pointer to the audio data buffer.
public IntPtr AudioData;
-
- /// The number of bytes of valid audio data in the buffer.
public uint AudioDataByteSize;
-
- /// User data pointer.
public IntPtr UserData;
-
- /// Number of packet descriptions (for VBR data).
public uint PacketDescriptionCapacity;
-
- /// Pointer to packet descriptions.
public IntPtr PacketDescriptions;
-
- /// Number of valid packet descriptions.
public uint PacketDescriptionCount;
}
///
- /// AudioChannelLayout structure (simplified - only the tag is used).
+ /// AudioChannelLayout structure.
/// Reference: CoreAudioTypes.h
///
[StructLayout(LayoutKind.Sequential)]
- internal struct AudioChannelLayout {
- /// Channel layout tag.
+ internal struct AudioChannelLayout
+ {
public uint ChannelLayoutTag;
-
- /// Channel bitmap.
public uint ChannelBitmap;
-
- /// Number of channel descriptions.
public uint NumberChannelDescriptions;
+ }
- // AudioChannelDescription array follows (variable length)
- // We don't need it for our use case
+ ///
+ /// AudioObjectPropertyAddress structure.
+ /// Reference: AudioHardwareBase.h
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct AudioObjectPropertyAddress
+ {
+ public uint Selector;
+ public uint Scope;
+ public uint Element;
}
///
/// AudioQueue output callback delegate.
- /// Reference: AudioQueue.h AudioQueueOutputCallback
- /// Called by CoreAudio when a buffer has been consumed and needs refilling.
+ /// Reference: AudioQueue.h AudioQueueOutputCallback.
///
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void AudioQueueOutputCallback(
@@ -111,14 +88,9 @@ internal delegate void AudioQueueOutputCallback(
IntPtr inAudioQueue,
IntPtr inBuffer);
- // ========================================================================
- // AudioQueue API
- // ========================================================================
-
///
- /// Creates a new output audio queue.
- /// Reference: AudioQueueNewOutput from AudioQueue.h
- /// SDL_coreaudio.m: prepare_audioqueue line ~908
+ /// Creates a playback AudioQueue bound to the calling thread's run loop.
+ /// Reference: SDL_coreaudio.m PrepareAudioQueue -> AudioQueueNewOutput.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueNewOutput")]
internal static extern int AudioQueueNewOutput(
@@ -131,9 +103,8 @@ internal static extern int AudioQueueNewOutput(
out IntPtr outAQ);
///
- /// Allocates an audio queue buffer.
- /// Reference: AudioQueueAllocateBuffer from AudioQueue.h
- /// SDL_coreaudio.m: prepare_audioqueue line ~965
+ /// Allocates an AudioQueue buffer.
+ /// Reference: SDL_coreaudio.m PrepareAudioQueue -> AudioQueueAllocateBuffer.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueAllocateBuffer")]
internal static extern int AudioQueueAllocateBuffer(
@@ -142,9 +113,8 @@ internal static extern int AudioQueueAllocateBuffer(
out IntPtr outBuffer);
///
- /// Enqueues a buffer for playback.
- /// Reference: AudioQueueEnqueueBuffer from AudioQueue.h
- /// SDL_coreaudio.m: outputCallback line ~489 and prepare_audioqueue line ~969
+ /// Enqueues an AudioQueue buffer for playback.
+ /// Reference: SDL_coreaudio.m PlaybackBufferReadyCallback / PrepareAudioQueue.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueEnqueueBuffer")]
internal static extern int AudioQueueEnqueueBuffer(
@@ -154,9 +124,8 @@ internal static extern int AudioQueueEnqueueBuffer(
IntPtr inPacketDescs);
///
- /// Starts the audio queue.
- /// Reference: AudioQueueStart from AudioQueue.h
- /// SDL_coreaudio.m: prepare_audioqueue line ~972
+ /// Starts the AudioQueue.
+ /// Reference: SDL_coreaudio.m PrepareAudioQueue -> AudioQueueStart.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueStart")]
internal static extern int AudioQueueStart(
@@ -164,9 +133,8 @@ internal static extern int AudioQueueStart(
IntPtr inStartTime);
///
- /// Stops the audio queue.
- /// Reference: AudioQueueStop from AudioQueue.h
- /// SDL_coreaudio.m: COREAUDIO_CloseDevice line ~683
+ /// Stops the AudioQueue.
+ /// Reference: SDL_coreaudio.m COREAUDIO_CloseDevice -> AudioQueueStop.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueStop")]
internal static extern int AudioQueueStop(
@@ -174,17 +142,15 @@ internal static extern int AudioQueueStop(
byte inImmediate);
///
- /// Flushes the audio queue.
- /// Reference: AudioQueueFlush from AudioQueue.h
- /// SDL_coreaudio.m: COREAUDIO_CloseDevice line ~682
+ /// Flushes queued playback buffers.
+ /// Reference: SDL_coreaudio.m COREAUDIO_CloseDevice -> AudioQueueFlush.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueFlush")]
internal static extern int AudioQueueFlush(IntPtr inAQ);
///
- /// Disposes the audio queue.
- /// Reference: AudioQueueDispose from AudioQueue.h
- /// SDL_coreaudio.m: COREAUDIO_CloseDevice line ~683
+ /// Disposes the AudioQueue and its buffers.
+ /// Reference: SDL_coreaudio.m COREAUDIO_CloseDevice -> AudioQueueDispose.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueDispose")]
internal static extern int AudioQueueDispose(
@@ -192,9 +158,8 @@ internal static extern int AudioQueueDispose(
byte inImmediate);
///
- /// Sets a property on the audio queue.
- /// Reference: AudioQueueSetProperty from AudioQueue.h
- /// SDL_coreaudio.m: prepare_audioqueue line ~930 (channel layout)
+ /// Sets an AudioQueue property using unmanaged memory.
+ /// Reference: SDL_coreaudio.m PrepareAudioQueue -> AudioQueueSetProperty.
///
[DllImport(AudioToolboxLib, EntryPoint = "AudioQueueSetProperty")]
internal static extern int AudioQueueSetProperty(
@@ -203,23 +168,66 @@ internal static extern int AudioQueueSetProperty(
IntPtr inData,
uint inDataSize);
- // ========================================================================
- // CoreFoundation RunLoop API
- // Used by the audioqueue_thread to run the AudioQueue callbacks.
- // Reference: SDL_coreaudio.m audioqueue_thread line ~1015
- // ========================================================================
+ ///
+ /// Sets an AudioQueue property using a pointer-sized value.
+ /// Reference: SDL_coreaudio.m AssignDeviceToAudioQueue -> kAudioQueueProperty_CurrentDevice.
+ ///
+ [DllImport(AudioToolboxLib, EntryPoint = "AudioQueueSetProperty")]
+ internal static extern int AudioQueueSetProperty(
+ IntPtr inAQ,
+ uint inID,
+ ref IntPtr inData,
+ uint inDataSize);
+
+ ///
+ /// Reads a UInt32 property value from a CoreAudio object.
+ /// Used for default device identifiers and boolean-style device flags.
+ ///
+ [DllImport(CoreAudioLib, EntryPoint = "AudioObjectGetPropertyData")]
+ internal static extern int AudioObjectGetPropertyData(
+ uint inObjectID,
+ ref AudioObjectPropertyAddress inAddress,
+ uint inQualifierDataSize,
+ IntPtr inQualifierData,
+ ref uint ioDataSize,
+ out uint outData);
+
+ ///
+ /// Reads an Int32 property value from a CoreAudio object.
+ /// Used for hog-mode process identifiers.
+ ///
+ [DllImport(CoreAudioLib, EntryPoint = "AudioObjectGetPropertyData")]
+ internal static extern int AudioObjectGetPropertyData(
+ uint inObjectID,
+ ref AudioObjectPropertyAddress inAddress,
+ uint inQualifierDataSize,
+ IntPtr inQualifierData,
+ ref uint ioDataSize,
+ out int outData);
+
+ ///
+ /// Reads a pointer-sized property value from a CoreAudio object.
+ /// Used for CFStringRef device UID values.
+ ///
+ [DllImport(CoreAudioLib, EntryPoint = "AudioObjectGetPropertyData")]
+ internal static extern int AudioObjectGetPropertyData(
+ uint inObjectID,
+ ref AudioObjectPropertyAddress inAddress,
+ uint inQualifierDataSize,
+ IntPtr inQualifierData,
+ ref uint ioDataSize,
+ out IntPtr outData);
///
/// Gets the current thread's run loop.
- /// Reference: CFRunLoopGetCurrent from CFRunLoop.h
+ /// Reference: SDL_coreaudio.m AudioQueueThreadEntry -> CFRunLoopGetCurrent.
///
[DllImport(CoreFoundationLib, EntryPoint = "CFRunLoopGetCurrent")]
internal static extern IntPtr CFRunLoopGetCurrent();
///
- /// Runs the run loop in a specific mode for a given duration.
- /// Reference: CFRunLoopRunInMode from CFRunLoop.h
- /// SDL_coreaudio.m: audioqueue_thread line ~1028
+ /// Runs the current thread's run loop for a bounded interval.
+ /// Reference: SDL_coreaudio.m AudioQueueThreadEntry -> CFRunLoopRunInMode.
///
[DllImport(CoreFoundationLib, EntryPoint = "CFRunLoopRunInMode")]
internal static extern int CFRunLoopRunInMode(
@@ -228,59 +236,117 @@ internal static extern int CFRunLoopRunInMode(
byte returnAfterSourceHandled);
///
- /// Gets the kCFRunLoopDefaultMode constant string.
- /// This is a CFStringRef constant.
+ /// Gets the main run loop.
+ /// Present for completeness alongside the other run-loop bindings.
///
[DllImport(CoreFoundationLib, EntryPoint = "CFRunLoopGetMain")]
internal static extern IntPtr CFRunLoopGetMain();
- // ========================================================================
- // CoreFoundation String Constants
- // kCFRunLoopDefaultMode is a global CFStringRef
- // ========================================================================
+ ///
+ /// Releases a CoreFoundation object retained by a property query.
+ ///
+ [DllImport(CoreFoundationLib, EntryPoint = "CFRelease")]
+ internal static extern void CFRelease(IntPtr cf);
///
- /// Get kCFRunLoopDefaultMode - we need to load this from the framework.
+ /// Loads the global kCFRunLoopDefaultMode symbol.
+ /// Reference: SDL_coreaudio.m AudioQueueThreadEntry uses kCFRunLoopDefaultMode.
///
- internal static IntPtr GetDefaultRunLoopMode() {
+ internal static IntPtr GetDefaultRunLoopMode()
+ {
IntPtr lib = NativeLibrary.Load(CoreFoundationLib);
IntPtr symbolAddr = NativeLibrary.GetExport(lib, "kCFRunLoopDefaultMode");
return Marshal.ReadIntPtr(symbolAddr);
}
+
+ ///
+ /// Creates a CoreAudio property-address struct with the main element filled in.
+ ///
+ internal static AudioObjectPropertyAddress CreatePropertyAddress(uint selector, uint scope)
+ {
+ return new AudioObjectPropertyAddress
+ {
+ Selector = selector,
+ Scope = scope,
+ Element = CoreAudioConstants.AudioObjectPropertyElementMain
+ };
+ }
}
///
-/// CoreAudio / AudioToolbox constants.
-/// Reference: CoreAudioTypes.h, AudioQueue.h
+/// CoreAudio and AudioToolbox constants used by the managed macOS playback port.
+/// References: CoreAudioTypes.h, AudioHardwareBase.h, AudioQueue.h, SDL_coreaudio.m.
///
-internal static class CoreAudioConstants {
- // noErr
+internal static class CoreAudioConstants
+{
internal const int NoErr = 0;
+ internal const uint AudioObjectSystemObject = 1;
+ internal const uint AudioFormatLinearPcm = 0x6C70636D;
+ internal const uint LinearPcmFormatFlagIsFloat = 1 << 0;
+ internal const uint LinearPcmFormatFlagIsBigEndian = 1 << 1;
+ internal const uint LinearPcmFormatFlagIsSignedInteger = 1 << 2;
+ internal const uint LinearPcmFormatFlagIsPacked = 1 << 3;
+ internal const uint AudioQueuePropertyChannelLayout = 0x61716368;
+ internal static readonly uint AudioQueuePropertyCurrentDevice = MakeFourCc("aqcd");
+ internal static readonly uint AudioHardwarePropertyDefaultOutputDevice = MakeFourCc("dOut");
+ internal static readonly uint AudioDevicePropertyDeviceUid = MakeFourCc("uid ");
+ internal static readonly uint AudioDevicePropertyDeviceIsAlive = MakeFourCc("livn");
+ internal static readonly uint AudioDevicePropertyHogMode = MakeFourCc("oink");
+ internal static readonly uint AudioObjectPropertyScopeGlobal = MakeFourCc("glob");
+ internal static readonly uint AudioDevicePropertyScopeOutput = MakeFourCc("outp");
+ internal const uint AudioObjectPropertyElementMain = 0;
+ internal const uint AudioChannelLayoutTagMono = (100 << 16) | 1;
+ internal const uint AudioChannelLayoutTagStereo = (101 << 16) | 2;
+ internal const uint AudioChannelLayoutTagQuadraphonic = (108 << 16) | 4;
+ internal const uint AudioChannelLayoutTagMpeg51A = (121 << 16) | 6;
+ internal const uint AudioChannelLayoutTagDvd4 = (133 << 16) | 3;
+ internal const uint AudioChannelLayoutTagDvd6 = (135 << 16) | 5;
+ internal const uint AudioChannelLayoutTagDvd12 = AudioChannelLayoutTagMpeg51A;
+ internal const uint AudioChannelLayoutTagWave61 = (188 << 16) | 7;
+ internal const uint AudioChannelLayoutTagWave71 = (189 << 16) | 8;
+ internal const double MinimumAudioBufferTimeMs = 15.0;
- // kAudioFormatLinearPCM
- internal const uint AudioFormatLinearPcm = 0x6C70636D; // 'lpcm'
-
- // Format flags
- // Reference: CoreAudioTypes.h kLinearPCMFormatFlagIsFloat, kLinearPCMFormatFlagIsPacked
- internal const uint LinearPcmFormatFlagIsFloat = 1 << 0; // kAudioFormatFlagIsFloat
- internal const uint LinearPcmFormatFlagIsBigEndian = 1 << 1; // kAudioFormatFlagIsBigEndian
- internal const uint LinearPcmFormatFlagIsSignedInteger = 1 << 2; // kAudioFormatFlagIsSignedInteger
- internal const uint LinearPcmFormatFlagIsPacked = 1 << 3; // kAudioFormatFlagIsPacked
-
- // AudioQueue property IDs
- // kAudioQueueProperty_ChannelLayout
- internal const uint AudioQueuePropertyChannelLayout = 0x61716368; // 'aqch'
-
- // Channel layout tags
- // Reference: CoreAudioTypes.h
- internal const uint AudioChannelLayoutTagMono = (100 << 16) | 1; // kAudioChannelLayoutTag_Mono
- internal const uint AudioChannelLayoutTagStereo = (101 << 16) | 2; // kAudioChannelLayoutTag_Stereo
- internal const uint AudioChannelLayoutTagDvd4 = (134 << 16) | 3; // kAudioChannelLayoutTag_DVD_4 (L R LFE)
- internal const uint AudioChannelLayoutTagQuadraphonic = (108 << 16) | 4; // kAudioChannelLayoutTag_Quadraphonic
- internal const uint AudioChannelLayoutTagDvd6 = (136 << 16) | 5; // kAudioChannelLayoutTag_DVD_6 (L R LFE Ls Rs)
- internal const uint AudioChannelLayoutTagDvd12 = (142 << 16) | 6; // kAudioChannelLayoutTag_DVD_12 (L R C LFE Ls Rs)
+ private static uint MakeFourCc(string value)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(value);
+ if (value.Length != 4)
+ {
+ throw new ArgumentException("FourCC values must be exactly four characters long.", nameof(value));
+ }
+
+ return ((uint)value[0] << 24) |
+ ((uint)value[1] << 16) |
+ ((uint)value[2] << 8) |
+ (uint)value[3];
+ }
+}
- // Minimum audio buffer time in ms
- // Reference: SDL_coreaudio.m prepare_audioqueue line ~958
- internal const double MinimumAudioBufferTimeMs = 15.0;
+///
+/// Pure managed helper for SDL3 CoreAudio buffer-count policy.
+/// SDL3 uses three buffers by default and scales up only when the callback
+/// period is smaller than the minimum buffer time target.
+/// Reference: SDL/src/audio/coreaudio/SDL_coreaudio.m PrepareAudioQueue.
+///
+internal static class CoreAudioBufferPolicy
+{
+ ///
+ /// Computes the number of AudioQueue buffers SDL3 would allocate for playback.
+ ///
+ /// Playback sample rate in Hz.
+ /// Frames per callback period.
+ /// The number of queue buffers to allocate.
+ public static int ComputeAudioBufferCount(int sampleRate, int bufferFrames)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(sampleRate);
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferFrames);
+
+ int numAudioBuffers = 3;
+ double callbackPeriodMs = ((double)bufferFrames / sampleRate) * 1000.0;
+ if (callbackPeriodMs < CoreAudioConstants.MinimumAudioBufferTimeMs)
+ {
+ numAudioBuffers = (int)(Math.Ceiling(CoreAudioConstants.MinimumAudioBufferTimeMs / callbackPeriodMs) * 2);
+ }
+
+ return numAudioBuffers;
+ }
}
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/SdlCoreAudioDriver.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/SdlCoreAudioDriver.cs
index 8a6e402..42058df 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/SdlCoreAudioDriver.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/CoreAudio/SdlCoreAudioDriver.cs
@@ -6,190 +6,355 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Mac.CoreAudio;
using System.Threading;
///
-/// CoreAudio (AudioQueue) driver implementing ISdlAudioDriver.
-/// This is an exact port of SDL_coreaudio.m (SDL2) to C#.
+/// CoreAudio AudioQueue driver implementing .
+/// References:
+/// - SDL/src/audio/coreaudio/SDL_coreaudio.m
+/// - SDL/src/audio/coreaudio/SDL_coreaudio.h
+///
+/// This is a faithful port of SDL's macOS CoreAudio playback backend: default
+/// device preflight, AudioQueue ownership on a dedicated CFRunLoop thread, and
+/// the current_buffer / GetDeviceBuf / PlayDevice buffer-ready contract.
///
[SupportedOSPlatform("osx")]
-internal sealed class SdlCoreAudioDriver : ISdlAudioDriver {
+internal sealed class SdlCoreAudioDriver : ISdlAudioDriver
+{
+ ///
+ /// Byte value used to fill a buffer with silence.
+ /// Reference: SDL_audio.c device->silence_value, which is 0x00 for every
+ /// signed and floating point format, including the F32 format used here.
+ ///
+ private const byte SilenceValue = 0;
+
private IntPtr _audioQueue;
private IntPtr[] _audioBuffers = [];
- private IntPtr _mixBuffer;
- private int _mixBufferSize;
- private int _mixBufferOffset;
+ private IntPtr _currentBuffer;
private Thread? _audioQueueThread;
private volatile bool _shutdown;
private readonly ManualResetEventSlim _readySemaphore = new(false);
private string? _threadError;
private SdlAudioDevice? _device;
- private readonly object _mixerLock = new();
-
- // Must prevent GC of the delegate while AudioQueue is using it
private CoreAudioNativeMethods.AudioQueueOutputCallback? _outputCallbackDelegate;
private GCHandle _callbackHandle;
+ private GCHandle _deviceHandle;
private IntPtr _defaultRunLoopMode;
+ private uint _deviceId;
+
+ ///
+ /// Gets a value indicating whether CoreAudio owns the callback thread.
+ /// SDL3 marks CoreAudio as ProvidesOwnCallbackThread because
+ /// AudioQueue callbacks are driven by a CFRunLoop rather than SDL's
+ /// generic playback thread.
+ ///
+ public bool ProvidesOwnCallbackThread => true;
///
/// Opens the CoreAudio AudioQueue device.
- /// Reference: COREAUDIO_OpenDevice (SDL_coreaudio.m line 1062)
- ///
+ /// Reference: SDL_coreaudio.m COREAUDIO_OpenDevice.
+ ///
/// Flow:
- /// 1. Setup AudioStreamBasicDescription for float PCM
- /// 2. Create audioqueue_thread which calls prepare_audioqueue
- /// 3. Wait for ready semaphore
- /// 4. Return success/failure
+ /// 1. Preflight the default macOS output device (PrepareDevice).
+ /// 2. Create the ready semaphore and spawn the AudioQueue thread, so queue
+ /// creation happens on a thread that owns its own CFRunLoop.
+ /// 3. Wait for the thread, and propagate any thread_error it reported.
///
- public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error) {
+ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error)
+ {
obtainedSpec = desiredSpec;
sampleFrames = 0;
error = null;
_device = device;
_shutdown = false;
_threadError = null;
-
- // Reference: COREAUDIO_OpenDevice line 1098-1126
- // Setup AudioStreamBasicDescription for float LE format
- // SDL always uses AUDIO_F32LSB for the device spec on macOS
+ _deviceId = 0;
+ _currentBuffer = IntPtr.Zero;
int channels = desiredSpec.Channels;
int freq = desiredSpec.SampleRate;
int bufferFrames = desiredSpec.BufferFrames;
- // Calculate buffer size: frames * channels * sizeof(float)
- // Reference: SDL_CalculateAudioSpec equivalent
- _mixBufferSize = bufferFrames * channels * sizeof(float);
- _mixBufferOffset = _mixBufferSize; // Start fully consumed (triggers first callback fill)
-
- // Allocate the mix buffer
- // Reference: prepare_audioqueue line ~949-953
- _mixBuffer = Marshal.AllocHGlobal(_mixBufferSize);
- unsafe {
- NativeMemory.Clear(_mixBuffer.ToPointer(), (nuint)_mixBufferSize);
+ if (!PrepareDevice(out error))
+ {
+ return false;
}
- // Get kCFRunLoopDefaultMode
_defaultRunLoopMode = CoreAudioNativeMethods.GetDefaultRunLoopMode();
-
- // Reference: COREAUDIO_OpenDevice line 1127-1134
- // "This has to init in a new thread so it can get its own CFRunLoop."
_readySemaphore.Reset();
- _audioQueueThread = new Thread(() => AudioQueueThreadProc(freq, channels, bufferFrames)) {
+ // Reference: COREAUDIO_OpenDevice passes `device` as the AudioQueue user data.
+ _deviceHandle = GCHandle.Alloc(device);
+
+ _audioQueueThread = new Thread(() => AudioQueueThreadProc(freq, channels, bufferFrames))
+ {
Name = "CoreAudio-AudioQueue",
IsBackground = true
};
_audioQueueThread.Start();
- // Reference: COREAUDIO_OpenDevice line 1137-1138
- // SDL_SemWait(this->hidden->ready_semaphore)
_readySemaphore.Wait();
- if (_threadError != null) {
+ // Reference: COREAUDIO_OpenDevice
+ // "SDL_WaitThread(device->hidden->thread, NULL)" on thread_error.
+ if (_threadError != null)
+ {
+ _audioQueueThread.Join();
+ _audioQueueThread = null;
error = _threadError;
return false;
}
- // Reference: COREAUDIO_OpenDevice line 1140-1143
- // Reference: SDL_audio.c open_audio_device
- // If we don't allow negotiation, keep the desired buffer size
- int finalBufferFrames = bufferFrames;
- if (!desiredSpec.AllowNegotiate) {
- finalBufferFrames = desiredSpec.BufferFrames;
+ sampleFrames = bufferFrames;
+
+ return true;
+ }
+
+ ///
+ /// Resolves and validates the default macOS playback device.
+ /// Reference: SDL_coreaudio.m PrepareDevice.
+ ///
+ /// The full SDL3 backend works with discovered device handles; the managed
+ /// Spice86.Audio port currently supports default-device playback only, so it
+ /// mirrors SDL3's validation logic against the current default output device.
+ ///
+ /// Receives a human-readable failure description.
+ /// if the default output device is usable.
+ private bool PrepareDevice(out string? error)
+ {
+ error = null;
+
+ CoreAudioNativeMethods.AudioObjectPropertyAddress address =
+ CoreAudioNativeMethods.CreatePropertyAddress(
+ CoreAudioConstants.AudioHardwarePropertyDefaultOutputDevice,
+ CoreAudioConstants.AudioObjectPropertyScopeGlobal);
+
+ uint size = sizeof(uint);
+ int result = CoreAudioNativeMethods.AudioObjectGetPropertyData(
+ CoreAudioConstants.AudioObjectSystemObject,
+ ref address,
+ 0,
+ IntPtr.Zero,
+ ref size,
+ out uint deviceId);
+
+ if (result != CoreAudioConstants.NoErr || deviceId == 0)
+ {
+ error = $"CoreAudio: AudioObjectGetPropertyData(kAudioHardwarePropertyDefaultOutputDevice) failed with error {result}";
+ return false;
}
- obtainedSpec = new AudioSpec {
- SampleRate = freq,
- Channels = channels,
- BufferFrames = finalBufferFrames,
- Callback = desiredSpec.Callback,
- PostmixCallback = desiredSpec.PostmixCallback,
- AllowNegotiate = desiredSpec.AllowNegotiate
- };
- sampleFrames = finalBufferFrames;
+ address.Selector = CoreAudioConstants.AudioDevicePropertyDeviceIsAlive;
+ address.Scope = CoreAudioConstants.AudioDevicePropertyScopeOutput;
+ size = sizeof(uint);
+
+ result = CoreAudioNativeMethods.AudioObjectGetPropertyData(
+ deviceId,
+ ref address,
+ 0,
+ IntPtr.Zero,
+ ref size,
+ out uint alive);
+ if (result != CoreAudioConstants.NoErr)
+ {
+ error = $"CoreAudio: AudioObjectGetPropertyData(kAudioDevicePropertyDeviceIsAlive) failed with error {result}";
+ return false;
+ }
+
+ if (alive == 0)
+ {
+ error = "CoreAudio: requested default output device exists, but isn't alive.";
+ return false;
+ }
+
+ address.Selector = CoreAudioConstants.AudioDevicePropertyHogMode;
+ size = sizeof(int);
+
+ result = CoreAudioNativeMethods.AudioObjectGetPropertyData(
+ deviceId,
+ ref address,
+ 0,
+ IntPtr.Zero,
+ ref size,
+ out int hogModePid);
+
+ if (result == CoreAudioConstants.NoErr && hogModePid != -1)
+ {
+ error = "CoreAudio: default output device is being hogged.";
+ return false;
+ }
+
+ _deviceId = deviceId;
return true;
}
+ ///
+ /// Binds the AudioQueue to the previously selected CoreAudio device UID.
+ /// Reference: SDL_coreaudio.m AssignDeviceToAudioQueue.
+ ///
+ /// if the queue is bound to the selected device.
+ private bool AssignDeviceToAudioQueue()
+ {
+ if (_deviceId == 0)
+ {
+ _threadError = "CoreAudio: no output device was selected for AudioQueue binding.";
+ return false;
+ }
+
+ CoreAudioNativeMethods.AudioObjectPropertyAddress address =
+ CoreAudioNativeMethods.CreatePropertyAddress(
+ CoreAudioConstants.AudioDevicePropertyDeviceUid,
+ CoreAudioConstants.AudioDevicePropertyScopeOutput);
+
+ uint size = (uint)IntPtr.Size;
+ int result = CoreAudioNativeMethods.AudioObjectGetPropertyData(
+ _deviceId,
+ ref address,
+ 0,
+ IntPtr.Zero,
+ ref size,
+ out IntPtr deviceUid);
+
+ if (result != CoreAudioConstants.NoErr || deviceUid == IntPtr.Zero)
+ {
+ _threadError = $"CoreAudio: AudioObjectGetPropertyData(kAudioDevicePropertyDeviceUID) failed with error {result}";
+ return false;
+ }
+
+ try
+ {
+ result = CoreAudioNativeMethods.AudioQueueSetProperty(
+ _audioQueue,
+ CoreAudioConstants.AudioQueuePropertyCurrentDevice,
+ ref deviceUid,
+ (uint)IntPtr.Size);
+
+ if (result != CoreAudioConstants.NoErr)
+ {
+ _threadError = $"CoreAudio: AudioQueueSetProperty(kAudioQueueProperty_CurrentDevice) failed with error {result}";
+ return false;
+ }
+
+ return true;
+ }
+ finally
+ {
+ CoreAudioNativeMethods.CFRelease(deviceUid);
+ }
+ }
+
///
/// Closes the CoreAudio device.
- /// Reference: COREAUDIO_CloseDevice (SDL_coreaudio.m line 665)
- ///
+ /// Reference: SDL_coreaudio.m COREAUDIO_CloseDevice.
+ ///
/// Flow:
- /// 1. Set paused flag to feed silence from callback
- /// 2. AudioQueueFlush -> AudioQueueStop -> AudioQueueDispose
- /// 3. Wait for audioqueue_thread to finish
- /// 4. Free mix buffer
+ /// 1. AudioQueueFlush -> AudioQueueStop -> AudioQueueDispose, before joining
+ /// the thread, "or it might stall for a long time!"
+ /// 2. Wait for the AudioQueue thread.
+ /// 3. Release the callback and device handles.
///
- public void CloseDevice(SdlAudioDevice device) {
- // Reference: COREAUDIO_CloseDevice line 679
+ public void CloseDevice(SdlAudioDevice device)
+ {
+ // Reference: COREAUDIO_CloseDevice
// "if callback fires again, feed silence; don't call into the app."
// The shutdown flag is already set by SdlAudioDevice.Close()
+ _shutdown = true;
- // Reference: COREAUDIO_CloseDevice line 681-683
- // "dispose of the audio queue before waiting on the thread,
+ // Reference: COREAUDIO_CloseDevice
+ // "dispose of the audio queue before waiting on the thread,
// or it might stall for a long time!"
- if (_audioQueue != IntPtr.Zero) {
+ if (_audioQueue != IntPtr.Zero)
+ {
CoreAudioNativeMethods.AudioQueueFlush(_audioQueue);
CoreAudioNativeMethods.AudioQueueStop(_audioQueue, 0);
CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 0);
_audioQueue = IntPtr.Zero;
}
- // Reference: COREAUDIO_CloseDevice line 685-687
- // "SDL_WaitThread(this->hidden->thread, NULL)"
- _shutdown = true;
- if (_audioQueueThread != null && _audioQueueThread.IsAlive) {
+ // Reference: COREAUDIO_CloseDevice "SDL_WaitThread(device->hidden->thread, NULL)"
+ if (_audioQueueThread != null && _audioQueueThread.IsAlive)
+ {
_audioQueueThread.Join(TimeSpan.FromSeconds(5));
}
_audioQueueThread = null;
- // Free the mix buffer
- if (_mixBuffer != IntPtr.Zero) {
- Marshal.FreeHGlobal(_mixBuffer);
- _mixBuffer = IntPtr.Zero;
+ if (_callbackHandle.IsAllocated)
+ {
+ _callbackHandle.Free();
}
- // Free callback handle
- if (_callbackHandle.IsAllocated) {
- _callbackHandle.Free();
+ if (_deviceHandle.IsAllocated)
+ {
+ _deviceHandle.Free();
}
+ _currentBuffer = IntPtr.Zero;
+ _deviceId = 0;
_audioBuffers = [];
}
///
/// WaitDevice for CoreAudio is a no-op.
- /// CoreAudio uses ProvidesOwnCallbackThread, so SDL_RunAudio's
- /// WaitDevice/GetDeviceBuffer/PlayDevice loop is never used.
- /// The SdlAudioDevice thread will just idle, and CloseDevice's
- /// shutdown flag will break it out.
+ /// Reference: SDL marks CoreAudio as ProvidesOwnCallbackThread, so the
+ /// generic WaitDevice loop is never entered; the buffer-ready callback drives
+ /// each iteration instead.
///
- public void WaitDevice(SdlAudioDevice device) {
- // CoreAudio uses ProvidesOwnCallbackThread.
- // The SdlAudioDevice thread is idle; sleep to avoid busy-waiting.
- Thread.Sleep(100);
+ public void WaitDevice(SdlAudioDevice device)
+ {
}
///
- /// GetDeviceBuf for CoreAudio returns null.
- /// CoreAudio fills buffers directly in its outputCallback.
+ /// Returns the AudioQueue buffer that is currently being filled.
+ /// Reference: SDL_coreaudio.m COREAUDIO_GetDeviceBuf.
///
- public IntPtr GetDeviceBuf(SdlAudioDevice device) {
- return IntPtr.Zero;
+ public IntPtr GetDeviceBuf(SdlAudioDevice device)
+ {
+ // Reference: COREAUDIO_GetDeviceBuf
+ // "should have been called from PlaybackBufferReadyCallback"
+ IntPtr currentBuffer = _currentBuffer;
+ if (currentBuffer == IntPtr.Zero)
+ {
+ return IntPtr.Zero;
+ }
+
+ unsafe
+ {
+ CoreAudioNativeMethods.AudioQueueBuffer* bufPtr =
+ (CoreAudioNativeMethods.AudioQueueBuffer*)currentBuffer;
+ return bufPtr->AudioData;
+ }
}
///
- /// PlayDevice for CoreAudio is a no-op.
- /// CoreAudio enqueues buffers directly in its outputCallback.
+ /// Submits the filled AudioQueue buffer back to CoreAudio.
+ /// Reference: SDL_coreaudio.m COREAUDIO_PlayDevice.
///
- public void PlayDevice(SdlAudioDevice device) {
+ public void PlayDevice(SdlAudioDevice device)
+ {
+ // Reference: COREAUDIO_PlayDevice
+ // "should have been called from PlaybackBufferReadyCallback"
+ IntPtr currentBuffer = _currentBuffer;
+ if (currentBuffer == IntPtr.Zero)
+ {
+ return;
+ }
+
+ unsafe
+ {
+ CoreAudioNativeMethods.AudioQueueBuffer* bufPtr =
+ (CoreAudioNativeMethods.AudioQueueBuffer*)currentBuffer;
+ bufPtr->AudioDataByteSize = bufPtr->AudioDataBytesCapacity;
+ }
+
+ _currentBuffer = IntPtr.Zero;
+ CoreAudioNativeMethods.AudioQueueEnqueueBuffer(_audioQueue, currentBuffer, 0, IntPtr.Zero);
}
///
- /// ThreadInit for CoreAudio - sets thread priority.
- /// Reference: audioqueue_thread line 1020
- /// SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH)
+ /// ThreadInit for CoreAudio is a no-op in the managed SDL thread.
+ /// Reference: SDL3 does its priority adjustment in AudioQueueThreadEntry,
+ /// which corresponds to .
///
- public void ThreadInit(SdlAudioDevice device) {
+ public void ThreadInit(SdlAudioDevice device)
+ {
// CoreAudio manages its own thread. The SdlAudioDevice thread
// is essentially idle for CoreAudio.
}
@@ -197,12 +362,13 @@ public void ThreadInit(SdlAudioDevice device) {
///
/// ThreadDeinit for CoreAudio is a no-op.
///
- public void ThreadDeinit(SdlAudioDevice device) {
+ public void ThreadDeinit(SdlAudioDevice device)
+ {
}
///
/// The AudioQueue thread function.
- /// Reference: audioqueue_thread (SDL_coreaudio.m line 991)
+ /// Reference: SDL_coreaudio.m AudioQueueThreadEntry.
///
/// Flow:
/// 1. Call prepare_audioqueue (creates AudioQueue on this thread's CFRunLoop)
@@ -210,53 +376,63 @@ public void ThreadDeinit(SdlAudioDevice device) {
/// 3. Loop CFRunLoopRunInMode until shutdown
/// 4. On exit, drain remaining playback
///
- private void AudioQueueThreadProc(int sampleRate, int channels, int bufferFrames) {
- // Reference: audioqueue_thread line 1010-1013
- // prepare_audioqueue creates the AudioQueue bound to this thread's CFRunLoop
- if (!PrepareAudioQueue(sampleRate, channels, bufferFrames)) {
- _threadError = _threadError ?? "Failed to prepare AudioQueue";
+ private void AudioQueueThreadProc(int sampleRate, int channels, int bufferFrames)
+ {
+ // Reference: AudioQueueThreadEntry "SDL_PlaybackAudioThreadSetup(device)",
+ // which raises the thread to SDL_THREAD_PRIORITY_HIGH before preparing the queue.
+ Thread.CurrentThread.Priority = ThreadPriority.Highest;
+
+ // Reference: AudioQueueThreadEntry
+ // PrepareAudioQueue creates the AudioQueue bound to this thread's CFRunLoop
+ if (!PrepareAudioQueue(sampleRate, channels, bufferFrames))
+ {
+ _threadError ??= "Failed to prepare AudioQueue";
_readySemaphore.Set();
return;
}
- // Reference: audioqueue_thread line 1020
- // SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH)
- Thread.CurrentThread.Priority = ThreadPriority.AboveNormal;
-
- // Reference: audioqueue_thread line 1023
+ // Reference: AudioQueueThreadEntry
// "init was successful, alert parent thread and start running..."
_readySemaphore.Set();
- // Reference: audioqueue_thread line 1025-1059
- // Main run loop
- while (!_shutdown && (_device == null || !_device.ShutdownRequested)) {
- // Reference: audioqueue_thread line 1026
- // CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.10, 1)
- CoreAudioNativeMethods.CFRunLoopRunInMode(_defaultRunLoopMode, 0.10, 1);
+ // Reference: AudioQueueThreadEntry
+ // "This would be WaitDevice in the normal SDL audio thread, but we get
+ // *BufferReadyCallback calls here to know when to iterate."
+ while (!_shutdown && (_device == null || !_device.ShutdownRequested))
+ {
+ CFRunLoopRunInModeDefault(0.10, true);
}
- // Reference: audioqueue_thread line 1061-1064
- // "if (!this->iscapture)" - drain off any pending playback
- if (_device != null) {
- double secs = (((double)_mixBufferSize / sizeof(float)) / channels) / sampleRate * 2.0;
- CoreAudioNativeMethods.CFRunLoopRunInMode(_defaultRunLoopMode, secs, 0);
- }
+ // Reference: AudioQueueThreadEntry "Drain off any pending playback."
+ // const CFTimeInterval secs = (sample_frames / spec.freq) * 2.0
+ double secs = ((double)bufferFrames / sampleRate) * 2.0;
+ CFRunLoopRunInModeDefault(secs, false);
+ }
+
+ private void CFRunLoopRunInModeDefault(double seconds, bool returnAfterSourceHandled)
+ {
+ CoreAudioNativeMethods.CFRunLoopRunInMode(
+ _defaultRunLoopMode,
+ seconds,
+ (byte)(returnAfterSourceHandled ? 1 : 0));
}
///
/// Prepares the AudioQueue.
- /// Reference: prepare_audioqueue (SDL_coreaudio.m line 896)
+ /// Reference: SDL_coreaudio.m PrepareAudioQueue.
///
- /// Flow:
- /// 1. Create AudioQueueNewOutput with float PCM format
- /// 2. Set channel layout
- /// 3. Allocate and enqueue audio buffers
- /// 4. Start the AudioQueue
+ /// Actual managed flow:
+ /// 1. Create an output queue for the requested managed float mix format.
+ /// 2. Bind the queue to the selected CoreAudio default device.
+ /// 3. Apply SDL3-style channel-layout and buffer-count policy.
+ /// 4. Prime the queue with silence and start playback.
///
- private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames) {
+ private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames)
+ {
// Reference: prepare_audioqueue line ~896-900
// Setup AudioStreamBasicDescription
- CoreAudioNativeMethods.AudioStreamBasicDescription strdesc = new() {
+ CoreAudioNativeMethods.AudioStreamBasicDescription strdesc = new()
+ {
SampleRate = sampleRate,
FormatId = CoreAudioConstants.AudioFormatLinearPcm,
// Float LE + Packed (matching SDL's AUDIO_F32LSB path)
@@ -279,21 +455,29 @@ private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames) {
int result = CoreAudioNativeMethods.AudioQueueNewOutput(
ref strdesc,
_outputCallbackDelegate,
- IntPtr.Zero,
+ GCHandle.ToIntPtr(_deviceHandle),
currentRunLoop,
_defaultRunLoopMode,
0,
out _audioQueue);
- if (result != CoreAudioConstants.NoErr) {
+ if (result != CoreAudioConstants.NoErr)
+ {
_threadError = $"CoreAudio: AudioQueueNewOutput failed with error {result}";
return false;
}
- // Reference: prepare_audioqueue line ~920-944
- // Set channel layout
+ if (!AssignDeviceToAudioQueue())
+ {
+ CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 1);
+ _audioQueue = IntPtr.Zero;
+ return false;
+ }
+
+ // Reference: PrepareAudioQueue channel layout selection
CoreAudioNativeMethods.AudioChannelLayout layout = new();
- switch (channels) {
+ switch (channels)
+ {
case 1:
layout.ChannelLayoutTag = CoreAudioConstants.AudioChannelLayoutTagMono;
break;
@@ -312,62 +496,86 @@ private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames) {
case 6:
layout.ChannelLayoutTag = CoreAudioConstants.AudioChannelLayoutTagDvd12;
break;
+ case 7:
+ // Reference: PrepareAudioQueue
+ // "kAudioChannelLayoutTag_WAVE_6_1" on macOS 10.15+, which is the
+ // minimum supported by this runtime.
+ layout.ChannelLayoutTag = CoreAudioConstants.AudioChannelLayoutTagWave61;
+ break;
+ case 8:
+ // Reference: PrepareAudioQueue "kAudioChannelLayoutTag_WAVE_7_1".
+ layout.ChannelLayoutTag = CoreAudioConstants.AudioChannelLayoutTagWave71;
+ break;
default:
- _threadError = $"CoreAudio: Unsupported audio channels: {channels}";
+ // Reference: PrepareAudioQueue SDL_SetError("Unsupported audio channels")
+ _threadError = "Unsupported audio channels";
CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 1);
_audioQueue = IntPtr.Zero;
return false;
}
- if (layout.ChannelLayoutTag != 0) {
+ if (layout.ChannelLayoutTag != 0)
+ {
int layoutSize = Marshal.SizeOf();
IntPtr layoutPtr = Marshal.AllocHGlobal(layoutSize);
- try {
+ try
+ {
Marshal.StructureToPtr(layout, layoutPtr, false);
result = CoreAudioNativeMethods.AudioQueueSetProperty(
_audioQueue,
CoreAudioConstants.AudioQueuePropertyChannelLayout,
layoutPtr,
(uint)layoutSize);
- // Ignore errors - not critical (SDL does CHECK_RESULT but continues)
- } finally {
+ }
+ finally
+ {
Marshal.FreeHGlobal(layoutPtr);
}
+
+ // Reference: PrepareAudioQueue
+ // CHECK_RESULT("AudioQueueSetProperty (kAudioQueueProperty_ChannelLayout)")
+ if (result != CoreAudioConstants.NoErr)
+ {
+ _threadError = $"CoreAudio: AudioQueueSetProperty (kAudioQueueProperty_ChannelLayout) failed with error {result}";
+ CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 1);
+ _audioQueue = IntPtr.Zero;
+ return false;
+ }
}
// Reference: prepare_audioqueue line ~956-970
// Calculate number of audio buffers
// "Make sure we can feed the device a minimum amount of time"
uint bufferSizeBytes = (uint)(bufferFrames * channels * sizeof(float));
- double msecs = ((double)bufferFrames / sampleRate) * 1000.0;
- int numAudioBuffers = 2;
-
- if (msecs < CoreAudioConstants.MinimumAudioBufferTimeMs) {
- // Use more buffers if we have a VERY small sample set
- numAudioBuffers = (int)(Math.Ceiling(CoreAudioConstants.MinimumAudioBufferTimeMs / msecs) * 2);
- }
+ int numAudioBuffers = CoreAudioBufferPolicy.ComputeAudioBufferCount(sampleRate, bufferFrames);
_audioBuffers = new IntPtr[numAudioBuffers];
- for (int i = 0; i < numAudioBuffers; i++) {
+ for (int i = 0; i < numAudioBuffers; i++)
+ {
result = CoreAudioNativeMethods.AudioQueueAllocateBuffer(
_audioQueue,
bufferSizeBytes,
out _audioBuffers[i]);
- if (result != CoreAudioConstants.NoErr) {
+ if (result != CoreAudioConstants.NoErr)
+ {
_threadError = $"CoreAudio: AudioQueueAllocateBuffer failed with error {result}";
CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 1);
_audioQueue = IntPtr.Zero;
return false;
}
- // Fill with silence and set size
- // Reference: prepare_audioqueue line ~967
- unsafe {
+ // Reference: PrepareAudioQueue
+ // SDL_memset(device->hidden->audioBuffer[i]->mAudioData, device->silence_value, ...)
+ unsafe
+ {
CoreAudioNativeMethods.AudioQueueBuffer* bufPtr =
(CoreAudioNativeMethods.AudioQueueBuffer*)_audioBuffers[i];
- NativeMemory.Clear(bufPtr->AudioData.ToPointer(), bufPtr->AudioDataBytesCapacity);
+ NativeMemory.Fill(
+ bufPtr->AudioData.ToPointer(),
+ bufPtr->AudioDataBytesCapacity,
+ SilenceValue);
bufPtr->AudioDataByteSize = bufPtr->AudioDataBytesCapacity;
}
@@ -375,7 +583,8 @@ private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames) {
result = CoreAudioNativeMethods.AudioQueueEnqueueBuffer(
_audioQueue, _audioBuffers[i], 0, IntPtr.Zero);
- if (result != CoreAudioConstants.NoErr) {
+ if (result != CoreAudioConstants.NoErr)
+ {
_threadError = $"CoreAudio: AudioQueueEnqueueBuffer failed with error {result}";
CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 1);
_audioQueue = IntPtr.Zero;
@@ -386,7 +595,8 @@ private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames) {
// Reference: prepare_audioqueue line ~972
// Start the AudioQueue
result = CoreAudioNativeMethods.AudioQueueStart(_audioQueue, IntPtr.Zero);
- if (result != CoreAudioConstants.NoErr) {
+ if (result != CoreAudioConstants.NoErr)
+ {
_threadError = $"CoreAudio: AudioQueueStart failed with error {result}";
CoreAudioNativeMethods.AudioQueueDispose(_audioQueue, 1);
_audioQueue = IntPtr.Zero;
@@ -398,70 +608,95 @@ private bool PrepareAudioQueue(int sampleRate, int channels, int bufferFrames) {
///
/// The AudioQueue output callback.
- /// Reference: outputCallback (SDL_coreaudio.m line 461)
- ///
- /// This is called by CoreAudio when a buffer has been consumed and needs refilling.
- /// The callback directly invokes the user callback to get audio data.
- ///
- /// SDL flow (non-stream path, which is what we use):
- /// 1. Lock mixer_lock
- /// 2. While remaining bytes in buffer:
- /// a. If bufferOffset >= bufferSize, call user callback to fill mix buffer
- /// b. Copy from mix buffer to AudioQueue buffer
- /// 3. Enqueue the buffer back
- /// 4. Unlock mixer_lock
+ /// Reference: SDL_coreaudio.m PlaybackBufferReadyCallback.
+ ///
+ /// Flow:
+ /// 1. Publish the ready buffer as current_buffer.
+ /// 2. Run one playback iteration, which calls GetDeviceBuf, fills it, and
+ /// hands it back through PlayDevice.
+ /// 3. If the buffer is unexpectedly still pending, we're probably dying:
+ /// requeue it filled with the silence value.
///
- private void OutputCallback(IntPtr inUserData, IntPtr inAudioQueue, IntPtr inBuffer) {
- // Reference: outputCallback line 463-466
- // Check shutdown before and after lock
- if (_device == null || _device.ShutdownRequested) {
+ private void OutputCallback(IntPtr inUserData, IntPtr inAudioQueue, IntPtr inBuffer)
+ {
+ // Reference: PlaybackBufferReadyCallback
+ // "SDL_AudioDevice *device = (SDL_AudioDevice *)inUserData"
+ SdlAudioDevice? device = null;
+ if (inUserData != IntPtr.Zero)
+ {
+ device = GCHandle.FromIntPtr(inUserData).Target as SdlAudioDevice;
+ }
+
+ device ??= _device;
+ if (device == null)
+ {
return;
}
- lock (_mixerLock) {
- if (_device.ShutdownRequested) {
- return;
+ // Reference: PlaybackBufferReadyCallback
+ // "device->hidden->current_buffer = inBuffer"
+ _currentBuffer = inBuffer;
+
+ bool okay = PlaybackAudioThreadIterate(device);
+
+ // Reference: PlaybackBufferReadyCallback
+ // "buffer is unexpectedly here? We're probably dying, but try to
+ // requeue this buffer with silence."
+ if (!okay && _currentBuffer != IntPtr.Zero)
+ {
+ IntPtr currentBuffer = _currentBuffer;
+ _currentBuffer = IntPtr.Zero;
+
+ unsafe
+ {
+ CoreAudioNativeMethods.AudioQueueBuffer* bufPtr =
+ (CoreAudioNativeMethods.AudioQueueBuffer*)currentBuffer;
+ NativeMemory.Fill(
+ bufPtr->AudioData.ToPointer(),
+ bufPtr->AudioDataBytesCapacity,
+ SilenceValue);
+ }
+
+ CoreAudioNativeMethods.AudioQueueEnqueueBuffer(inAudioQueue, currentBuffer, 0, IntPtr.Zero);
+ }
+ }
+
+ ///
+ /// Runs a single playback iteration for the buffer that CoreAudio just released.
+ /// Reference: SDL_audio.c SDL_PlaybackAudioThreadIterate, as driven by
+ /// PlaybackBufferReadyCallback.
+ ///
+ /// The device owning the callback.
+ /// when the device is shutting down.
+ private bool PlaybackAudioThreadIterate(SdlAudioDevice device)
+ {
+ lock (device.MixerLock)
+ {
+ // Reference: SDL_PlaybackAudioThreadIterate
+ // "if (SDL_GetAtomicInt(&device->shutdown)) { return false; }"
+ if (_shutdown || device.ShutdownRequested)
+ {
+ return false;
+ }
+
+ IntPtr deviceBuffer = GetDeviceBuf(device);
+ if (deviceBuffer == IntPtr.Zero)
+ {
+ return false;
}
- unsafe {
+ int bufferSize;
+ unsafe
+ {
CoreAudioNativeMethods.AudioQueueBuffer* bufPtr =
- (CoreAudioNativeMethods.AudioQueueBuffer*)inBuffer;
-
- uint remaining = bufPtr->AudioDataBytesCapacity;
- byte* ptr = (byte*)bufPtr->AudioData;
-
- // Reference: outputCallback line 501-518 (non-stream path)
- while (remaining > 0) {
- if (_mixBufferOffset >= _mixBufferSize) {
- // Generate the data via the user callback
- // Reference: outputCallback line 504-505
- _device.FillAudioBuffer(_mixBuffer, _mixBufferSize);
- _mixBufferOffset = 0;
- }
-
- uint len = (uint)(_mixBufferSize - _mixBufferOffset);
- if (len > remaining) {
- len = remaining;
- }
-
- // Reference: outputCallback line 512-513
- Buffer.MemoryCopy(
- ((byte*)_mixBuffer + _mixBufferOffset),
- ptr,
- remaining,
- len);
-
- ptr += len;
- remaining -= len;
- _mixBufferOffset += (int)len;
- }
-
- // Reference: outputCallback line 487-489
- // Enqueue the buffer back and set its size
- CoreAudioNativeMethods.AudioQueueEnqueueBuffer(
- inAudioQueue, inBuffer, 0, IntPtr.Zero);
- bufPtr->AudioDataByteSize = bufPtr->AudioDataBytesCapacity;
+ (CoreAudioNativeMethods.AudioQueueBuffer*)_currentBuffer;
+ bufferSize = (int)bufPtr->AudioDataBytesCapacity;
}
+
+ device.FillAudioBuffer(deviceBuffer, bufferSize);
+ PlayDevice(device);
+
+ return true;
}
}
}
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/SdlMacBackend.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/SdlMacBackend.cs
index 9f55422..21d05e5 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/SdlMacBackend.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Mac/SdlMacBackend.cs
@@ -6,24 +6,30 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Mac;
using Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Mac.CoreAudio;
///
-/// SDL audio backend for macOS using CoreAudio (AudioQueue).
-/// Reference: DOSBox Staging's SDL audio integration (mixer.cpp)
+/// SDL-style audio backend for macOS using CoreAudio AudioQueue.
+/// Mirrors the playback-only path of SDL3's CoreAudio backend in
+/// SDL/src/audio/coreaudio/SDL_coreaudio.m as closely as the managed
+/// Spice86.Audio abstraction allows.
///
-/// NOTE: CoreAudio uses ProvidesOwnCallbackThread. The AudioQueue manages
-/// its own callback thread via CFRunLoop, so the SdlAudioDevice thread
-/// is mostly idle. The outputCallback (in SdlCoreAudioDriver) directly
-/// fills audio buffers from the user callback.
+/// Actual usage in this repository is narrower than SDL3 itself:
+/// playback only, default-device selection only, and a managed float callback
+/// contract that is translated into the backend's native queue model.
///
[SupportedOSPlatform("osx")]
-public sealed class SdlMacBackend : IAudioBackend {
+public sealed class SdlMacBackend : IAudioBackend
+{
private readonly SdlAudioDevice _device;
private AudioDeviceState _state = AudioDeviceState.Stopped;
private string? _lastError;
///
/// Initializes a new instance of the class.
+ /// The backend delegates SDL-style device lifecycle management to
+ /// and CoreAudio-specific queue control to
+ /// .
///
- public SdlMacBackend() {
+ public SdlMacBackend()
+ {
_device = new SdlAudioDevice(new SdlCoreAudioDriver());
}
@@ -37,11 +43,13 @@ public SdlMacBackend() {
public string? LastError => _lastError;
///
- public bool Open(AudioSpec desiredSpec) {
+ public bool Open(AudioSpec desiredSpec)
+ {
ArgumentNullException.ThrowIfNull(desiredSpec);
ArgumentNullException.ThrowIfNull(desiredSpec.Callback);
- if (!_device.Open(desiredSpec)) {
+ if (!_device.Open(desiredSpec))
+ {
_lastError = _device.LastError;
_state = AudioDeviceState.Error;
return false;
@@ -52,8 +60,10 @@ public bool Open(AudioSpec desiredSpec) {
}
///
- public void Start() {
- if (_state == AudioDeviceState.Playing) {
+ public void Start()
+ {
+ if (_state == AudioDeviceState.Playing)
+ {
return;
}
@@ -62,8 +72,10 @@ public void Start() {
}
///
- public void Pause() {
- if (_state != AudioDeviceState.Playing) {
+ public void Pause()
+ {
+ if (_state != AudioDeviceState.Playing)
+ {
return;
}
@@ -72,13 +84,15 @@ public void Pause() {
}
///
- public void Close() {
+ public void Close()
+ {
_device.Close();
_state = AudioDeviceState.Stopped;
}
///
- public void Dispose() {
+ public void Dispose()
+ {
Close();
}
}
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/SdlAudioDevice.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/SdlAudioDevice.cs
index 2e81e06..f73618d 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/SdlAudioDevice.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/SdlAudioDevice.cs
@@ -8,7 +8,8 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl;
/// SDL audio device abstraction. Manages the audio thread and callback lifecycle.
/// Reference: SDL_AudioDevice from SDL_sysaudio.h, open_audio_device/close_audio_device/SDL_RunAudio from SDL_audio.c
///
-internal sealed class SdlAudioDevice {
+internal sealed class SdlAudioDevice
+{
private readonly ISdlAudioDriver _driver;
private readonly object _mixerLock = new();
private Thread? _audioThread;
@@ -18,7 +19,8 @@ internal sealed class SdlAudioDevice {
private IntPtr _workBuffer;
private SdlAudioDeviceCore? _core;
- public SdlAudioDevice(ISdlAudioDriver driver) {
+ public SdlAudioDevice(ISdlAudioDriver driver)
+ {
_driver = driver;
}
@@ -53,6 +55,14 @@ public SdlAudioDevice(ISdlAudioDriver driver) {
///
internal bool ShutdownRequested => _shutdown;
+ ///
+ /// Gets the device-level mixer lock.
+ /// Reference: SDL_audio.c device->lock, which drivers acquire through
+ /// SDL_PlaybackAudioThreadIterate. Drivers that provide their own callback
+ /// thread lock this same object instead of using a private lock.
+ ///
+ internal object MixerLock => _mixerLock;
+
///
/// Whether the device is enabled.
/// Reference: SDL_AtomicGet(&device->enabled)
@@ -63,7 +73,8 @@ public SdlAudioDevice(ISdlAudioDriver driver) {
/// Marks the device as disconnected.
/// Reference: SDL_OpenedAudioDeviceDisconnected
///
- internal void SetDeviceDisconnected() {
+ internal void SetDeviceDisconnected()
+ {
_enabled = false;
}
@@ -74,7 +85,8 @@ internal void SetDeviceDisconnected() {
/// The audio thread is created here and waits for the startup semaphore,
/// matching SDL's open_audio_device which creates the thread and SemWaits.
///
- public bool Open(AudioSpec desiredSpec) {
+ public bool Open(AudioSpec desiredSpec)
+ {
// Reference: open_audio_device lines 1468-1470
_shutdown = false;
_paused = true;
@@ -84,16 +96,19 @@ public bool Open(AudioSpec desiredSpec) {
int bufferFrames = desiredSpec.BufferFrames > 0
? desiredSpec.BufferFrames
: GetDefaultSamplesFromFreq(desiredSpec.SampleRate);
- AudioSpec requestedSpec = new AudioSpec {
+ AudioSpec requestedSpec = new AudioSpec
+ {
SampleRate = desiredSpec.SampleRate,
Channels = desiredSpec.Channels,
BufferFrames = bufferFrames,
Callback = desiredSpec.Callback,
- PostmixCallback = desiredSpec.PostmixCallback
+ PostmixCallback = desiredSpec.PostmixCallback,
+ AllowNegotiate = desiredSpec.AllowNegotiate
};
bool ok = _driver.OpenDevice(this, requestedSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error);
- if (!ok) {
+ if (!ok)
+ {
LastError = error;
return false;
}
@@ -102,22 +117,31 @@ public bool Open(AudioSpec desiredSpec) {
ObtainedSpec = obtainedSpec;
SampleFrames = sampleFrames;
BufferSizeBytes = SampleFrames * ObtainedSpec.Channels * sizeof(float);
- if (ObtainedSpec.Callback != null) {
+ if (ObtainedSpec.Callback != null)
+ {
_core = new SdlAudioDeviceCore(ObtainedSpec, BufferSizeBytes);
}
+ if (_driver.ProvidesOwnCallbackThread)
+ {
+ return true;
+ }
+
// Reference: open_audio_device line 1512
// device->work_buffer = (Uint8 *)SDL_malloc(device->callbackspec.size)
_workBuffer = Marshal.AllocHGlobal(BufferSizeBytes);
- unsafe {
+ unsafe
+ {
NativeMemory.Clear(_workBuffer.ToPointer(), (nuint)BufferSizeBytes);
}
// Reference: open_audio_device lines 1548-1572
// SDL creates the audio thread during open_audio_device and waits
// for it to signal via a semaphore that ThreadInit has completed.
- using (SemaphoreSlim startupSemaphore = new SemaphoreSlim(0, 1)) {
- _audioThread = new Thread(() => RunAudio(startupSemaphore)) {
+ using (SemaphoreSlim startupSemaphore = new SemaphoreSlim(0, 1))
+ {
+ _audioThread = new Thread(() => RunAudio(startupSemaphore))
+ {
Name = "SDL-Audio-Playback",
IsBackground = true
};
@@ -132,7 +156,8 @@ public bool Open(AudioSpec desiredSpec) {
/// Unpauses the audio device.
/// Reference: SDL_PauseAudioDevice(device, 0)
///
- public void Start() {
+ public void Start()
+ {
_paused = false;
}
@@ -140,7 +165,8 @@ public void Start() {
/// Pauses audio playback.
/// Reference: SDL_PauseAudioDevice(device, 1)
///
- public void Pause() {
+ public void Pause()
+ {
_paused = true;
}
@@ -148,22 +174,26 @@ public void Pause() {
/// Closes the device and stops the audio thread.
/// Reference: close_audio_device() from SDL_audio.c lines 1196-1236
///
- public void Close() {
+ public void Close()
+ {
// Reference: close_audio_device lines 1204-1209
// Lock, set paused+shutdown+enabled, unlock, then wait for thread.
- lock (_mixerLock) {
+ lock (_mixerLock)
+ {
_paused = true;
_shutdown = true;
_enabled = false;
}
- if (_audioThread != null && _audioThread.IsAlive) {
+ if (_audioThread != null && _audioThread.IsAlive)
+ {
_audioThread.Join(TimeSpan.FromSeconds(2));
}
_audioThread = null;
_core = null;
- if (_workBuffer != IntPtr.Zero) {
+ if (_workBuffer != IntPtr.Zero)
+ {
Marshal.FreeHGlobal(_workBuffer);
_workBuffer = IntPtr.Zero;
}
@@ -175,7 +205,8 @@ public void Close() {
/// The audio thread main loop.
/// Reference: SDL_RunAudio from SDL_audio.c lines 672-804
///
- private unsafe void RunAudio(SemaphoreSlim startupSemaphore) {
+ private unsafe void RunAudio(SemaphoreSlim startupSemaphore)
+ {
// SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL)
Thread.CurrentThread.Priority = ThreadPriority.Highest;
@@ -186,40 +217,52 @@ private unsafe void RunAudio(SemaphoreSlim startupSemaphore) {
_driver.ThreadInit(this);
// Loop, filling the audio buffers
- while (!_shutdown) {
+ while (!_shutdown)
+ {
IntPtr data;
// if (!device->stream && SDL_AtomicGet(&device->enabled))
- if (_enabled) {
+ if (_enabled)
+ {
data = _driver.GetDeviceBuf(this);
- } else {
+ }
+ else
+ {
data = IntPtr.Zero;
}
bool usingWorkBuffer = data == IntPtr.Zero;
- if (usingWorkBuffer) {
+ if (usingWorkBuffer)
+ {
data = _workBuffer;
}
int dataLen = BufferSizeBytes;
// SDL_LockMutex(device->mixer_lock)
- lock (_mixerLock) {
- if (_paused) {
+ lock (_mixerLock)
+ {
+ if (_paused)
+ {
// SDL_memset(data, device->callbackspec.silence, data_len)
NativeMemory.Clear(data.ToPointer(), (nuint)dataLen);
- } else if (_core != null) {
+ }
+ else if (_core != null)
+ {
_core.FillDeviceBuffer(data, dataLen);
}
}
// SDL_UnlockMutex(device->mixer_lock)
- if (usingWorkBuffer) {
+ if (usingWorkBuffer)
+ {
// nothing to do; pause like we queued a buffer to play.
// delay = ((device->spec.samples * 1000) / device->spec.freq)
int delay = (SampleFrames * 1000) / ObtainedSpec.SampleRate;
Thread.Sleep(delay);
- } else {
+ }
+ else
+ {
// current_audio.impl.PlayDevice(device)
_driver.PlayDevice(this);
// current_audio.impl.WaitDevice(device)
@@ -230,7 +273,8 @@ private unsafe void RunAudio(SemaphoreSlim startupSemaphore) {
// Wait for the audio to drain.
// delay = ((device->spec.samples * 1000) / device->spec.freq) * 2
int drainDelay = ((SampleFrames * 1000) / ObtainedSpec.SampleRate) * 2;
- if (drainDelay > 100) {
+ if (drainDelay > 100)
+ {
drainDelay = 100;
}
Thread.Sleep(drainDelay);
@@ -243,14 +287,17 @@ private unsafe void RunAudio(SemaphoreSlim startupSemaphore) {
/// Fills the audio buffer via the callback or with silence.
/// Reference: SDL_RunAudio callback invocation (lines 720-770)
///
- internal unsafe void FillAudioBuffer(IntPtr bufferPtr, int bufferBytes) {
+ internal unsafe void FillAudioBuffer(IntPtr bufferPtr, int bufferBytes)
+ {
// Reference: SDL_RunAudio lines 740-743
- if (_paused) {
+ if (_paused)
+ {
NativeMemory.Clear(bufferPtr.ToPointer(), (nuint)bufferBytes);
return;
}
- if (_core != null) {
+ if (_core != null)
+ {
_core.FillDeviceBuffer(bufferPtr, bufferBytes);
return;
}
@@ -262,11 +309,13 @@ internal unsafe void FillAudioBuffer(IntPtr bufferPtr, int bufferBytes) {
/// Computes the default sample frames from frequency.
/// Reference: GetDefaultSamplesFromFreq in SDL_audio.c
///
- private static int GetDefaultSamplesFromFreq(int frequency) {
+ private static int GetDefaultSamplesFromFreq(int frequency)
+ {
// Pick a default of ~46 ms at desired frequency
int maxSampleFrames = (frequency / 1000) * 46;
int current = 1;
- while (current < maxSampleFrames) {
+ while (current < maxSampleFrames)
+ {
current *= 2;
}
return current;
diff --git a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Windows/Wasapi/SdlWasapiDriver.cs b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Windows/Wasapi/SdlWasapiDriver.cs
index d297880..f63c6ba 100644
--- a/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Windows/Wasapi/SdlWasapiDriver.cs
+++ b/src/Spice86.Audio/Backend/Audio/CrossPlatform/Sdl/Windows/Wasapi/SdlWasapiDriver.cs
@@ -8,7 +8,8 @@ namespace Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Windows.Wasapi;
using Spice86.Audio.Backend.Audio.CrossPlatform.Sdl;
[SupportedOSPlatform("windows")]
-internal sealed partial class SdlWasapiDriver : ISdlAudioDriver {
+internal sealed partial class SdlWasapiDriver : ISdlAudioDriver
+{
private const uint ClsctxAll = 0x17;
private const int WaitTimeoutMs = 200;
private const uint WaitObject0 = 0;
@@ -27,20 +28,26 @@ internal sealed partial class SdlWasapiDriver : ISdlAudioDriver {
private IntPtr _avrtHandle;
private IntPtr _taskHandle;
- public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error) {
+ public bool ProvidesOwnCallbackThread => false;
+
+ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error)
+ {
obtainedSpec = desiredSpec;
sampleFrames = 0;
error = null;
- try {
+ try
+ {
Type? enumeratorType = Type.GetTypeFromCLSID(SdlWasapiGuids.ClsidMmDeviceEnumerator);
- if (enumeratorType == null) {
+ if (enumeratorType == null)
+ {
error = "Failed to get MMDeviceEnumerator type";
return false;
}
object? enumeratorObj = Activator.CreateInstance(enumeratorType);
- if (enumeratorObj is not IMMDeviceEnumerator enumerator) {
+ if (enumeratorObj is not IMMDeviceEnumerator enumerator)
+ {
error = "Failed to create MMDeviceEnumerator";
return false;
}
@@ -48,7 +55,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
_deviceEnumerator = enumerator;
int hr = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console, out IMMDevice deviceEndpoint);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
error = $"Failed to get default audio endpoint: 0x{hr:X8}";
return false;
}
@@ -57,7 +65,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
Guid iidAudioClient = SdlWasapiGuids.IidIaudioClient;
hr = _device.Activate(ref iidAudioClient, ClsctxAll, IntPtr.Zero, out object audioClientObj);
- if (SdlWasapiResult.Failed(hr) || audioClientObj is not IAudioClient audioClient) {
+ if (SdlWasapiResult.Failed(hr) || audioClientObj is not IAudioClient audioClient)
+ {
error = $"Failed to activate audio client: 0x{hr:X8}";
return false;
}
@@ -67,7 +76,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Reference: SDL_wasapi.c WASAPI_PrepDevice
// Create event handle for buffer notifications
_bufferEvent = NativeMethods.CreateEventW(IntPtr.Zero, false, false, null);
- if (_bufferEvent == IntPtr.Zero) {
+ if (_bufferEvent == IntPtr.Zero)
+ {
error = "Failed to create event handle";
return false;
}
@@ -76,14 +86,16 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Get the device's mix format and use it as a base
IntPtr waveformatPtr = IntPtr.Zero;
hr = _audioClient.GetMixFormat(out waveformatPtr);
- if (SdlWasapiResult.Failed(hr) || waveformatPtr == IntPtr.Zero) {
+ if (SdlWasapiResult.Failed(hr) || waveformatPtr == IntPtr.Zero)
+ {
error = $"Failed to get mix format: 0x{hr:X8}";
return false;
}
long defaultPeriod = 0;
- try {
+ try
+ {
// Reference: SDL_wasapi.c line ~444
// this->spec.channels = (Uint8)waveformat->nChannels;
// SDL adopts the device's native channel count
@@ -93,7 +105,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Reference: SDL_wasapi.c WASAPI_PrepDevice
// GetDevicePeriod is called before Initialize in SDL
hr = _audioClient.GetDevicePeriod(out defaultPeriod, out _);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
error = $"Failed to get device period: 0x{hr:X8}";
return false;
}
@@ -103,7 +116,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Only add AutoConvertPcm + SrcDefaultQuality when sample rate differs.
// Modify the mix format's sample rate in-place.
AudioClientStreamFlags streamflags = AudioClientStreamFlags.None;
- if (desiredSpec.SampleRate != (int)waveformat.SamplesPerSec) {
+ if (desiredSpec.SampleRate != (int)waveformat.SamplesPerSec)
+ {
streamflags |= AudioClientStreamFlags.AutoConvertPcm |
AudioClientStreamFlags.SrcDefaultQuality;
waveformat.SamplesPerSec = (uint)desiredSpec.SampleRate;
@@ -113,25 +127,30 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
streamflags |= AudioClientStreamFlags.EventCallback;
hr = _audioClient.Initialize(AudioClientShareMode.Shared, streamflags, 0, 0, waveformatPtr, IntPtr.Zero);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
error = $"Failed to initialize audio client: 0x{hr:X8}";
return false;
}
// Store adopted channel count for later use
_channels = deviceChannels;
- } finally {
+ }
+ finally
+ {
NativeMethods.CoTaskMemFree(waveformatPtr);
}
hr = _audioClient.SetEventHandle(_bufferEvent);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
error = $"Failed to set event handle: 0x{hr:X8}";
return false;
}
hr = _audioClient.GetBufferSize(out uint bufferFrameCount);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
error = $"Failed to get buffer size: 0x{hr:X8}";
return false;
}
@@ -146,7 +165,8 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Regardless of what we calculated for the period size, clamp it
// to the expected hardware buffer size.
// Reference: SDL_wasapi.c line ~499
- if (calculatedFrames > (int)bufferFrameCount) {
+ if (calculatedFrames > (int)bufferFrameCount)
+ {
calculatedFrames = (int)bufferFrameCount;
}
@@ -154,13 +174,15 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// If the device suggests a different sample size and we don't allow negotiation,
// keep the desired buffer size instead of adopting the device's calculated size.
int obtainedBufferFrames = calculatedFrames;
- if (!desiredSpec.AllowNegotiate && calculatedFrames != desiredSpec.BufferFrames) {
+ if (!desiredSpec.AllowNegotiate && calculatedFrames != desiredSpec.BufferFrames)
+ {
obtainedBufferFrames = desiredSpec.BufferFrames;
}
Guid iidRenderClient = SdlWasapiGuids.IidIaudioRenderClient;
hr = _audioClient.GetService(ref iidRenderClient, out IntPtr renderClientPtr);
- if (SdlWasapiResult.Failed(hr) || renderClientPtr == IntPtr.Zero) {
+ if (SdlWasapiResult.Failed(hr) || renderClientPtr == IntPtr.Zero)
+ {
error = $"Failed to get render client: 0x{hr:X8}";
return false;
}
@@ -178,12 +200,14 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
// Reference: SDL_wasapi.c WASAPI_PrepDevice line ~536
// IAudioClient_Start(client) is called at the end of PrepDevice
hr = _audioClient.Start();
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
error = $"Failed to start audio client: 0x{hr:X8}";
return false;
}
- obtainedSpec = new AudioSpec {
+ obtainedSpec = new AudioSpec
+ {
SampleRate = desiredSpec.SampleRate,
Channels = _channels,
BufferFrames = obtainedBufferFrames,
@@ -195,68 +219,87 @@ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSp
sampleFrames = obtainedBufferFrames;
return true;
- } catch (COMException ex) {
+ }
+ catch (COMException ex)
+ {
error = $"COM exception during Open: {ex.Message} (0x{ex.HResult:X8})";
return false;
}
}
- public void CloseDevice(SdlAudioDevice device) {
- if (_audioClient != null) {
+ public void CloseDevice(SdlAudioDevice device)
+ {
+ if (_audioClient != null)
+ {
_audioClient.Stop();
_audioClient.Reset();
}
- if (_renderClient != null) {
+ if (_renderClient != null)
+ {
Marshal.ReleaseComObject(_renderClient);
_renderClient = null;
}
- if (_audioClient != null) {
+ if (_audioClient != null)
+ {
Marshal.ReleaseComObject(_audioClient);
_audioClient = null;
}
- if (_device != null) {
+ if (_device != null)
+ {
Marshal.ReleaseComObject(_device);
_device = null;
}
- if (_deviceEnumerator != null) {
+ if (_deviceEnumerator != null)
+ {
Marshal.ReleaseComObject(_deviceEnumerator);
_deviceEnumerator = null;
}
- if (_bufferEvent != IntPtr.Zero) {
+ if (_bufferEvent != IntPtr.Zero)
+ {
NativeMethods.CloseHandle(_bufferEvent);
_bufferEvent = IntPtr.Zero;
}
}
- public void WaitDevice(SdlAudioDevice device) {
- if (_audioClient == null || _renderClient == null || _bufferEvent == IntPtr.Zero) {
+ public void WaitDevice(SdlAudioDevice device)
+ {
+ if (_audioClient == null || _renderClient == null || _bufferEvent == IntPtr.Zero)
+ {
device.SetDeviceDisconnected();
return;
}
// Reference: SDL_wasapi.c WASAPI_WaitDevice
- while (true) {
+ while (true)
+ {
uint waitResult = NativeMethods.WaitForSingleObjectEx(_bufferEvent, WaitTimeoutMs, false);
- if (waitResult == WaitObject0) {
+ if (waitResult == WaitObject0)
+ {
int hr = _audioClient.GetCurrentPadding(out uint padding);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
device.SetDeviceDisconnected();
return;
}
// const UINT32 maxpadding = this->spec.samples;
// if (padding <= maxpadding) { break; }
- if (padding <= (uint)_sampleFrames) {
+ if (padding <= (uint)_sampleFrames)
+ {
return;
}
- } else if (waitResult == WaitTimeout) {
+ }
+ else if (waitResult == WaitTimeout)
+ {
continue;
- } else {
+ }
+ else
+ {
_audioClient.Stop();
device.SetDeviceDisconnected();
return;
@@ -264,25 +307,31 @@ public void WaitDevice(SdlAudioDevice device) {
}
}
- public IntPtr GetDeviceBuf(SdlAudioDevice device) {
- if (_renderClient == null) {
+ public IntPtr GetDeviceBuf(SdlAudioDevice device)
+ {
+ if (_renderClient == null)
+ {
device.SetDeviceDisconnected();
return IntPtr.Zero;
}
// Reference: SDL_wasapi.c WASAPI_GetDeviceBuf
- while (true) {
+ while (true)
+ {
int hr = _renderClient.GetBuffer((uint)_sampleFrames, out IntPtr dataPtr);
- if (hr == SdlWasapiResult.AudioClientEBufferTooLarge) {
+ if (hr == SdlWasapiResult.AudioClientEBufferTooLarge)
+ {
// WASAPI_WaitDevice(this)
WaitDevice(device);
- if (!device.Enabled) {
+ if (!device.Enabled)
+ {
return IntPtr.Zero;
}
continue;
}
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
device.SetDeviceDisconnected();
return IntPtr.Zero;
}
@@ -291,8 +340,10 @@ public IntPtr GetDeviceBuf(SdlAudioDevice device) {
}
}
- public void PlayDevice(SdlAudioDevice device) {
- if (_renderClient == null) {
+ public void PlayDevice(SdlAudioDevice device)
+ {
+ if (_renderClient == null)
+ {
device.SetDeviceDisconnected();
return;
}
@@ -300,12 +351,14 @@ public void PlayDevice(SdlAudioDevice device) {
// Reference: SDL_wasapi.c WASAPI_PlayDevice
// WasapiFailed(this, IAudioRenderClient_ReleaseBuffer(this->hidden->render, this->spec.samples, 0))
int hr = _renderClient.ReleaseBuffer((uint)_sampleFrames, 0);
- if (SdlWasapiResult.Failed(hr)) {
+ if (SdlWasapiResult.Failed(hr))
+ {
device.SetDeviceDisconnected();
}
}
- public void ThreadInit(SdlAudioDevice device) {
+ public void ThreadInit(SdlAudioDevice device)
+ {
// Reference: SDL_wasapi_win32.c WASAPI_PlatformThreadInit
// this thread uses COM.
NativeMethods.CoInitializeEx(IntPtr.Zero, NativeMethods.COINIT_MULTITHREADED);
@@ -313,9 +366,11 @@ public void ThreadInit(SdlAudioDevice device) {
// Reference: SDL_wasapi_win32.c WASAPI_PlatformThreadInit
// Set this thread to very high "Pro Audio" priority.
_avrtHandle = NativeMethods.LoadLibraryW("avrt.dll");
- if (_avrtHandle != IntPtr.Zero) {
+ if (_avrtHandle != IntPtr.Zero)
+ {
IntPtr procAddr = NativeMethods.GetProcAddress(_avrtHandle, "AvSetMmThreadCharacteristicsW");
- if (procAddr != IntPtr.Zero) {
+ if (procAddr != IntPtr.Zero)
+ {
AvSetMmThreadCharacteristicsWDelegate avSetMmThread =
Marshal.GetDelegateForFunctionPointer(procAddr);
uint taskIndex = 0;
@@ -324,10 +379,13 @@ public void ThreadInit(SdlAudioDevice device) {
}
}
- public void ThreadDeinit(SdlAudioDevice device) {
- if (_taskHandle != IntPtr.Zero && _avrtHandle != IntPtr.Zero) {
+ public void ThreadDeinit(SdlAudioDevice device)
+ {
+ if (_taskHandle != IntPtr.Zero && _avrtHandle != IntPtr.Zero)
+ {
IntPtr procAddr = NativeMethods.GetProcAddress(_avrtHandle, "AvRevertMmThreadCharacteristics");
- if (procAddr != IntPtr.Zero) {
+ if (procAddr != IntPtr.Zero)
+ {
AvRevertMmThreadCharacteristicsDelegate avRevert =
Marshal.GetDelegateForFunctionPointer(procAddr);
avRevert(_taskHandle);
@@ -335,7 +393,8 @@ public void ThreadDeinit(SdlAudioDevice device) {
_taskHandle = IntPtr.Zero;
}
- if (_avrtHandle != IntPtr.Zero) {
+ if (_avrtHandle != IntPtr.Zero)
+ {
NativeMethods.FreeLibrary(_avrtHandle);
_avrtHandle = IntPtr.Zero;
}
@@ -349,7 +408,8 @@ public void ThreadDeinit(SdlAudioDevice device) {
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate bool AvRevertMmThreadCharacteristicsDelegate(IntPtr taskHandle);
- private static partial class NativeMethods {
+ private static partial class NativeMethods
+ {
public const uint COINIT_MULTITHREADED = 0x0;
[LibraryImport("ole32.dll")]
diff --git a/src/Spice86.Audio/Properties/AssemblyInfo.cs b/src/Spice86.Audio/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9de173f
--- /dev/null
+++ b/src/Spice86.Audio/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Spice86.Audio.Tests")]
\ No newline at end of file
diff --git a/tests/Spice86.Audio.Tests/Backend/CoreAudioBufferPolicyTest.cs b/tests/Spice86.Audio.Tests/Backend/CoreAudioBufferPolicyTest.cs
new file mode 100644
index 0000000..7a62b17
--- /dev/null
+++ b/tests/Spice86.Audio.Tests/Backend/CoreAudioBufferPolicyTest.cs
@@ -0,0 +1,34 @@
+namespace Spice86.Audio.Tests.Backend;
+
+using FluentAssertions;
+
+using Spice86.Audio.Backend.Audio.CrossPlatform.Sdl.Mac.CoreAudio;
+
+using Xunit;
+
+public class CoreAudioBufferPolicyTest
+{
+ [Fact]
+ public void ComputeAudioBufferCount_UsesThreeBuffers_WhenPeriodAlreadyExceedsMinimum()
+ {
+ int bufferCount = CoreAudioBufferPolicy.ComputeAudioBufferCount(48_000, 1024);
+
+ bufferCount.Should().Be(3);
+ }
+
+ [Fact]
+ public void ComputeAudioBufferCount_ScalesUp_WhenPeriodIsSmall()
+ {
+ int bufferCount = CoreAudioBufferPolicy.ComputeAudioBufferCount(48_000, 256);
+
+ bufferCount.Should().Be(6);
+ }
+
+ [Fact]
+ public void ComputeAudioBufferCount_ThrowsForInvalidSampleRate()
+ {
+ Action act = () => CoreAudioBufferPolicy.ComputeAudioBufferCount(0, 256);
+
+ act.Should().Throw();
+ }
+}
\ No newline at end of file
diff --git a/tests/Spice86.Audio.Tests/Backend/SdlAudioDeviceTest.cs b/tests/Spice86.Audio.Tests/Backend/SdlAudioDeviceTest.cs
new file mode 100644
index 0000000..87914e8
--- /dev/null
+++ b/tests/Spice86.Audio.Tests/Backend/SdlAudioDeviceTest.cs
@@ -0,0 +1,74 @@
+namespace Spice86.Audio.Tests.Backend;
+
+using FluentAssertions;
+
+using Spice86.Audio.Backend.Audio.CrossPlatform;
+using Spice86.Audio.Backend.Audio.CrossPlatform.Sdl;
+
+using Xunit;
+
+public class SdlAudioDeviceTest
+{
+ [Fact]
+ public void Open_ForwardsAllowNegotiateToDriver()
+ {
+ CaptureDriver driver = new();
+ SdlAudioDevice device = new(driver);
+
+ AudioSpec desiredSpec = new()
+ {
+ SampleRate = 48_000,
+ Channels = 2,
+ BufferFrames = 512,
+ AllowNegotiate = false,
+ Callback = static _ => { }
+ };
+
+ bool opened = device.Open(desiredSpec);
+
+ opened.Should().BeTrue();
+ driver.CapturedSpec.Should().NotBeNull();
+ driver.CapturedSpec!.AllowNegotiate.Should().BeFalse();
+ }
+
+ private sealed class CaptureDriver : ISdlAudioDriver
+ {
+ public bool ProvidesOwnCallbackThread => true;
+
+ public AudioSpec? CapturedSpec { get; private set; }
+
+ public bool OpenDevice(SdlAudioDevice device, AudioSpec desiredSpec, out AudioSpec obtainedSpec, out int sampleFrames, out string? error)
+ {
+ CapturedSpec = desiredSpec;
+ obtainedSpec = desiredSpec;
+ sampleFrames = desiredSpec.BufferFrames;
+ error = null;
+ return true;
+ }
+
+ public void CloseDevice(SdlAudioDevice device)
+ {
+ }
+
+ public void WaitDevice(SdlAudioDevice device)
+ {
+ }
+
+ public IntPtr GetDeviceBuf(SdlAudioDevice device)
+ {
+ return IntPtr.Zero;
+ }
+
+ public void PlayDevice(SdlAudioDevice device)
+ {
+ }
+
+ public void ThreadInit(SdlAudioDevice device)
+ {
+ }
+
+ public void ThreadDeinit(SdlAudioDevice device)
+ {
+ }
+ }
+}
\ No newline at end of file