Browse Source

作品合成播放器更换

Steven 8 months ago
parent
commit
17d0b17942
18 changed files with 869 additions and 245 deletions
  1. 22 8
      KulexiuForStudent/KulexiuForStudent.xcodeproj/project.pbxproj
  2. 3 0
      KulexiuForStudent/KulexiuForStudent/Common/Base/BaseViewController/KSBaseViewController.m
  3. 1 0
      KulexiuForStudent/KulexiuForStudent/Common/Base/NavigationController/CustomNavViewController.m
  4. 3 1
      KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMediaMergeView.h
  5. 123 227
      KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMediaMergeView.m
  6. 3 3
      KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.m
  7. 21 4
      KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.xib
  8. 74 0
      KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.h
  9. 528 0
      KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.m
  10. BIN
      KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/CloudAccompanyLibrary
  11. 56 0
      KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/Headers/KSAudioEnginePlayer.h
  12. BIN
      KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeDirectory
  13. BIN
      KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeRequirements-1
  14. 15 0
      KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeResources
  15. BIN
      KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeSignature
  16. 18 2
      KulexiuForStudent/KulexiuForStudent/ToolKit/KSToolLibrary.framework/Headers/KSMediaEditor.h
  17. 2 0
      KulexiuForStudent/KulexiuForStudent/ToolKit/KSToolLibrary.framework/Headers/UIViewController+KSExtension.h
  18. BIN
      KulexiuForStudent/KulexiuForStudent/ToolKit/KSToolLibrary.framework/KSToolLibrary

+ 22 - 8
KulexiuForStudent/KulexiuForStudent.xcodeproj/project.pbxproj

@@ -564,7 +564,6 @@
 		BC7663172827E49900C91A1D /* NotiferMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = BC7663122827E49800C91A1D /* NotiferMessageCell.m */; };
 		BC7663182827E49900C91A1D /* NotiferMessageCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC7663132827E49900C91A1D /* NotiferMessageCell.xib */; };
 		BC7DECA02C2D571A00154524 /* AudioEnginePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = BC7DEC9F2C2D571A00154524 /* AudioEnginePlayer.m */; };
-		BC7DECA42C2D58AD00154524 /* CloudAccompanyLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC7DECA32C2D58AD00154524 /* CloudAccompanyLibrary.framework */; };
 		BC7FF6A12BEB71610092E0DE /* client.p12 in Resources */ = {isa = PBXBuildFile; fileRef = BC3BF62F2B9EAFC800831494 /* client.p12 */; };
 		BC802D8528B872AB0079E350 /* KSLiveAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC802D8428B872AB0079E350 /* KSLiveAlertView.m */; };
 		BC802D8728B872B40079E350 /* KSLiveAlertView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC802D8628B872B40079E350 /* KSLiveAlertView.xib */; };
@@ -891,7 +890,6 @@
 		BCD9294F28F8FCA4006793E4 /* AudioKit in Frameworks */ = {isa = PBXBuildFile; productRef = BCD9294E28F8FCA4006793E4 /* AudioKit */; };
 		BCD959C928DB071B00B70314 /* MusicTagView.m in Sources */ = {isa = PBXBuildFile; fileRef = BCD959C828DB071B00B70314 /* MusicTagView.m */; };
 		BCD959CC28DB0BAB00B70314 /* KSImageShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCD959CB28DB0BAB00B70314 /* KSImageShareViewController.m */; };
-		BCDA2C122BF5F29900ED16AC /* KSToolLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCDA2C112BF5F29900ED16AC /* KSToolLibrary.framework */; };
 		BCDE358E289A7D8700A9A560 /* KSGroupTagImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = BCDE358D289A7D8700A9A560 /* KSGroupTagImageView.m */; };
 		BCECE2452B3D670500C0D555 /* FeedbackListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCECE2292B3D670500C0D555 /* FeedbackListViewController.m */; };
 		BCECE2462B3D670500C0D555 /* FeedbackViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BCECE22A2B3D670500C0D555 /* FeedbackViewController.m */; };
@@ -910,6 +908,9 @@
 		BCECE2532B3D670500C0D555 /* FeedbackListNavView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCECE2402B3D670500C0D555 /* FeedbackListNavView.xib */; };
 		BCECE2542B3D670500C0D555 /* FeedbackBodyView.m in Sources */ = {isa = PBXBuildFile; fileRef = BCECE2412B3D670500C0D555 /* FeedbackBodyView.m */; };
 		BCECE2552B3D670500C0D555 /* FeedbackNavView.m in Sources */ = {isa = PBXBuildFile; fileRef = BCECE2432B3D670500C0D555 /* FeedbackNavView.m */; };
+		BCED0AF72C4651F800369AED /* KSMergeEnginePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = BCED0AF52C4651F800369AED /* KSMergeEnginePlayer.m */; };
+		BCED0AF92C4653A900369AED /* KSToolLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCED0AF82C4653A800369AED /* KSToolLibrary.framework */; };
+		BCED0AFB2C4653DE00369AED /* CloudAccompanyLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BCED0AFA2C4653DD00369AED /* CloudAccompanyLibrary.framework */; };
 		BCED5CA7284F55A0009A42DE /* FriendListModel.m in Sources */ = {isa = PBXBuildFile; fileRef = BCED5CA6284F55A0009A42DE /* FriendListModel.m */; };
 		BCF425DD2AB8665200BCD942 /* TenantHomeBannerView.m in Sources */ = {isa = PBXBuildFile; fileRef = BCF425DC2AB8665200BCD942 /* TenantHomeBannerView.m */; };
 		BCF425DF2AB8665900BCD942 /* TenantHomeBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BCF425DE2AB8665900BCD942 /* TenantHomeBannerView.xib */; };
@@ -1925,7 +1926,6 @@
 		BC7663142827E49900C91A1D /* NotiferHeadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotiferHeadView.h; sourceTree = "<group>"; };
 		BC7DEC9E2C2D571A00154524 /* AudioEnginePlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioEnginePlayer.h; sourceTree = "<group>"; };
 		BC7DEC9F2C2D571A00154524 /* AudioEnginePlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioEnginePlayer.m; sourceTree = "<group>"; };
-		BC7DECA32C2D58AD00154524 /* CloudAccompanyLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = CloudAccompanyLibrary.framework; sourceTree = "<group>"; };
 		BC802D8328B872AB0079E350 /* KSLiveAlertView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSLiveAlertView.h; sourceTree = "<group>"; };
 		BC802D8428B872AB0079E350 /* KSLiveAlertView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSLiveAlertView.m; sourceTree = "<group>"; };
 		BC802D8628B872B40079E350 /* KSLiveAlertView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = KSLiveAlertView.xib; sourceTree = "<group>"; };
@@ -2440,7 +2440,6 @@
 		BCD959C828DB071B00B70314 /* MusicTagView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MusicTagView.m; sourceTree = "<group>"; };
 		BCD959CA28DB0BAB00B70314 /* KSImageShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSImageShareViewController.h; sourceTree = "<group>"; };
 		BCD959CB28DB0BAB00B70314 /* KSImageShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSImageShareViewController.m; sourceTree = "<group>"; };
-		BCDA2C112BF5F29900ED16AC /* KSToolLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = KSToolLibrary.framework; sourceTree = "<group>"; };
 		BCDE358C289A7D8700A9A560 /* KSGroupTagImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSGroupTagImageView.h; sourceTree = "<group>"; };
 		BCDE358D289A7D8700A9A560 /* KSGroupTagImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSGroupTagImageView.m; sourceTree = "<group>"; };
 		BCECE2282B3D670500C0D555 /* FeedbackViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedbackViewController.h; sourceTree = "<group>"; };
@@ -2470,6 +2469,10 @@
 		BCECE2422B3D670500C0D555 /* CourseFileDisplayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CourseFileDisplayView.h; sourceTree = "<group>"; };
 		BCECE2432B3D670500C0D555 /* FeedbackNavView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeedbackNavView.m; sourceTree = "<group>"; };
 		BCECE2442B3D670500C0D555 /* FeedbackListNavView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeedbackListNavView.h; sourceTree = "<group>"; };
+		BCED0AF42C4651F800369AED /* KSMergeEnginePlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KSMergeEnginePlayer.h; sourceTree = "<group>"; };
+		BCED0AF52C4651F800369AED /* KSMergeEnginePlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSMergeEnginePlayer.m; sourceTree = "<group>"; };
+		BCED0AF82C4653A800369AED /* KSToolLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = KSToolLibrary.framework; sourceTree = "<group>"; };
+		BCED0AFA2C4653DD00369AED /* CloudAccompanyLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = CloudAccompanyLibrary.framework; sourceTree = "<group>"; };
 		BCED5CA5284F55A0009A42DE /* FriendListModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FriendListModel.h; sourceTree = "<group>"; };
 		BCED5CA6284F55A0009A42DE /* FriendListModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FriendListModel.m; sourceTree = "<group>"; };
 		BCF425DB2AB8665200BCD942 /* TenantHomeBannerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TenantHomeBannerView.h; sourceTree = "<group>"; };
@@ -2564,9 +2567,9 @@
 			files = (
 				BCA1136828A3A5CF007FAFB9 /* Accelerate.framework in Frameworks */,
 				BCFEE1932AD15C0E000E888F /* SoundpipeAudioKit in Frameworks */,
-				BC7DECA42C2D58AD00154524 /* CloudAccompanyLibrary.framework in Frameworks */,
+				BCED0AFB2C4653DE00369AED /* CloudAccompanyLibrary.framework in Frameworks */,
 				BCFEE18D2AD15BD4000E888F /* AudioKitEX in Frameworks */,
