Initial work on audio device hotplug support.
authorRyan C. Gordon <icculus@icculus.org>
Mon, 16 Mar 2015 02:11:39 -0400
changeset 9393 ed79a66e57e5
parent 9392 92e23eff9b89
child 9394 bb28e5281770
Initial work on audio device hotplug support. This fills in the core pieces and fully implements it for Mac OS X. Most other platforms, at the moment, will report a disconnected device if it fails to write audio, but don't notice if the system's device list changed at all.
include/SDL_events.h
src/audio/SDL_audio.c
src/audio/SDL_sysaudio.h
src/audio/alsa/SDL_alsa_audio.c
src/audio/arts/SDL_artsaudio.c
src/audio/bsd/SDL_bsdaudio.c
src/audio/coreaudio/SDL_coreaudio.c
src/audio/disk/SDL_diskaudio.c
src/audio/dsp/SDL_dspaudio.c
src/audio/esd/SDL_esdaudio.c
src/audio/fusionsound/SDL_fsaudio.c
src/audio/paudio/SDL_paudio.c
src/audio/pulseaudio/SDL_pulseaudio.c
src/audio/qsa/SDL_qsa_audio.c
src/audio/sndio/SDL_sndioaudio.c
src/audio/sun/SDL_sunaudio.c
src/audio/xaudio2/SDL_xaudio2.c
test/Makefile.in
test/testaudiohotplug.c
--- a/include/SDL_events.h	Wed Mar 18 10:09:39 2015 -0400
+++ b/include/SDL_events.h	Mon Mar 16 02:11:39 2015 -0400
@@ -110,6 +110,10 @@
     SDL_JOYDEVICEADDED,         /**< A new joystick has been inserted into the system */
     SDL_JOYDEVICEREMOVED,       /**< An opened joystick has been removed */
 
+    /* Audio hotplug events */
+    SDL_AUDIODEVICEADDED = 0x700,  /**< A new audio device is available */
+    SDL_AUDIODEVICEREMOVED,        /**< An audio device has been removed. */
+
     /* Game controller events */
     SDL_CONTROLLERAXISMOTION  = 0x650, /**< Game controller axis motion */
     SDL_CONTROLLERBUTTONDOWN,          /**< Game controller button pressed */
@@ -382,6 +386,20 @@
     Sint32 which;       /**< The joystick device index for the ADDED event, instance id for the REMOVED or REMAPPED event */
 } SDL_ControllerDeviceEvent;
 
+/**
+ *  \brief Audio device event structure (event.adevice.*)
+ */
+typedef struct SDL_AudioDeviceEvent
+{
+    Uint32 type;        /**< ::SDL_AUDIODEVICEADDED, or ::SDL_AUDIODEVICEREMOVED */
+    Uint32 timestamp;
+    Uint32 which;       /**< The audio device index for the ADDED event (valid until next SDL_GetNumAudioDevices() call), SDL_AudioDeviceID for the REMOVED event */
+    Uint8 iscapture;    /**< zero if an output device, non-zero if a capture device. */
+    Uint8 padding1;
+    Uint8 padding2;
+    Uint8 padding3;
+} SDL_AudioDeviceEvent;
+
 
 /**
  *  \brief Touch finger event structure (event.tfinger.*)
@@ -516,6 +534,7 @@
     SDL_ControllerAxisEvent caxis;      /**< Game Controller axis event data */
     SDL_ControllerButtonEvent cbutton;  /**< Game Controller button event data */
     SDL_ControllerDeviceEvent cdevice;  /**< Game Controller device event data */
+    SDL_AudioDeviceEvent adevice;   /**< Audio device event data */
     SDL_QuitEvent quit;             /**< Quit request event data */
     SDL_UserEvent user;             /**< Custom event data */
     SDL_SysWMEvent syswm;           /**< System dependent window event data */
--- a/src/audio/SDL_audio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/SDL_audio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -333,6 +333,144 @@
 }
 #endif
 
+/* device hotplug support... */
+
+/* this function expects its caller to hold current_audio.detection_lock */
+static int
+add_audio_device(const char *_name, char ***_devices, int *_devCount)
+{
+    char *name = SDL_strdup(_name);
+    int retval = -1;
+
+    if (name != NULL) {
+        char **devices = *_devices;
+        int devCount = *_devCount;
+        void *ptr = SDL_realloc(devices, (devCount+1) * sizeof(char*));
+        if (ptr == NULL) {
+            SDL_free(name);
+        } else {
+            retval = devCount;
+            devices = (char **) ptr;
+            devices[devCount++] = name;
+            *_devices = devices;
+            *_devCount = devCount;
+        }
+    }
+
+    return retval;
+}
+
+static int
+add_capture_device(const char *name)
+{
+    /* !!! FIXME: add this later. SDL_assert(current_audio.impl.HasCaptureSupport);*/
+    return add_audio_device(name, &current_audio.inputDevices, &current_audio.inputDeviceCount);
+}
+
+static int
+add_output_device(const char *name)
+{
+    return add_audio_device(name, &current_audio.outputDevices, &current_audio.outputDeviceCount);
+}
+
+static void
+free_device_list(char ***devices, int *devCount)
+{
+    int i = *devCount;
+    if ((i > 0) && (*devices != NULL)) {
+        while (i--) {
+            SDL_free((*devices)[i]);
+        }
+    }
+
+    SDL_free(*devices);
+
+    *devices = NULL;
+    *devCount = 0;
+}
+
+static void
+perform_full_device_redetect(const int iscapture)
+{
+    SDL_LockMutex(current_audio.detection_lock);
+
+    if (iscapture) {
+        if (!current_audio.impl.OnlyHasDefaultOutputDevice) {
+            free_device_list(&current_audio.outputDevices, &current_audio.outputDeviceCount);
+            current_audio.impl.DetectDevices(SDL_FALSE, add_output_device);
+        }
+    } else {
+        if ((current_audio.impl.HasCaptureSupport) && (!current_audio.impl.OnlyHasDefaultInputDevice)) {
+            free_device_list(&current_audio.inputDevices, &current_audio.inputDeviceCount);
+            current_audio.impl.DetectDevices(SDL_TRUE, add_capture_device);
+        }
+    }
+
+    SDL_UnlockMutex(current_audio.detection_lock);
+}
+
+/* The audio backends call this when a new device is plugged in. */
+void
+SDL_AudioDeviceConnected(const int iscapture, const char *name)
+{
+    int device_index = -1;
+
+    SDL_LockMutex(current_audio.detection_lock);
+    if (iscapture) {
+        device_index = add_capture_device(name);
+    } else {
+        device_index = add_output_device(name);
+    }
+    SDL_UnlockMutex(current_audio.detection_lock);
+
+    if (device_index != -1) {
+        /* Post the event, if desired */
+        if (SDL_GetEventState(SDL_AUDIODEVICEADDED) == SDL_ENABLE) {
+            SDL_Event event;
+            event.adevice.type = SDL_AUDIODEVICEADDED;
+            event.adevice.which = device_index;
+            event.adevice.iscapture = iscapture;
+            SDL_PushEvent(&event);
+        }
+    }
+}
+
+/* The audio backends call this when a device is unplugged. */
+void
+SDL_AudioDeviceDisconnected(const int iscapture, SDL_AudioDevice *device)
+{
+    /* device==NULL means an unopened device was lost; do the redetect only. */
+    if (device != NULL) {
+        SDL_assert(get_audio_device(device->id) == device);
+        SDL_assert(device->enabled);  /* called more than once?! */
+
+        /* Ends the audio callback and mark the device as STOPPED, but the
+           app still needs to close the device to free resources. */
+        current_audio.impl.LockDevice(device);
+        device->enabled = 0;
+        current_audio.impl.UnlockDevice(device);
+
+        /* Post the event, if desired */
+        if (SDL_GetEventState(SDL_AUDIODEVICEREMOVED) == SDL_ENABLE) {
+            SDL_Event event;
+            event.adevice.type = SDL_AUDIODEVICEREMOVED;
+            event.adevice.which = device->id;
+            event.adevice.iscapture = device->iscapture ? 1 : 0;
+            SDL_PushEvent(&event);
+        }
+    }
+
+    /* we don't really know which name (if any) was associated with this
+       device in the device list, so drop the entire list and rebuild it.
+       (we should probably change the API in 2.1 to make this more clear?) */
+    if (iscapture) {
+        current_audio.need_capture_device_redetect = SDL_TRUE;
+    } else {
+        current_audio.need_output_device_redetect = SDL_TRUE;
+    }
+}
+
+
 
 /* buffer queueing support... */
 
@@ -690,6 +828,13 @@
 
             /* !!! FIXME: this should be LockDevice. */
             SDL_LockMutex(device->mixer_lock);
+
+            /* Check again, in case device was removed while a lock was held. */
+            if (!device->enabled) {
+                SDL_UnlockMutex(device->mixer_lock);
+                break;
+            }
+
             if (device->paused) {
                 SDL_memset(stream, silence, stream_len);
             } else {
@@ -821,8 +966,34 @@
         return -1;            /* No driver was available, so fail. */
     }
 
+    current_audio.detection_lock = SDL_CreateMutex();
+
     finalize_audio_entry_points();
 
+    /* Make sure we have a list of devices available at startup. */
+    perform_full_device_redetect(SDL_TRUE);
+    perform_full_device_redetect(SDL_FALSE);
+
+    /* Post an add event for each initial device, if desired */
+    if (SDL_GetEventState(SDL_AUDIODEVICEADDED) == SDL_ENABLE) {
+        SDL_Event event;
+
+        SDL_zero(event);
+        event.adevice.type = SDL_AUDIODEVICEADDED;
+
+        event.adevice.iscapture = 0;
+        for (i = 0; i < current_audio.outputDeviceCount; i++) {
+            event.adevice.which = i;
+            SDL_PushEvent(&event);
+        }
+
+        event.adevice.iscapture = 1;
+        for (i = 0; i < current_audio.inputDeviceCount; i++) {
+            event.adevice.which = i;
+            SDL_PushEvent(&event);
+        }
+    }
+
     return 0;
 }
 
@@ -835,53 +1006,6 @@
     return current_audio.name;
 }
 
-static void
-free_device_list(char ***devices, int *devCount)
-{
-    int i = *devCount;
-    if ((i > 0) && (*devices != NULL)) {
-        while (i--) {
-            SDL_free((*devices)[i]);
-        }
-    }
-
-    SDL_free(*devices);
-
-    *devices = NULL;
-    *devCount = 0;
-}
-
-static
-void SDL_AddCaptureAudioDevice(const char *_name)
-{
-    char *name = NULL;
-    void *ptr = SDL_realloc(current_audio.inputDevices,
-                          (current_audio.inputDeviceCount+1) * sizeof(char*));
-    if (ptr == NULL) {
-        return;  /* oh well. */
-    }
-
-    current_audio.inputDevices = (char **) ptr;
-    name = SDL_strdup(_name);  /* if this returns NULL, that's okay. */
-    current_audio.inputDevices[current_audio.inputDeviceCount++] = name;
-}
-
-static
-void SDL_AddOutputAudioDevice(const char *_name)
-{
-    char *name = NULL;
-    void *ptr = SDL_realloc(current_audio.outputDevices,
-                          (current_audio.outputDeviceCount+1) * sizeof(char*));
-    if (ptr == NULL) {
-        return;  /* oh well. */
-    }
-
-    current_audio.outputDevices = (char **) ptr;
-    name = SDL_strdup(_name);  /* if this returns NULL, that's okay. */
-    current_audio.outputDevices[current_audio.outputDeviceCount++] = name;
-}
-
-
 int
 SDL_GetNumAudioDevices(int iscapture)
 {
@@ -903,18 +1027,20 @@
         return 1;
     }
 
-    if (iscapture) {
-        free_device_list(&current_audio.inputDevices,
-                         &current_audio.inputDeviceCount);
-        current_audio.impl.DetectDevices(iscapture, SDL_AddCaptureAudioDevice);
-        retval = current_audio.inputDeviceCount;
-    } else {
-        free_device_list(&current_audio.outputDevices,
-                         &current_audio.outputDeviceCount);
-        current_audio.impl.DetectDevices(iscapture, SDL_AddOutputAudioDevice);
-        retval = current_audio.outputDeviceCount;
+    if (current_audio.need_capture_device_redetect) {
+        current_audio.need_capture_device_redetect = SDL_FALSE;
+        perform_full_device_redetect(SDL_TRUE);
     }
 
+    if (current_audio.need_output_device_redetect) {
+        current_audio.need_output_device_redetect = SDL_FALSE;
+        perform_full_device_redetect(SDL_FALSE);
+    }
+
+    SDL_LockMutex(current_audio.detection_lock);
+    retval = iscapture ? current_audio.inputDeviceCount : current_audio.outputDeviceCount;
+    SDL_UnlockMutex(current_audio.detection_lock);
+
     return retval;
 }
 
@@ -922,6 +1048,8 @@
 const char *
 SDL_GetAudioDeviceName(int index, int iscapture)
 {
+    const char *retval = NULL;
+
     if (!SDL_WasInit(SDL_INIT_AUDIO)) {
         SDL_SetError("Audio subsystem is not initialized");
         return NULL;
@@ -950,16 +1078,18 @@
         return DEFAULT_OUTPUT_DEVNAME;
     }
 
-    if (iscapture) {
-        if (index >= current_audio.inputDeviceCount) {
-            goto no_such_device;
-        }
-        return current_audio.inputDevices[index];
-    } else {
-        if (index >= current_audio.outputDeviceCount) {
-            goto no_such_device;
-        }
-        return current_audio.outputDevices[index];
+    SDL_LockMutex(current_audio.detection_lock);
+    if (iscapture && (index < current_audio.inputDeviceCount)) {
+        retval = current_audio.inputDevices[index];
+    } else if (!iscapture && (index < current_audio.outputDeviceCount)) {
+        retval = current_audio.outputDevices[index];
+    }
+    SDL_UnlockMutex(current_audio.detection_lock);
+
+    /* !!! FIXME: a device could be removed after being returned here, freeing retval's pointer. */
+
+    if (retval != NULL) {
+        return retval;
     }
 
 no_such_device:
@@ -1077,6 +1207,18 @@
         return 0;
     }
 
+    /* Find an available device ID... */
+    for (id = min_id - 1; id < SDL_arraysize(open_devices); id++) {
+        if (open_devices[id] == NULL) {
+            break;
+        }
+    }
+
+    if (id == SDL_arraysize(open_devices)) {
+        SDL_SetError("Too many open audio devices");
+        return 0;
+    }
+
     if (!obtained) {
         obtained = &_obtained;
     }
@@ -1135,6 +1277,7 @@
         return 0;
     }
     SDL_zerop(device);
+    device->id = id + 1;
     device->spec = *obtained;
     device->enabled = 1;
     device->paused = 1;
@@ -1150,12 +1293,6 @@
         }
     }
 
-    /* force a device detection if we haven't done one yet. */
-    if ( ((iscapture) && (current_audio.inputDevices == NULL)) ||
-         ((!iscapture) && (current_audio.outputDevices == NULL)) ) {
-        SDL_GetNumAudioDevices(iscapture);
-    }
-
     if (current_audio.impl.OpenDevice(device, devname, iscapture) < 0) {
         close_audio_device(device);
         return 0;
@@ -1247,25 +1384,14 @@
         device->spec.userdata = device;
     }
 
-    /* Find an available device ID and store the structure... */
-    for (id = min_id - 1; id < SDL_arraysize(open_devices); id++) {
-        if (open_devices[id] == NULL) {
-            open_devices[id] = device;
-            break;
-        }
-    }
-
-    if (id == SDL_arraysize(open_devices)) {
-        SDL_SetError("Too many open audio devices");
-        close_audio_device(device);
-        return 0;
-    }
+    /* add it to our list of open devices. */
+    open_devices[id] = device;
 
     /* Start the audio thread if necessary */
     if (!current_audio.impl.ProvidesOwnCallbackThread) {
         /* Start the audio thread */
         char name[64];
-        SDL_snprintf(name, sizeof (name), "SDLAudioDev%d", (int) (id + 1));
+        SDL_snprintf(name, sizeof (name), "SDLAudioDev%d", (int) device->id);
 /* !!! FIXME: this is nasty. */
 #if defined(__WIN32__) && !defined(HAVE_LIBC)
 #undef SDL_CreateThread
@@ -1278,13 +1404,13 @@
         device->thread = SDL_CreateThread(SDL_RunAudio, name, device);
 #endif
         if (device->thread == NULL) {
-            SDL_CloseAudioDevice(id + 1);
+            SDL_CloseAudioDevice(device->id);
             SDL_SetError("Couldn't create audio thread");
             return 0;
         }
     }
 
-    return id + 1;
+    return device->id;
 }
 
 
@@ -1431,12 +1557,16 @@
 
     /* Free the driver data */
     current_audio.impl.Deinitialize();
+
     free_device_list(&current_audio.outputDevices,
                      &current_audio.outputDeviceCount);
     free_device_list(&current_audio.inputDevices,
                      &current_audio.inputDeviceCount);
-    SDL_memset(&current_audio, '\0', sizeof(current_audio));
-    SDL_memset(open_devices, '\0', sizeof(open_devices));
+
+    SDL_DestroyMutex(current_audio.detection_lock);
+
+    SDL_zero(current_audio);
+    SDL_zero(open_devices);
 }
 
 #define NUM_FORMATS 10
--- a/src/audio/SDL_sysaudio.h	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/SDL_sysaudio.h	Mon Mar 16 02:11:39 2015 -0400
@@ -31,7 +31,16 @@
 #define _THIS   SDL_AudioDevice *_this
 
 /* Used by audio targets during DetectDevices() */
-typedef void (*SDL_AddAudioDevice)(const char *name);
+typedef int (*SDL_AddAudioDevice)(const char *name);
+
+/* Audio targets should call this as devices are hotplugged. Don't call
+   during DetectDevices(), this is for hotplugging a device later. */
+extern void SDL_AudioDeviceConnected(const int iscapture, const char *name);
+
+/* Audio targets should call this as devices are unplugged.
+  (device) can be NULL if an unopened device is lost. */
+extern void SDL_AudioDeviceDisconnected(const int iscapture, SDL_AudioDevice *device);
+
 
 /* This is the size of a packet when using SDL_QueueAudio(). We allocate
    these as necessary and pool them, under the assumption that we'll
@@ -92,6 +101,12 @@
 
     SDL_AudioDriverImpl impl;
 
+    /* A mutex for device detection */
+    SDL_mutex *detection_lock;
+
+    SDL_bool need_capture_device_redetect;
+    SDL_bool need_output_device_redetect;
+
     char **outputDevices;
     int outputDeviceCount;
 
@@ -114,6 +129,7 @@
 {
     /* * * */
     /* Data common to all devices */
+    SDL_AudioDeviceID id;
 
     /* The current audio specification (shared with audio thread) */
     SDL_AudioSpec spec;
--- a/src/audio/alsa/SDL_alsa_audio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/alsa/SDL_alsa_audio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -320,7 +320,7 @@
                 /* Hmm, not much we can do - abort */
                 fprintf(stderr, "ALSA write failed (unrecoverable): %s\n",
                         ALSA_snd_strerror(status));
-                this->enabled = 0;
+                SDL_AudioDeviceDisconnected(SDL_FALSE, this);
                 return;
             }
             continue;
--- a/src/audio/arts/SDL_artsaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/arts/SDL_artsaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -151,7 +151,7 @@
         /* Check every 10 loops */
         if (this->hidden->parent && (((++cnt) % 10) == 0)) {
             if (kill(this->hidden->parent, 0) < 0 && errno == ESRCH) {
-                this->enabled = 0;
+                SDL_AudioDeviceDisconnected(SDL_FALSE, this);
             }
         }
     }
@@ -179,7 +179,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if (written < 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", written);
--- a/src/audio/bsd/SDL_bsdaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/bsd/SDL_bsdaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -150,7 +150,7 @@
                the user know what happened.
              */
             fprintf(stderr, "SDL: %s\n", message);
-            this->enabled = 0;
+            SDL_AudioDeviceDisconnected(SDL_FALSE, this);
             /* Don't try to close - may hang */
             this->hidden->audio_fd = -1;
 #ifdef DEBUG_AUDIO
@@ -195,7 +195,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if (written < 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", written);
--- a/src/audio/coreaudio/SDL_coreaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/coreaudio/SDL_coreaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -40,13 +40,50 @@
     }
 
 #if MACOSX_COREAUDIO
-typedef void (*addDevFn)(const char *name, AudioDeviceID devId, void *data);
+static const AudioObjectPropertyAddress devlist_address = {
+    kAudioHardwarePropertyDevices,
+    kAudioObjectPropertyScopeGlobal,
+    kAudioObjectPropertyElementMaster
+};
+
+typedef void (*addDevFn)(const char *name, const int iscapture, AudioDeviceID devId, void *data);
+
+typedef struct AudioDeviceList
+{
+    AudioDeviceID devid;
+    SDL_bool alive;
+    struct AudioDeviceList *next;
+} AudioDeviceList;
+
+static AudioDeviceList *output_devs = NULL;
+static AudioDeviceList *capture_devs = NULL;
+
+static SDL_bool
+add_to_internal_dev_list(const int iscapture, AudioDeviceID devId)
+{
+    AudioDeviceList *item = (AudioDeviceList *) SDL_malloc(sizeof (AudioDeviceList));
+    if (item == NULL) {
+        return SDL_FALSE;
+    }
+    item->devid = devId;
+    item->alive = SDL_TRUE;
+    item->next = iscapture ? capture_devs : output_devs;
+    if (iscapture) {
+        capture_devs = item;
+    } else {
+        output_devs = item;
+    }
+
+    return SDL_TRUE;
+}
 
 static void
-addToDevList(const char *name, AudioDeviceID devId, void *data)
+addToDevList(const char *name, const int iscapture, AudioDeviceID devId, void *data)
 {
     SDL_AddAudioDevice addfn = (SDL_AddAudioDevice) data;
-    addfn(name);
+    if (add_to_internal_dev_list(iscapture, devId)) {
+        addfn(name);
+    }
 }
 
 typedef struct
@@ -57,7 +94,7 @@
 } FindDevIdData;
 
 static void
-findDevId(const char *name, AudioDeviceID devId, void *_data)
+findDevId(const char *name, const int iscapture, AudioDeviceID devId, void *_data)
 {
     FindDevIdData *data = (FindDevIdData *) _data;
     if (!data->found) {
@@ -77,14 +114,8 @@
     UInt32 i = 0;
     UInt32 max = 0;
 
-    AudioObjectPropertyAddress addr = {
-        kAudioHardwarePropertyDevices,
-        kAudioObjectPropertyScopeGlobal,
-        kAudioObjectPropertyElementMaster
-    };
-
-    result = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr,
-                                            0, NULL, &size);
+    result = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject,
+                                            &devlist_address, 0, NULL, &size);
     if (result != kAudioHardwareNoError)
         return;
 
@@ -92,8 +123,8 @@
     if (devs == NULL)
         return;
 
-    result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr,
-                                        0, NULL, &size, devs);
+    result = AudioObjectGetPropertyData(kAudioObjectSystemObject,
+                                        &devlist_address, 0, NULL, &size, devs);
     if (result != kAudioHardwareNoError)
         return;
 
@@ -105,10 +136,17 @@
         AudioBufferList *buflist = NULL;
         int usable = 0;
         CFIndex len = 0;
+        const AudioObjectPropertyAddress addr = {
+            kAudioDevicePropertyStreamConfiguration,
+            iscapture ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
+            kAudioObjectPropertyElementMaster
+        };
 
-        addr.mScope = iscapture ? kAudioDevicePropertyScopeInput :
-                        kAudioDevicePropertyScopeOutput;
-        addr.mSelector = kAudioDevicePropertyStreamConfiguration;
+        const AudioObjectPropertyAddress nameaddr = {
+            kAudioObjectPropertyName,
+            iscapture ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
+            kAudioObjectPropertyElementMaster
+        };
 
         result = AudioObjectGetPropertyDataSize(dev, &addr, 0, NULL, &size);
         if (result != noErr)
@@ -136,9 +174,9 @@
         if (!usable)
             continue;
 
-        addr.mSelector = kAudioObjectPropertyName;
+
         size = sizeof (CFStringRef);
-        result = AudioObjectGetPropertyData(dev, &addr, 0, NULL, &size, &cfstr);
+        result = AudioObjectGetPropertyData(dev, &nameaddr, 0, NULL, &size, &cfstr);
         if (result != kAudioHardwareNoError)
             continue;
 
@@ -169,18 +207,96 @@
                    ((iscapture) ? "capture" : "output"),
                    (int) *devCount, ptr, (int) dev);
 #endif
-            addfn(ptr, dev, addfndata);
+            addfn(ptr, iscapture, dev, addfndata);
         }
         SDL_free(ptr);  /* addfn() would have copied the string. */
     }
 }
 
 static void
+free_audio_device_list(AudioDeviceList **list)
+{
+    AudioDeviceList *item = *list;
+    while (item) {
+        AudioDeviceList *next = item->next;
+        SDL_free(item);
+        item = next;
+    }
+    *list = NULL;
+}
+
+static void
 COREAUDIO_DetectDevices(int iscapture, SDL_AddAudioDevice addfn)
 {
+    free_audio_device_list(iscapture ? &capture_devs : &output_devs);
     build_device_list(iscapture, addToDevList, addfn);
 }
 
+static void
+build_device_change_list(const char *name, const int iscapture, AudioDeviceID devId, void *data)
+{
+    AudioDeviceList **list = (AudioDeviceList **) data;
+    AudioDeviceList *item;
+    for (item = *list; item != NULL; item = item->next) {
+        if (item->devid == devId) {
+            item->alive = SDL_TRUE;
+            return;
+        }
+    }
+
+    add_to_internal_dev_list(iscapture, devId);  /* new device, add it. */
+    SDL_AudioDeviceConnected(iscapture, name);
+}
+
+static SDL_bool
+reprocess_device_list(const int iscapture, AudioDeviceList **list)
+{
+    SDL_bool was_disconnect = SDL_FALSE;
+    AudioDeviceList *item;
+    AudioDeviceList *prev = NULL;
+    for (item = *list; item != NULL; item = item->next) {
+        item->alive = SDL_FALSE;
+    }
+
+    build_device_list(iscapture, build_device_change_list, list);
+
+    /* free items in the list that aren't still alive. */
+    item = *list;
+    while (item != NULL) {
+        AudioDeviceList *next = item->next;
+        if (item->alive) {
+            prev = item;
+        } else {
+            was_disconnect = SDL_TRUE;
+            if (prev) {
+                prev->next = item->next;
+            } else {
+                *list = item->next;
+            }
+            SDL_free(item);
+        }
+        item = next;
+    }
+
+    return was_disconnect;
+}
+
+
+/* this is called when the system's list of available audio devices changes. */
+static OSStatus
+device_list_changed(AudioObjectID systemObj, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data)
+{
+    if (reprocess_device_list(SDL_TRUE, &capture_devs)) {
+        SDL_AudioDeviceDisconnected(SDL_TRUE, NULL);
+    }
+
+    if (reprocess_device_list(SDL_FALSE, &output_devs)) {
+        SDL_AudioDeviceDisconnected(SDL_FALSE, NULL);
+    }
+
+    return 0;
+}
+
 static int
 find_device_by_name(_THIS, const char *devname, int iscapture)
 {
@@ -317,11 +433,54 @@
 }
 
 
+#if MACOSX_COREAUDIO
+static const AudioObjectPropertyAddress alive_address =
+{
+    kAudioDevicePropertyDeviceIsAlive,
+    kAudioObjectPropertyScopeGlobal,
+    kAudioObjectPropertyElementMaster
+};
+
+static OSStatus
+device_unplugged(AudioObjectID devid, UInt32 num_addr, const AudioObjectPropertyAddress *addrs, void *data)
+{
+    SDL_AudioDevice *this = (SDL_AudioDevice *) data;
+    SDL_bool dead = SDL_FALSE;
+    UInt32 isAlive = 1;
+    UInt32 size = sizeof (isAlive);
+    OSStatus error;
+
+    if (!this->enabled) {
+        return 0;  /* already known to be dead. */
+    }
+
+    error = AudioObjectGetPropertyData(this->hidden->deviceID, &alive_address,
+                                       0, NULL, &size, &isAlive);
+
+    if (error == kAudioHardwareBadDeviceError) {
+        dead = SDL_TRUE;  /* device was unplugged. */
+    } else if ((error == kAudioHardwareNoError) && (!isAlive)) {
+        dead = SDL_TRUE;  /* device died in some other way. */
+    }
+
+    if (dead) {
+        SDL_AudioDeviceDisconnected(this->iscapture, this);
+    }
+
+    return 0;
+}
+#endif
+
 static void
 COREAUDIO_CloseDevice(_THIS)
 {
     if (this->hidden != NULL) {
         if (this->hidden->audioUnitOpened) {
+            #if MACOSX_COREAUDIO
+            /* Unregister our disconnect callback. */
+            AudioObjectRemovePropertyListener(this->hidden->deviceID, &alive_address, device_unplugged, this);
+            #endif
+
             AURenderCallbackStruct callback;
             const AudioUnitElement output_bus = 0;
             const AudioUnitElement input_bus = 1;
@@ -355,7 +514,6 @@
     }
 }
 
-
 static int
 prepare_audiounit(_THIS, const char *devname, int iscapture,
                   const AudioStreamBasicDescription * strdesc)
@@ -454,6 +612,11 @@
     result = AudioOutputUnitStart(this->hidden->audioUnit);
     CHECK_RESULT("AudioOutputUnitStart");
 
+#if MACOSX_COREAUDIO
+    /* Fire a callback if the device stops being "alive" (disconnected, etc). */
+    AudioObjectAddPropertyListener(this->hidden->deviceID, &alive_address, device_unplugged, this);
+#endif
+
     /* We're running! */
     return 1;
 }
@@ -527,15 +690,27 @@
     return 0;   /* good to go. */
 }
 
