Merge pull request #22727 from hrydgard/ios-airplay

iOS: AirPlay audio fixes, stuck touch and exit crash
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-22 13:32:34 +02:00
commit 18f2a59fc2
7 files changed
+59 -10

No files matched your search

+9
View File
@@ -82,6 +82,14 @@ __attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate clas
iOSCoreAudioInit();
}
// for AVAudioSessionRouteChangeNotification
-(void) handleAudioRouteChange:(NSNotification *)notification {
NSNumber *reason = notification.userInfo[AVAudioSessionRouteChangeReasonKey];
char buf[32];
snprintf(buf, sizeof(buf), "change, reason %d", reason.intValue);
iOSCoreAudioLogRoute(buf);
}
-(BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.launchOptions = launchOptions;
@@ -91,6 +99,7 @@ __attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate clas
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil];
return YES;
}
+2
View File
@@ -26,6 +26,8 @@ public:
void Moved(NSSet *touches, UIView *view);
void Ended(NSSet *touches, UIView *view);
void Cancelled(NSSet *touches, UIView *view);
// Lifts every touch still down. UIKit doesn't always end the touches that pull down Control Center.
void CancelAll();
private:
void SendTouchEvent(CGPoint point, UIView *view, int code, int pointerId);
int ToTouchID(UITouch *uiTouch, bool allowAllocate);
+12
View File
@@ -381,6 +381,18 @@ void TouchTracker::Cancelled(NSSet *touches, UIView *view) {
}
}
void TouchTracker::CancelAll() {
for (int touchId = 0; touchId < (int)ARRAY_SIZE(touches_); ++touchId) {
if (touches_[touchId]) {
TouchInput input{};
input.flags = TouchInputFlags::UP | TouchInputFlags::CANCEL;
input.id = touchId;
NativeTouch(input);
touches_[touchId] = nullptr;
}
}
}
#if !PPSSPP_PLATFORM(TVOS)
void ProcessAccelerometerData(CMAccelerometerData *accData) {
CMAcceleration acc = accData.acceleration;
+4
View File
@@ -337,6 +337,10 @@ static int GetPickerRequestId(id picker) {
- (void)willResignActive {
[self requestExitVulkanRenderLoop];
#if !PPSSPP_PLATFORM(TVOS)
g_touchTracker.CancelAll();
#endif
#if !PPSSPP_PLATFORM(TVOS)
// Stop accelerometer updates
if (self.motionManager.accelerometerActive) {
+3
View File
@@ -25,6 +25,9 @@ void iOSCoreAudioShutdown();
// Applies the audio settings (mix with others, respect silent mode) to the session.
void iOSCoreAudioUpdateSession();
// Logs where the session's output currently goes (speaker, headphones, AirPlay...).
void iOSCoreAudioLogRoute(const char *reason);
// Microphone input, for sceUsbMic. These are called on the CPU thread. Input is buffered, and
// iOSCoreAudioPollRecording hands it over to Microphone::addAudioData.
void iOSCoreAudioStartRecording(int sampleRate);
+23 -4
View File
@@ -65,13 +65,17 @@ void iOSCoreAudioUpdateSession() {
#else
// Hacky hack to force iOS to re-evaluate.
// Switching from CatogoryPlayback to CategoryPlayback with an option otherwise does nothing.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAudioProcessing error:&error];
NSError *hackError = nil;
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAudioProcessing error:&hackError]) {
NSLog(@"Switching to AudioProcessing failed: %@", hackError);
}
// Here, we apply the settings.
if (g_micEnabled) {
// Recording needs PlayAndRecord, which ignores the silent switch. A2DP rather than the
// hands-free profile keeps Bluetooth headphones at full quality, with the built-in mic as input.
AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetoothA2DP;
// Unlike the playback categories, PlayAndRecord needs to be told to allow AirPlay.
AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetoothA2DP | AVAudioSessionCategoryOptionAllowAirPlay;
if (g_Config.bAudioMixWithOthers) {
options |= AVAudioSessionCategoryOptionMixWithOthers;
}
@@ -93,7 +97,17 @@ void iOSCoreAudioUpdateSession() {
#endif
if (error) {
NSLog(@"%@", error);
NSLog(@"Setting the audio session category failed: %@", error);
}
}
void iOSCoreAudioLogRoute(const char *reason) {
AVAudioSession *session = [AVAudioSession sharedInstance];
for (AVAudioSessionPortDescription *port in session.currentRoute.outputs) {
NSLog(@"Audio route (%s): %@ '%@' (category %@, options %d)", reason, port.portType, port.portName, session.category, (int)session.categoryOptions);
}
for (AVAudioSessionPortDescription *port in session.currentRoute.inputs) {
NSLog(@"Audio input (%s): %@ '%@'", reason, port.portType, port.portName);
}
}
@@ -204,6 +218,8 @@ void iOSCoreAudioInit() {
}
}
iOSCoreAudioLogRoute("init");
if (audioInstance) {
// Already running
return;
@@ -307,14 +323,15 @@ void iOSCoreAudioShutdown()
// Brings the unit in line with g_micEnabled. Main thread.
static void UpdateMicrophone() {
iOSCoreAudioUpdateSession();
if (!audioInstance) {
// Sets up the session too.
iOSCoreAudioInit();
return;
}
if (!g_Config.bEnableSound && !g_micEnabled) {
// The unit was only running for the microphone.
iOSCoreAudioShutdown();
iOSCoreAudioUpdateSession();
return;
}
if (g_micEnabled == g_micInputOn && (!g_micEnabled || g_micSampleRate == g_micInputRate)) {
@@ -323,6 +340,8 @@ static void UpdateMicrophone() {
AudioOutputUnitStop(audioInstance);
AudioUnitUninitialize(audioInstance);
// Only with the unit stopped: iOS refuses (with '!pri') to leave PlayAndRecord while the input runs.
iOSCoreAudioUpdateSession();
ConfigureInput(audioInstance, g_micEnabled);
OSStatus err = AudioUnitInitialize(audioInstance);
if (err == noErr) {
+6 -6
View File
@@ -506,12 +506,12 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
}
case SystemRequestType::EXIT_APP:
// NOTE: on iOS, this is considered a crash and not a valid way to exit.
exit(0);
// The below seems right, but causes hangs. See #12140.
// dispatch_async(dispatch_get_main_queue(), ^{
// [sharedViewController shutdown];
// exit(0);
// });
// We're on the render thread. Calling exit() here would destroy its still joinable std::thread,
// which aborts, so let the main thread join it first.
dispatch_async(dispatch_get_main_queue(), ^{
[sharedViewController shutdown];
exit(0);
});
break;
case SystemRequestType::BROWSE_FOR_FILE:
{