SDL: aaudio: Deal with device recovery.

From 482c2389535a8366187e5b859624bb66b3565f71 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Sat, 30 Sep 2023 16:33:30 -0400
Subject: [PATCH] aaudio: Deal with device recovery.

Android claims to work with multiple devices, but doesn't actually appear to
(at least, afaict), and it will report tons of devices that all just seem
to play to the current default output, so for now, turn this off and only
expose a default device.

And then, with that default output, attempt to recover on errors by throwing
away the current AAudioStream and building a new one.

This let me plug/unplug a set of headphones from the headphone jack and audio
would switch correctly to the new output.
---
 src/audio/aaudio/SDL_aaudio.c  | 113 +++++++++++++++++++++++++--------
 src/core/android/SDL_android.c |   4 ++
 src/core/android/SDL_android.h |   3 +
 3 files changed, 95 insertions(+), 25 deletions(-)

diff --git a/src/audio/aaudio/SDL_aaudio.c b/src/audio/aaudio/SDL_aaudio.c
index 4fe4c00f6060..5b4e6efb7248 100644
--- a/src/audio/aaudio/SDL_aaudio.c
+++ b/src/audio/aaudio/SDL_aaudio.c
@@ -85,7 +85,7 @@ static void AAUDIO_errorCallback(AAudioStream *stream, void *userData, aaudio_re
     // You MUST NOT close the audio stream from this callback, so we cannot call SDL_AudioDeviceDisconnected here.
     // Just flag the device so we can kill it in WaitDevice/PlayDevice instead.
     SDL_AudioDevice *device = (SDL_AudioDevice *) userData;
-    SDL_AtomicSet(&device->hidden->error_callback_triggered, 1);
+    SDL_AtomicSet(&device->hidden->error_callback_triggered, (int) error);  // AAUDIO_OK is zero, so !triggered means no error.
     SDL_PostSemaphore(device->hidden->semaphore);  // in case we're blocking in WaitDevice.
 }
 
@@ -169,14 +169,61 @@ static void AAUDIO_WaitDevice(SDL_AudioDevice *device)
     SDL_WaitSemaphore(device->hidden->semaphore);
 }
 
+static int BuildAAudioStream(SDL_AudioDevice *device);
+
+static int RecoverAAudioDeviceIfFailed(SDL_AudioDevice *device)
+{
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    const aaudio_result_t err = (aaudio_result_t) SDL_AtomicGet(&hidden->error_callback_triggered);
+    if (err) {
+        SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "aaudio: Audio device triggered error %d (%s)", (int) err, ctx.AAudio_convertResultToText(err));
+
+        // attempt to build a new stream, in case there's a new default device.
+        ctx.AAudioStream_requestStop(hidden->stream);
+        ctx.AAudioStream_close(hidden->stream);
+        hidden->stream = NULL;
+
+        SDL_aligned_free(hidden->mixbuf);
+        hidden->mixbuf = NULL;
+
+        SDL_DestroySemaphore(hidden->semaphore);
+        hidden->semaphore = NULL;
+
+        const int prev_sample_frames = device->sample_frames;
+        SDL_AudioSpec prevspec;
+        SDL_copyp(&prevspec, &device->spec);
+
+        if (BuildAAudioStream(device) == -1) {
+            return -1;  // oh well, we tried.
+        }
+
+        // we don't know the new device spec until we open the new device, so we saved off the old one and force it back
+        // so SDL_AudioDeviceFormatChanged can set up all the important state if necessary and then set it back to the new spec.
+        const int new_sample_frames = device->sample_frames;
+        SDL_AudioSpec newspec;
+        SDL_copyp(&newspec, &device->spec);
+
+        device->sample_frames = prev_sample_frames;
+        SDL_copyp(&device->spec, &prevspec);
+        if (SDL_AudioDeviceFormatChangedAlreadyLocked(device, &newspec, new_sample_frames) == -1) {
+            return -1;  // ugh
+        }
+
+        // we're recovering from PlayDevice, so wait until the data callback fires so we know we fed the pending buffer to the device.
+        SDL_WaitSemaphore(device->hidden->semaphore);
+    }
+
+    return 0;
+}
+
+
 static int AAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen)
 {
     struct SDL_PrivateAudioData *hidden = device->hidden;
 
     // AAUDIO_dataCallback picks up our work and unblocks AAUDIO_WaitDevice. But make sure we didn't fail here.
-    if (SDL_AtomicGet(&hidden->error_callback_triggered)) {
-        SDL_AtomicSet(&hidden->error_callback_triggered, 0);
-        return -1;
+    if (RecoverAAudioDeviceIfFailed(device) == -1) {
+        return -1;  // oh well, we went down hard.
     }
 
     SDL_MemoryBarrierRelease();
@@ -219,34 +266,18 @@ static void AAUDIO_CloseDevice(SDL_AudioDevice *device)
             SDL_DestroySemaphore(hidden->semaphore);
         }
 
-        SDL_free(hidden->mixbuf);
+        SDL_aligned_free(hidden->mixbuf);
         SDL_free(hidden);
         device->hidden = NULL;
     }
 }
 
-static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
+static int BuildAAudioStream(SDL_AudioDevice *device)
 {
-    struct SDL_PrivateAudioData *hidden;
+    struct SDL_PrivateAudioData *hidden = device->hidden;
     const SDL_bool iscapture = device->iscapture;
     aaudio_result_t res;
 
-    SDL_assert(device->handle != NULL);  // AAUDIO_UNSPECIFIED is zero, so legit devices should all be non-zero.
-
-    LOGI(__func__);
-
-    if (iscapture) {
-        if (!Android_JNI_RequestPermission("android.permission.RECORD_AUDIO")) {
-            LOGI("This app doesn't have RECORD_AUDIO permission");
-            return SDL_SetError("This app doesn't have RECORD_AUDIO permission");
-        }
-    }
-
-    hidden = device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden));
-    if (hidden == NULL) {
-        return SDL_OutOfMemory();
-    }
-
     SDL_AtomicSet(&hidden->error_callback_triggered, 0);
 
     AAudioStreamBuilder *builder = NULL;
