Skip to content

Instantly share code, notes, and snippets.

@icculus
Created August 5, 2024 02:50
Show Gist options
  • Save icculus/b021a110eb1bcdfa72f169a620e2d46e to your computer and use it in GitHub Desktop.
Save icculus/b021a110eb1bcdfa72f169a620e2d46e to your computer and use it in GitHub Desktop.
Standalone reproduction case for CoreAudio hotplugging issue
// This is only intended for macOS; I cut out the iOS-specific pieces when pulling this code out of SDL for a reproduction case!
// clang -Wall -O0 -ggdb3 -fobjc-arc -o coreaudio-replug-problem coreaudio-replug-problem.m -framework AudioToolbox
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <math.h>
#include <pthread.h>
#include <signal.h>
#include <assert.h>
#include <CoreAudio/CoreAudio.h>
#include <AudioToolbox/AudioToolbox.h>
#include <AudioUnit/AudioUnit.h>
#define CHECK_RESULT(msg) \
if (result != noErr) { \
printf("CoreAudio error (%s): %d\n", msg, (int)result); \
return -1; \
}
typedef struct MyCoreAudioDevice
{
char *name;
AudioDeviceID devid;
pthread_t thread;
AudioQueueRef audioQueue;
int numAudioBuffers;
AudioQueueBufferRef *audioBuffer;
AudioQueueBufferRef current_buffer;
AudioStreamBasicDescription strdesc;
char *thread_error;
BOOL thread_ready;
struct MyCoreAudioDevice *next;
struct MyCoreAudioDevice *prev;
BOOL shutdown;
BOOL tried_open;
int total_samples_generated;
} MyCoreAudioDevice;
static const AudioObjectPropertyAddress devlist_address = {
kAudioHardwarePropertyDevices,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
static const AudioObjectPropertyAddress alive_address = {
kAudioDevicePropertyDeviceIsAlive,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
static OSStatus DeviceAliveNotification(AudioObjectID devid, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data);
static void CloseAudioDevice(MyCoreAudioDevice *device);
static MyCoreAudioDevice known_devices;
static MyCoreAudioDevice *AddAudioDevice(const char *name, AudioObjectID devid)
{
MyCoreAudioDevice *device = calloc(1, sizeof (*device));
device->name = strdup(name);
device->devid = devid;
device->prev = &known_devices;
device->next = known_devices.next;
if (device->next) {
device->next->prev = device;
}
known_devices.next = device;
return device;
}
static void RemoveAudioDevice(MyCoreAudioDevice *device)
{
AudioObjectRemovePropertyListener(device->devid, &alive_address, DeviceAliveNotification, device);
CloseAudioDevice(device);
if (device->next) {
device->next->prev = device->prev;
}
device->prev->next = device->next;
free(device->name);
free(device);
}
// callback that fires when a device is unplugged, etc.
static OSStatus DeviceAliveNotification(AudioObjectID devid, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data)
{
MyCoreAudioDevice *device = (MyCoreAudioDevice *) data;
assert(device->devid == devid);
UInt32 alive = 1;
UInt32 size = sizeof(alive);
const OSStatus error = AudioObjectGetPropertyData(devid, addrs, 0, NULL, &size, &alive);
BOOL dead = NO;
if (error == kAudioHardwareBadDeviceError) {
dead = YES; // device was unplugged.
} else if ((error == kAudioHardwareNoError) && (!alive)) {
dead = YES; // device died in some other way.
}
if (dead) {
printf("COREAUDIO: device '%s' is lost!\n", device->name);
RemoveAudioDevice(device);
}
return noErr;
}
// This only _adds_ new devices. Removal is handled by devices triggering kAudioDevicePropertyDeviceIsAlive property changes.
static void RefreshPhysicalDevices(void)
{
UInt32 size = 0;
AudioDeviceID *devs = NULL;
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size) != kAudioHardwareNoError) {
return;
} else if ((devs = (AudioDeviceID *) malloc(size)) == NULL) {
return;
} else if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &devlist_address, 0, NULL, &size, devs) != kAudioHardwareNoError) {
free(devs);
return;
}
const UInt32 total_devices = (UInt32) (size / sizeof(AudioDeviceID));
for (UInt32 i = 0; i < total_devices; i++) {
for (MyCoreAudioDevice *i = known_devices.next; i != NULL; i = i->next) {
for (int j = 0; j < total_devices; j++) {
if (i->devid == devs[j]) {
devs[j] = 0; // The system and us both agree it's already here, don't check it again.
break;
}
}
}
}
// any non-zero items remaining in `devs` are new devices to be added.
const AudioObjectPropertyAddress addr = {
kAudioDevicePropertyStreamConfiguration,
kAudioDevicePropertyScopeOutput,
kAudioObjectPropertyElementMain
};
const AudioObjectPropertyAddress nameaddr = {
kAudioObjectPropertyName,
kAudioDevicePropertyScopeOutput,
kAudioObjectPropertyElementMain
};
for (UInt32 i = 0; i < total_devices; i++) {
const AudioDeviceID dev = devs[i];
if (!dev) {
continue; // already added.
}
AudioBufferList *buflist = NULL;
if (AudioObjectGetPropertyDataSize(dev, &addr, 0, NULL, &size) != noErr) {
continue;
} else if ((buflist = (AudioBufferList *)calloc(1, size)) == NULL) {
continue;
}
OSStatus result = AudioObjectGetPropertyData(dev, &addr, 0, NULL, &size, buflist);
int channels = 0;
if (result == noErr) {
for (UInt32 j = 0; j < buflist->mNumberBuffers; j++) {
channels += buflist->mBuffers[j].mNumberChannels;
}
}
free(buflist);
if (channels == 0) {
continue;
}
CFStringRef cfstr = NULL;
size = sizeof(CFStringRef);
if (AudioObjectGetPropertyData(dev, &nameaddr, 0, NULL, &size, &cfstr) != kAudioHardwareNoError) {
continue;
}
CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8);
char *name = (char *)malloc(len + 1);
int usable = ((name != NULL) && (CFStringGetCString(cfstr, name, len + 1, kCFStringEncodingUTF8)));
CFRelease(cfstr);
if (usable) {
// Some devices have whitespace at the end...trim it.
len = (CFIndex) strlen(name);
while ((len > 0) && (name[len - 1] == ' ')) {
len--;
}
usable = (len > 0);
}
if (usable) {
name[len] = '\0';
printf("COREAUDIO: Found playback device #%d: '%s' (devid %d)\n", (int)i, name, (int)dev);
MyCoreAudioDevice *device = AddAudioDevice(name, dev);
if (device) {
AudioObjectAddPropertyListener(dev, &alive_address, DeviceAliveNotification, device);
}
}
free(name); // AddAudioDevice() would have copied the string.
}
free(devs);
}
// this is called when the system's list of available audio devices changes.
static OSStatus DeviceListChangedNotification(AudioObjectID systemObj, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data)
{
RefreshPhysicalDevices();
return noErr;
}
static void COREAUDIO_DetectDevices()
{
RefreshPhysicalDevices();
AudioObjectAddPropertyListener(kAudioObjectSystemObject, &devlist_address, DeviceListChangedNotification, NULL);
}
static int COREAUDIO_PlayDevice(MyCoreAudioDevice *device, const UInt8 *buffer, int buffer_size)
{
AudioQueueBufferRef current_buffer = device->current_buffer;
assert(current_buffer != NULL); // should have been called from PlaybackBufferReadyCallback
assert(buffer == (UInt8 *) current_buffer->mAudioData);
current_buffer->mAudioDataByteSize = current_buffer->mAudioDataBytesCapacity;
device->current_buffer = NULL;
AudioQueueEnqueueBuffer(device->audioQueue, current_buffer, 0, NULL);
return 0;
}
static float *COREAUDIO_GetDeviceBuf(MyCoreAudioDevice *device, int *buffer_size)
{
AudioQueueBufferRef current_buffer = device->current_buffer;
assert(current_buffer != NULL); // should have been called from PlaybackBufferReadyCallback
assert(current_buffer->mAudioData != NULL);
*buffer_size = (int) current_buffer->mAudioDataBytesCapacity;
return (float *) current_buffer->mAudioData;
}
static void PlaybackBufferReadyCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
{
MyCoreAudioDevice *device = (MyCoreAudioDevice *)inUserData;
assert(inBuffer != NULL); // ...right?
assert(device->current_buffer == NULL); // shouldn't have anything pending
device->current_buffer = inBuffer;
int bufsize = 0;
float *dst = COREAUDIO_GetDeviceBuf(device, &bufsize);
const int samples = bufsize / sizeof (float);
int total_samples_generated = device->total_samples_generated;
for (int i = 0; i < samples; i++) {
/* You don't have to care about this math; we're just generating a simple sine wave as we go.
https://en.wikipedia.org/wiki/Sine_wave */
const float time = total_samples_generated / 48000.0f;
const int sine_freq = 500; /* run the wave at 500Hz */
dst[i] = sinf(6.283185f * sine_freq * time);
total_samples_generated++;
}
device->total_samples_generated = total_samples_generated;
COREAUDIO_PlayDevice(device, (const UInt8 *) dst, bufsize);
}
static void COREAUDIO_CloseDevice(MyCoreAudioDevice *device)
{
device->shutdown = YES;
// dispose of the audio queue before waiting on the thread, or it might stall for a long time!
if (device->audioQueue) {
AudioQueueFlush(device->audioQueue);
AudioQueueStop(device->audioQueue, 0);
AudioQueueDispose(device->audioQueue, 0);
device->audioQueue = 0;
}
if (device->thread) {
pthread_join(device->thread, NULL);
device->thread = 0;
}
// AudioQueueDispose() frees the actual buffer objects.
free(device->audioBuffer);
device->audioBuffer = NULL;
free(device->thread_error);
device->thread_error = NULL;
}
static int PrepareDevice(MyCoreAudioDevice *device)
{
OSStatus result = noErr;
UInt32 size = 0;
AudioObjectPropertyAddress addr = {
0,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMain
};
UInt32 alive = 0;
size = sizeof(alive);
addr.mSelector = kAudioDevicePropertyDeviceIsAlive;
addr.mScope = kAudioDevicePropertyScopeOutput;
result = AudioObjectGetPropertyData(device->devid, &addr, 0, NULL, &size, &alive);
CHECK_RESULT("AudioDeviceGetProperty (kAudioDevicePropertyDeviceIsAlive)");
if (!alive) {
printf("CoreAudio: requested device exists, but isn't alive.\n");
return -1;
}
// some devices don't support this property, so errors are fine here.
pid_t pid = 0;
size = sizeof(pid);
addr.mSelector = kAudioDevicePropertyHogMode;
result = AudioObjectGetPropertyData(device->devid, &addr, 0, NULL, &size, &pid);
if ((result == noErr) && (pid != -1)) {
printf("CoreAudio: requested device is being hogged.\n");
return -1;
}
return 0;
}
static int AssignDeviceToAudioQueue(MyCoreAudioDevice *device)
{
const AudioObjectPropertyAddress prop = {
kAudioDevicePropertyDeviceUID,
kAudioDevicePropertyScopeOutput,
kAudioObjectPropertyElementMain
};
OSStatus result;
CFStringRef devuid;
UInt32 devuidsize = sizeof(devuid);
result = AudioObjectGetPropertyData(device->devid, &prop, 0, NULL, &devuidsize, &devuid);
CHECK_RESULT("AudioObjectGetPropertyData (kAudioDevicePropertyDeviceUID)");
result = AudioQueueSetProperty(device->audioQueue, kAudioQueueProperty_CurrentDevice, &devuid, devuidsize);
CFRelease(devuid); // Release devuid; we're done with it and AudioQueueSetProperty should have retained if it wants to keep it.
CHECK_RESULT("AudioQueueSetProperty (kAudioQueueProperty_CurrentDevice)");
return 0;
}
static int PrepareAudioQueue(MyCoreAudioDevice *device)
{
const AudioStreamBasicDescription *strdesc = &device->strdesc;
OSStatus result;
assert(CFRunLoopGetCurrent() != NULL);
result = AudioQueueNewOutput(strdesc, PlaybackBufferReadyCallback, device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 0, &device->audioQueue);
CHECK_RESULT("AudioQueueNewOutput");
if (AssignDeviceToAudioQueue(device) < 0) {
return -1;
}
// Set the channel layout for the audio queue
AudioChannelLayout layout;
memset(&layout, 0, sizeof (layout));
layout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
// Make sure we can feed the device a minimum amount of time
const double MINIMUM_AUDIO_BUFFER_TIME_MS = 15.0;
int numAudioBuffers = 2;
const double msecs = (1024 / (48000.0)) * 1000.0;
if (msecs < MINIMUM_AUDIO_BUFFER_TIME_MS) { // use more buffers if we have a VERY small sample set.
numAudioBuffers = ((int)ceil(MINIMUM_AUDIO_BUFFER_TIME_MS / msecs) * 2);
}
device->numAudioBuffers = numAudioBuffers;
device->audioBuffer = calloc(numAudioBuffers, sizeof(AudioQueueBufferRef));
if (device->audioBuffer == NULL) {
return -1;
}
//printf("COREAUDIO: numAudioBuffers == %d\n", numAudioBuffers);
for (int i = 0; i < numAudioBuffers; i++) {
result = AudioQueueAllocateBuffer(device->audioQueue, 4096, &device->audioBuffer[i]);
CHECK_RESULT("AudioQueueAllocateBuffer");
memset(device->audioBuffer[i]->mAudioData, 0, device->audioBuffer[i]->mAudioDataBytesCapacity);
device->audioBuffer[i]->mAudioDataByteSize = device->audioBuffer[i]->mAudioDataBytesCapacity;
// !!! FIXME: should we use AudioQueueEnqueueBufferWithParameters and specify all frames be "trimmed" so these are immediately ready to refill with SDL callback data?
result = AudioQueueEnqueueBuffer(device->audioQueue, device->audioBuffer[i], 0, NULL);
CHECK_RESULT("AudioQueueEnqueueBuffer");
}
result = AudioQueueStart(device->audioQueue, NULL);
CHECK_RESULT("AudioQueueStart");
return 0; // We're running!
}
static void *AudioQueueThreadEntry(void *arg)
{
MyCoreAudioDevice *device = (MyCoreAudioDevice *)arg;
if (PrepareAudioQueue(device) < 0) {
device->thread_error = strdup("PrepareAudioQueue failed");
device->thread_ready = YES;
return NULL;
}
// init was successful, alert parent thread and start running...
device->thread_ready = YES;
// This would be WaitDevice/WaitRecordingDevice in the normal SDL audio thread, but we get *BufferReadyCallback calls here to know when to iterate.
while (!device->shutdown) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.10, 1);
}
// Drain off any pending playback.
const CFTimeInterval secs = (((CFTimeInterval)1024) / ((CFTimeInterval)48000)) * 2.0;
CFRunLoopRunInMode(kCFRunLoopDefaultMode, secs, 0);
return NULL;
}
static int COREAUDIO_OpenDevice(MyCoreAudioDevice *device)
{
device->tried_open = YES;
// Initialize all variables that we clean on shutdown
// Setup a AudioStreamBasicDescription with the requested format
AudioStreamBasicDescription *strdesc = &device->strdesc;
strdesc->mFormatID = kAudioFormatLinearPCM;
strdesc->mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsFloat;
strdesc->mChannelsPerFrame = 1;
strdesc->mSampleRate = 48000;
strdesc->mFramesPerPacket = 1;
strdesc->mBitsPerChannel = 32;
strdesc->mBytesPerFrame = strdesc->mChannelsPerFrame * strdesc->mBitsPerChannel / 8;
strdesc->mBytesPerPacket = strdesc->mBytesPerFrame * strdesc->mFramesPerPacket;
if (PrepareDevice(device) < 0) {
return -1;
}
// This has to init in a new thread so it can get its own CFRunLoop. :/
device->thread_ready = NO;
device->shutdown = NO;
if (pthread_create(&device->thread, NULL, AudioQueueThreadEntry, device) != 0) {
printf("Failed to create thread!\n");
return -1;
}
while (!device->thread_ready) {
usleep(10000);
}
if (device->thread_error != NULL) {
printf("Error initing thread: %s\n", device->thread_error);
return -1;
}
return 0;
}
static void CloseAudioDevice(MyCoreAudioDevice *device)
{
printf("Closing device '%s' ...\n", device->name);
COREAUDIO_CloseDevice(device);
//device->tried_open = NO;
}
static int OpenAudioDevice(MyCoreAudioDevice *device)
{
printf("Opening device '%s' ...\n", device->name);
const int retval = COREAUDIO_OpenDevice(device);
if (retval == -1) {
CloseAudioDevice(device);
}
return retval;
}
static void RemoveAllAudioDevices(void)
{
while (known_devices.next != NULL) {
RemoveAudioDevice(known_devices.next);
}
}
static BOOL done = 0;
static void CtrlCPressed(int sig)
{
done = YES;
}
int main(int argc, char **argv)
{
signal(SIGINT, CtrlCPressed); // listen for CTRL-C to terminate the app.
assert(CFRunLoopGetCurrent() != NULL);
COREAUDIO_DetectDevices(); // get an initial list of devices, which will update as we hotplug. We'll open them as we see them!
printf("Ready to go. Hit CTRL-C to quit!\n");
while (!done) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0, 1);
for (MyCoreAudioDevice *i = known_devices.next; i != NULL; i = i->next) {
if (!i->tried_open) {
OpenAudioDevice(i);
}
}
}
// shutdown!
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &devlist_address, DeviceListChangedNotification, NULL);
RemoveAllAudioDevices();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment