Browse Source

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.
Ryan C. Gordon 10 years ago
parent
commit
0e02ce0856

+ 19 - 0
include/SDL_events.h

@@ -110,6 +110,10 @@ typedef enum
     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 @@ typedef struct SDL_ControllerDeviceEvent
     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 @@ typedef union SDL_Event
     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 */

+ 221 - 91
src/audio/SDL_audio.c

@@ -333,6 +333,144 @@ SDL_StreamDeinit(SDL_AudioStreamer * stream)
 }
 #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 @@ SDL_RunAudio(void *devicep)
 
             /* !!! 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 @@ SDL_AudioInit(const char *driver_name)
         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 @@ SDL_GetCurrentAudioDriver()
     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 @@ SDL_GetNumAudioDevices(int iscapture)
         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 @@ SDL_GetNumAudioDevices(int iscapture)
 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 @@ SDL_GetAudioDeviceName(int index, int iscapture)
         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 @@ open_audio_device(const char *devname, int iscapture,
         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 @@ open_audio_device(const char *devname, int iscapture,
         return 0;
     }
     SDL_zerop(device);
+    device->id = id + 1;
     device->spec = *obtained;
     device->enabled = 1;
     device->paused = 1;
@@ -1150,12 +1293,6 @@ open_audio_device(const char *devname, int iscapture,
         }
     }
 
-    /* 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 @@ open_audio_device(const char *devname, int iscapture,
         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 @@ open_audio_device(const char *devname, int iscapture,
         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 @@ SDL_AudioQuit(void)
 
     /* 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

+ 17 - 1
src/audio/SDL_sysaudio.h

@@ -31,7 +31,16 @@ typedef struct SDL_AudioDevice SDL_AudioDevice;
 #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 @@ typedef struct SDL_AudioDriver
 
     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 @@ struct SDL_AudioDevice
 {
     /* * * */
     /* Data common to all devices */
+    SDL_AudioDeviceID id;
 
     /* The current audio specification (shared with audio thread) */
     SDL_AudioSpec spec;

+ 1 - 1
src/audio/alsa/SDL_alsa_audio.c

@@ -320,7 +320,7 @@ ALSA_PlayDevice(_THIS)
                 /* 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;

+ 2 - 2
src/audio/arts/SDL_artsaudio.c

@@ -151,7 +151,7 @@ ARTS_WaitDevice(_THIS)
         /* 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 @@ ARTS_PlayDevice(_THIS)
 
     /* 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);

+ 2 - 2
src/audio/bsd/SDL_bsdaudio.c

@@ -150,7 +150,7 @@ BSDAUDIO_WaitDevice(_THIS)
                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 @@ BSDAUDIO_PlayDevice(_THIS)
 
     /* 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);

+ 197 - 22
src/audio/coreaudio/SDL_coreaudio.c

@@ -40,13 +40,50 @@ static void COREAUDIO_CloseDevice(_THIS);
     }
 
 #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 @@ typedef struct
 } 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 @@ build_device_list(int iscapture, addDevFn addfn, void *addfndata)
     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 @@ build_device_list(int iscapture, addDevFn addfn, void *addfndata)
     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 @@ build_device_list(int iscapture, addDevFn addfn, void *addfndata)
         AudioBufferList *buflist = NULL;
         int usable = 0;
         CFIndex len = 0;
-
-        addr.mScope = iscapture ? kAudioDevicePropertyScopeInput :
-                        kAudioDevicePropertyScopeOutput;
-        addr.mSelector = kAudioDevicePropertyStreamConfiguration;
+        const AudioObjectPropertyAddress addr = {
+            kAudioDevicePropertyStreamConfiguration,
+            iscapture ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
+            kAudioObjectPropertyElementMaster
+        };
+
+        const AudioObjectPropertyAddress nameaddr = {
+            kAudioObjectPropertyName,
+            iscapture ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput,
+            kAudioObjectPropertyElementMaster
+        };
 
         result = AudioObjectGetPropertyDataSize(dev, &addr, 0, NULL, &size);
         if (result != noErr)
@@ -136,9 +174,9 @@ build_device_list(int iscapture, addDevFn addfn, void *addfndata)
         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 @@ build_device_list(int iscapture, addDevFn addfn, void *addfndata)
                    ((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 @@ inputCallback(void *inRefCon,
 }
 
 
+#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 @@ COREAUDIO_CloseDevice(_THIS)
     }
 }
 
-
 static int
 prepare_audiounit(_THIS, const char *devname, int iscapture,
                   const AudioStreamBasicDescription * strdesc)
@@ -454,6 +612,11 @@ prepare_audiounit(_THIS, const char *devname, int iscapture,
     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 @@ COREAUDIO_OpenDevice(_THIS, const char *devname, int iscapture)
     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;
 

+ 1 - 1
src/audio/disk/SDL_diskaudio.c

@@ -71,7 +71,7 @@ DISKAUD_PlayDevice(_THIS)
 
     /* 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);

+ 1 - 1
src/audio/dsp/SDL_dspaudio.c

@@ -270,7 +270,7 @@ DSP_PlayDevice(_THIS)
     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);

+ 2 - 2
src/audio/esd/SDL_esdaudio.c

@@ -129,7 +129,7 @@ ESD_WaitDevice(_THIS)
         /* 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 @@ ESD_PlayDevice(_THIS)
 
     /* If we couldn't write, assume fatal error for now */
     if (written < 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 

+ 1 - 1
src/audio/fusionsound/SDL_fsaudio.c

@@ -143,7 +143,7 @@ SDL_FS_PlayDevice(_THIS)
                                       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);

