ASGarageBandHelper.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. //
  2. // ASGarageBandHelper.m
  3. // AIPlayRingtones
  4. //
  5. // Created by mini on 2025/5/28.
  6. //
  7. #import "ASGarageBandHelper.h"
  8. #import "ExtAudioConverter.h"
  9. // AudioTool.m
  10. @implementation AudioTool
  11. - (void)convertToBandFrom:(NSURL *)inputURL
  12. outputPath:(NSString *)outputPath
  13. templatePath:(NSString *)templatePath
  14. completion:(void(^)(NSURL * _Nullable))completion {
  15. // Copy template file
  16. [NSFileManager.defaultManager copyItemAtPath:templatePath toPath:outputPath error:nil];
  17. if (![NSFileManager.defaultManager fileExistsAtPath:outputPath]) {
  18. completion(nil);
  19. return;
  20. }
  21. // Convert audio
  22. ExtAudioConverter *converter = [[ExtAudioConverter alloc] init];
  23. converter.inputFile = inputURL.path;
  24. converter.outputFile = [outputPath stringByAppendingPathComponent:@"/Media/ringtone.aiff"];
  25. converter.outputFileType = kAudioFileAIFFType;
  26. BOOL success = [converter convert];
  27. if (success && [NSFileManager.defaultManager fileExistsAtPath:outputPath]) {
  28. completion([NSURL fileURLWithPath:outputPath]);
  29. } else {
  30. completion(nil);
  31. }
  32. NSLog(@"=== Band file path: %@", outputPath);
  33. }
  34. @end
  35. @interface ASGarageBandHelper ()
  36. @property (nonatomic, strong) NSURL *currentBandURL;
  37. @end
  38. static ASGarageBandHelper *instance = nil;
  39. @implementation ASGarageBandHelper
  40. //+ (instancetype)sharedInstance {
  41. //
  42. // static dispatch_once_t onceToken;
  43. // dispatch_once(&onceToken, ^{
  44. // instance = [[ASGarageBandHelper alloc] init];
  45. // });
  46. // return instance;
  47. //}
  48. + (instancetype)createHelper {
  49. instance = [[ASGarageBandHelper alloc] init];
  50. return instance;
  51. }
  52. - (instancetype)init {
  53. self = [super init];
  54. if (self) {
  55. _playerContainer = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
  56. _audioProcessor = [[AudioTool alloc] init];
  57. }
  58. return self;
  59. }
  60. - (BOOL)verifyGarageBandInstalled {
  61. NSURL *appURL = [NSURL URLWithString:@"garageband://"];
  62. if ([[UIApplication sharedApplication] canOpenURL:appURL]) {
  63. NSLog(@"GarageBand is installed");
  64. return YES;
  65. }
  66. UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
  67. message:@"GarageBand is not installed, you need to install it."
  68. preferredStyle:UIAlertControllerStyleAlert];
  69. [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
  70. style:UIAlertActionStyleCancel
  71. handler:nil]];
  72. [alert addAction:[UIAlertAction actionWithTitle:@"Download"
  73. style:UIAlertActionStyleDefault
  74. handler:^(UIAlertAction * _Nonnull action) {
  75. NSURL *appStoreURL = [NSURL URLWithString:@"https://apps.apple.com/app/id408709785"];
  76. if ([[UIApplication sharedApplication] canOpenURL:appStoreURL]) {
  77. [[UIApplication sharedApplication] openURL:appStoreURL options:@{} completionHandler:nil];
  78. }
  79. }]];
  80. [self.presentingController presentViewController:alert animated:YES completion:nil];
  81. return NO;
  82. }
  83. - (void)shareToGarageBandFrom:(UIViewController *)controller
  84. withAudio:(NSURL *)fileURL
  85. fileName:(nullable NSString *)fileName
  86. completion:(nullable void(^)(BOOL success))completion {
  87. self.presentingController = controller;
  88. if (![self verifyGarageBandInstalled]) {
  89. if (completion) completion(NO);
  90. return;
  91. }
  92. if (![NSFileManager.defaultManager fileExistsAtPath:fileURL.path]) {
  93. if (completion) completion(NO);
  94. return;
  95. }
  96. [self prepareBandFileWithAudio:fileURL
  97. fileName:fileName
  98. completion:^(NSURL * _Nullable bandURL) {
  99. if (bandURL) {
  100. if (completion) completion(YES);
  101. [self presentSharingForBandFile:bandURL];
  102. } else {
  103. if (completion) completion(NO);
  104. NSLog(@"Failed to create band file");
  105. }
  106. }];
  107. }
  108. - (void)prepareBandFileWithAudio:(NSURL *)audioURL
  109. fileName:(NSString *)fileName
  110. completion:(void(^)(NSURL * _Nullable))completion {
  111. NSString *fileNameToUse = fileName ?: @"Ringtone";
  112. NSURL *templateURL = [self bandTemplatePath];
  113. NSURL *outputDirectory = [self bandOutputDirectory];
  114. if (!templateURL || !outputDirectory) {
  115. completion(nil);
  116. return;
  117. }
  118. NSURL *outputURL = [[outputDirectory URLByAppendingPathComponent:fileNameToUse] URLByAppendingPathExtension:@"band"];
  119. [self.audioProcessor convertToBandFrom:audioURL
  120. outputPath:outputURL.path
  121. templatePath:templateURL.path
  122. completion:^(NSURL * _Nullable resultURL) {
  123. completion(resultURL);
  124. }];
  125. }
  126. - (void)presentSharingForBandFile:(NSURL *)bandURL {
  127. dispatch_async(dispatch_get_main_queue(), ^{
  128. [self setupTutorialPlayer];
  129. UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[bandURL]
  130. applicationActivities:nil];
  131. [self.presentingController presentViewController:shareSheet
  132. animated:YES
  133. completion:^{
  134. [self attemptPictureInPicture];
  135. }];
  136. // Fallback check
  137. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  138. if (self.presentingController.presentedViewController != shareSheet) {
  139. NSLog(@"Presentation failed");
  140. [self.playerContainer removeFromSuperview];
  141. }
  142. });
  143. });
  144. }
  145. - (void)setupTutorialPlayer {
  146. NSURL *videoURL = [NSBundle.mainBundle URLForResource:@"tutorial-ring" withExtension:@"mp4"];
  147. if (!videoURL || !AVPictureInPictureController.isPictureInPictureSupported) {
  148. return;
  149. }
  150. NSError *error;
  151. [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
  152. withOptions:AVAudioSessionCategoryOptionMixWithOthers
  153. error:&error];
  154. AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:[AVAsset assetWithURL:videoURL]];
  155. if (!self.mediaPlayer) {
  156. self.mediaPlayer = [AVPlayer playerWithPlayerItem:item];
  157. } else {
  158. [self.mediaPlayer replaceCurrentItemWithPlayerItem:item];
  159. }
  160. self.mediaPlayer.allowsExternalPlayback = YES;
  161. [self.presentingController.view addSubview:self.playerContainer];
  162. AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:self.mediaPlayer];
  163. layer.frame = CGRectMake(0, 0, 1, 1);
  164. [self.playerContainer.layer addSublayer:layer];
  165. self.pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:layer];
  166. self.pipController.delegate = self;
  167. }
  168. - (void)attemptPictureInPicture {
  169. if (self.mediaPlayer.status == AVPlayerStatusReadyToPlay) {
  170. [self.pipController startPictureInPicture];
  171. [self.mediaPlayer play];
  172. return;
  173. }
  174. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  175. [self attemptPictureInPicture];
  176. });
  177. }
  178. #pragma mark - PiP Delegate
  179. - (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
  180. [self.playerContainer removeFromSuperview];
  181. }
  182. - (BOOL)playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart:(AVPlayerViewController *)playerViewController {
  183. return YES;
  184. }
  185. - (void)playerViewController:(AVPlayerViewController *)playerViewController
  186. restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler {
  187. dispatch_async(dispatch_get_main_queue(), ^{
  188. if (!playerViewController.presentingViewController) {
  189. [self.presentingController presentViewController:playerViewController
  190. animated:NO
  191. completion:^{
  192. completionHandler(YES);
  193. }];
  194. } else {
  195. completionHandler(YES);
  196. }
  197. });
  198. }
  199. - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController
  200. failedToStartPictureInPictureWithError:(NSError *)error {
  201. NSLog(@"Failed to start Picture in Picture: %@", error.localizedDescription);
  202. }
  203. #pragma mark - File Path Helpers
  204. - (NSURL *)bandTemplatePath {
  205. NSURL *directory = [self bandOutputDirectory];
  206. if (!directory) return nil;
  207. NSString *path = [directory.path stringByAppendingPathComponent:@"placeHolderBand.band"];
  208. if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
  209. NSString *localPath = [NSBundle.mainBundle pathForResource:@"null" ofType:@"band"];
  210. if (localPath) {
  211. [NSFileManager.defaultManager copyItemAtPath:localPath toPath:path error:nil];
  212. }
  213. }
  214. return [NSURL fileURLWithPath:path];
  215. }
  216. - (NSURL *)bandOutputDirectory {
  217. NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
  218. NSURL *ringFolder = [documents URLByAppendingPathComponent:@"ringDirectory"];
  219. return [self createDirectoryIfNeeded:ringFolder];
  220. }
  221. - (NSURL *)createDirectoryIfNeeded:(NSURL *)url {
  222. BOOL isDir = NO;
  223. if ([NSFileManager.defaultManager fileExistsAtPath:url.path isDirectory:&isDir] && isDir) {
  224. return url;
  225. }
  226. NSError *error;
  227. [NSFileManager.defaultManager createDirectoryAtURL:url
  228. withIntermediateDirectories:YES
  229. attributes:nil
  230. error:&error];
  231. return error ? nil : url;
  232. }
  233. @end