+static void
+COREAUDIO_Deinitialize(void)
+{
+#if MACOSX_COREAUDIO
+    AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &devlist_address, device_list_changed, NULL);
+    free_audio_device_list(&capture_devs);
+    free_audio_device_list(&output_devs);
+#endif
+}
+
 static int
 COREAUDIO_Init(SDL_AudioDriverImpl * impl)
 {
     /* Set the function pointers */
     impl->OpenDevice = COREAUDIO_OpenDevice;
     impl->CloseDevice = COREAUDIO_CloseDevice;
+    impl->Deinitialize = COREAUDIO_Deinitialize;
 
 #if MACOSX_COREAUDIO
     impl->DetectDevices = COREAUDIO_DetectDevices;
+    AudioObjectAddPropertyListener(kAudioObjectSystemObject, &devlist_address, device_list_changed, NULL);
 #else
     impl->OnlyHasDefaultOutputDevice = 1;
 
--- a/src/audio/disk/SDL_diskaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/disk/SDL_diskaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -71,7 +71,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if (written != this->hidden->mixlen) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", written);
--- a/src/audio/dsp/SDL_dspaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/dsp/SDL_dspaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -270,7 +270,7 @@
     const int mixlen = this->hidden->mixlen;
     if (write(this->hidden->audio_fd, mixbuf, mixlen) == -1) {
         perror("Audio write");
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", mixlen);
--- a/src/audio/esd/SDL_esdaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/esd/SDL_esdaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -129,7 +129,7 @@
         /* Check every 10 loops */
         if (this->hidden->parent && (((++cnt) % 10) == 0)) {
             if (kill(this->hidden->parent, 0) < 0 && errno == ESRCH) {
-                this->enabled = 0;
+                SDL_AudioDeviceDisconnected(SDL_FALSE, this);
             }
         }
     }
@@ -161,7 +161,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if (written < 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 
--- a/src/audio/fusionsound/SDL_fsaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/fusionsound/SDL_fsaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -143,7 +143,7 @@
                                       this->hidden->mixsamples);
     /* If we couldn't write, assume fatal error for now */
     if (ret) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", this->hidden->mixlen);
--- a/src/audio/paudio/SDL_paudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/paudio/SDL_paudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -176,7 +176,7 @@
              * the user know what happened.
              */
             fprintf(stderr, "SDL: %s - %s\n", strerror(errno), message);
-            this->enabled = 0;
+            SDL_AudioDeviceDisconnected(SDL_FALSE, this);
             /* Don't try to close - may hang */
             this->hidden->audio_fd = -1;
 #ifdef DEBUG_AUDIO
@@ -212,7 +212,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if (written < 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", written);
--- a/src/audio/pulseaudio/SDL_pulseaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/pulseaudio/SDL_pulseaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -302,7 +302,7 @@
         if (PULSEAUDIO_pa_context_get_state(h->context) != PA_CONTEXT_READY ||
             PULSEAUDIO_pa_stream_get_state(h->stream) != PA_STREAM_READY ||
             PULSEAUDIO_pa_mainloop_iterate(h->mainloop, 1, NULL) < 0) {
-            this->enabled = 0;
+            SDL_AudioDeviceDisconnected(SDL_FALSE, this);
             return;
         }
         if (PULSEAUDIO_pa_stream_writable_size(h->stream) >= h->mixlen) {
@@ -318,7 +318,7 @@
     struct SDL_PrivateAudioData *h = this->hidden;
     if (PULSEAUDIO_pa_stream_write(h->stream, h->mixbuf, h->mixlen, NULL, 0LL,
                                    PA_SEEK_RELATIVE) < 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 
--- a/src/audio/qsa/SDL_qsa_audio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/qsa/SDL_qsa_audio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -300,7 +300,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if (towrite != 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 
--- a/src/audio/sndio/SDL_sndioaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/sndio/SDL_sndioaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -158,7 +158,7 @@
 
     /* If we couldn't write, assume fatal error for now */
     if ( written == 0 ) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 #ifdef DEBUG_AUDIO
     fprintf(stderr, "Wrote %d bytes of audio data\n", written);
--- a/src/audio/sun/SDL_sunaudio.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/sun/SDL_sunaudio.c	Mon Mar 16 02:11:39 2015 -0400
@@ -158,7 +158,7 @@
         if (write(this->hidden->audio_fd, this->hidden->ulaw_buf,
             this->hidden->fragsize) < 0) {
             /* Assume fatal error, for now */
-            this->enabled = 0;
+            SDL_AudioDeviceDisconnected(SDL_FALSE, this);
         }
         this->hidden->written += this->hidden->fragsize;
     } else {
@@ -168,7 +168,7 @@
         if (write(this->hidden->audio_fd, this->hidden->mixbuf,
             this->spec.size) < 0) {
             /* Assume fatal error, for now */
-            this->enabled = 0;
+            SDL_AudioDeviceDisconnected(SDL_FALSE, this);
         }
         this->hidden->written += this->hidden->fragsize;
     }
--- a/src/audio/xaudio2/SDL_xaudio2.c	Wed Mar 18 10:09:39 2015 -0400
+++ b/src/audio/xaudio2/SDL_xaudio2.c	Mon Mar 16 02:11:39 2015 -0400
@@ -221,7 +221,7 @@
 
     if (result != S_OK) {  /* uhoh, panic! */
         IXAudio2SourceVoice_FlushSourceBuffers(source);
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 
--- a/test/Makefile.in	Wed Mar 18 10:09:39 2015 -0400
+++ b/test/Makefile.in	Mon Mar 16 02:11:39 2015 -0400
@@ -38,6 +38,7 @@
 	testloadso$(EXE) \
 	testlock$(EXE) \
 	testmultiaudio$(EXE) \
+	testaudiohotplug$(EXE) \
 	testnative$(EXE) \
 	testoverlay2$(EXE) \
 	testplatform$(EXE) \
@@ -105,6 +106,9 @@
 testmultiaudio$(EXE): $(srcdir)/testmultiaudio.c
 	$(CC) -o $@ $^ $(CFLAGS) $(LIBS)
 
+testaudiohotplug$(EXE): $(srcdir)/testaudiohotplug.c
+	$(CC) -o $@ $^ $(CFLAGS) $(LIBS)
+
 testatomic$(EXE): $(srcdir)/testatomic.c
 	$(CC) -o $@ $^ $(CFLAGS) $(LIBS)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/testaudiohotplug.c	Mon Mar 16 02:11:39 2015 -0400
@@ -0,0 +1,182 @@
+/*
+  Copyright (C) 1997-2014 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+
+/* Program to test hotplugging of audio devices */
+
+#include "SDL_config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#if HAVE_SIGNAL_H
+#include <signal.h>
+#endif
+
+#ifdef __EMSCRIPTEN__
+#include <emscripten/emscripten.h>
+#endif
+
+#include "SDL.h"
+
+static SDL_AudioSpec spec;
+static Uint8 *sound = NULL;     /* Pointer to wave data */
+static Uint32 soundlen = 0;     /* Length of wave data */
+
+static int posindex = 0;
+static Uint32 positions[64];
+
+/* Call this instead of exit(), so we can clean up SDL: atexit() is evil. */
+static void
+quit(int rc)
+{
+    SDL_Quit();
+    exit(rc);
+}
+
+void SDLCALL
+fillerup(void *_pos, Uint8 * stream, int len)
+{
+    Uint32 pos = *((Uint32 *) _pos);
+    Uint8 *waveptr;
+    int waveleft;
+
+    /* Set up the pointers */
+    waveptr = sound + pos;
+    waveleft = soundlen - pos;
+
+    /* Go! */
+    while (waveleft <= len) {
+        SDL_memcpy(stream, waveptr, waveleft);
+        stream += waveleft;
+        len -= waveleft;
+        waveptr = sound;
+        waveleft = soundlen;
+        pos = 0;
+    }
+    SDL_memcpy(stream, waveptr, len);
+    pos += len;
+    *((Uint32 *) _pos) = pos;
+}
+
+static int done = 0;
+void
+poked(int sig)
+{
+    done = 1;
+}
+
+static void
+iteration()
+{
+    SDL_Event e;
+    SDL_AudioDeviceID dev;
+    while (SDL_PollEvent(&e)) {
+        if (e.type == SDL_QUIT) {
+            done = 1;
+        } else if (e.type == SDL_AUDIODEVICEADDED) {
+            const char *name = SDL_GetAudioDeviceName(e.adevice.which, 0);
+            SDL_Log("New %s audio device: %s\n", e.adevice.iscapture ? "capture" : "output", name);
+            if (!e.adevice.iscapture) {
+                positions[posindex] = 0;
+                spec.userdata = &positions[posindex++];
+                spec.callback = fillerup;
+                dev = SDL_OpenAudioDevice(name, 0, &spec, NULL, 0);
+                if (!dev) {
+                    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't open '%s': %s\n", name, SDL_GetError());
+                } else {
+                    SDL_Log("Opened '%s' as %u\n", name, (unsigned int) dev);
+                    SDL_PauseAudioDevice(dev, 0);
+                }
+            }
+        } else if (e.type == SDL_AUDIODEVICEREMOVED) {
+            dev = (SDL_AudioDeviceID) e.adevice.which;
+            SDL_Log("%s device %u removed.\n", e.adevice.iscapture ? "capture" : "output", (unsigned int) dev);
+            SDL_CloseAudioDevice(dev);
+        }
+    }
+}
+
+#ifdef __EMSCRIPTEN__
+void
+loop()
+{
+    if(done)
+        emscripten_cancel_main_loop();
+    else
+        iteration();
+}
+#endif
+
+int
+main(int argc, char *argv[])
+{
+    int i;
+    char filename[4096];
+
+	/* Enable standard application logging */
+	SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
+
+    /* Load the SDL library */
+    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s\n", SDL_GetError());
+        return (1);
+    }
+
+    SDL_CreateWindow("testaudiohotplug", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
+
+    if (argc > 1) {
+        SDL_strlcpy(filename, argv[1], sizeof(filename));
+    } else {
+        SDL_strlcpy(filename, "sample.wav", sizeof(filename));
+    }
+    /* Load the wave file into memory */
+    if (SDL_LoadWAV(filename, &spec, &sound, &soundlen) == NULL) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't load %s: %s\n", filename, SDL_GetError());
+        quit(1);
+    }
+
+#if HAVE_SIGNAL_H
+    /* Set the signals */
+#ifdef SIGHUP
+    signal(SIGHUP, poked);
+#endif
+    signal(SIGINT, poked);
+#ifdef SIGQUIT
+    signal(SIGQUIT, poked);
+#endif
+    signal(SIGTERM, poked);
+#endif /* HAVE_SIGNAL_H */
+
+    /* Show the list of available drivers */
+    SDL_Log("Available audio drivers:");
+    for (i = 0; i < SDL_GetNumAudioDrivers(); ++i) {
+		SDL_Log("%i: %s", i, SDL_GetAudioDriver(i));
+    }
+
+    SDL_Log("Using audio driver: %s\n", SDL_GetCurrentAudioDriver());
+
+#ifdef __EMSCRIPTEN__
+    emscripten_set_main_loop(loop, 0, 1);
+#else
+    while (!done) {
+        SDL_Delay(100);
+        iteration();
+    }
+#endif
+
+    /* Clean up on signal */
+    SDL_Quit();
+    SDL_FreeWAV(sound);
+    return (0);
+}
+
+/* vi: set ts=4 sw=4 expandtab: */