Skip to content

Commit 7ffbf71

Browse files
committed
coreaudio: Refactor iOS audio session management.
This makes UpdateAudioSession only deal with setting the category/options; actually setting the session active and adding interruption listeners is moved to COREAUDIO_OpenDevice. OpenDevice exclusively calls UpdateAudioSession now; there's no reason to change the session during device close...even if we could loosen the session's config when closing a recording device but leaving playback running (or vice-versa), it doesn't seem worth it. Likewise, deactivating the session and removing listeners is now handled in COREAUDIO_CloseDevice. The attempt to try setting a more limited category if setting the session to simultaneous recording and playback fails has been removed; this would presumably cause problems in general, and different problems depending on which device you opened first. It's better to just fail in this case, I think. A bunch of code that proved superfluous is now gone; we don't enumerate all devices to count open ones (we just maintain a simple global counter instead, as atomic ints, just in case this might have a subtle threading concern). The (Pause|Resume)AllDevices() code is gone, as we don't need it anymore with the simplified session management. We still pause/resume per-device in the interruption listener. This should also fix a subtle crash bug, where we sometimes fail to change the session on close, causing an early return from UpdateAudioSession and thus never unregistering the listeners, which would touch a free'd pointer if the the listener fires later. Now the listeners are always unregistered in CloseDevice and UpdateAudioSession is never called from there at all. Closes libsdl-org#15439.
1 parent 1db6d53 commit 7ffbf71

1 file changed

Lines changed: 77 additions & 129 deletions

File tree

src/audio/coreaudio/SDL_coreaudio.m

