From a74722ed746a54f176a1fb44c8d9d1ae7db3d950 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 27 Apr 2026 10:25:25 -0400
Subject: [PATCH] coreaudio: Always init/deinit session listener on iOS.
Previously, if UpdateAudioSession() failed on close--which it might if
something strange has happened with the system's audio configuration--the
listener wouldn't be deregistered, and would risk touching a free'd pointer
if the app moved to or from the background afterwards, firing an event handler
that should have been deregistered.
Closes #15439.
---
src/audio/coreaudio/SDL_coreaudio.m | 70 +++++++++++++++--------------
1 file changed, 37 insertions(+), 33 deletions(-)
diff --git a/src/audio/coreaudio/SDL_coreaudio.m b/src/audio/coreaudio/SDL_coreaudio.m
index dda5f573c7d7f..471a472c7f214 100644
--- a/src/audio/coreaudio/SDL_coreaudio.m
+++ b/src/audio/coreaudio/SDL_coreaudio.m
@@ -502,39 +502,6 @@ static bool UpdateAudioSession(SDL_AudioDevice *device, bool open, bool allow_pl
[session setActive:NO error:nil];
session_active = false;
}
-
- if (open) {
- SDLInterruptionListener *listener = [SDLInterruptionListener new];
- listener.device = device;
-
- [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. */
- // Note: object: below needs to be nil, as otherwise it filters by the object, and session doesn't send foreground / active notifications.
- [center addObserver:listener
- selector:@selector(applicationBecameActive:)
- name:UIApplicationDidBecomeActiveNotification
- object:nil];
-
- [center addObserver:listener
- selector:@selector(applicationBecameActive:)
- name:UIApplicationWillEnterForegroundNotification
- object:nil];
-
- device->hidden->interruption_listener = CFBridgingRetain(listener);
- } else {
- SDLInterruptionListener *listener = nil;
- listener = (SDLInterruptionListener *)CFBridgingRelease(device->hidden->interruption_listener);
- [center removeObserver:listener];
- @synchronized(listener) {
- listener.device = NULL;
- }
- }
}
return true;
@@ -627,6 +594,17 @@ static void COREAUDIO_CloseDevice(SDL_AudioDevice *device)
return;
}
+ #ifndef MACOSX_COREAUDIO
+ if (device->hidden->interruption_listener) {
+ SDLInterruptionListener *listener = (SDLInterruptionListener *)CFBridgingRelease(device->hidden->interruption_listener);
+ device->hidden->interruption_listener = nil;
+ [center removeObserver:listener];
+ @synchronized(listener) {
+ listener.device = NULL;
+ }
+ }
+ #endif
+
// dispose of the audio queue before waiting on the thread, or it might stall for a long time!
if (device->hidden->audioQueue) {
AudioQueueFlush(device->hidden->audioQueue);
@@ -998,6 +976,32 @@ static bool COREAUDIO_OpenDevice(SDL_AudioDevice *device)
return SDL_SetError("%s", device->hidden->thread_error);
}
+#ifndef MACOSX_COREAUDIO
+ SDLInterruptionListener *listener = [SDLInterruptionListener new];
+ listener.device = device;
+
+ [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. */
+ // Note: object: below needs to be nil, as otherwise it filters by the object, and session doesn't send foreground / active notifications.
+ [center addObserver:listener
+ selector:@selector(applicationBecameActive:)
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
+
+ [center addObserver:listener
+ selector:@selector(applicationBecameActive:)
+ name:UIApplicationWillEnterForegroundNotification
+ object:nil];
+
+ device->hidden->interruption_listener = CFBridgingRetain(listener);
+#endif
+
return (device->hidden->thread != NULL);
}