|
@@ -0,0 +1,288 @@
|
|
|
+//
|
|
|
+// ASGarageBandHelper.m
|
|
|
+// AIPlayRingtones
|
|
|
+//
|
|
|
+// Created by mini on 2025/5/28.
|
|
|
+//
|
|
|
+
|
|
|
+#import "ASGarageBandHelper.h"
|
|
|
+#import "ExtAudioConverter.h"
|
|
|
+
|
|
|
+
|
|
|
+// AudioTool.m
|
|
|
+@implementation AudioTool
|
|
|
+
|
|
|
+- (void)convertToBandFrom:(NSURL *)inputURL
|
|
|
+ outputPath:(NSString *)outputPath
|
|
|
+ templatePath:(NSString *)templatePath
|
|
|
+ completion:(void(^)(NSURL * _Nullable))completion {
|
|
|
+
|
|
|
+ // Copy template file
|
|
|
+ [NSFileManager.defaultManager copyItemAtPath:templatePath toPath:outputPath error:nil];
|
|
|
+
|
|
|
+ if (![NSFileManager.defaultManager fileExistsAtPath:outputPath]) {
|
|
|
+ completion(nil);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Convert audio
|
|
|
+ ExtAudioConverter *converter = [[ExtAudioConverter alloc] init];
|
|
|
+ converter.inputFile = inputURL.path;
|
|
|
+ converter.outputFile = [outputPath stringByAppendingPathComponent:@"/Media/ringtone.aiff"];
|
|
|
+ converter.outputFileType = kAudioFileAIFFType;
|
|
|
+ BOOL success = [converter convert];
|
|
|
+
|
|
|
+ if (success && [NSFileManager.defaultManager fileExistsAtPath:outputPath]) {
|
|
|
+ completion([NSURL fileURLWithPath:outputPath]);
|
|
|
+ } else {
|
|
|
+ completion(nil);
|
|
|
+ }
|
|
|
+
|
|
|
+ NSLog(@"=== Band file path: %@", outputPath);
|
|
|
+}
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
+@interface ASGarageBandHelper ()
|
|
|
+@property (nonatomic, strong) NSURL *currentBandURL;
|
|
|
+@end
|
|
|
+static ASGarageBandHelper *instance = nil;
|
|
|
+@implementation ASGarageBandHelper
|
|
|
+
|
|
|
+//+ (instancetype)sharedInstance {
|
|
|
+//
|
|
|
+// static dispatch_once_t onceToken;
|
|
|
+// dispatch_once(&onceToken, ^{
|
|
|
+// instance = [[ASGarageBandHelper alloc] init];
|
|
|
+// });
|
|
|
+// return instance;
|
|
|
+//}
|
|
|
+
|
|
|
++ (instancetype)createHelper {
|
|
|
+ instance = [[ASGarageBandHelper alloc] init];
|
|
|
+ return instance;
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)init {
|
|
|
+ self = [super init];
|
|
|
+ if (self) {
|
|
|
+ _playerContainer = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
|
+ _audioProcessor = [[AudioTool alloc] init];
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (BOOL)verifyGarageBandInstalled {
|
|
|
+ NSURL *appURL = [NSURL URLWithString:@"garageband://"];
|
|
|
+ if ([[UIApplication sharedApplication] canOpenURL:appURL]) {
|
|
|
+ NSLog(@"GarageBand is installed");
|
|
|
+ return YES;
|
|
|
+ }
|
|
|
+
|
|
|
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
|
|
|
+ message:@"GarageBand is not installed, you need to install it."
|
|
|
+ preferredStyle:UIAlertControllerStyleAlert];
|
|
|
+
|
|
|
+ [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
|
|
+ style:UIAlertActionStyleCancel
|
|
|
+ handler:nil]];
|
|
|
+
|
|
|
+ [alert addAction:[UIAlertAction actionWithTitle:@"Download"
|
|
|
+ style:UIAlertActionStyleDefault
|
|
|
+ handler:^(UIAlertAction * _Nonnull action) {
|
|
|
+ NSURL *appStoreURL = [NSURL URLWithString:@"https://apps.apple.com/app/id408709785"];
|
|
|
+ if ([[UIApplication sharedApplication] canOpenURL:appStoreURL]) {
|
|
|
+ [[UIApplication sharedApplication] openURL:appStoreURL options:@{} completionHandler:nil];
|
|
|
+ }
|
|
|
+ }]];
|
|
|
+
|
|
|
+ [self.presentingController presentViewController:alert animated:YES completion:nil];
|
|
|
+ return NO;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)shareToGarageBandFrom:(UIViewController *)controller
|
|
|
+ withAudio:(NSURL *)fileURL
|
|
|
+ fileName:(nullable NSString *)fileName
|
|
|
+ completion:(nullable void(^)(BOOL success))completion {
|
|
|
+
|
|
|
+ self.presentingController = controller;
|
|
|
+
|
|
|
+ if (![self verifyGarageBandInstalled]) {
|
|
|
+ if (completion) completion(NO);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (![NSFileManager.defaultManager fileExistsAtPath:fileURL.path]) {
|
|
|
+ if (completion) completion(NO);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ [self prepareBandFileWithAudio:fileURL
|
|
|
+ fileName:fileName
|
|
|
+ completion:^(NSURL * _Nullable bandURL) {
|
|
|
+ if (bandURL) {
|
|
|
+ if (completion) completion(YES);
|
|
|
+ [self presentSharingForBandFile:bandURL];
|
|
|
+ } else {
|
|
|
+ if (completion) completion(NO);
|
|
|
+ NSLog(@"Failed to create band file");
|
|
|
+ }
|
|
|
+ }];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)prepareBandFileWithAudio:(NSURL *)audioURL
|
|
|
+ fileName:(NSString *)fileName
|
|
|
+ completion:(void(^)(NSURL * _Nullable))completion {
|
|
|
+
|
|
|
+ NSString *fileNameToUse = fileName ?: @"Ringtone";
|
|
|
+ NSURL *templateURL = [self bandTemplatePath];
|
|
|
+ NSURL *outputDirectory = [self bandOutputDirectory];
|
|
|
+
|
|
|
+ if (!templateURL || !outputDirectory) {
|
|
|
+ completion(nil);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSURL *outputURL = [[outputDirectory URLByAppendingPathComponent:fileNameToUse] URLByAppendingPathExtension:@"band"];
|
|
|
+
|
|
|
+ [self.audioProcessor convertToBandFrom:audioURL
|
|
|
+ outputPath:outputURL.path
|
|
|
+ templatePath:templateURL.path
|
|
|
+ completion:^(NSURL * _Nullable resultURL) {
|
|
|
+ completion(resultURL);
|
|
|
+ }];
|
|
|
+}
|
|
|
+
|
|
|
+- (void)presentSharingForBandFile:(NSURL *)bandURL {
|
|
|
+ dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
+ [self setupTutorialPlayer];
|
|
|
+
|
|
|
+ UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[bandURL]
|
|
|
+ applicationActivities:nil];
|
|
|
+
|
|
|
+ [self.presentingController presentViewController:shareSheet
|
|
|
+ animated:YES
|
|
|
+ completion:^{
|
|
|
+ [self attemptPictureInPicture];
|
|
|
+ }];
|
|
|
+
|
|
|
+ // Fallback check
|
|
|
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
+ if (self.presentingController.presentedViewController != shareSheet) {
|
|
|
+ NSLog(@"Presentation failed");
|
|
|
+ [self.playerContainer removeFromSuperview];
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+- (void)setupTutorialPlayer {
|
|
|
+ NSURL *videoURL = [NSBundle.mainBundle URLForResource:@"tutorial-ring" withExtension:@"mp4"];
|
|
|
+ if (!videoURL || !AVPictureInPictureController.isPictureInPictureSupported) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSError *error;
|
|
|
+ [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
|
|
|
+ withOptions:AVAudioSessionCategoryOptionMixWithOthers
|
|
|
+ error:&error];
|
|
|
+
|
|
|
+ AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:[AVAsset assetWithURL:videoURL]];
|
|
|
+ if (!self.mediaPlayer) {
|
|
|
+ self.mediaPlayer = [AVPlayer playerWithPlayerItem:item];
|
|
|
+ } else {
|
|
|
+ [self.mediaPlayer replaceCurrentItemWithPlayerItem:item];
|
|
|
+ }
|
|
|
+
|
|
|
+ self.mediaPlayer.allowsExternalPlayback = YES;
|
|
|
+ [self.presentingController.view addSubview:self.playerContainer];
|
|
|
+
|
|
|
+ AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:self.mediaPlayer];
|
|
|
+ layer.frame = CGRectMake(0, 0, 1, 1);
|
|
|
+ [self.playerContainer.layer addSublayer:layer];
|
|
|
+
|
|
|
+ self.pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:layer];
|
|
|
+ self.pipController.delegate = self;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)attemptPictureInPicture {
|
|
|
+ if (self.mediaPlayer.status == AVPlayerStatusReadyToPlay) {
|
|
|
+ [self.pipController startPictureInPicture];
|
|
|
+ [self.mediaPlayer play];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
+ [self attemptPictureInPicture];
|
|
|
+
|
|
|
+ });
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - PiP Delegate
|
|
|
+- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
|
|
+ [self.playerContainer removeFromSuperview];
|
|
|
+}
|
|
|
+
|
|
|
+- (BOOL)playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart:(AVPlayerViewController *)playerViewController {
|
|
|
+ return YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)playerViewController:(AVPlayerViewController *)playerViewController
|
|
|
+restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler {
|
|
|
+ dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
+ if (!playerViewController.presentingViewController) {
|
|
|
+ [self.presentingController presentViewController:playerViewController
|
|
|
+ animated:NO
|
|
|
+ completion:^{
|
|
|
+ completionHandler(YES);
|
|
|
+ }];
|
|
|
+ } else {
|
|
|
+ completionHandler(YES);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController
|
|
|
+failedToStartPictureInPictureWithError:(NSError *)error {
|
|
|
+ NSLog(@"Failed to start Picture in Picture: %@", error.localizedDescription);
|
|
|
+
|
|
|
+}
|
|
|
+#pragma mark - File Path Helpers
|
|
|
+- (NSURL *)bandTemplatePath {
|
|
|
+ NSURL *directory = [self bandOutputDirectory];
|
|
|
+ if (!directory) return nil;
|
|
|
+
|
|
|
+ NSString *path = [directory.path stringByAppendingPathComponent:@"placeHolderBand.band"];
|
|
|
+
|
|
|
+ if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
|
|
+ NSString *localPath = [NSBundle.mainBundle pathForResource:@"null" ofType:@"band"];
|
|
|
+ if (localPath) {
|
|
|
+ [NSFileManager.defaultManager copyItemAtPath:localPath toPath:path error:nil];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return [NSURL fileURLWithPath:path];
|
|
|
+}
|
|
|
+
|
|
|
+- (NSURL *)bandOutputDirectory {
|
|
|
+ NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
|
+ NSURL *ringFolder = [documents URLByAppendingPathComponent:@"ringDirectory"];
|
|
|
+
|
|
|
+ return [self createDirectoryIfNeeded:ringFolder];
|
|
|
+}
|
|
|
+
|
|
|
+- (NSURL *)createDirectoryIfNeeded:(NSURL *)url {
|
|
|
+ BOOL isDir = NO;
|
|
|
+ if ([NSFileManager.defaultManager fileExistsAtPath:url.path isDirectory:&isDir] && isDir) {
|
|
|
+ return url;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSError *error;
|
|
|
+ [NSFileManager.defaultManager createDirectoryAtURL:url
|
|
|
+ withIntermediateDirectories:YES
|
|
|
+ attributes:nil
|
|
|
+ error:&error];
|
|
|
+ return error ? nil : url;
|
|
|
+}
|
|
|
+
|
|
|
+@end
|