-				BCDA2C122BF5F29900ED16AC /* KSToolLibrary.framework in Frameworks */,
+				BCED0AF92C4653A900369AED /* KSToolLibrary.framework in Frameworks */,
 				BC8B6E152856E20800866917 /* WebKit.framework in Frameworks */,
 				BC8A45CB283DDEA100094BBB /* AVFoundation.framework in Frameworks */,
 				BCD9294F28F8FCA4006793E4 /* AudioKit in Frameworks */,
@@ -4443,6 +4446,7 @@
 		BC38C3D42AF893B300ABFCC2 /* AudioMerge */ = {
 			isa = PBXGroup;
 			children = (
+				BCED0AF62C4651F800369AED /* MergePlayer */,
 				BC38C3DE2AF893B300ABFCC2 /* AudioPlayAnimationView */,
 				BC38C3D62AF893B300ABFCC2 /* KSAudioAnimationView.h */,
 				BC38C3E72AF893B300ABFCC2 /* KSAudioAnimationView.m */,
@@ -4657,9 +4661,9 @@
 			children = (
 				BC00A65F2BB58FDB00231B74 /* LLPhotoBrowse.bundle */,
 				BC00A6602BB58FDC00231B74 /* WMPlayer.bundle */,
-				BCDA2C112BF5F29900ED16AC /* KSToolLibrary.framework */,
+				BCED0AF82C4653A800369AED /* KSToolLibrary.framework */,
 				BC3A55682BAA7B19002E1616 /* KSTunerLibrary.framework */,
-				BC7DECA32C2D58AD00154524 /* CloudAccompanyLibrary.framework */,
+				BCED0AFA2C4653DD00369AED /* CloudAccompanyLibrary.framework */,
 				BC3BF62F2B9EAFC800831494 /* client.p12 */,
 			);
 			path = ToolKit;
@@ -6121,6 +6125,15 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BCED0AF62C4651F800369AED /* MergePlayer */ = {
+			isa = PBXGroup;
+			children = (
+				BCED0AF42C4651F800369AED /* KSMergeEnginePlayer.h */,
+				BCED0AF52C4651F800369AED /* KSMergeEnginePlayer.m */,
+			);
+			path = MergePlayer;
+			sourceTree = "<group>";
+		};
 		BCF880D82B91C4700007B8F0 /* configuration */ = {
 			isa = PBXGroup;
 			children = (
@@ -6991,6 +7004,7 @@
 				BC119241280ED9E000A716F7 /* AccompanyDetailViewController.m in Sources */,
 				BCB6346F27F6D29600ACFDCF /* LiveroomTimeManager.m in Sources */,
 				27F9033627E87C8B00C08A19 /* MineNavView.m in Sources */,
+				BCED0AF72C4651F800369AED /* KSMergeEnginePlayer.m in Sources */,
 				BC50171527FC0D8300F8BCBC /* SubjectChooseBodyView.m in Sources */,
 				BC8C2C5C2823F57100FBA5D5 /* KSAddressPickerView.m in Sources */,
 				2723B62727F157D500E0B90B /* GroupMemberViewController.m in Sources */,

+ 3 - 0
KulexiuForStudent/KulexiuForStudent/Common/Base/BaseViewController/KSBaseViewController.m

@@ -455,6 +455,9 @@
 
 // viewWillDisappear 使用
 - (BOOL)isViewPopDismiss {
+    if (self.is_poppingToRoot) {
+        return YES;
+    }
     NSArray *viewControllers = self.navigationController.viewControllers;//获取当前的视图控制其
     if (viewControllers.count > 1 && [viewControllers objectAtIndex:viewControllers.count-2] == self) { //当前视图控制器在栈中,故为push操作
         return NO;

+ 1 - 0
KulexiuForStudent/KulexiuForStudent/Common/Base/NavigationController/CustomNavViewController.m

@@ -81,6 +81,7 @@
 }
 
 - (NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated {
+    self.visibleViewController.is_poppingToRoot = YES;
     if (self.viewControllers.count > 1) {
         self.topViewController.hidesBottomBarWhenPushed = NO;
     }

+ 3 - 1
KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMediaMergeView.h

@@ -22,7 +22,9 @@ NS_ASSUME_NONNULL_BEGIN
 
 @property (nonatomic, strong) NSString *desc;
 
-// 偏移时间 (录制声音和伴奏声音的偏移  + 伴奏提前 - 伴奏延迟 ms )
+@property (nonatomic, assign) float musicSpeed;
+
+// 偏移时间 (录制声音和伴奏声音的偏移)offsetTime 收音延迟+ 播放延迟 ms
 - (void)configWithVideoUrl:(NSURL *)videoUrl bgAudioUrl:(NSURL *)bgAudioUrl remoteBgUrl:(NSString *)remoteBgUrl recordUrl:(NSURL *)recordUrl offsetTime:(NSInteger)offsetTime mergeCallback:(MergeCallback)callback;
 
 - (void)configRemoteVideoUrl:(NSString *)remoteVideoUrl bgAudioUrl:(NSString *)remoteBgAudioUrl recordUrl:(NSString *)remoteRecrodUrl jsonConfig:(NSString *)jsonConfig callback:(DraftEditCallback)callback;

+ 123 - 227
KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMediaMergeView.m

@@ -11,7 +11,6 @@
 #import <KSToolLibrary/KSMediaEditor.h>
 #import "KSPlayerView.h"
 #import "KSPlayerSliderView.h"
-#import <CloudAccompanyLibrary/kSNewPlayer.h>
 #import "KSVideoPlayerView.h"
 #import "KSAudioPlayAnimationView.h"
 #import "KSNewAlertView.h"
@@ -25,7 +24,9 @@
 #import "KSUMShareManager.h"
 #import "KSLogManager.h"
 
-@interface KSMediaMergeView ()<kSNewPlayerManagerDelegate,KSVideoPlayerViewDelegate,RSKImageCropViewControllerDelegate,RSKImageCropViewControllerDataSource>
+#import "KSMergeEnginePlayer.h"
+
+@interface KSMediaMergeView ()<KSMergeEnginePlayerDelegate,KSVideoPlayerViewDelegate,RSKImageCropViewControllerDelegate,RSKImageCropViewControllerDataSource>
 
 @property (nonatomic, strong) KSAudioAnimationView *animationView;
 
@@ -43,15 +44,12 @@
 
 @property (nonatomic, strong) KSPlayerSliderView *playControlView;
 
-@property (nonatomic, strong) kSNewPlayer *bgPlayer;
-
-@property (nonatomic, strong) kSNewPlayer *recordPlayer;
+@property (nonatomic, strong) KSMergeEnginePlayer *mergePlayer;
 
 @property (nonatomic, assign) BOOL isPause;
 
 @property (nonatomic, strong) NSString *filePath;
 
-
 @property (nonatomic, assign) NSInteger originalOffset;
 
 @property (nonatomic, strong) KSVideoPlayerView *videoView;
@@ -112,9 +110,6 @@
 
 @property (nonatomic, assign) CGFloat bgPlayerRate;
 
-@property (nonatomic, assign) NSInteger addTime;
-
-//@property (nonatomic, strong) UILabel *offsetTimeLabel;
 
 @property (nonatomic, assign) NSInteger evaluateDelay;
 
@@ -133,9 +128,7 @@
         self.bgPlayerRate = 1.0;
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appEnterBackground) name:@"appEnterBackground" object:nil];
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherLogin) name:@"otherLogin" object:nil];
-        self.addTime = 0;
         [self configAudioSession];
-
     }
     return self;
 }
@@ -174,17 +167,19 @@
     self.videoUrl = videoUrl;
     self.bgAudioUrl = bgAudioUrl;
     self.recordUrl = recordUrl;
-    self.defaultDelay = -offsetTime;  // default delay = evaluate delay
-    self.evaluateDelay = -offsetTime;  // 播放延迟和收音延迟
+    self.defaultDelay = offsetTime;  // default delay = evaluate delay
+    self.evaluateDelay = offsetTime;  // 播放延迟和收音延迟
     self.originalOffset = 0;
     [self.contrlView configWithOffsetTime:0];
     self.contrlView.hideBackView = NO;
     [self configUI];
-    [self configPlayer];
+    [self modifyBgSpeed:self.bgAudioUrl callback:^{
+        [self configPlayer];
+    }];
 }
 
 - (void)configRemoteVideoUrl:(NSString *)remoteVideoUrl bgAudioUrl:(NSString *)remoteBgAudioUrl recordUrl:(NSString *)remoteRecrodUrl jsonConfig:(NSString *)jsonConfig callback:(DraftEditCallback)callback {
-    self.addTime = 0;
+    
     if (callback) {
         self.draftCallback = callback;
     }
@@ -201,14 +196,23 @@
     
     self.preJsonDic = [jsonConfig mj_JSONObject];
     self.originalOffset = [self.preJsonDic ks_integerValueForKey:@"offset"];
-    self.defaultDelay = [self.preJsonDic ks_integerValueForKey:@"defaultDelay"];
-    self.evaluateDelay = [self.preJsonDic ks_integerValueForKey:@"evaluateDelay"];  // 播放延迟和收音延迟
+    NSInteger defaultDelay = [self.preJsonDic ks_integerValueForKey:@"defaultDelay"];
+    NSInteger evaluateDelay = [self.preJsonDic ks_integerValueForKey:@"evaluateDelay"];
+    self.defaultDelay = labs(defaultDelay);
+    self.evaluateDelay = labs(evaluateDelay);  // 播放延迟和收音延迟
+    if ([[self.preJsonDic allKeys] containsObject:@"speedRate"]) {
+        self.musicSpeed = [self.preJsonDic ks_floatValueForKey:@"speedRate"];
+    }
+    else {
+        self.musicSpeed = 1.0f;
+    }
     self.offsetTime = self.originalOffset;
     [self.contrlView configWithOffsetTime:self.originalOffset];
     self.contrlView.hideBackView = YES;
     [self downloadFileSource];
 }
 
