Created
August 5, 2024 02:50
-
-
Save icculus/b021a110eb1bcdfa72f169a620e2d46e to your computer and use it in GitHub Desktop.
Standalone reproduction case for CoreAudio hotplugging issue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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