From a990a34ac44003946ea35d020d0b6993da6e024a Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Tue, 14 Apr 2020 22:26:02 -0700 Subject: [PATCH] Cleanly switch between audio recording, playback, and both, on iOS --- src/audio/coreaudio/SDL_coreaudio.m | 106 +++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/src/audio/coreaudio/SDL_coreaudio.m b/src/audio/coreaudio/SDL_coreaudio.m index 52b40b6dd..b8d786d79 100644 --- a/src/audio/coreaudio/SDL_coreaudio.m +++ b/src/audio/coreaudio/SDL_coreaudio.m @@ -280,11 +280,47 @@ device_list_changed(AudioObjectID systemObj, UInt32 num_addr, const AudioObjectP #endif -static int open_playback_devices = 0; -static int open_capture_devices = 0; +static int open_playback_devices; +static int open_capture_devices; +static int num_open_devices; +static SDL_AudioDevice **open_devices; #if !MACOSX_COREAUDIO +static BOOL session_active = NO; + +static void pause_audio_devices() +{ + int i; + + if (!open_devices) { + return; + } + + for (i = 0; i < num_open_devices; ++i) { + SDL_AudioDevice *device = open_devices[i]; + if (device->hidden->audioQueue && !device->hidden->interrupted) { + AudioQueuePause(device->hidden->audioQueue); + } + } +} + +static void resume_audio_devices() +{ + int i; + + if (!open_devices) { + return; + } + + for (i = 0; i < num_open_devices; ++i) { + SDL_AudioDevice *device = open_devices[i]; + if (device->hidden->audioQueue && !device->hidden->interrupted) { + AudioQueueStart(device->hidden->audioQueue, NULL); + } + } +} + static void interruption_begin(_THIS) { if (this != NULL && this->hidden->audioQueue != NULL) { @@ -379,9 +415,18 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP | AVAudioSessionCategoryOptionAllowAirPlay; } + if (category == AVAudioSessionCategoryPlayback || + category == AVAudioSessionCategoryPlayAndRecord) { + options |= AVAudioSessionCategoryOptionDuckOthers; + } if ([session respondsToSelector:@selector(setCategory:mode:options:error:)]) { if (![session.category isEqualToString:category] || session.categoryOptions != options) { + /* Stop the current session so we don't interrupt other application audio */ + pause_audio_devices(); + [session setActive:NO error:nil]; + session_active = NO; + if (![session setCategory:category mode:mode options:options error:&err]) { NSString *desc = err.description; SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String); @@ -390,6 +435,11 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec } } else { if (![session.category isEqualToString:category]) { + /* Stop the current session so we don't interrupt other application audio */ + pause_audio_devices(); + [session setActive:NO error:nil]; + session_active = NO; + if (![session setCategory:category error:&err]) { NSString *desc = err.description; SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String); @@ -398,7 +448,7 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec } } - if (open && (open_playback_devices + open_capture_devices) == 1) { + if ((open_playback_devices || open_capture_devices) && !session_active) { if (![session setActive:YES error:&err]) { if ([err code] == AVAudioSessionErrorCodeResourceNotAvailable && category == AVAudioSessionCategoryPlayAndRecord) { @@ -409,8 +459,12 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec SDL_SetError("Could not activate Audio Session: %s", desc.UTF8String); return NO; } - } else if (!open_playback_devices && !open_capture_devices) { + session_active = YES; + resume_audio_devices(); + } else if (!open_playback_devices && !open_capture_devices && session_active) { + pause_audio_devices(); [session setActive:NO error:nil]; + session_active = NO; } if (open) { @@ -438,14 +492,12 @@ static BOOL update_audio_session(_THIS, SDL_bool open, SDL_bool allow_playandrec this->hidden->interruption_listener = CFBridgingRetain(listener); } else { - if (this->hidden->interruption_listener != NULL) { - SDLInterruptionListener *listener = nil; - listener = (SDLInterruptionListener *) CFBridgingRelease(this->hidden->interruption_listener); - [center removeObserver:listener]; - @synchronized (listener) { - listener.device = NULL; - } - } + SDLInterruptionListener *listener = nil; + listener = (SDLInterruptionListener *) CFBridgingRelease(this->hidden->interruption_listener); + [center removeObserver:listener]; + @synchronized (listener) { + listener.device = NULL; + } } } @@ -471,7 +523,7 @@ outputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffe Uint8 *ptr = (Uint8 *) inBuffer->mAudioData; while (remaining > 0) { - if ( SDL_AudioStreamAvailable(this->stream) == 0 ) { + if (SDL_AudioStreamAvailable(this->stream) == 0) { /* Generate the data */ SDL_LockMutex(this->mixer_lock); (*this->callbackspec.callback)(this->callbackspec.userdata, @@ -480,10 +532,10 @@ outputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffe this->hidden->bufferOffset = 0; SDL_AudioStreamPut(this->stream, this->hidden->buffer, this->hidden->bufferSize); } - if ( SDL_AudioStreamAvailable(this->stream) > 0 ) { + if (SDL_AudioStreamAvailable(this->stream) > 0) { int got; UInt32 len = SDL_AudioStreamAvailable(this->stream); - if ( len > remaining ) + if (len > remaining) len = remaining; got = SDL_AudioStreamGet(this->stream, ptr, len); SDL_assert((got < 0) || (got == len)); @@ -529,7 +581,7 @@ outputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffe static void inputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumberPacketDescriptions, - const AudioStreamPacketDescription *inPacketDescs ) + const AudioStreamPacketDescription *inPacketDescs) { SDL_AudioDevice *this = (SDL_AudioDevice *) inUserData; @@ -619,6 +671,7 @@ static void COREAUDIO_CloseDevice(_THIS) { const SDL_bool iscapture = this->iscapture; + int i; /* !!! FIXME: what does iOS do when a bluetooth audio device vanishes? Headphones unplugged? */ /* !!! FIXME: (we only do a "default" device on iOS right now...can we do more?) */ @@ -638,6 +691,20 @@ COREAUDIO_CloseDevice(_THIS) update_audio_session(this, SDL_FALSE, SDL_TRUE); #endif + for (i = 0; i < num_open_devices; ++i) { + if (open_devices[i] == this) { + --num_open_devices; + if (i < num_open_devices) { + SDL_memmove(&open_devices[i], &open_devices[i+1], sizeof(open_devices[i])*(num_open_devices - i)); + } + break; + } + } + if (num_open_devices == 0) { + SDL_free(open_devices); + open_devices = NULL; + } + /* if callback fires again, feed silence; don't call into the app. */ SDL_AtomicSet(&this->paused, 1); @@ -942,6 +1009,7 @@ COREAUDIO_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) AudioStreamBasicDescription *strdesc; SDL_AudioFormat test_format = SDL_FirstAudioFormat(this->spec.format); int valid_datatype = 0; + SDL_AudioDevice **new_open_devices; /* Initialize all variables that we clean on shutdown */ this->hidden = (struct SDL_PrivateAudioData *) @@ -959,6 +1027,12 @@ COREAUDIO_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) open_playback_devices++; } + new_open_devices = (SDL_AudioDevice **)SDL_realloc(open_devices, sizeof(open_devices[0]) * (num_open_devices + 1)); + if (new_open_devices) { + open_devices = new_open_devices; + open_devices[num_open_devices++] = this; + } + #if !MACOSX_COREAUDIO if (!update_audio_session(this, SDL_TRUE, SDL_TRUE)) { return -1;