+
 - (void)downloadFileSource {
     [LOADING_MANAGER showCustomLoading:@"资源下载中"];
     
@@ -236,6 +240,21 @@
     }
 }
 
+// 修改文件速度
+- (void)modifyBgSpeed:(NSURL *)musicUrl callback:(void(^)(void))callback {
+    if (self.musicSpeed != 1) {
+        [KSMediaEditor modifyAudioFileSpeed:musicUrl rate:self.musicSpeed completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
+            if (isSuccess) {
+                self.bgAudioUrl = [NSURL fileURLWithPath:outPath];
+                callback();
+            }
+        }];
+    }
+    else {
+        callback();
+    }
+}
+
 - (void)downloadAudio {
     if (![NSString isEmptyString:self.remoteRecrodUrl]) {
         dispatch_group_enter(self.requestGroup);
@@ -257,8 +276,11 @@
         [KSNetworkingManager downloadFileRequestWithFileUrl:self.remoteBgAudioUrl progress:^(int64_t bytesRead, int64_t totalBytes) {
             
         } success:^(NSURL * _Nonnull fileUrl) {
-            dispatch_group_leave(self.requestGroup);
             self.bgAudioUrl = fileUrl;
+            [self modifyBgSpeed:self.bgAudioUrl callback:^{
+                dispatch_group_leave(self.requestGroup);
+            }];
+
         } faliure:^(NSError * _Nonnull error) {
             dispatch_group_leave(self.requestGroup);
 
@@ -266,34 +288,29 @@
     }
 }
 - (void)configPlayer {
-
-    if (self.bgAudioUrl) {
-        [self.bgPlayer preparePlayNativeSongWithPath:self.bgAudioUrl];
-    }
     
-    if (self.recordUrl) {
-        [self.recordPlayer preparePlayNativeSongWithPath:self.recordUrl];
-    }
     if (self.isVideoPlay) {
         [self.videoView preparePlayNativeVideoWithPath:self.videoUrl];
         self.videoView.isMute = YES;
     }
+    if (self.bgAudioUrl && self.recordUrl) {
+        [self.mergePlayer prepareNativeSongWithUrl:self.recordUrl bgMusic:self.bgAudioUrl];;
+    }
     
     // 音量同步
     NSInteger accompanyVolume = 100;
     if ([[self.preJsonDic allKeys] containsObject:@"accompanyVolume"]) {
         accompanyVolume = [self.preJsonDic ks_integerValueForKey:@"accompanyVolume"];
     }
-    self.accompanyVolume = accompanyVolume;
-    self.bgPlayer.volume = accompanyVolume / 100.0;
-
     NSInteger originalVolume = 100;
     if ([[self.preJsonDic allKeys] containsObject:@"originalVolume"]) {
         originalVolume = [self.preJsonDic ks_integerValueForKey:@"originalVolume"];
     }
+    
+    self.accompanyVolume = accompanyVolume;
     self.originalVolume = originalVolume;
-    self.recordPlayer.volume = originalVolume / 100.0;
-
+    
+    [self.mergePlayer changeVolume:accompanyVolume / 100.0 recordVolume:originalVolume / 100.0];
     [self.contrlView configRecordVolume:originalVolume bgVolume:accompanyVolume];
 }
 
@@ -376,9 +393,7 @@
         make.left.mas_equalTo(self.mas_left).offset(10);
         make.top.mas_equalTo(self.mas_top).offset(10);
     }];
-    
-    [self refreshTotalTime];
-    
+        
     UILabel *songLabel = [[UILabel alloc] init];
     songLabel.font = [UIFont systemFontOfSize:16.0f weight:UIFontWeightSemibold];
     songLabel.text = [NSString returnNoNullStringWithString:self.songName];
@@ -390,23 +405,12 @@
         make.height.mas_equalTo(22);
         make.right.mas_greaterThanOrEqualTo(self.contrlView.mas_left).offset(20);
     }];
-    
-//    self.offsetTimeLabel = [[UILabel alloc] init];
-//    self.offsetTimeLabel.font = [UIFont systemFontOfSize:14.0f weight:UIFontWeightSemibold];
-//    self.offsetTimeLabel.textColor = self.isVideoPlay ? HexRGB(0xffffff) : HexRGB(0x131415);
-//    [self addSubview:self.offsetTimeLabel];
-//    [self.offsetTimeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
-//        make.height.mas_equalTo(22);
-//        make.centerX.mas_equalTo(self.mas_centerX);
-//        make.centerY.mas_equalTo(songLabel.mas_centerY);
-//    }];
-    
-//    [self startPlay];
-    // 缓冲完成后播放
 }
 
 - (void)refreshTotalTime {
-    float totalTime = [self.recordPlayer getTotalDuration];
+    NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
+    [self.mergePlayer changeRecordDelay:realOffsetTime];
+    float totalTime = [self.mergePlayer getTotalTime] / 1000.0;
     [self.playControlView configWithDuration:totalTime];
 }
 
@@ -457,32 +461,17 @@
 }
 
 - (void)startPlay {
-    self.bgPlayerRate = 1.0;
+    
     self.playControlView.isPlay = YES;
     self.animationView.isPlay = YES;
-    NSInteger recordPlayerPosition = self.playControlView.playScheduleTime*1000;
-    [self.recordPlayer seekToTimePlay:recordPlayerPosition];
     
+    NSInteger playPosition = self.playControlView.playScheduleTime*1000;
     NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
-    NSInteger offsetTime = recordPlayerPosition + realOffsetTime;
-    if (offsetTime >= 0) {
-        [self.bgPlayer seekToTimePlay:offsetTime];
-    }
-    else {
-        [self.bgPlayer seekToStart];
-        @weakObj(self);
-        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((labs(offsetTime) + self.addTime) * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
-            @strongObj(self);
-//            NSLog(@"--bgPlayer start---");
-            if (self.playControlView.isPlay == YES) {
-                [self.bgPlayer resumePlay];
-                [self snapPlayerProgress];
-            }
-        });
-    }
+    [self.mergePlayer changeRecordDelay:realOffsetTime];
+    [self.mergePlayer seekToTimePlay:playPosition];
     
     if (self.isVideoPlay) {
-        [self.videoView seekToTimePlay:recordPlayerPosition];
+        [self.videoView seekToTimePlay:playPosition];
     }
     else {
         [self.playAnimationView startAnimation];
@@ -493,8 +482,8 @@
 - (void)stopPlay {
     self.animationView.isPlay = NO;
     self.playControlView.isPlay = NO;
-    [self.bgPlayer puasePlay];
-    [self.recordPlayer puasePlay];
+    [self.mergePlayer stopPlay];
+
     if (self.isVideoPlay) {
         [self.videoView puasePlay];
     }
@@ -518,33 +507,12 @@
             break;
         case PLAYERTYPE_RATE:
         {
-            [self.recordPlayer seekOffsetTime:rate*1000];
-            
-            NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
-            NSInteger offsetTime = rate*1000 + realOffsetTime;
-            if (offsetTime >= 0) {
-                [self.bgPlayer seekOffsetTime:offsetTime];
-            }
-            else {
-                [self.bgPlayer puasePlay];
-                if (self.recordPlayer.isPlaying) {
-                    [self.bgPlayer seekToStart];
-                    @weakObj(self);
-                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((labs(offsetTime) + self.addTime) * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
-                        @strongObj(self);
-                        if (self.playControlView.isPlay == YES) {
-                            [self.bgPlayer resumePlay];
-                            [self snapPlayerProgress];
-                        }
-                        
-                    });
-                }
-                else {
-                    [self.bgPlayer seekToStart];
-                }
+            if (self.mergePlayer.isPlaying) {
+                [self.mergePlayer seekToTimePlay:rate*1000];
             }
             
             if (self.isVideoPlay) {
+//                NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
                 [self.videoView seekOffsetTime:rate*1000];
             }
         }
@@ -554,20 +522,6 @@
     }
 }
 
-// 对齐播放延迟
-- (void)snapPlayerProgress {
-    @weakObj(self);
-    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
-        @strongObj(self);
-        if (self.playControlView.isPlay == YES) {
-            CMTime time = [self.recordPlayer getCurrentPlayTime];
-            NSInteger newDelayTime = CMTimeGetSeconds(time) * 1000 + self.offsetTime + self.evaluateDelay;
-            [self.recordPlayer seekOffsetTimeNoPuase:CMTimeGetSeconds(time) * 1000];
-            [self.bgPlayer seekOffsetTime:newDelayTime];
-        }
-        
-    });
-}
 
 - (void)startTimer {
     [self.timer setFireDate:[NSDate distantPast]];
@@ -601,22 +555,22 @@
             self.hasModify = YES;
             self.accompanyVolume = bgVolume;
             self.originalVolume = recordVolume;
-            
-            self.bgPlayer.volume = bgVolume / 100.0;
-            self.recordPlayer.volume = recordVolume / 100.0;
+            [self.mergePlayer changeVolume:bgVolume / 100.0 recordVolume:recordVolume / 100.0];
         }
             break;
         case MERGEACTION_DELAY:  // offset
         {
             self.hasModify = YES;
-            if (self.bgPlayer.isPlaying) {
-                CMTime time = [self.recordPlayer getCurrentPlayTime];
-                NSInteger newDelayTime = CMTimeGetSeconds(time) * 1000 + offsetTime + self.evaluateDelay;
-                [self.recordPlayer seekOffsetTime:CMTimeGetSeconds(time) * 1000];
-                [self.bgPlayer seekOffsetTime:newDelayTime];
-            }
-            
             self.offsetTime = offsetTime;
+            NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
+            if (self.mergePlayer.isPlaying) { // 调整延迟
+    
+                [self.mergePlayer seekOffsetTime:realOffsetTime];
+            }
+            if (self.videoView.isPlaying) {
+//                [self.videoView seekOffsetTime:realOffsetTime];
+            }
+            [self refreshTotalTime];
         }
             break;
         case MERGEACTION_SAVE: // 保存
