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.
main
Ryan C. Gordon 2023-09-30 16:33:30 -04:00
parent a8813b58a6
commit 482c238953
No known key found for this signature in database
GPG Key ID: FA148B892AB48044
3 changed files with 95 additions and 25 deletions

View File

@ -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. // 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. // Just flag the device so we can kill it in WaitDevice/PlayDevice instead.
SDL_AudioDevice *device = (SDL_AudioDevice *) userData; 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. 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); 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) static int AAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen)
{ {
struct SDL_PrivateAudioData *hidden = device->hidden; struct SDL_PrivateAudioData *hidden = device->hidden;
// AAUDIO_dataCallback picks up our work and unblocks AAUDIO_WaitDevice. But make sure we didn't fail here. // 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)) { if (RecoverAAudioDeviceIfFailed(device) == -1) {
SDL_AtomicSet(&hidden->error_callback_triggered, 0); return -1; // oh well, we went down hard.
return -1;
} }
SDL_MemoryBarrierRelease(); SDL_MemoryBarrierRelease();
@ -219,34 +266,18 @@ static void AAUDIO_CloseDevice(SDL_AudioDevice *device)
SDL_DestroySemaphore(hidden->semaphore); SDL_DestroySemaphore(hidden->semaphore);
} }
SDL_free(hidden->mixbuf); SDL_aligned_free(hidden->mixbuf);
SDL_free(hidden); SDL_free(hidden);
device->hidden = NULL; 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; const SDL_bool iscapture = device->iscapture;
aaudio_result_t res; 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); SDL_AtomicSet(&hidden->error_callback_triggered, 0);
AAudioStreamBuilder *builder = NULL; AAudioStreamBuilder *builder = NULL;
@ -262,9 +293,11 @@ static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
ctx.AAudioStreamBuilder_setSampleRate(builder, device->spec.freq); ctx.AAudioStreamBuilder_setSampleRate(builder, device->spec.freq);
ctx.AAudioStreamBuilder_setChannelCount(builder, device->spec.channels); ctx.AAudioStreamBuilder_setChannelCount(builder, device->spec.channels);
#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
const int aaudio_device_id = (int) ((size_t) device->handle); const int aaudio_device_id = (int) ((size_t) device->handle);
LOGI("Opening device id %d", aaudio_device_id); LOGI("Opening device id %d", aaudio_device_id);
ctx.AAudioStreamBuilder_setDeviceId(builder, aaudio_device_id); ctx.AAudioStreamBuilder_setDeviceId(builder, aaudio_device_id);
#endif
const aaudio_direction_t direction = (iscapture ? AAUDIO_DIRECTION_INPUT : AAUDIO_DIRECTION_OUTPUT); const aaudio_direction_t direction = (iscapture ? AAUDIO_DIRECTION_INPUT : AAUDIO_DIRECTION_OUTPUT);
ctx.AAudioStreamBuilder_setDirection(builder, direction); ctx.AAudioStreamBuilder_setDirection(builder, direction);
@ -321,7 +354,7 @@ static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
// Allocate a double buffered mixing buffer // Allocate a double buffered mixing buffer
hidden->num_buffers = 2; hidden->num_buffers = 2;
hidden->mixbuf_bytes = (hidden->num_buffers * device->buffer_size); 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) { if (hidden->mixbuf == NULL) {
return SDL_OutOfMemory(); return SDL_OutOfMemory();
} }
@ -343,9 +376,33 @@ static int AAUDIO_OpenDevice(SDL_AudioDevice *device)
} }
LOGI("SDL AAudioStream_requestStart OK"); LOGI("SDL AAudioStream_requestStart OK");
return 0; 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) static SDL_bool PauseOneDevice(SDL_AudioDevice *device, void *userdata)
{ {
struct SDL_PrivateAudioData *hidden = (struct SDL_PrivateAudioData *)device->hidden; 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->ThreadInit = Android_AudioThreadInit;
impl->DetectDevices = Android_StartAudioHotplug;
impl->Deinitialize = AAUDIO_Deinitialize; impl->Deinitialize = AAUDIO_Deinitialize;
impl->OpenDevice = AAUDIO_OpenDevice; impl->OpenDevice = AAUDIO_OpenDevice;
impl->CloseDevice = AAUDIO_CloseDevice; impl->CloseDevice = AAUDIO_CloseDevice;
@ -461,6 +517,13 @@ static SDL_bool AAUDIO_Init(SDL_AudioDriverImpl *impl)
impl->HasCaptureSupport = SDL_TRUE; 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"); LOGI("SDL AAUDIO_Init OK");
return SDL_TRUE; return SDL_TRUE;
} }

View File

@ -1006,6 +1006,7 @@ JNIEXPORT void JNICALL
SDL_JAVA_AUDIO_INTERFACE(addAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_capture, SDL_JAVA_AUDIO_INTERFACE(addAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_capture,
jstring name, jint device_id) jstring name, jint device_id)
{ {
#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
if (SDL_GetCurrentAudioDriver() != NULL) { if (SDL_GetCurrentAudioDriver() != NULL) {
void *handle = (void *)((size_t)device_id); void *handle = (void *)((size_t)device_id);
if (!SDL_FindPhysicalAudioDeviceByHandle(handle)) { 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); (*env)->ReleaseStringUTFChars(env, name, utf8name);
} }
} }
#endif
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
SDL_JAVA_AUDIO_INTERFACE(removeAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_capture, SDL_JAVA_AUDIO_INTERFACE(removeAudioDevice)(JNIEnv *env, jclass jcls, jboolean is_capture,
jint device_id) jint device_id)
{ {
#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES
if (SDL_GetCurrentAudioDriver() != NULL) { if (SDL_GetCurrentAudioDriver() != NULL) {
SDL_Log("Removing device with handle %d, capture %d", device_id, is_capture); SDL_Log("Removing device with handle %d, capture %d", device_id, is_capture);
SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)device_id))); SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)device_id)));
} }
#endif
} }
/* Paddown */ /* Paddown */

View File

@ -32,6 +32,9 @@ extern "C" {
#include "../../audio/SDL_sysaudio.h" #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 */ /* Interface from the SDL library into the Android Java activity */
extern void Android_JNI_SetActivityTitle(const char *title); extern void Android_JNI_SetActivityTitle(const char *title);
extern void Android_JNI_SetWindowStyle(SDL_bool fullscreen); extern void Android_JNI_SetWindowStyle(SDL_bool fullscreen);