Commit f0fca2880f9e882da97546adae5084518f2ef3f4

Alex Szpakowski 2016-09-18T19:22:09

Handle audio interruptions on iOS/tvOS. Fixes bugs 2569 and 2960.

diff --git a/src/audio/coreaudio/SDL_coreaudio.h b/src/audio/coreaudio/SDL_coreaudio.h
index b40b5bb..b7e5b8e 100644
--- a/src/audio/coreaudio/SDL_coreaudio.h
+++ b/src/audio/coreaudio/SDL_coreaudio.h
@@ -34,6 +34,7 @@
 #include <CoreServices/CoreServices.h>
 #else
 #import <AVFoundation/AVFoundation.h>
+#import <UIKit/UIApplication.h>
 #endif
 
 #include <AudioToolbox/AudioToolbox.h>
@@ -56,6 +57,9 @@ struct SDL_PrivateAudioData
     SDL_atomic_t shutdown;
 #if MACOSX_COREAUDIO
     AudioDeviceID deviceID;
+#else
+    SDL_bool interrupted;
+    CFTypeRef interruption_listener;
 #endif
 };
 
diff --git a/src/audio/coreaudio/SDL_coreaudio.m b/src/audio/coreaudio/SDL_coreaudio.m
index 23dd7ce..1beb95f 100644
--- a/src/audio/coreaudio/SDL_coreaudio.m
+++ b/src/audio/coreaudio/SDL_coreaudio.m
@@ -273,10 +273,58 @@ static int open_playback_devices = 0;
 static int open_capture_devices = 0;
 
 #if !MACOSX_COREAUDIO
-static BOOL update_audio_session()
+
+static void interruption_begin(_THIS)
+{
+    if (this != NULL && this->hidden->audioQueue != NULL) {
+        this->hidden->interrupted = SDL_TRUE;
+        AudioQueuePause(this->hidden->audioQueue);
+    }
+}
+
+static void interruption_end(_THIS)
+{
+    if (this != NULL && this->hidden != NULL && this->hidden->audioQueue != NULL
+    && this->hidden->interrupted) {
+        this->hidden->interrupted = SDL_FALSE;
+        AudioQueueStart(this->hidden->audioQueue, NULL);
+    }
+}
+
+@interface SDLInterruptionListener : NSObject
+
+@property (nonatomic, assign) SDL_AudioDevice *device;
+
+@end
+
+@implementation SDLInterruptionListener
+
+- (void)audioSessionInterruption:(NSNotification *)note
+{
+    @synchronized (self) {
+        NSNumber *type = note.userInfo[AVAudioSessionInterruptionTypeKey];
+        if (type.unsignedIntegerValue == AVAudioSessionInterruptionTypeBegan) {
+            interruption_begin(self.device);
+        } else {
+            interruption_end(self.device);
+        }
+    }
+}
+
+- (void)applicationBecameActive:(NSNotification *)note
+{
+    @synchronized (self) {
+        interruption_end(self.device);
+    }
+}
+
+@end
+
+static BOOL update_audio_session(_THIS, SDL_bool open)
 {
     @autoreleasepool {
         AVAudioSession *session = [AVAudioSession sharedInstance];
+        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
         NSString *category;
         NSError *err = nil;
 
@@ -291,6 +339,12 @@ static BOOL update_audio_session()
             category = AVAudioSessionCategoryAmbient;
         }
 
+        if (![session setCategory:category error:&err]) {
+            NSString *desc = err.description;
+            SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String);
+            return NO;
+        }
+
         if (open_playback_devices + open_capture_devices == 1) {
             if (![session setActive:YES error:&err]) {
                 NSString *desc = err.description;
@@ -301,10 +355,38 @@ static BOOL update_audio_session()
             [session setActive:NO error:nil];
         }
 
-        if (![session setCategory:category error:&err]) {
-            NSString *desc = err.description;
-            SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String);
-            return NO;
+        if (open) {
+            SDLInterruptionListener *listener = [SDLInterruptionListener new];
+            listener.device = this;
+
+            [center addObserver:listener
+                       selector:@selector(audioSessionInterruption:)
+                           name:AVAudioSessionInterruptionNotification
+                         object:session];
+
+            /* An interruption end notification is not guaranteed to be sent if
+             we were previously interrupted... resuming if needed when the app
+             becomes active seems to be the way to go. */
+            [center addObserver:listener
+                       selector:@selector(applicationBecameActive:)
+                           name:UIApplicationDidBecomeActiveNotification
+                         object:session];
+
+            [center addObserver:listener
+                       selector:@selector(applicationBecameActive:)
+                           name:UIApplicationWillEnterForegroundNotification
+                         object:session];
+
+            this->hidden->interruption_listener = CFBridgingRetain(listener);
+        } else {
+            if (this->hidden->interruption_listener != NULL) {
+                SDLInterruptionListener *listener = nil;
+                listener = (SDLInterruptionListener *) CFBridgingRelease(this->hidden->interruption_listener);
+                @synchronized (listener) {
+                    listener.device = NULL;
+                }
+                [center removeObserver:listener];
+            }
         }
     }
 
@@ -441,6 +523,10 @@ COREAUDIO_CloseDevice(_THIS)
     AudioObjectRemovePropertyListener(this->hidden->deviceID, &alive_address, device_unplugged, this);
 #endif
 
+#if !MACOSX_COREAUDIO
+    update_audio_session(this, SDL_FALSE);
+#endif
+
     if (this->hidden->thread) {
         SDL_AtomicSet(&this->hidden->shutdown, 1);
         SDL_WaitThread(this->hidden->thread, NULL);
@@ -468,10 +554,6 @@ COREAUDIO_CloseDevice(_THIS)
     } else {
         open_playback_devices--;
     }
-
-#if !MACOSX_COREAUDIO
-    update_audio_session();
-#endif
 }
 
 #if MACOSX_COREAUDIO
@@ -649,7 +731,7 @@ COREAUDIO_OpenDevice(_THIS, void *handle, const char *devname, int iscapture)
     }
 
 #if !MACOSX_COREAUDIO
-    if (!update_audio_session()) {
+    if (!update_audio_session(this, SDL_TRUE)) {
         return -1;
     }
 #endif