From 0e02ce08563cdf8158924ed51c5444bb4888d4d4 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Mon, 16 Mar 2015 02:11:39 -0400 Subject: [PATCH] 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 | 19 ++ src/audio/SDL_audio.c | 312 ++++++++++++++++++-------- src/audio/SDL_sysaudio.h | 18 +- src/audio/alsa/SDL_alsa_audio.c | 2 +- src/audio/arts/SDL_artsaudio.c | 4 +- src/audio/bsd/SDL_bsdaudio.c | 4 +- src/audio/coreaudio/SDL_coreaudio.c | 217 ++++++++++++++++-- src/audio/disk/SDL_diskaudio.c | 2 +- src/audio/dsp/SDL_dspaudio.c | 2 +- src/audio/esd/SDL_esdaudio.c | 4 +- src/audio/fusionsound/SDL_fsaudio.c | 2 +- src/audio/paudio/SDL_paudio.c | 4 +- src/audio/pulseaudio/SDL_pulseaudio.c | 4 +- src/audio/qsa/SDL_qsa_audio.c | 2 +- src/audio/sndio/SDL_sndioaudio.c | 2 +- src/audio/sun/SDL_sunaudio.c | 4 +- src/audio/xaudio2/SDL_xaudio2.c | 2 +- test/Makefile.in | 4 + test/testaudiohotplug.c | 182 +++++++++++++++ 19 files changed, 658 insertions(+), 132 deletions(-) create mode 100644 test/testaudiohotplug.c diff --git a/include/SDL_events.h b/include/SDL_events.h index 09ab9c2f2..521b65be1 100644 --- a/include/SDL_events.h +++ b/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 */ diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 4cc2ef546..519e86a22 100644 --- a/src/audio/SDL_audio.c +++ b/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, ¤t_audio.inputDevices, ¤t_audio.inputDeviceCount); +} + +static int +add_output_device(const char *name) +{ + return add_audio_device(name, ¤t_audio.outputDevices, ¤t_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(¤t_audio.outputDevices, ¤t_audio.outputDeviceCount); + current_audio.impl.DetectDevices(SDL_FALSE, add_output_device); + } + } else { + if ((current_audio.impl.HasCaptureSupport) && (!current_audio.impl.OnlyHasDefaultInputDevice)) { + free_device_list(¤t_audio.inputDevices, ¤t_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(¤t_audio.inputDevices, - ¤t_audio.inputDeviceCount); - current_audio.impl.DetectDevices(iscapture, SDL_AddCaptureAudioDevice); - retval = current_audio.inputDeviceCount; - } else { - free_device_list(¤t_audio.outputDevices, - ¤t_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(¤t_audio.outputDevices, ¤t_audio.outputDeviceCount); free_device_list(¤t_audio.inputDevices, ¤t_audio.inputDeviceCount); - SDL_memset(¤t_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 diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index 972828c28..55cb2ecce 100644 --- a/src/audio/SDL_sysaudio.h +++ b/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; diff --git a/src/audio/alsa/SDL_alsa_audio.c b/src/audio/alsa/SDL_alsa_audio.c index 1f3def3f6..b600474d2 100644 --- a/src/audio/alsa/SDL_alsa_audio.c +++ b/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; diff --git a/src/audio/arts/SDL_artsaudio.c b/src/audio/arts/SDL_artsaudio.c index 72fba70cc..7a8cf6605 100644 --- a/src/audio/arts/SDL_artsaudio.c +++ b/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); diff --git a/src/audio/bsd/SDL_bsdaudio.c b/src/audio/bsd/SDL_bsdaudio.c index f415fe040..2488bef6c 100644 --- a/src/audio/bsd/SDL_bsdaudio.c +++ b/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); diff --git a/src/audio/coreaudio/SDL_coreaudio.c b/src/audio/coreaudio/SDL_coreaudio.c index a263ae441..5493d3c59 100644 --- a/src/audio/coreaudio/SDL_coreaudio.c +++ b/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; + 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 @@ 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; diff --git a/src/audio/disk/SDL_diskaudio.c b/src/audio/disk/SDL_diskaudio.c index cc4e3efc6..4b9214819 100644 --- a/src/audio/disk/SDL_diskaudio.c +++ b/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); diff --git a/src/audio/dsp/SDL_dspaudio.c b/src/audio/dsp/SDL_dspaudio.c index eea5a5f86..5807a571c 100644 --- a/src/audio/dsp/SDL_dspaudio.c +++ b/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); diff --git a/src/audio/esd/SDL_esdaudio.c b/src/audio/esd/SDL_esdaudio.c index e675272dc..69c672a70 100644 --- a/src/audio/esd/SDL_esdaudio.c +++ b/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); } } diff --git a/src/audio/fusionsound/SDL_fsaudio.c b/src/audio/fusionsound/SDL_fsaudio.c index b8367715f..643002dc6 100644 --- a/src/audio/fusionsound/SDL_fsaudio.c +++ b/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); diff --git a/src/audio/paudio/SDL_paudio.c b/src/audio/paudio/SDL_paudio.c index 032d8d2cd..d3c2d82b1 100644 --- a/src/audio/paudio/SDL_paudio.c +++ b/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); diff --git a/src/audio/pulseaudio/SDL_pulseaudio.c b/src/audio/pulseaudio/SDL_pulseaudio.c index 915d5d6db..e108280b9 100644 --- a/src/audio/pulseaudio/SDL_pulseaudio.c +++ b/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); } } diff --git a/src/audio/qsa/SDL_qsa_audio.c b/src/audio/qsa/SDL_qsa_audio.c index d6b8a6800..4121204ab 100644 --- a/src/audio/qsa/SDL_qsa_audio.c +++ b/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); } } diff --git a/src/audio/sndio/SDL_sndioaudio.c b/src/audio/sndio/SDL_sndioaudio.c index 5c0636500..e4a85a73b 100644 --- a/src/audio/sndio/SDL_sndioaudio.c +++ b/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); diff --git a/src/audio/sun/SDL_sunaudio.c b/src/audio/sun/SDL_sunaudio.c index 7efe30ece..569f9ba1d 100644 --- a/src/audio/sun/SDL_sunaudio.c +++ b/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; } diff --git a/src/audio/xaudio2/SDL_xaudio2.c b/src/audio/xaudio2/SDL_xaudio2.c index 85ac14602..37fdf593b 100644 --- a/src/audio/xaudio2/SDL_xaudio2.c +++ b/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); } } diff --git a/test/Makefile.in b/test/Makefile.in index 078e4eb99..1a8cec614 100644 --- a/test/Makefile.in +++ b/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) diff --git a/test/testaudiohotplug.c b/test/testaudiohotplug.c new file mode 100644 index 000000000..08bfa74de --- /dev/null +++ b/test/testaudiohotplug.c @@ -0,0 +1,182 @@ +/* + Copyright (C) 1997-2014 Sam Lantinga + + 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 +#include + +#if HAVE_SIGNAL_H +#include +#endif + +#ifdef __EMSCRIPTEN__ +#include +#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: */