// 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 #include #include #include #include #include #include #include #include #include #include #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; }