Lines changed: 77 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -313,32 +313,6 @@ static void COREAUDIO_DetectDevices(SDL_AudioDevice **default_playback, SDL_Audi
313313

314314
static bool session_active = false;
315315

316-
static bool PauseOneAudioDevice(SDL_AudioDevice *device, void *userdata)
317-
{
318-
if (device->hidden && device->hidden->audioQueue && !device->hidden->interrupted) {
319-
AudioQueuePause(device->hidden->audioQueue);
320-
}
321-
return false; // keep enumerating devices until we've paused them all.
322-
}
323-
324-
static void PauseAudioDevices(void)
325-
{
326-
(void) SDL_FindPhysicalAudioDeviceByCallback(PauseOneAudioDevice, NULL);
327-
}
328-
329-
static bool ResumeOneAudioDevice(SDL_AudioDevice *device, void *userdata)
330-
{
331-
if (device->hidden && device->hidden->audioQueue && !device->hidden->interrupted) {
332-
AudioQueueStart(device->hidden->audioQueue, NULL);
333-
}
334-
return false; // keep enumerating devices until we've resumed them all.
335-
}
336-
337-
static void ResumeAudioDevices(void)
338-
{
339-
(void) SDL_FindPhysicalAudioDeviceByCallback(ResumeOneAudioDevice, NULL);
340-
}
341-
342316
static void InterruptionBegin(SDL_AudioDevice *device)
343317
{
344318
if (device != NULL && device->hidden != NULL && device->hidden->audioQueue != NULL) {
@@ -383,40 +357,22 @@ - (void)applicationBecameActive:(NSNotification *)note
383357

384358
@end
385359

386-
typedef struct
387-
{
388-
int playback;
389-
int recording;
390-
} CountOpenAudioDevicesData;
391360

392-
static bool CountOpenAudioDevices(SDL_AudioDevice *device, void *userdata)
393-
{
394-
CountOpenAudioDevicesData *data = (CountOpenAudioDevicesData *) userdata;
395-
if (device->hidden != NULL) { // assume it's open if hidden != NULL
396-
if (device->recording) {
397-
data->recording++;
398-
} else {
399-
data->playback++;
400-
}
401-
}
402-
return false; // keep enumerating until all devices have been checked.
403-
}
361+
static SDL_AtomicInt open_playback_device_count;
362+
static SDL_AtomicInt open_recording_device_count;
404363

405-
static bool UpdateAudioSession(SDL_AudioDevice *device, bool open, bool allow_playandrecord)
364+
static bool UpdateAudioSession(SDL_AudioDevice *device, bool allow_playandrecord)
406365
{
407366
@autoreleasepool {
408367
AVAudioSession *session = [AVAudioSession sharedInstance];
409-
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
410-
411368
NSString *category = AVAudioSessionCategoryPlayback;
412369
NSString *mode = AVAudioSessionModeDefault;
413370
NSUInteger options = AVAudioSessionCategoryOptionMixWithOthers;
414371
NSError *err = nil;
415372
const char *hint;
416373

417-
CountOpenAudioDevicesData data;
418-
SDL_zero(data);
419-
(void) SDL_FindPhysicalAudioDeviceByCallback(CountOpenAudioDevices, &data);
374+
const int opened_for_playback = SDL_GetAtomicInt(&open_playback_device_count);
375+
const int opened_for_recording = SDL_GetAtomicInt(&open_recording_device_count);
420376

421377
hint = SDL_GetHint(SDL_HINT_AUDIO_CATEGORY);
422378
if (hint) {
@@ -436,14 +392,14 @@ static bool UpdateAudioSession(SDL_AudioDevice *device, bool open, bool allow_pl
436392
category = AVAudioSessionCategoryPlayAndRecord;
437393
}
438394
}
439-
} else if (data.playback && data.recording) {
395+
} else if (opened_for_playback && opened_for_recording) {
440396
if (allow_playandrecord) {
441397
category = AVAudioSessionCategoryPlayAndRecord;
442398
} else {
443399
// We already failed play and record with AVAudioSessionErrorCodeResourceNotAvailable
444400
return false;
445401
}
446-
} else if (data.recording) {
402+
} else if (opened_for_recording) {
447403
category = AVAudioSessionCategoryRecord;
448404
}
449405

@@ -469,72 +425,12 @@ static bool UpdateAudioSession(SDL_AudioDevice *device, bool open, bool allow_pl
469425
}
470426

471427
if (![session.category isEqualToString:category] || session.categoryOptions != options) {
472-
// Stop the current session so we don't interrupt other application audio
473-
PauseAudioDevices();
474-
[session setActive:NO error:nil];
475-
session_active = false;
476-
477428
if (![session setCategory:category mode:mode options:options error:&err]) {
478429
NSString *desc = err.description;
479430
SDL_SetError("Could not set Audio Session category: %s", desc.UTF8String);
480431
return false;
481432
}
482433
}
483-
484-
if ((data.playback || data.recording) && !session_active) {
485-
if (![session setActive:YES error:&err]) {
486-
if ([err code] == AVAudioSessionErrorCodeResourceNotAvailable &&
487-
category == AVAudioSessionCategoryPlayAndRecord) {
488-
if (UpdateAudioSession(device, open, false)) {
489-
return true;
490-
} else {
491-
return SDL_SetError("Could not activate Audio Session: Resource not available");
492-
}
493-
}
494-
495-
NSString *desc = err.description;
496-
return SDL_SetError("Could not activate Audio Session: %s", desc.UTF8String);
497-
}
498-
session_active = true;
499-
ResumeAudioDevices();
500-
} else if (!data.playback && !data.recording && session_active) {
501-
PauseAudioDevices();
502-
[session setActive:NO error:nil];
503-
session_active = false;
504-
}
505-
506-
if (open) {
507-
SDLInterruptionListener *listener = [SDLInterruptionListener new];
508-
listener.device = device;
509-
510-
[center addObserver:listener
511-
selector:@selector(audioSessionInterruption:)
512-
name:AVAudioSessionInterruptionNotification
513-
object:session];
514-
515-
/* An interruption end notification is not guaranteed to be sent if
516-
we were previously interrupted... resuming if needed when the app
517-
becomes active seems to be the way to go. */
518-
// Note: object: below needs to be nil, as otherwise it filters by the object, and session doesn't send foreground / active notifications.
519-
[center addObserver:listener
520-
selector:@selector(applicationBecameActive:)
521-
name:UIApplicationDidBecomeActiveNotification
522-
object:nil];
523-
524-
[center addObserver:listener
525-
selector:@selector(applicationBecameActive:)
526-
name:UIApplicationWillEnterForegroundNotification
527-
object:nil];
528-
529-
device->hidden->interruption_listener = CFBridgingRetain(listener);
530-
} else {
531-
SDLInterruptionListener *listener = nil;
532-
listener = (SDLInterruptionListener *)CFBridgingRelease(device->hidden->interruption_listener);
533-
[center removeObserver:listener];
534-
@synchronized(listener) {
535-
listener.device = NULL;
536-
}
537-
}
538434
}
539435

540436
return true;
@@ -640,7 +536,14 @@ static void COREAUDIO_CloseDevice(SDL_AudioDevice *device)
640536
}
641537

642538
#ifndef MACOSX_COREAUDIO
643-
UpdateAudioSession(device, false, true);
539+
if (device->hidden->interruption_listener) {
540+
SDLInterruptionListener *listener = (SDLInterruptionListener *)CFBridgingRelease(device->hidden->interruption_listener);
541+
device->hidden->interruption_listener = NULL;
542+
[[NSNotificationCenter defaultCenter] removeObserver:listener];
543+
@synchronized(listener) {
544+
listener.device = NULL;
545+
}
546+
}
644547
#endif
645548

646549
if (device->hidden->ready_semaphore) {
@@ -651,6 +554,18 @@ static void COREAUDIO_CloseDevice(SDL_AudioDevice *device)
651554
SDL_free(device->hidden->audioBuffer);
652555
SDL_free(device->hidden->thread_error);
653556
SDL_free(device->hidden);
557+
558+
#ifndef MACOSX_COREAUDIO
559+
SDL_AtomicDecRef(device->recording ? &open_recording_device_count : &open_playback_device_count);
560+
561+
SDL_assert(SDL_GetAtomicInt(&open_playback_device_count) >= 0);
562+
SDL_assert(SDL_GetAtomicInt(&open_recording_device_count) >= 0);
563+
564+
if (session_active && !SDL_GetAtomicInt(&open_playback_device_count) && !SDL_GetAtomicInt(&open_recording_device_count)) {
565+
[[AVAudioSession sharedInstance] setActive:NO error:nil];
566+
session_active = false;
567+
}
568+
#endif
654569
}
655570

656571
#ifdef MACOSX_COREAUDIO
@@ -899,26 +814,32 @@ static bool COREAUDIO_OpenDevice(SDL_AudioDevice *device)
899814
}
900815

901816
#ifndef MACOSX_COREAUDIO
902-
if (!UpdateAudioSession(device, true, true)) {
817+
SDL_AtomicIncRef(device->recording ? &open_recording_device_count : &open_playback_device_count);
818+
if (!UpdateAudioSession(device, true)) {
903819
return false;
904820
}
905821

822+
AVAudioSession *session = [AVAudioSession sharedInstance];
906823
// Stop CoreAudio from doing expensive audio rate conversion
907-
@autoreleasepool {
908-
AVAudioSession *session = [AVAudioSession sharedInstance];
909-
[session setPreferredSampleRate:device->spec.freq error:nil];
910-
device->spec.freq = (int)session.sampleRate;
911-
#ifdef SDL_PLATFORM_TVOS
912-
if (device->recording) {
913-
[session setPreferredInputNumberOfChannels:device->spec.channels error:nil];
914-
device->spec.channels = (int)session.preferredInputNumberOfChannels;
915-
} else {
916-
[session setPreferredOutputNumberOfChannels:device->spec.channels error:nil];
917-
device->spec.channels = (int)session.preferredOutputNumberOfChannels;
824+
[session setPreferredSampleRate:device->spec.freq error:nil];
825+
device->spec.freq = (int)session.sampleRate;
826+
#ifdef SDL_PLATFORM_TVOS
827+
if (device->recording) {
828+
[session setPreferredInputNumberOfChannels:device->spec.channels error:nil];
829+
device->spec.channels = (int)session.preferredInputNumberOfChannels;
830+
} else {
831+
[session setPreferredOutputNumberOfChannels:device->spec.channels error:nil];
832+
device->spec.channels = (int)session.preferredOutputNumberOfChannels;
833+
}
834+
#else
835+
// Calling setPreferredOutputNumberOfChannels seems to break audio output on iOS
836+
#endif // SDL_PLATFORM_TVOS
837+
838+
if (!session_active) {
839+
if (![session setActive:YES error:&err]) {
840+
return SDL_SetError("Could not activate Audio Session: %s", err.description.UTF8String);
918841
}
919-
#else
920-
// Calling setPreferredOutputNumberOfChannels seems to break audio output on iOS
921-
#endif // SDL_PLATFORM_TVOS
842+
session_active = true;
922843
}
923844
#endif
924845

@@ -992,13 +913,40 @@ static bool COREAUDIO_OpenDevice(SDL_AudioDevice *device)
992913
SDL_DestroySemaphore(device->hidden->ready_semaphore);
993914
device->hidden->ready_semaphore = NULL;
994915

995-
if ((device->hidden->thread != NULL) && (device->hidden->thread_error != NULL)) {
916+
if (device->hidden->thread_error != NULL) {
996917
SDL_WaitThread(device->hidden->thread, NULL);
997918
device->hidden->thread = NULL;
998919
return SDL_SetError("%s", device->hidden->thread_error);
999920
}
1000921

1001-
return (device->hidden->thread != NULL);
922+
#ifndef MACOSX_COREAUDIO
923+
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
924+
SDLInterruptionListener *listener = [SDLInterruptionListener new];
925+
listener.device = device;
926+
927+
[center addObserver:listener
928+
selector:@selector(audioSessionInterruption:)
929+
name:AVAudioSessionInterruptionNotification
930+
object:session];
931+
932+
/* An interruption end notification is not guaranteed to be sent if
933+
we were previously interrupted... resuming if needed when the app
934+
becomes active seems to be the way to go. */
935+
// Note: object: below needs to be nil, as otherwise it filters by the object, and session doesn't send foreground / active notifications.
936+
[center addObserver:listener
937+
selector:@selector(applicationBecameActive:)
938+
name:UIApplicationDidBecomeActiveNotification
939+
object:nil];
940+
941+
[center addObserver:listener
942+
selector:@selector(applicationBecameActive:)
943+
name:UIApplicationWillEnterForegroundNotification
944+
object:nil];
945+
946+
device->hidden->interruption_listener = CFBridgingRetain(listener);
947+
#endif
948+
949+
return true;
1002950
}
1003951

1004952
static void COREAUDIO_DeinitializeStart(void)

0 commit comments

Comments
 (0)