@@ -821,8 +775,10 @@
     CGFloat rate = 1.0/3;
     if (self.isVideoPlay) {
         [LOADING_MANAGER showProgressLoading:@"视频合成中" progress:0];
-        CGFloat offsetTime = self.offsetTime + self.evaluateDelay;
-        [KSMediaEditor mixVideoWithRecordAudio:self.recordUrl recordVolume:self.recordPlayer.volume bgAudio:self.bgAudioUrl bgAudioVolume:self.bgPlayer.volume offsetTime:offsetTime videoUrlStr:self.videoUrl completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
+        NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay; // 演奏偏移
+        NSInteger adjustOffset = -realOffsetTime;
+
+        [KSMediaEditor mixVideoWithRecordAudio:self.recordUrl recordVolume:self.mergePlayer.recordVolume bgAudio:self.bgAudioUrl bgAudioVolume:self.mergePlayer.bgVolume offsetTime:adjustOffset rate:1 videoUrlStr:self.videoUrl completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
             [LOADING_MANAGER showProgressLoading:@"视频合成中" progress:progress];
             // 保存文件到指定文件夹
             if (isSuccess) {
@@ -843,8 +799,9 @@
     }
     else {
         [LOADING_MANAGER showProgressLoading:@"音频合成中" progress:0];
-        NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
-        [KSMediaEditor mixRecordAudio:self.recordUrl recordVolume:self.recordPlayer.volume bgAudio:self.bgAudioUrl bgAudioVolume:self.bgPlayer.volume offsetTime:realOffsetTime completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
+        NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay; // 演奏偏移
+        NSInteger adjustOffset = -realOffsetTime;
+        [KSMediaEditor mixRecordAudio:self.recordUrl recordVolume:self.mergePlayer.recordVolume bgAudio:self.bgAudioUrl bgAudioVolume:self.mergePlayer.bgVolume rate:1 offsetTime:adjustOffset completion:^(NSString * _Nonnull outPath, BOOL isSuccess, NSString * _Nonnull desc) {
             // 保存文件到指定文件夹
             [LOADING_MANAGER showProgressLoading:@"音频合成中" progress:progress];
 
@@ -933,116 +890,31 @@
 }
 
 - (void)freePlayer {
-    if (_bgPlayer) {
-         [self.bgPlayer freePlayer];
-    }
-    if (_recordPlayer) {
-         [self.recordPlayer freePlayer];
+    if (_mergePlayer) {
+        [self.mergePlayer freePlayer];
     }
     if (_videoView) {
         [self.videoView freePlayer];
     }
 }
 
-- (kSNewPlayer *)bgPlayer {
-    if (!_bgPlayer) {
-        _bgPlayer = [[kSNewPlayer alloc] init];
-        _bgPlayer.delegate = self;
-    }
-    return _bgPlayer;
-}
-
-- (kSNewPlayer *)recordPlayer {
-    if (!_recordPlayer) {
-        _recordPlayer = [[kSNewPlayer alloc] init];
-        _recordPlayer.delegate = self;
+- (KSMergeEnginePlayer *)mergePlayer {
+    if (!_mergePlayer) {
+        _mergePlayer = [[KSMergeEnginePlayer alloc] init];
+        _mergePlayer.delegate = self;
     }
-    return _recordPlayer;
+    return _mergePlayer;
 }
 
 
 #pragma mark ----- player delegate
 
 - (void)videoPlayerIsReadyPlay:(AVPlayer *)player {
-    if (self.bgPlayer.isReady && self.recordPlayer.isReady) {
+    if (self.mergePlayer.isReady) {
         [self startPlay];
     }
 }
 
-- (void)playerIsReadyPlay:(kSNewPlayer *)player {
-    if (player == self.recordPlayer) {
-        [self refreshTotalTime];
-    }
-    if (self.isVideoPlay) {
-        if (self.bgPlayer.isReady && self.recordPlayer.isReady && self.videoView.isReady) {
-            [self startPlay];
-        }
-    }
-    else {
-        if (self.bgPlayer.isReady && self.recordPlayer.isReady) {
-            [self startPlay];
-        }
-    }
-}
-
-
-- (void)getSongCurrentTime:(NSInteger)currentTime andTotalTime:(NSInteger)totalTime andProgress:(CGFloat)progress currentInterval:(NSTimeInterval)currentInterval playTime:(NSTimeInterval)playTime inPlayer:(kSNewPlayer *_Nonnull)player {
-    if (player == self.recordPlayer) {
-//        NSLog(@"--- ----  ----- %f", playTime);
-        self.playControlView.playScheduleTime = (NSInteger)(playTime / 1000);
-        NSInteger realOffset = (NSInteger)(CMTimeGetSeconds([self.bgPlayer getCurrentPlayTime]) *1000 - CMTimeGetSeconds([self.recordPlayer getCurrentPlayTime])*1000);
-//        NSLog(@" offset ---- %ld" , realOffset);
-        //  如果延迟大于11ms 调整
-        NSInteger expectOffset = self.offsetTime + self.evaluateDelay;
-        NSLog(@"---- expectOffset == %@", [NSString stringWithFormat:@"实际: %ld, 期望偏差 %ld", realOffset, expectOffset]);
-//        self.offsetTimeLabel.text = [NSString stringWithFormat:@"实际: %ld, 期望偏差 %ld", realOffset, expectOffset];
-//        if (labs(labs(realOffset) - labs(expectOffset)) >= 11) { // 需要调整
-//
-//            if (labs(realOffset) < labs(expectOffset)) { // 要扩大差距
-//                if (expectOffset > 0) {  // 300
-//                    if (self.bgPlayerRate != 1.02) {
-//                        self.bgPlayerRate = 1.02;
-//                        [self.bgPlayer configPlayerRate:1.02];
-//                        [self.recordPlayer configPlayerRate:0.98];
-//                    }
-//                }
-//                else { // expectOffset < 0 -300
-//                    if (self.bgPlayerRate != 0.98) {
-//                        self.bgPlayerRate = 0.98;
-//                        [self.bgPlayer configPlayerRate:0.98];
-//                        [self.recordPlayer configPlayerRate:1.02];
-//                    }
-//                }
-//            }
-//            else { // 需要减小差距
-//                if (realOffset > 0) {
-//                    if (self.bgPlayerRate != 0.98) {
-//                        self.bgPlayerRate = 0.98;
-//                        [self.bgPlayer configPlayerRate:0.98];
-//                        [self.recordPlayer configPlayerRate:1.02];
-//                    }
-//                }
-//                else {
-//                    if (self.bgPlayerRate != 1.02) {
-//                        self.bgPlayerRate = 1.02;
-//                        [self.bgPlayer configPlayerRate:1.02];
-//                        [self.recordPlayer configPlayerRate:0.98];
-//                    }
-//                }
-//            }
-//        }
-//        else {
-//            if (self.bgPlayerRate != 1.0) {
-//                self.bgPlayerRate = 1.0;
-//                [self.recordPlayer configPlayerRate:1.0];
-//                [self.bgPlayer configPlayerRate:1.0];
-//            }
-//        }
-    }
-    else {
-//        NSLog(@"------- bgPlayer   ----- %f", playTime);
-    }
-}
 
 - (void)videoPlayerOccurError:(AVPlayer *)player {
     NSError *error = player.error;
@@ -1067,36 +939,59 @@
     }
 }
 
+#pragma mark ------- merge Player delegate
+- (void)updatePlayProgress:(NSInteger)playTime andTotalTime:(NSInteger)totalTime andProgress:(CGFloat)progress currentInterval:(NSTimeInterval)currentInterval inPlayer:(KSMergeEnginePlayer *)player {
+    /*
+    CMTime videoPlayTime = [self.videoView getCurrentPlayTime];
+    NSTimeInterval videoTime = CMTimeGetSeconds(videoPlayTime) * 1000;
+    NSLog(@"----- offset %f ", playTime - videoTime);
+     */
+    dispatch_main_async_safe(^{
+        self.playControlView.playScheduleTime = (NSInteger)(playTime / 1000);
+    });
+}
 
-- (void)playFinished:(kSNewPlayer *)player {
-    if (player == self.recordPlayer) {
-        
-        [self.recordPlayer puasePlay];
-        [self.recordPlayer seekToStart];
-        
-        [self.bgPlayer puasePlay];
-        [self.bgPlayer seekToStart];
-        
+- (void)enginePlayerIsReadyPlay:(KSMergeEnginePlayer *)player {
+    dispatch_main_async_safe(^{
+        [self refreshTotalTime];
+        if (self.isVideoPlay) {
+            if (self.videoView.isReady) {
+                [self startPlay];
+            }
+        }
+        else {
+            [self startPlay];
+        }
+    });
+    
+}
+
+- (void)enginePlayFinished:(KSMergeEnginePlayer *)player {
+    dispatch_main_async_safe(^{
+        [self.mergePlayer stopPlay];
         self.animationView.isPlay = NO;
         self.playControlView.isPlay = NO;
         [self.playAnimationView stopAnimation];
+        
         [self stopTimer];
+        self.playControlView.playScheduleTime = 0;
+        
         if (self.isVideoPlay) {
             [self.videoView puasePlay];
             [self.videoView seekToStart];
         }
-    }
+        
+    });
 }
 
-- (void)playerDidError:(kSNewPlayer *)player {
+- (void)enginePlayerDidError:(KSMergeEnginePlayer *)player error:(NSError *)error {
     // 播放出现问题
     [self stopPlay];
-    NSError *error = player.player.error;
     if (error) {
         NSLog(@"-- error desc - %@", error.description);
         NSMutableDictionary *parm = [NSMutableDictionary dictionary];
         [parm setValue:UserDefault(UIDKey) forKey:@"userId"];
-        [parm setValue:@"KSMediaMergeViewMp3Player" forKey:@"Location"];
+        [parm setValue:@"KSMergeEnginePlayer" forKey:@"Location"];
         [parm setValue:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] forKey:@"version"];
         [parm setValue:@(error.code) forKey:@"errorCode"];
         [parm setValue:error.description forKey:@"errorDesc"];
@@ -1111,6 +1006,7 @@
             
         }];
     }
+    
 }
 
 - (KSVideoPlayerView *)videoView {

+ 3 - 3
KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.m

@@ -302,15 +302,15 @@
     _offsetTime = offsetTime;
     NSString *tips = @"";
     if (offsetTime > 0) {
-        tips = [NSString stringWithFormat:@"已为您对齐演奏伴奏,演奏延后%zd毫秒", labs(offsetTime)];
+        tips = [NSString stringWithFormat:@"演奏提前%zd毫秒", labs(offsetTime)];
         self.pointLeft.constant = 87 + labs(offsetTime) / 20.0 * 3;
     }
     else if (offsetTime == 0) {
-        tips = @"演奏伴奏没有对齐?试试调整这里";
+        tips = @"拖动指针移动演奏音频";
         self.pointLeft.constant = 87;
     }
     else {
-        tips = [NSString stringWithFormat:@"已为您对齐演奏伴奏,演奏提前%zd毫秒", labs(offsetTime)];
+        tips = [NSString stringWithFormat:@"演奏延后%zd毫秒", labs(offsetTime)];
         self.pointLeft.constant = 87 - labs(offsetTime) / 20.0 * 3;
     }
     self.offsetTipsLabel.text = tips;

+ 21 - 4
KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.xib

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
     <device id="retina6_12" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
@@ -155,8 +155,8 @@
                                 <action selector="leftAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="fHh-PV-Mtc"/>
                             </connections>
                         </button>
-                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="已为您对齐演奏伴奏,演奏提前10毫秒" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ses-18-GMw">
-                            <rect key="frame" x="47" y="290" width="195" height="15"/>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="拖动指针移动演奏音频" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ses-18-GMw">
+                            <rect key="frame" x="88.333333333333314" y="290" width="112.66666666666669" height="15"/>
                             <constraints>
                                 <constraint firstAttribute="height" constant="15" id="To3-OM-9PS"/>
                             </constraints>
@@ -282,6 +282,18 @@
                             <rect key="frame" x="26" y="90" width="247" height="40"/>
                             <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         </view>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="演奏延后" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzg-Xa-du5">
+                            <rect key="frame" x="17" y="290.66666666666669" width="45" height="14"/>
+                            <fontDescription key="fontDescription" type="system" pointSize="11"/>
+                            <color key="textColor" red="0.56078431370000004" green="0.56078431370000004" blue="0.56078431370000004" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
+                            <nil key="highlightedColor"/>
+                        </label>
+                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="演奏提前" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gDU-D3-OZv">
+                            <rect key="frame" x="237" y="290.66666666666669" width="45" height="14"/>
+                            <fontDescription key="fontDescription" type="system" pointSize="11"/>
+                            <color key="textColor" red="0.56078431370000004" green="0.56078431370000004" blue="0.56078431370000004" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
+                            <nil key="highlightedColor"/>
+                        </label>
                     </subviews>
                     <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                     <constraints>
@@ -295,9 +307,12 @@
                         <constraint firstItem="ucS-Gd-cx3" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="26" id="7rR-H1-Deg"/>
                         <constraint firstItem="ses-18-GMw" firstAttribute="centerX" secondItem="dNG-od-Opv" secondAttribute="centerX" id="83a-36-AvC"/>
                         <constraint firstItem="96g-7r-vnm" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="26" id="A0h-S5-779"/>
+                        <constraint firstItem="xzg-Xa-du5" firstAttribute="centerX" secondItem="96g-7r-vnm" secondAttribute="centerX" id="BoY-ap-VeD"/>
                         <constraint firstItem="ucS-Gd-cx3" firstAttribute="top" secondItem="Z4q-fo-efi" secondAttribute="bottom" constant="7" id="Brh-qW-hLO"/>
+                        <constraint firstItem="ses-18-GMw" firstAttribute="centerY" secondItem="xzg-Xa-du5" secondAttribute="centerY" id="CeS-09-Vvp"/>
                         <constraint firstItem="DAF-iV-Mgw" firstAttribute="bottom" secondItem="ucS-Gd-cx3" secondAttribute="bottom" constant="5" id="Cmy-M2-R1u"/>
                         <constraint firstItem="oyf-C4-Pe3" firstAttribute="top" secondItem="ffV-Sq-WQc" secondAttribute="bottom" constant="18" id="EGe-wi-jn3"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="centerY" secondItem="xzg-Xa-du5" secondAttribute="centerY" id="Ebk-nq-Udu"/>
                         <constraint firstItem="ucS-Gd-cx3" firstAttribute="top" secondItem="if2-R5-Tul" secondAttribute="bottom" constant="16" id="Eyw-26-LDZ"/>
                         <constraint firstItem="f5Q-wh-6l8" firstAttribute="bottom" secondItem="GuK-qa-jwH" secondAttribute="bottom" id="IBd-T0-PP8"/>
                         <constraint firstItem="53S-gG-Fsg" firstAttribute="leading" secondItem="Jqd-bC-HWT" secondAttribute="trailing" constant="20" id="JYZ-kj-UgG"/>
@@ -316,7 +331,9 @@
                         <constraint firstItem="if2-R5-Tul" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="26" id="Tbj-uW-5vm"/>
                         <constraint firstItem="if2-R5-Tul" firstAttribute="top" secondItem="pQK-tY-S1I" secondAttribute="bottom" constant="20" id="U6A-D9-FMf"/>
                         <constraint firstItem="MX8-jr-euo" firstAttribute="centerY" secondItem="gDH-uC-2DO" secondAttribute="centerY" id="W2M-EL-mO7"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="centerY" secondItem="xzg-Xa-du5" secondAttribute="centerY" id="W3y-sV-qYf"/>
                         <constraint firstItem="Z4q-fo-efi" firstAttribute="centerX" secondItem="dNG-od-Opv" secondAttribute="leading" constant="267" id="WVs-oc-QrV"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="centerX" secondItem="imR-Qo-Jpf" secondAttribute="centerX" id="YAs-zN-DUP"/>
                         <constraint firstItem="slK-h7-P25" firstAttribute="top" secondItem="XPO-nW-Uox" secondAttribute="bottom" constant="12" id="ZPv-OR-vc2"/>
                         <constraint firstItem="f5Q-wh-6l8" firstAttribute="centerX" secondItem="Jqd-bC-HWT" secondAttribute="centerX" id="aUL-2e-p91"/>
                         <constraint firstItem="ZEM-tW-WX9" firstAttribute="top" secondItem="dNG-od-Opv" secondAttribute="top" constant="14" id="ddY-o0-jeB"/>

+ 74 - 0
KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.h

@@ -0,0 +1,74 @@
+//
+//  KSMergeEnginePlayer.h
+//  MutiPlayDemo
+//
+//  Created by 王智 on 2024/6/17.
+//
+
+#import <Foundation/Foundation.h>
+
+@class KSMergeEnginePlayer;
+
+@protocol KSMergeEnginePlayerDelegate <NSObject>
+
+/// 播放进度回调
+/// - Parameters:
+///   - playTime: 播放时间
+///   - totalTime: 播放总时长
+///   - progress: 进度
+///   - currentInterval: 当前时间
+///   - player: 播放器对象
+- (void)updatePlayProgress:(NSInteger)playTime andTotalTime:(NSInteger)totalTime andProgress:(CGFloat)progress currentInterval:(NSTimeInterval)currentInterval inPlayer:(KSMergeEnginePlayer *_Nonnull)player;
+
+@optional
+
+- (void)enginePlayFinished:(KSMergeEnginePlayer *_Nonnull)player;
+
+- (void)enginePlayerIsReadyPlay:(KSMergeEnginePlayer *_Nonnull)player;
+
+- (void)enginePlayerDidError:(KSMergeEnginePlayer *_Nonnull)player error:(NSError *_Nonnull)error;
+
+@end
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KSMergeEnginePlayer : NSObject
+
+@property (nonatomic, weak) id <KSMergeEnginePlayerDelegate>delegate;
+
+@property (nonatomic, assign) BOOL isReady;
+
+@property (nonatomic, assign) BOOL isPlaying;
+
+@property (nonatomic, assign) float bgVolume;
+
+@property (nonatomic, assign) float recordVolume;
+
+// 加载数据
+- (void)prepareNativeSongWithUrl:(NSURL *)recordAudioUrl bgMusic:(NSURL *)bgMusicUrl;
+
+// 修改录音真实延迟 delayMs
+- (void)changeRecordDelay:(NSInteger)delayMs;
+
+// 从某个位置开始播放 ms
+- (void)seekToTimePlay:(NSInteger)time;
+
+- (void)seekToTime:(NSInteger)time;
+// 调整偏移
+- (void)seekOffsetTime:(NSInteger)offsetTime;
+
+- (void)stopPlay;
+
+- (void)freePlayer;
+
+// 获取当前时间
+- (NSTimeInterval)getCurrentPlayTime;
+
+- (NSTimeInterval)getTotalTime;
+
+
+- (void)changeVolume:(float)bgVolume recordVolume:(float)recordVolume;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 528 - 0
KulexiuForStudent/KulexiuForStudent/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.m

@@ -0,0 +1,528 @@
+//
+//  KSMergeEnginePlayer.m
+//  MutiPlayDemo
+//
+//  Created by 王智 on 2024/6/17.
+//
+
+#import "KSMergeEnginePlayer.h"
+#import <AVFoundation/AVFoundation.h>
+
+
+
+@interface KSMergeEnginePlayer ()
+/** 定时器 */
+@property (nonatomic, strong) NSTimer *timer;
+
+@property (nonatomic, strong) AVAudioEngine *audioEngine;
+
+@property (nonatomic, strong) AVAudioPlayerNode *nodePlayer;
+
+@property (nonatomic, strong) AVAudioFile *audioFile;
+@property (nonatomic, strong) AVAudioFormat *audioFormat;
+
+@property (nonatomic, strong) AVAudioFile *bgAudioFile;
+@property (nonatomic, strong) AVAudioFormat *bgAudioFormat;
+
+@property (nonatomic, assign) NSTimeInterval totalDuration;
+
+@property (nonatomic, assign) NSInteger offsetTime; // 延迟时间
+
+@property (nonatomic, assign) AVAudioFramePosition startPosition; // 开始位置
+
+@property (nonatomic, strong) dispatch_queue_t sourceQueue;
+
+@property (nonatomic, strong) AVAudioPCMBuffer *mixBuffer;
+
+@property (nonatomic, strong) AVAudioPCMBuffer *bgBuffer;
+
+@property (nonatomic, strong) AVAudioPCMBuffer *recordBuffer;
+
+@property (nonatomic, assign) AVAudioFramePosition currentFrame;
+
+@property (nonatomic, assign) double sampleRate;
+
+@property (nonatomic, assign) BOOL stopMix; // 是否停止mix
+
+@property (nonatomic, strong) dispatch_semaphore_t mixChangeSemaphore; // mix信号量
+
+@property (nonatomic, assign) BOOL stopChangeVolume; // 是否停止音量修改循环
+
+@property (nonatomic, strong) dispatch_semaphore_t volumeChangeSemaphore;
+
+@end
+
+
+@implementation KSMergeEnginePlayer
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        [self configDefault];
+    }
+    return self;
+}
+
+- (void)configDefault {
+    self.recordVolume = 1.0f;
+    self.bgVolume = 1.0f;
+    self.mixChangeSemaphore = dispatch_semaphore_create(1); // 初始化信号量
+    self.volumeChangeSemaphore = dispatch_semaphore_create(1); // 初始化信号量,初始值为1
+}
+
+- (void)configEngine {
+    [self setupAudioSession];
+    
+    self.audioEngine = [[AVAudioEngine alloc] init];
+    self.nodePlayer = [[AVAudioPlayerNode alloc] init];
+    
+    // attach node
+    [self.audioEngine attachNode:self.nodePlayer];
+    
+}
+
+- (void)setupAudioSession {
+    NSError *err = nil;
+    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
+    
+    @try {
+        [audioSession setActive:YES error:&err];
+    } @catch (NSException *exception) {
+        
+    } @finally {
+        
+    }
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:audioSession];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChange:) name:AVAudioSessionRouteChangeNotification object:audioSession];
+}
+
+
+- (void)startEngine {
+    // 启动engine
+    NSError *error = nil;
+    @try {
+        [self.audioEngine startAndReturnError:&error];
+    } @catch (NSException *exception) {
+        NSLog(@"--------Exception: %@", exception);
+    } @finally {
+        if (error) {
+            self.audioEngine = nil;
+            // 错误回调
+            if (self.delegate && [self.delegate respondsToSelector:@selector(enginePlayerDidError:error:)]) {
+                [self.delegate enginePlayerDidError:self error:error];
+            }
+        }
+    }
+}
+
+- (void)prepareNativeSongWithUrl:(NSURL *)recordAudioUrl bgMusic:(NSURL *)bgMusicUrl {
+    [self loadAuidoFile:recordAudioUrl isBgm:NO];
+    [self loadAuidoFile:bgMusicUrl isBgm:YES];
+    self.sampleRate = self.audioFile.fileFormat.sampleRate;
+    [self configEngine];
+    
+    AVAudioFormat *outputFormat = [self.audioEngine.mainMixerNode outputFormatForBus:0];
+    [self.audioEngine connect:self.nodePlayer to:self.audioEngine.mainMixerNode format:outputFormat];
+    [self startEngine];
+    
+    if (self.audioEngine && self.audioEngine.isRunning) {
+        dispatch_async(self.sourceQueue, ^{
+            [self prepareBufferFrame];
+        });
+    }
+}
+
+
+- (void)loadAuidoFile:(NSURL *)audioFileUrl isBgm:(BOOL)isBgm {
+    dispatch_sync(self.sourceQueue, ^{
+        NSError *error = nil;
+        AVAudioFile *audioFile = nil;
+        AVAudioFormat *audioFormat = nil;
+        @try {
+            audioFile = [[AVAudioFile alloc] initForReading:audioFileUrl error:&error];
+            audioFormat = audioFile.processingFormat;
+            
+        } @catch (NSException *exception) {
+            audioFile = nil;
+            audioFormat = nil;
+        } @finally {
+            if (error) {
+                // 错误回调
+            }
+            else { // 加载成功
+                if (isBgm) {
+                    self.bgAudioFile = audioFile;
+                    self.bgAudioFormat = audioFormat;
+                }
+                else {
+                    self.audioFile = audioFile;
+                    self.audioFormat = audioFormat;
+                }
+            }
+        }
+    });
+}
+
+- (void)prepareBufferFrame {
+    AVAudioFrameCount minFrameCount = (AVAudioFrameCount)MIN(self.bgAudioFile.length, self.audioFile.length);
+    // mixBuffer
+    AVAudioFormat *outputFormat = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:self.bgAudioFormat.sampleRate channels:2];
+    self.mixBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:outputFormat frameCapacity:minFrameCount];
+    self.mixBuffer.frameLength = minFrameCount;
+    
+    self.bgBuffer = [self loadAudioSegment:self.bgAudioFile startFrame:0 frameCount:minFrameCount];
+    self.recordBuffer = [self loadAudioSegment:self.audioFile startFrame:0 frameCount:minFrameCount];
+    
+    if (self.delegate && [self.delegate respondsToSelector:@selector(enginePlayerIsReadyPlay:)]) {
+        self.isReady = YES;
+        [self.delegate enginePlayerIsReadyPlay:self];
+    }
+}
+
+- (AVAudioPCMBuffer *)loadAudioSegment:(AVAudioFile *)audioFile startFrame:(AVAudioFramePosition)startFrame frameCount:(AVAudioFrameCount)frameCount {
+    AVAudioFormat *audioFromat = audioFile.processingFormat;
+    AVAudioFrameCount frameToRead = (AVAudioFrameCount)MIN(frameCount, (AVAudioFrameCount)audioFile.length - startFrame);
+    if (startFrame > audioFile.length) {
+        return nil;
+    }
+    AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFromat frameCapacity:frameToRead];
+    buffer.frameLength = frameToRead;
+    
+    audioFile.framePosition = startFrame;
+    if (frameToRead > 0) {
+        @try {
+            [audioFile readIntoBuffer:buffer frameCount:frameToRead error:nil];
+        } @catch (NSException *exception) {
+            
+        } @finally {
+            
+        }
+    }
+    return buffer;
+}
+
+
+- (void)mixBuffers:(AVAudioPCMBuffer *)bgBuffer bgBufferVolume:(float)bgBufferVolume withRecordBuffer:(AVAudioPCMBuffer *)recordBuffer recordVolume:(float)recordVolume offset:(NSInteger)offsetTime startPosition:(AVAudioFrameCount)startPosition {
+    if (!bgBuffer && !recordBuffer) {
+        return;
+    }
+    NSLog(@"------- start");
+    
+    AVAudioFrameCount minFrameCount = MIN(bgBuffer.frameLength, recordBuffer.frameLength);
+    AVAudioFrameCount offsetFrame = labs(offsetTime)/1000.0 * recordBuffer.format.sampleRate;
+    
+    float *bgLeftChannel = bgBuffer.floatChannelData[0];
+    float *bgRightChannel = bgBuffer.floatChannelData[1];
+    // 录音文件未单声道
+    float *recordLeftChannel = recordBuffer.floatChannelData[0];
+    
+    float *mixLeftChannel = self.mixBuffer.floatChannelData[0];
+    float *mixRightChannel = self.mixBuffer.floatChannelData[1];
+    
+    for (int frame = 0; frame < minFrameCount; frame++) {
+        if (self.stopMix) {
+            NSLog(@"------- stop mix");
+            dispatch_semaphore_signal(self.mixChangeSemaphore); // 释放信号量
+            return;
+        }
+        int bgFrame = frame+startPosition;
+        float leftChannel = (bgFrame < bgBuffer.frameLength) ? bgLeftChannel[bgFrame] : 0;
+        float rightChannel = (bgFrame < bgBuffer.frameLength) ? bgRightChannel[bgFrame] : 0;
+        
+        int recordFrame = (offsetTime < 0) ? (bgFrame - offsetFrame) : (bgFrame + offsetFrame);
+        
+        float recordData = (recordFrame >= 0 && recordFrame < recordBuffer.frameLength) ? recordLeftChannel[recordFrame] : 0;
+        
+        
+        float mixLeftData = [self mixChannelData:leftChannel bgVolume:bgBufferVolume recordData:recordData recordVolume:recordVolume];
+        float mixRightData = [self mixChannelData:rightChannel bgVolume:bgBufferVolume recordData:recordData recordVolume:recordVolume];
+        
+        mixLeftChannel[frame] = MAX(-1.0, MIN(1.0, mixLeftData));
+        mixRightChannel[frame] = MAX(-1.0, MIN(1.0, mixRightData));
+    }
+    NSLog(@"---------finish");
+    
+}
+
+- (float)mixChannelData:(float)bgData bgVolume:(float)bgVolume recordData:(float)recordData recordVolume:(float)recordVolume {
+    return (bgData * bgVolume + recordData * recordVolume) / 2;
+}
+
+- (void)changeVolume:(float)bgVolume recordVolume:(float)recordVolume {
+    
+    NSLog(@"bg volume ---- %f,  record volume ---- %f", bgVolume, recordVolume);
+    self.bgVolume = bgVolume;
+    self.recordVolume = recordVolume;
+    if (self.bgBuffer && self.recordBuffer) {
+        self.stopChangeVolume = YES;
+        // 停止上一次修改音量
+        dispatch_async(self.sourceQueue, ^{
+            // 等待上一次的操作完成
+            dispatch_semaphore_wait(self.volumeChangeSemaphore, DISPATCH_TIME_FOREVER);
+            self.stopChangeVolume = NO;
+            // 开始新的音量修改操作
+            AVAudioFramePosition startFrame = self.currentFrame;
+            NSLog(@"----- current frame -----%lld", startFrame);
+            [self modifyMixBuffer:self.bgBuffer bgBufferVolume:bgVolume withRecordBuffer:self.recordBuffer recordVolume:recordVolume offset:self.offsetTime startPosition:startFrame tagIndex:0];
+            // 释放信号量,标记音量修改操作完成
+            dispatch_semaphore_signal(self.volumeChangeSemaphore);
+        });
+    }
+}
+
+
+- (void)modifyMixBuffer:(AVAudioPCMBuffer *)bgBuffer bgBufferVolume:(float)bgBufferVolume withRecordBuffer:(AVAudioPCMBuffer *)recordBuffer recordVolume:(float)recordVolume offset:(NSInteger)offsetTime startPosition:(AVAudioFramePosition)startFrame tagIndex:(NSInteger)tagIndex {
+    
+    AVAudioFrameCount minFrameCount = MIN(bgBuffer.frameLength, recordBuffer.frameLength);
+    AVAudioFrameCount offsetFrame = labs(offsetTime)/1000.0 * recordBuffer.format.sampleRate;
+    
+    float *bgLeftChannel = bgBuffer.floatChannelData[0];
+    float *bgRightChannel = bgBuffer.floatChannelData[1];
+    // 录音文件未单声道
+    float *recordLeftChannel = recordBuffer.floatChannelData[0];
+    
+    float *mixLeftChannel = self.mixBuffer.floatChannelData[0];
+    float *mixRightChannel = self.mixBuffer.floatChannelData[1];
+    
+    // 先处理后续播放的buffer
+    NSLog(@"------- volume change start");
+    for (int frame = (int)startFrame; frame < minFrameCount; frame++) {
+        if (self.stopChangeVolume) {
+            NSLog(@"------- stop volume change");
+            dispatch_semaphore_signal(self.volumeChangeSemaphore); // 释放信号量
+            return;
+        }
+        
+        float leftChannel = bgLeftChannel[frame];
+        float rightChannel = bgRightChannel[frame];
+        
+        int recordFrame = (offsetTime < 0) ? (frame - offsetFrame) : (frame + offsetFrame);
+        float recordData = (recordFrame >= 0 && recordFrame < recordBuffer.frameLength) ? recordLeftChannel[recordFrame] : 0;
+        
+        float mixLeftData = [self mixChannelData:leftChannel bgVolume:bgBufferVolume recordData:recordData recordVolume:recordVolume];
+        float mixRightData = [self mixChannelData:rightChannel bgVolume:bgBufferVolume recordData:recordData recordVolume:recordVolume];
+        
+        mixLeftChannel[frame-self.startPosition] = MAX(-1.0, MIN(1.0, mixLeftData));
+        mixRightChannel[frame-self.startPosition] = MAX(-1.0, MIN(1.0, mixRightData));
+    }
+    NSLog(@"------- volume change end");
+}
+
+- (void)scheduleBufferFromPosition:(AVAudioFramePosition)startPosition {
+    
+    self.stopMix = YES;
+    self.startPosition = startPosition;
+    dispatch_async(self.sourceQueue, ^{
+        // 等待上一次的操作完成
+        dispatch_semaphore_wait(self.mixChangeSemaphore, DISPATCH_TIME_FOREVER);
+        self.stopMix = NO;
+        
+        [self mixBuffers:self.bgBuffer bgBufferVolume:self.bgVolume withRecordBuffer:self.recordBuffer recordVolume:self.recordVolume offset:self.offsetTime startPosition:(AVAudioFrameCount)startPosition];
+        // 释放信号量,标记修改操作完成
+        dispatch_semaphore_signal(self.mixChangeSemaphore);
+        // 加载缓冲区
+        [self.nodePlayer scheduleBuffer:self.mixBuffer atTime:nil options:AVAudioPlayerNodeBufferInterruptsAtLoop completionHandler:^{
+            
+        }];
+    });
+}
+
+
+
+// 打断处理
+- (void)handleInterruption:(NSNotification *)notification {
+    NSDictionary *info = notification.userInfo;
+    
+    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
+    if (type == AVAudioSessionInterruptionTypeBegan) {
+        //Handle InterruptionBegan
+        if (self.delegate && [self.delegate respondsToSelector:@selector(enginePlayerDidError:error:)]) {
+            NSError *error = [[NSError alloc] initWithDomain:NSCocoaErrorDomain code:99999 userInfo:@{@"errorDesc" : @"播放被打断"}];
+            [self.delegate enginePlayerDidError:self error:error];
+        }
+    }
+    else if (type == AVAudioSessionInterruptionTypeEnded) {
+        AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
+        if (options == AVAudioSessionInterruptionOptionShouldResume) {
+            //Handle Resume
+            [[AVAudioSession sharedInstance] setActive:YES error:nil];
+        }
+    }
+}
+
+- (void)handleRouteChange:(NSNotification *)notification {
+    
+    NSDictionary *info = notification.userInfo;
+    if ([notification.name isEqualToString:AVAudioSessionRouteChangeNotification]) {
+        AVAudioSessionRouteChangeReason routeChangeReason = [[info valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
+        if (routeChangeReason == AVAudioSessionRouteChangeReasonCategoryChange) {
+            return;
+        }
+        if (self.delegate && [self.delegate respondsToSelector:@selector(enginePlayerDidError:error:)]) {
+            NSError *error = [[NSError alloc] initWithDomain:NSCocoaErrorDomain code:99999 userInfo:@{@"errorDesc" : @"播放被打断"}];
+            [self.delegate enginePlayerDidError:self error:error];
+        }
+    }
+}
+
+
+#pragma mark ------ play action
+
+- (void)changeRecordDelay:(NSInteger)delayMs {
+    self.offsetTime = delayMs;
+}
+
+- (void)seekToTimePlay:(NSInteger)time {
+    if (self.audioEngine.isRunning == NO) {
+        [self startEngine];
+    }
+    if (self.audioEngine.isRunning) {
+        [self seekAudioWithStartTime:time needPlay:YES];
+    }
+}
+
+- (void)stopPlay {
+    self.stopMix = YES;
+    self.stopChangeVolume = YES;
+    if (self.isPlaying) {
+        [self.nodePlayer stop];
+    }
+    self.isPlaying = NO;
+    [self stopTimer];
+}
+
+- (void)seekToTime:(NSInteger)time {
+    if (self.audioEngine.isRunning == NO) {
+        [self startEngine];
+    }
+    if (self.audioEngine.isRunning) {
+        [self seekAudioWithStartTime:time needPlay:NO];
+    }
+}
+
+- (void)seekAudioWithStartTime:(NSTimeInterval)startTime needPlay:(BOOL)needPlay {
+    if (self.audioEngine.isRunning == NO) {
+        [self startEngine];
+    }
+    if (self.audioEngine.isRunning) {
+        if (self.nodePlayer.isPlaying) {
+            [self.nodePlayer stop];
+        }
+    }
+    
+    // 停止修改音量循环
+    self.stopChangeVolume = YES;
+    
+    AVAudioFramePosition startFrame = startTime / 1000.0 * self.audioFormat.sampleRate;
+    // 跳转进度
+    self.currentFrame = startFrame;
+    [self scheduleBufferFromPosition:startFrame];
+
+    if (needPlay) {
+        [self.nodePlayer play];
+        self.isPlaying = YES;
+        [self startTimer];
+    }
+}
+
+// 调整偏移
+- (void)seekOffsetTime:(NSInteger)offsetTime {
+    self.offsetTime = offsetTime;
+    NSTimeInterval currentTime = [self getCurrentPlayTime];
+    [self seekToTimePlay:currentTime];
+}
+
+- (void)freePlayer {
+    
+    if (self.isPlaying) {
+        [self stopPlay];
+    }
+    [self.audioEngine stop];
+}
+
+- (void)startTimer {
+    
+    [self.timer setFireDate:[NSDate distantPast]];
+}
+
+- (void)stopTimer {
+    
+    [self.timer setFireDate:[NSDate distantFuture]];//暂停计时器
+}
+
+#pragma mark ---- lazying
+- (dispatch_queue_t)sourceQueue {
+    if (!_sourceQueue) {
+        _sourceQueue = dispatch_queue_create("ks_MutilSourceQueue", DISPATCH_QUEUE_SERIAL);
+    }
+    return _sourceQueue;
+}
+
+- (NSTimer *)timer {
+    
+    if (!_timer) {
+        __weak typeof(self)weakSelf = self;
+        _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
+            [weakSelf timeFunction];
+        }];
+        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
+        [_timer setFireDate:[NSDate distantFuture]];
+    }
+    return _timer;
+}
+
+- (void)timeFunction {
+    self.totalDuration = [self getTotalTime];
+    NSTimeInterval currentTime = [self getCurrentPlayTime];
+    float progress = currentTime/self.totalDuration;
+    NSDate *date = [NSDate date];
+    NSTimeInterval inteveral = [date timeIntervalSince1970];
+    if (currentTime > self.totalDuration) {
+        if (self.delegate && [self.delegate respondsToSelector:@selector(enginePlayFinished:)]) {
+            [self.delegate enginePlayFinished:self];
+        }
+    }
+    else {
+        if (self.delegate && [self.delegate respondsToSelector:@selector(updatePlayProgress:andTotalTime:andProgress:currentInterval:inPlayer:)]) {
+            [self.delegate updatePlayProgress:currentTime andTotalTime:self.totalDuration andProgress:progress currentInterval:inteveral*1000 inPlayer:self];
+        }
+    }
+    
+}
+
+
+- (NSTimeInterval)getCurrentPlayTime {
+    AVAudioTime *nodeTime = [self.nodePlayer lastRenderTime];
+    if (nodeTime && self.bgAudioFile) {
+        AVAudioTime *playerTime = [self.nodePlayer playerTimeForNodeTime:nodeTime];
+        AVAudioFramePosition currentFrame = self.currentFrame;
+        
+        if (playerTime) {
+            self.sampleRate = [playerTime sampleRate];
+            AVAudioFramePosition currentFrame = [playerTime sampleTime];
+            if (currentFrame <= 0) {
+                currentFrame = 0;
+            }
+            currentFrame += self.startPosition;
+            self.currentFrame = currentFrame;
+        }
+        else {
+            NSLog(@"播放已停止");
+        }
+        double elapsedSamples = (double)currentFrame;
+
+        NSTimeInterval currentTime = elapsedSamples / self.sampleRate;
+//        NSLog(@"当前时间----- %f",currentTime*1000);
+        return currentTime*1000;
+    }
+    else {
+        return 0;
+    }
+}
+
+- (NSTimeInterval)getTotalTime {
+    NSTimeInterval recordTotalDuration = (AVAudioFramePosition)self.audioFile.length * 1000.0 / self.audioFormat.sampleRate;
+    return recordTotalDuration;
+}
+@end

BIN
KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/CloudAccompanyLibrary


+ 56 - 0
KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/Headers/KSAudioEnginePlayer.h

@@ -0,0 +1,56 @@
+//
+//  KSAudioEnginePlayer.h
+//  KulexiuForStudent
+//
+//  Created by 王智 on 2024/5/22.
+//
+
+#import <Foundation/Foundation.h>
+
+@class KSAudioEnginePlayer;
+
+@protocol KSAudioEnginePlayerDelegate <NSObject>
+
+- (void)updatePlayProgress:(NSInteger)playTime andTotalTime:(NSInteger)totalTime andProgress:(CGFloat)progress currentInterval:(NSTimeInterval)currentInterval inPlayer:(KSAudioEnginePlayer *_Nonnull)player;
+
+@optional
+
+- (void)enginePlayFinished:(KSAudioEnginePlayer *_Nonnull)player;
+
+- (void)enginePlayerIsReadyPlay:(KSAudioEnginePlayer *_Nonnull)player;
+
+- (void)enginePlayerDidError:(KSAudioEnginePlayer *_Nonnull)player error:(NSError *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KSAudioEnginePlayer : NSObject
+
+@property (nonatomic, weak) id <KSAudioEnginePlayerDelegate>delegate;
+
+@property (nonatomic, assign) float rate; // 播放速度
+
+@property (nonatomic, assign) BOOL isReady;
+
+@property (nonatomic, assign) BOOL isPlaying;
+
+@property (nonatomic, assign) BOOL isMute; // 是否禁音
+
+- (void)prepareNativeSongWithUrl:(NSURL *)nativeMusicUrl;
+
+- (void)startPlay;
+
+- (void)stopPlay;
+
+- (void)freePlayer;
+
+// 从某个位置开始播放 ms
+- (void)seekToTimePlay:(NSInteger)time;
+
+- (NSTimeInterval)getCurrentPlayTime;
+
+@end
+
+
+NS_ASSUME_NONNULL_END

BIN
KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeDirectory


BIN
KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeRequirements-1


+ 15 - 0
KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeResources

@@ -48,6 +48,10 @@
 		<data>
 		eLki/9nclvBB2jEfWoAC8RTKPmo=
 		</data>
+		<key>Headers/KSAudioEnginePlayer.h</key>
+		<data>
+		vUx1NStzPE9YSx4NiREJR7hOrcU=
+		</data>
 		<key>Headers/KSCloudBeatView.h</key>
 		<data>
 		FqYnDhW4vpMweCzjgV4ICFCUiUM=
@@ -192,6 +196,17 @@
 			1p5UJ4lux8DGU+WmeiiCX9v+XDWkHzGH5ishaDGmJrU=
 			</data>
 		</dict>
+		<key>Headers/KSAudioEnginePlayer.h</key>
+		<dict>
+			<key>hash</key>
+			<data>
+			vUx1NStzPE9YSx4NiREJR7hOrcU=
+			</data>
+			<key>hash2</key>
+			<data>
+			4XHDsmMi3w1nRxJyc2N9JOx9K4kw+TQuxHiDKMhw6Ng=
+			</data>
+		</dict>
 		<key>Headers/KSCloudBeatView.h</key>
 		<dict>
 			<key>hash</key>

BIN
KulexiuForStudent/KulexiuForStudent/ToolKit/CloudAccompanyLibrary.framework/_CodeSignature/CodeSignature


+ 18 - 2
KulexiuForStudent/KulexiuForStudent/ToolKit/KSToolLibrary.framework/Headers/KSMediaEditor.h

@@ -7,6 +7,8 @@
 
 #import <Foundation/Foundation.h>
 
+// 合成时,伴奏固定,移动演奏声音
+
 NS_ASSUME_NONNULL_BEGIN
 
 @interface KSMediaEditor : NSObject
@@ -18,12 +20,14 @@ NS_ASSUME_NONNULL_BEGIN
 ///   - recordVolume: 录制文件音量 0-1
 ///   - bgAudioUrl: 背景音
 ///   - bgAudioVolume: 背景音乐音量 0-1
-///   - offsetTime: 偏移时间 (录制声音和伴奏声音的偏移  + 伴奏提前 - 伴奏延迟 ms )
+///   - rate: 伴奏速度
+///   - offsetTime: 调整演奏时间 (需要调整录制声音偏移  + 演奏延迟ms  - 演奏提前ms )
 ///   - completionHandle: 完成回调
 + (void)mixRecordAudio:(NSURL *)recordUrl
           recordVolume:(float)recordVolume
                bgAudio:(NSURL *)bgAudioUrl
          bgAudioVolume:(float)bgAudioVolume
+                  rate:(float)rate
             offsetTime:(NSInteger)offsetTime
             completion:(void (^)(NSString *outPath,BOOL isSuccess, NSString *desc))completionHandle;
 
@@ -34,7 +38,8 @@ NS_ASSUME_NONNULL_BEGIN
 ///   - recordVolume: 录制文件音量0-1
 ///   - bgAudioUrl: 背景音
 ///   - bgAudioVolume: 背景音乐音量0-1
-///   - offsetTime: 偏移时间 (录制声音和伴奏声音的偏移  + 伴奏提前 - 伴奏延迟 ms )
+///   - offsetTime: 调整演奏时间 (需要调整录制声音偏移  + 演奏延迟ms  - 演奏提前ms )
+///   - rate: 伴奏速度
 ///   - videoUrl: 视频地址
 ///   - completionHandle: 完成回调
 + (void)mixVideoWithRecordAudio:(NSURL *)recordUrl
@@ -42,6 +47,7 @@ NS_ASSUME_NONNULL_BEGIN
                         bgAudio:(NSURL *)bgAudioUrl
                   bgAudioVolume:(float)bgAudioVolume
                      offsetTime:(NSInteger)offsetTime
+                           rate:(float)rate
                     videoUrlStr:(NSURL *)videoUrl
                      completion:(void (^)(NSString *outPath,BOOL isSuccess, NSString *desc))completionHandle;
 
@@ -59,6 +65,16 @@ NS_ASSUME_NONNULL_BEGIN
                    recordVolume:(float)recordVolume
                     videoUrlStr:(NSURL *)videoUrl
                      completion:(void (^)(NSString *outPath,BOOL isSuccess, NSString *desc))completionHandle;
+
+
+/// 修改音频文件速度
+/// - Parameters:
+///   - musicUrl: 音频文件
+///   - rate: 速度
+///   - completionHandle: 完成回调
++ (void)modifyAudioFileSpeed:(NSURL *)musicUrl
+                        rate:(float)rate
+                  completion:(void (^)(NSString *outPath,BOOL isSuccess, NSString *desc))completionHandle;
 @end
 
 NS_ASSUME_NONNULL_END

+ 2 - 0
KulexiuForStudent/KulexiuForStudent/ToolKit/KSToolLibrary.framework/Headers/UIViewController+KSExtension.h

@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
 
 // 是否横屏
 @property (nonatomic, assign) BOOL ks_landScape;
+// 是否返回根视图
+@property (nonatomic, assign) BOOL is_poppingToRoot;
 
 @end
 

BIN
KulexiuForStudent/KulexiuForStudent/ToolKit/KSToolLibrary.framework/KSToolLibrary