@@ -262,9 +293,11 @@ static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
     ctx.AAudioStreamBuilder_setSampleRate(builder, device->spec.freq);
     ctx.AAudioStreamBuilder_setChannelCount(builder, device->spec.channels);
 
+#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
     const int aaudio_device_id = (int) ((size_t) device->handle);
     LOGI("Opening device id %d", aaudio_device_id);
     ctx.AAudioStreamBuilder_setDeviceId(builder, aaudio_device_id);
+#endif
 
     const aaudio_direction_t direction = (iscapture ? AAUDIO_DIRECTION_INPUT : AAUDIO_DIRECTION_OUTPUT);
     ctx.AAudioStreamBuilder_setDirection(builder, direction);
@@ -321,7 +354,7 @@ static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
     // Allocate a double buffered mixing buffer
     hidden->num_buffers = 2;
     hidden->mixbuf_bytes = (hidden->num_buffers * device->buffer_size);
-    hidden->mixbuf = (Uint8 *)SDL_malloc(hidden->mixbuf_bytes);
+    hidden->mixbuf = (Uint8 *)SDL_aligned_alloc(SDL_SIMDGetAlignment(), hidden->mixbuf_bytes);
     if (hidden->mixbuf == NULL) {
         return SDL_OutOfMemory();
     }
@@ -343,9 +376,33 @@ static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
     }
 
     LOGI("SDL AAudioStream_requestStart OK");
+
     return 0;
 }
 
+static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
+{
+#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
+    SDL_assert(device->handle != NULL);  // AAUDIO_UNSPECIFIED is zero, so legit devices should all be non-zero.
+#endif
+
+    LOGI(__func__);
+
+    if (device->iscapture) {
+        if (!Android_JNI_RequestPermission("android.permission.RECORD_AUDIO")) {
+            LOGI("This app doesn't have RECORD_AUDIO permission");
+            return SDL_SetError("This app doesn't have RECORD_AUDIO permission");
+        }
+    }
+
+    device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden));
+    if (device->hidden == NULL) {
+        return SDL_OutOfMemory();
+    }
+
+    return BuildAAudioStream(device);
+}
+
 static SDL_bool PauseOneDevice(SDL_AudioDevice *device, void *userdata)
 {
     struct SDL_PrivateAudioData *hidden = (struct SDL_PrivateAudioData *)device->hidden;
@@ -449,7 +506,6 @@ static SDL_bool AAUDIO_Init(SDL_AudioDriverImpl *impl)
     }
 
     impl->ThreadInit = Android_AudioThreadInit;
-    impl->DetectDevices = Android_StartAudioHotplug;
     impl->Deinitialize = AAUDIO_Deinitialize;
     impl->OpenDevice = AAUDIO_OpenDevice;
     impl->CloseDevice = AAUDIO_CloseDevice;
@@ -461,6 +517,13 @@ static SDL_bool AAUDIO_Init(SDL_AudioDriverImpl *impl)
 
     impl->HasCaptureSupport = SDL_TRUE;
 
+#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
+    impl->DetectDevices = Android_StartAudioHotplug;
+#else
+    impl->OnlyHasDefaultOutputDevice = SDL_TRUE;
+    impl->OnlyHasDefaultCaptureDevice = SDL_TRUE;
+#endif
+
     LOGI("SDL AAUDIO_Init OK");
     return SDL_TRUE;
 }
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index 41a0ad002e03..34c939e3b206 100644
--- a/src/core/android/SDL_android.c
+++ b/src/core/android/SDL_android.c
@@ -1006,6 +1006,7 @@ JNIEXPORT void JNICALL
 SDL_JAVA_AUDIO_INTERFACE(addAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_capture,
                                          jstring name, jint device_id)
 {
+#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
     if (SDL_GetCurrentAudioDriver() != NULL) {
         void *handle = (void *)((size_t)device_id);
         if (!SDL_FindPhysicalAudioDeviceByHandle(handle)) {
@@ -1014,16 +1015,19 @@ SDL_JAVA_AUDIO_INTERFACE(addAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_c
             (*env)->ReleaseStringUTFChars(env, name, utf8name);
         }
     }
+#endif
 }
 
 JNIEXPORT void JNICALL
 SDL_JAVA_AUDIO_INTERFACE(removeAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_capture,
                                             jint device_id)
 {
+#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
     if (SDL_GetCurrentAudioDriver() != NULL) {
         SDL_Log("Removing device with handle %d, capture %d", device_id, is_capture);
         SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)device_id)));
     }
+#endif
 }
 
 /* Paddown */
diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h
index bc090b88eb98..10c95161f932 100644
--- a/src/core/android/SDL_android.h
+++ b/src/core/android/SDL_android.h
@@ -32,6 +32,9 @@ extern "C" {
 
 #include "../../audio/SDL_sysaudio.h"
 
+// this appears to be broken right now (on Android, not SDL, I think...?).
+#define ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES 0
+
 /* Interface from the SDL library into the Android Java activity */
 extern void Android_JNI_SetActivityTitle(const char *title);
 extern void Android_JNI_SetWindowStyle(SDL_bool fullscreen);