mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 07:20:31 +02:00
sceUsbMic input comes from the input side of the RemoteIO unit that already does the output. The input callback only copies samples into a lock-free ring buffer, sized so it never allocates, and the CPU thread drains it into Microphone::addAudioData when sceUsbMic polls, the way the SDL backend works. Reconfiguring the unit happens on the main thread. Whether the game is recording is kept apart from whether the unit's input is on, so recording resumes after an interruption or a trip to the background, which shut the unit down. The unit also runs, silently, when sound is off but the game is recording. While recording, the session is PlayAndRecord, with Bluetooth through A2DP so headphones keep full quality and the built-in mic is the input. Permission is asked for on iOS 17+ through AVAudioApplication. Also moves the microphone recorder homebrew used to test this from Tools/psp-microphone to pspautotests, as demos/microphone, and updates the submodule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
263 lines
9.0 KiB
Plaintext
263 lines
9.0 KiB
Plaintext
#import "AppDelegate.h"
|
|
#import "SceneDelegate.h"
|
|
#import "iOSCoreAudio.h"
|
|
#import "Common/System/System.h"
|
|
#import "Common/System/NativeApp.h"
|
|
#import "Core/System.h"
|
|
#import "Core/Config.h"
|
|
#import "Core/Util/PathUtil.h"
|
|
#import "Core/Util/RecentFiles.h"
|
|
#import "Common/Log.h"
|
|
#import "Common/File/Path.h"
|
|
#import "Common/File/DirListing.h"
|
|
#import "Common/File/FileUtil.h"
|
|
#import "IAPManager.h"
|
|
|
|
#import <AVFoundation/AVFoundation.h>
|
|
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// TODO: Unfortunate hack to force link SceneDelegate class.
|
|
// This is necessary because otherwise classes from static libraries aren't loaded.
|
|
// We should move all the iOS code into the main binary directly to avoid this problem.
|
|
__attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate class];
|
|
|
|
@implementation AppDelegate
|
|
|
|
// This will be called when the user receives and dismisses a phone call
|
|
// or other interruption to the audio session
|
|
// Registered in application:didFinishLaunchingWithOptions:
|
|
// for AVAudioSessionInterruptionNotification
|
|
-(void) handleAudioSessionInterruption:(NSNotification *)notification {
|
|
NSNumber *interruptionType = notification.userInfo[AVAudioSessionInterruptionTypeKey];
|
|
|
|
// Sanity check in case it's somehow not an NSNumber
|
|
if (![interruptionType respondsToSelector:@selector(unsignedIntegerValue)]) {
|
|
return; // Lets not crash
|
|
}
|
|
|
|
switch ([interruptionType unsignedIntegerValue]) {
|
|
case AVAudioSessionInterruptionTypeBegan:
|
|
INFO_LOG(Log::System, "ios audio session interruption beginning");
|
|
iOSCoreAudioShutdown();
|
|
break;
|
|
|
|
case AVAudioSessionInterruptionTypeEnded:
|
|
INFO_LOG(Log::System, "ios audio session interruption ending");
|
|
/*
|
|
* Only try to reinit audio if in the foreground, otherwise
|
|
* it may fail. Instead, trust that sceneDidBecomeActive
|
|
* will do it later.
|
|
*/
|
|
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
|
|
iOSCoreAudioInit();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
};
|
|
}
|
|
|
|
// This will be called when the iOS's shared media process was reset
|
|
// Registered in application:didFinishLaunchingWithOptions:
|
|
// for AVAudioSessionMediaServicesWereResetNotification
|
|
-(void) handleMediaServicesWereReset:(NSNotification *)notification {
|
|
INFO_LOG(Log::System, "ios media services were reset - reinitializing audio");
|
|
|
|
/*
|
|
When media services were reset, Apple recommends:
|
|
1) Dispose of orphaned audio objects (such as players, recorders,
|
|
converters, or audio queues) and create new ones
|
|
2) Reset any internal audio states being tracked, including all
|
|
properties of AVAudioSession
|
|
3) When appropriate, reactivate the AVAudioSession instance using the
|
|
setActive:error: method
|
|
We accomplish this by shutting down and reinitializing audio
|
|
*/
|
|
|
|
iOSCoreAudioShutdown();
|
|
iOSCoreAudioInit();
|
|
}
|
|
|
|
-(BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
|
self.launchOptions = launchOptions;
|
|
|
|
#if PPSSPP_PLATFORM(IOS_APP_STORE)
|
|
[IAPManager sharedIAPManager]; // Kick off the IAPManager early.
|
|
#endif // IOS_APP_STORE
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMediaServicesWereReset:) name:AVAudioSessionMediaServicesWereResetNotification object:nil];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)applicationWillTerminate:(UIApplication *)application {
|
|
}
|
|
|
|
// In AppDelegate.mm
|
|
- (void)processFilePath:(NSString *)path {
|
|
// Convert NSString to std::string
|
|
std::string cppPath([path UTF8String]);
|
|
|
|
// Try to fixup the path, the app directory changes on every bootup.
|
|
Path gamePath(cppPath);
|
|
TryUpdateSavedPath(&gamePath);
|
|
|
|
// Call the C++ function to handle the file
|
|
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, gamePath.ToString());
|
|
}
|
|
|
|
// Gathers the game library as a flat list of file paths. This mirrors what the
|
|
// user sees in the "Games" tab. Duplicates are
|
|
// removed while preserving order.
|
|
static std::vector<std::string> GatherGameLibrary() {
|
|
std::vector<std::string> paths;
|
|
std::set<std::string> seen;
|
|
|
|
auto addPath = [&](const std::string &p) {
|
|
if (p.empty()) {
|
|
return;
|
|
}
|
|
if (seen.insert(p).second) {
|
|
paths.push_back(p);
|
|
}
|
|
};
|
|
|
|
const Path &gamesDir = g_Config.currentDirectory;
|
|
if (!gamesDir.empty()) {
|
|
std::vector<File::FileInfo> fileInfo;
|
|
if (File::GetFilesInDir(gamesDir, &fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:")) {
|
|
for (const File::FileInfo &info : fileInfo) {
|
|
if (info.isDirectory) {
|
|
// Detect installed/extracted PSP game folders.
|
|
if (File::Exists(info.fullName / "EBOOT.PBP") ||
|
|
File::Exists(info.fullName / "PSP_GAME/SYSDIR")) {
|
|
addPath(info.fullName.ToString());
|
|
}
|
|
} else {
|
|
addPath(info.fullName.ToString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also include recently played games.
|
|
for (const std::string &recent : g_recentFiles.GetRecentFiles()) {
|
|
addPath(recent);
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
- (void)exportLibraryToScheme:(NSString *)callerScheme {
|
|
if (callerScheme.length == 0) {
|
|
NSLog(@"exportLibraryToScheme: empty caller scheme, ignoring");
|
|
return;
|
|
}
|
|
|
|
// Build a list of games. Each game's "titleId" is set to the file path so
|
|
// the receiving app can build a "ppsspp://open?path=<path>" deep link from it.
|
|
std::vector<std::string> library = GatherGameLibrary();
|
|
|
|
NSMutableArray<NSDictionary *> *games = [NSMutableArray arrayWithCapacity:library.size()];
|
|
for (const std::string &file : library) {
|
|
NSString *path = [[NSString alloc] initWithBytes:file.data()
|
|
length:file.length()
|
|
encoding:NSUTF8StringEncoding];
|
|
if (path.length == 0) {
|
|
continue;
|
|
}
|
|
// Create a human-readable title from the filename (without extension).
|
|
NSString *titleName = [[path lastPathComponent] stringByDeletingPathExtension];
|
|
if (titleName.length == 0) {
|
|
titleName = [path lastPathComponent];
|
|
}
|
|
[games addObject:@{
|
|
@"titleName": titleName ?: @"",
|
|
@"titleId": path,
|
|
@"developer": @"",
|
|
@"version": @"",
|
|
}];
|
|
}
|
|
|
|
NSError *error = nil;
|
|
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:games options:0 error:&error];
|
|
if (!jsonData || error) {
|
|
NSLog(@"exportLibraryToScheme: failed to serialize library: %@", error);
|
|
return;
|
|
}
|
|
|
|
// base64url-encode (URL-safe alphabet, no padding) to keep it URL-safe.
|
|
NSString *encoded = [jsonData base64EncodedStringWithOptions:0];
|
|
encoded = [encoded stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
|
encoded = [encoded stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
|
encoded = [encoded stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
|
|
|
NSString *urlString = [NSString stringWithFormat:@"%@://ppsspp?games=%@", callerScheme, encoded];
|
|
NSURL *returnURL = [NSURL URLWithString:urlString];
|
|
if (!returnURL) {
|
|
NSLog(@"exportLibraryToScheme: failed to build return URL for scheme '%@'", callerScheme);
|
|
return;
|
|
}
|
|
|
|
NSLog(@"exportLibraryToScheme: returning %lu games to scheme '%@'", (unsigned long)games.count, callerScheme);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[[UIApplication sharedApplication] openURL:returnURL options:@{} completionHandler:nil];
|
|
});
|
|
}
|
|
|
|
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
|
|
switch (g_Config.iScreenRotation) {
|
|
case ROTATION_LOCKED_HORIZONTAL:
|
|
return UIInterfaceOrientationMaskLandscapeRight;
|
|
case ROTATION_LOCKED_VERTICAL:
|
|
case ROTATION_LOCKED_VERTICAL180:
|
|
if (IS_IPAD()) {
|
|
// iPad supports both portrait orientations, so allow them.
|
|
return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown;
|
|
}
|
|
// We do not support reverse portrait on iOS.
|
|
return UIInterfaceOrientationMaskPortrait;
|
|
case ROTATION_LOCKED_HORIZONTAL180:
|
|
return UIInterfaceOrientationMaskLandscapeLeft;
|
|
case ROTATION_AUTO_HORIZONTAL:
|
|
return UIInterfaceOrientationMaskLandscape;
|
|
case ROTATION_AUTO:
|
|
default:
|
|
return UIInterfaceOrientationMaskAll;
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
void copyDeepLinkForPath(std::string_view filePath) {
|
|
// 1. Convert std::string_view to NSString
|
|
// We use the length and data to avoid issues with null terminators
|
|
NSString *pathString = [[NSString alloc] initWithBytes:filePath.data()
|
|
length:filePath.length()
|
|
encoding:NSUTF8StringEncoding];
|
|
|
|
// 2. Percent-encode the path
|
|
// This handles spaces, slashes, and special characters for the URL query
|
|
NSCharacterSet *allowedChars = [NSCharacterSet URLQueryAllowedCharacterSet];
|
|
NSString *encodedPath = [pathString stringByAddingPercentEncodingWithAllowedCharacters:allowedChars];
|
|
|
|
// 3. Construct the full URL
|
|
// Format: scheme://host?query
|
|
NSString *deepLink = [NSString stringWithFormat:@"ppsspp://open?path=%@", encodedPath];
|
|
|
|
// 4. Copy to the iOS System Clipboard (UIPasteboard)
|
|
[UIPasteboard generalPasteboard].string = deepLink;
|
|
|
|
// Optional: Log it for debugging
|
|
NSLog(@"Deep link copied: %@", deepLink);
|
|
}
|