+ 2 - 2
src/audio/paudio/SDL_paudio.c

@@ -176,7 +176,7 @@ PAUDIO_WaitDevice(_THIS)
              * 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 @@ PAUDIO_PlayDevice(_THIS)
 
     /* 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);

+ 2 - 2
src/audio/pulseaudio/SDL_pulseaudio.c

@@ -302,7 +302,7 @@ PULSEAUDIO_WaitDevice(_THIS)
         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 @@ PULSEAUDIO_PlayDevice(_THIS)
     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);
     }
 }
 

+ 1 - 1
src/audio/qsa/SDL_qsa_audio.c

@@ -300,7 +300,7 @@ QSA_PlayDevice(_THIS)
 
     /* If we couldn't write, assume fatal error for now */
     if (towrite != 0) {
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 

+ 1 - 1
src/audio/sndio/SDL_sndioaudio.c

@@ -158,7 +158,7 @@ SNDIO_PlayDevice(_THIS)
 
     /* 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);

+ 2 - 2
src/audio/sun/SDL_sunaudio.c

@@ -158,7 +158,7 @@ SUNAUDIO_PlayDevice(_THIS)
         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 @@ SUNAUDIO_PlayDevice(_THIS)
         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;
     }

+ 1 - 1
src/audio/xaudio2/SDL_xaudio2.c

@@ -221,7 +221,7 @@ XAUDIO2_PlayDevice(_THIS)
 
     if (result != S_OK) {  /* uhoh, panic! */
         IXAudio2SourceVoice_FlushSourceBuffers(source);
-        this->enabled = 0;
+        SDL_AudioDeviceDisconnected(SDL_FALSE, this);
     }
 }
 

+ 4 - 0
test/Makefile.in

@@ -38,6 +38,7 @@ TARGETS = \
 	testloadso$(EXE) \
 	testlock$(EXE) \
 	testmultiaudio$(EXE) \
+	testaudiohotplug$(EXE) \
 	testnative$(EXE) \
 	testoverlay2$(EXE) \
 	testplatform$(EXE) \
@@ -105,6 +106,9 @@ testautomation$(EXE): $(srcdir)/testautomation.c \
 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)
 

+ 182 - 0
test/testaudiohotplug.c

@@ -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: */