Browse Source

接入整合云教练

Steven 6 months ago
parent
commit
7c7f2dbb4f
60 changed files with 2676 additions and 200 deletions
  1. 56 0
      KulexiuForTeacher/KulexiuForTeacher.xcodeproj/project.pbxproj
  2. 6 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/Contents.json
  3. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_delay.imageset/Contents.json
  4. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_delay.imageset/merge_guide_delay@2x.png
  5. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_delay.imageset/merge_guide_delay@3x.png
  6. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_finishBg.imageset/Contents.json
  7. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_finishBg.imageset/merge_guide_finishBg@2x.png
  8. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_finishBg.imageset/merge_guide_finishBg@3x.png
  9. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_forward.imageset/Contents.json
  10. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_forward.imageset/merge_guide_forward@2x.png
  11. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_forward.imageset/merge_guide_forward@3x.png
  12. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_nextBg.imageset/Contents.json
  13. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_nextBg.imageset/merge_guide_nextBg@2x.png
  14. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_nextBg.imageset/merge_guide_nextBg@3x.png
  15. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_offset.imageset/Contents.json
  16. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_offset.imageset/merge_guide_offset@2x.png
  17. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_offset.imageset/merge_guide_offset@3x.png
  18. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_publish.imageset/Contents.json
  19. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_publish.imageset/merge_guide_publish@2x.png
  20. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_publish.imageset/merge_guide_publish@3x.png
  21. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retry.imageset/Contents.json
  22. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retry.imageset/merge_guide_retry@2x.png
  23. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retry.imageset/merge_guide_retry@3x.png
  24. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retryBg.imageset/Contents.json
  25. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retryBg.imageset/merge_guide_retryBg@2x.png
  26. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retryBg.imageset/merge_guide_retryBg@3x.png
  27. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_saveDraft.imageset/Contents.json
  28. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_saveDraft.imageset/merge_guide_saveDraft@2x.png
  29. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_saveDraft.imageset/merge_guide_saveDraft@3x.png
  30. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_skip.imageset/Contents.json
  31. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_skip.imageset/merge_guide_skip@2x.png
  32. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_skip.imageset/merge_guide_skip@3x.png
  33. 22 0
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_volume.imageset/Contents.json
  34. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_volume.imageset/merge_guide_volume@2x.png
  35. BIN
      KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_volume.imageset/merge_guide_volume@3x.png
  36. 11 1
      KulexiuForTeacher/KulexiuForTeacher/Common/Base/AccompanyWebView/KSCloudWebManager.m
  37. 10 0
      KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSNetworkingManager.h
  38. 16 0
      KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSNetworkingManager.m
  39. 1 1
      KulexiuForTeacher/KulexiuForTeacher/Common/Define/KSDomain.h
  40. 546 106
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/KSMediaMergeView.m
  41. 14 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.h
  42. 95 35
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.xib
  43. 6 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeAudioAnimation/KSRealtimeAnalyzer.m
  44. 2 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeAudioAnimation/KSSpectrumView.h
  45. 41 34
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeAudioAnimation/KSSpectrumView.m
  46. 36 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideManager.h
  47. 125 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideManager.m
  48. 28 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideModel.h
  49. 12 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideModel.m
  50. 34 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideView.h
  51. 249 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideView.m
  52. 33 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeMusicStaffView.h
  53. 276 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeMusicStaffView.m
  54. 4 1
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.h
  55. 74 22
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.m
  56. 26 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeTipsAlert.h
  57. 102 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeTipsAlert.m
  58. 127 0
      KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeTipsAlert.xib
  59. 82 0
      KulexiuForTeacher/KulexiuForTeacher/Common/ThirdPart/CBAutoScrollLabel/CBAutoScrollLabel.h
  60. 422 0
      KulexiuForTeacher/KulexiuForTeacher/Common/ThirdPart/CBAutoScrollLabel/CBAutoScrollLabel.m

+ 56 - 0
KulexiuForTeacher/KulexiuForTeacher.xcodeproj/project.pbxproj

@@ -709,6 +709,13 @@
 		BC74010D2CD203B80056756A /* KSCloudPremissionAlertView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC7401062CD203B80056756A /* KSCloudPremissionAlertView.xib */; };
 		BC74010E2CD203B80056756A /* delayCheck_musicAni.json in Resources */ = {isa = PBXBuildFile; fileRef = BC7400FB2CD203B80056756A /* delayCheck_musicAni.json */; };
 		BC74010F2CD203B80056756A /* DelayCheckTipsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC7400FF2CD203B80056756A /* DelayCheckTipsView.xib */; };
+		BC7401122CD20E8C0056756A /* MergeMusicStaffView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC7401112CD20E8C0056756A /* MergeMusicStaffView.m */; };
+		BC7401302CD264560056756A /* KSBaseGuideManager.m in Sources */ = {isa = PBXBuildFile; fileRef = BC74012A2CD264560056756A /* KSBaseGuideManager.m */; };
+		BC7401312CD264560056756A /* KSBaseGuideView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC74012E2CD264560056756A /* KSBaseGuideView.m */; };
+		BC7401322CD264560056756A /* KSBaseGuideModel.m in Sources */ = {isa = PBXBuildFile; fileRef = BC74012C2CD264560056756A /* KSBaseGuideModel.m */; };
+		BC7401362CD264690056756A /* MergeTipsAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = BC7401342CD264690056756A /* MergeTipsAlert.m */; };
+		BC7401372CD264690056756A /* MergeTipsAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC7401352CD264690056756A /* MergeTipsAlert.xib */; };
+		BC74013B2CD265340056756A /* CBAutoScrollLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = BC7401392CD265340056756A /* CBAutoScrollLabel.m */; };
 		BC76146A280D4F670080FD1F /* HomeworkDetailModel.m in Sources */ = {isa = PBXBuildFile; fileRef = BC761468280D4F660080FD1F /* HomeworkDetailModel.m */; };
 		BC76146D280D571B0080FD1F /* HomeworkVideoView.m in Sources */ = {isa = PBXBuildFile; fileRef = BC76146C280D571B0080FD1F /* HomeworkVideoView.m */; };
 		BC76146F280D57220080FD1F /* HomeworkVideoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BC76146E280D57220080FD1F /* HomeworkVideoView.xib */; };
@@ -2322,6 +2329,19 @@
 		BC7401042CD203B80056756A /* KSCloudPremissionAlertView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSCloudPremissionAlertView.h; sourceTree = "<group>"; };
 		BC7401052CD203B80056756A /* KSCloudPremissionAlertView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSCloudPremissionAlertView.m; sourceTree = "<group>"; };
 		BC7401062CD203B80056756A /* KSCloudPremissionAlertView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = KSCloudPremissionAlertView.xib; sourceTree = "<group>"; };
+		BC7401102CD20E8C0056756A /* MergeMusicStaffView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MergeMusicStaffView.h; sourceTree = "<group>"; };
+		BC7401112CD20E8C0056756A /* MergeMusicStaffView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MergeMusicStaffView.m; sourceTree = "<group>"; };
+		BC7401292CD264560056756A /* KSBaseGuideManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSBaseGuideManager.h; sourceTree = "<group>"; };
+		BC74012A2CD264560056756A /* KSBaseGuideManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSBaseGuideManager.m; sourceTree = "<group>"; };
+		BC74012B2CD264560056756A /* KSBaseGuideModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSBaseGuideModel.h; sourceTree = "<group>"; };
+		BC74012C2CD264560056756A /* KSBaseGuideModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSBaseGuideModel.m; sourceTree = "<group>"; };
+		BC74012D2CD264560056756A /* KSBaseGuideView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KSBaseGuideView.h; sourceTree = "<group>"; };
+		BC74012E2CD264560056756A /* KSBaseGuideView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSBaseGuideView.m; sourceTree = "<group>"; };
+		BC7401332CD264690056756A /* MergeTipsAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MergeTipsAlert.h; sourceTree = "<group>"; };
+		BC7401342CD264690056756A /* MergeTipsAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MergeTipsAlert.m; sourceTree = "<group>"; };
+		BC7401352CD264690056756A /* MergeTipsAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MergeTipsAlert.xib; sourceTree = "<group>"; };
+		BC7401382CD265340056756A /* CBAutoScrollLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CBAutoScrollLabel.h; sourceTree = "<group>"; };
+		BC7401392CD265340056756A /* CBAutoScrollLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CBAutoScrollLabel.m; sourceTree = "<group>"; };
 		BC761468280D4F660080FD1F /* HomeworkDetailModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeworkDetailModel.m; sourceTree = "<group>"; };
 		BC761469280D4F660080FD1F /* HomeworkDetailModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeworkDetailModel.h; sourceTree = "<group>"; };
 		BC76146B280D571B0080FD1F /* HomeworkVideoView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HomeworkVideoView.h; sourceTree = "<group>"; };
@@ -3378,6 +3398,7 @@
 		2779309B27E30F770010E277 /* ThirdPart */ = {
 			isa = PBXGroup;
 			children = (
+				BC74013A2CD265340056756A /* CBAutoScrollLabel */,
 				BC8B6E222856ED0600866917 /* UMSocialSDK */,
 				2779324327E30FD70010E277 /* FSCalendar */,
 			);
@@ -5091,6 +5112,7 @@
 		BC38C4002AF900E100ABFCC2 /* AudioMerge */ = {
 			isa = PBXGroup;
 			children = (
+				BC74012F2CD264560056756A /* MergeGuide */,
 				BC85A9D22C6B4D4A003C1ABE /* MergeAudioAnimation */,
 				BCED0AE52C463E8D00369AED /* MergePlayer */,
 				BC38C4102AF900E100ABFCC2 /* AudioPlayAnimationView */,
@@ -5099,6 +5121,11 @@
 				BC38C40F2AF900E100ABFCC2 /* KSAudioAnimationView.xib */,
 				BC38C41A2AF900E100ABFCC2 /* KSMediaMergeView.h */,
 				BC38C4072AF900E100ABFCC2 /* KSMediaMergeView.m */,
+				BC7401332CD264690056756A /* MergeTipsAlert.h */,
+				BC7401342CD264690056756A /* MergeTipsAlert.m */,
+				BC7401352CD264690056756A /* MergeTipsAlert.xib */,
+				BC7401102CD20E8C0056756A /* MergeMusicStaffView.h */,
+				BC7401112CD20E8C0056756A /* MergeMusicStaffView.m */,
 				BC3BF6422B9FED9900831494 /* ShareFunctionView.h */,
 				BC3BF6442B9FED9B00831494 /* ShareFunctionView.m */,
 				BC3BF6432B9FED9A00831494 /* ShareFunctionView.xib */,
@@ -6040,6 +6067,28 @@
 			path = KSCloudPremissionAlert;
 			sourceTree = "<group>";
 		};
+		BC74012F2CD264560056756A /* MergeGuide */ = {
+			isa = PBXGroup;
+			children = (
+				BC7401292CD264560056756A /* KSBaseGuideManager.h */,
+				BC74012A2CD264560056756A /* KSBaseGuideManager.m */,
+				BC74012B2CD264560056756A /* KSBaseGuideModel.h */,
+				BC74012C2CD264560056756A /* KSBaseGuideModel.m */,
+				BC74012D2CD264560056756A /* KSBaseGuideView.h */,
+				BC74012E2CD264560056756A /* KSBaseGuideView.m */,
+			);
+			path = MergeGuide;
+			sourceTree = "<group>";
+		};
+		BC74013A2CD265340056756A /* CBAutoScrollLabel */ = {
+			isa = PBXGroup;
+			children = (
+				BC7401382CD265340056756A /* CBAutoScrollLabel.h */,
+				BC7401392CD265340056756A /* CBAutoScrollLabel.m */,
+			);
+			path = CBAutoScrollLabel;
+			sourceTree = "<group>";
+		};
 		BC7CFF992817CBC400CAEB21 /* WithDraw */ = {
 			isa = PBXGroup;
 			children = (
@@ -7474,6 +7523,7 @@
 				BC74010E2CD203B80056756A /* delayCheck_musicAni.json in Resources */,
 				BC74010F2CD203B80056756A /* DelayCheckTipsView.xib in Resources */,
 				BCE6A09127F823BE00C97704 /* LiveCourseCell.xib in Resources */,
+				BC7401372CD264690056756A /* MergeTipsAlert.xib in Resources */,
 				BC3673D828A606A500059721 /* accomapny_animation_1.png in Resources */,
 				275B172B27EB269F0081FDEF /* ChatAddressHeaderView.xib in Resources */,
 				BC41104F280678ED00800BD9 /* HomeworkSortView.xib in Resources */,
@@ -8305,6 +8355,7 @@
 				BC4218002C4E439B00C70B2F /* KSUserDetailNavView.m in Sources */,
 				BCDF82292A8A3080005F8B82 /* ZoomControl.m in Sources */,
 				277931CB27E30FC20010E277 /* KSGifRefreshHeader.m in Sources */,
+				BC7401122CD20E8C0056756A /* MergeMusicStaffView.m in Sources */,
 				2755C07E27EC95CC007D9070 /* GroupNoticeViewController.m in Sources */,
 				BCC305F828FD4C0800C39762 /* KSChatUserDetailViewController.m in Sources */,
 				BC0D951D2AC2868400E54D3F /* KSWebLoadRefreshView.m in Sources */,
@@ -8386,6 +8437,9 @@
 				BC2456E3286BE85A00D1F7C0 /* MineStyleEmptyView.m in Sources */,
 				277D432F27E9A50800107DB7 /* PhoneChangeBodyView.m in Sources */,
 				BC71DE8F2A89C937003F165E /* TXRTCService.m in Sources */,
+				BC7401302CD264560056756A /* KSBaseGuideManager.m in Sources */,
+				BC7401312CD264560056756A /* KSBaseGuideView.m in Sources */,
+				BC7401322CD264560056756A /* KSBaseGuideModel.m in Sources */,
 				BC106B792A8F4586000759A9 /* TXLiveMessageRejectAllSeat.m in Sources */,
 				BC71DF212A89FABD003F165E /* TxRTCRoomConfig.m in Sources */,
 				BC1553482AB31EEC00C1C347 /* TenangGroupCreateBottomView.m in Sources */,
@@ -8472,6 +8526,7 @@
 				BC1553572AB336A200C1C347 /* TenantStuModel.m in Sources */,
 				BC28582F2809451B0024697C /* EvaluateCouseCell.m in Sources */,
 				BCC9F43027F69BD200647449 /* HTTPResult.m in Sources */,
+				BC7401362CD264690056756A /* MergeTipsAlert.m in Sources */,
 				BCA723FF2806AEA000DA0D0D /* AccompanyHomeworkCell.m in Sources */,
 				277931E527E30FC20010E277 /* NSMutableAttributedString+CZHExtention.m in Sources */,
 				BCF61BE42804248F0000ACFE /* InstrumentChooseViewController.m in Sources */,
@@ -8498,6 +8553,7 @@
 				27A2F62D27E70D0A009E2380 /* UserInfoManager.m in Sources */,
 				BC3DE088280D89E90027DC0E /* EvaluateDetailViewController.m in Sources */,
 				BC023803286594EA005560CA /* KSTipsAlert.m in Sources */,
+				BC74013B2CD265340056756A /* CBAutoScrollLabel.m in Sources */,
 				BCF1BA5427F5CB5800FA36C4 /* LiveSeatApplyView.m in Sources */,
 				BCE6A0A427F8517900C97704 /* MineVideoCell.m in Sources */,
 				BC0A22C0284752900065C1AB /* WhiteboardListCell.m in Sources */,

+ 6 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_delay.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_delay@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_delay@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_delay.imageset/merge_guide_delay@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_delay.imageset/merge_guide_delay@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_finishBg.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_finishBg@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_finishBg@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_finishBg.imageset/merge_guide_finishBg@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_finishBg.imageset/merge_guide_finishBg@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_forward.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_forward@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_forward@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_forward.imageset/merge_guide_forward@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_forward.imageset/merge_guide_forward@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_nextBg.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_nextBg@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_nextBg@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_nextBg.imageset/merge_guide_nextBg@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_nextBg.imageset/merge_guide_nextBg@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_offset.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_offset@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_offset@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_offset.imageset/merge_guide_offset@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_offset.imageset/merge_guide_offset@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_publish.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_publish@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_publish@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_publish.imageset/merge_guide_publish@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_publish.imageset/merge_guide_publish@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retry.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_retry@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_retry@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retry.imageset/merge_guide_retry@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retry.imageset/merge_guide_retry@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retryBg.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_retryBg@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_retryBg@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retryBg.imageset/merge_guide_retryBg@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_retryBg.imageset/merge_guide_retryBg@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_saveDraft.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_saveDraft@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_saveDraft@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_saveDraft.imageset/merge_guide_saveDraft@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_saveDraft.imageset/merge_guide_saveDraft@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_skip.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_skip@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_skip@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_skip.imageset/merge_guide_skip@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_skip.imageset/merge_guide_skip@3x.png


+ 22 - 0
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_volume.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "merge_guide_volume@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "merge_guide_volume@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_volume.imageset/merge_guide_volume@2x.png


BIN
KulexiuForTeacher/KulexiuForTeacher/Assets.xcassets/MergeGuide/merge_guide_volume.imageset/merge_guide_volume@3x.png


+ 11 - 1
KulexiuForTeacher/KulexiuForTeacher/Common/Base/AccompanyWebView/KSCloudWebManager.m

@@ -70,7 +70,17 @@
 - (void)showWebView:(NSDictionary *)parm fromController:(CustomNavViewController *)navCtrl {
     KSCloudWebViewController *ctrl = [[KSCloudWebViewController alloc] init];
     ctrl.webViewDelegate = self;
-    ctrl.url = [parm ks_stringValueForKey:@"url"];
+#pragma mark ----- 拼接token
+    NSString *url = [parm ks_stringValueForKey:@"url"];
+    NSString *sepectString = [url containsString:@"?"] ? @"&" : @"?";
+    NSString *tokenStr = UserDefault(TokenKey);
+    if (![NSString isEmptyString:tokenStr]) {
+        NSString *token = [[NSString stringWithFormat:@"Authorization=%@ %@", UserDefault(Token_type), tokenStr] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
+        url = [NSString stringWithFormat:@"%@%@%@",url, sepectString, token];
+    }
+    ctrl.url = url;
+#pragma mark --------
+//    ctrl.url = [parm ks_stringValueForKey:@"url"];
     ctrl.parmDic = parm;
     NSInteger orientation = [parm ks_integerValueForKey:@"orientation"];
     BOOL isLandScape = orientation == 0 ? YES : NO;

+ 10 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSNetworkingManager.h

@@ -1456,6 +1456,16 @@ NS_ASSUME_NONNULL_BEGIN
 /// @param faliure 失败
 + (void)tenantGroupRequest:(NSString *)post success:(void(^)(NSDictionary *dic))success faliure:(void(^)(NSError *error))faliure;
 
+#pragma mark ------ 草稿
+// 测评记录查询详情
+// userMusic/musicPracticeRecord
+
+/// 根据测评记录查询作品相关信息
+/// @param get get
+/// @param recordId 测评记录ID
+/// @param success 成功
+/// @param faliure 失败
++ (void)musicPracticeRecordRequest:(NSString *)get recordId:(NSString *)recordId success:(void(^)(NSDictionary *dic))success faliure:(void(^)(NSError *error))faliure;
 // /userMusic/save
 
 /// 保存草稿、发布作品

+ 16 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/Base/KSNetworkingManager.m

@@ -2701,6 +2701,22 @@
 
 }
 
+#pragma mark ------ 草稿
+// 测评记录查询详情
+// userMusic/musicPracticeRecord
+
+/// 根据测评记录查询作品相关信息
+/// @param get get
+/// @param recordId 测评记录ID
+/// @param success 成功
+/// @param faliure 失败
++ (void)musicPracticeRecordRequest:(NSString *)get recordId:(NSString *)recordId success:(void(^)(NSDictionary *dic))success faliure:(void(^)(NSError *error))faliure {
+    
+    NSString *url = [NSString stringWithFormat:@"%@%@%@/%@", hostURL,@"/api-teacher", @"/userMusic/musicPracticeRecord",recordId];
+    NSMutableDictionary *parm = [NSMutableDictionary dictionary];
+    [self request:get andWithUrl:url and:parm success:success faliure:faliure];
+}
+
 // /userMusic/save
 
 /// 保存草稿、发布作品

+ 1 - 1
KulexiuForTeacher/KulexiuForTeacher/Common/Define/KSDomain.h

@@ -32,6 +32,6 @@
 #define OPEN_URL ([NSString stringWithFormat:@"https://%@",OPEN_DOMAIN])
 
 // 云教练
-#define CLOUD_URL ([NSString stringWithFormat:@"https://%@/%@",ACCOMPANY_DOMAIN, @"klx-music-score"])
+#define CLOUD_URL ([NSString stringWithFormat:@"https://%@%@",ACCOMPANY_DOMAIN, WEBPATH])
 
 #endif /* KSDomain_h */

+ 546 - 106
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/KSMediaMergeView.m

@@ -13,18 +13,30 @@
 #import "KSPlayerSliderView.h"
 #import "KSVideoPlayerView.h"
 #import "KSAudioPlayAnimationView.h"
-#import "KSNewAlertView.h"
+#import "MergeTipsAlert.h"
 #import "MusicPublistAlert.h"
 #import "KSMediaManager.h"
 #import <RSKImageCropper/RSKImageCropper.h>
 #import "KSVideoCropViewController.h"
 #import <KSAudioSessionManager.h>
-#import "ShareFunctionView.h"
 
+#import "ShareFunctionView.h"
 #import "KSUMShareManager.h"
 #import "KSLogManager.h"
 
 #import "KSMergeEnginePlayer.h"
+#import "MergeMusicStaffView.h"
+
+// 功能引导
+#import "KSBaseGuideManager.h"
+// 音波动画
+#import "KSSpectrumView.h"
+
+// 滚动label
+#import "CBAutoScrollLabel.h"
+#import "UIView+KSLayer.h"
+
+#define WEB_TOP (10)
 
 @interface KSMediaMergeView ()<KSMergeEnginePlayerDelegate,KSVideoPlayerViewDelegate,RSKImageCropViewControllerDelegate,RSKImageCropViewControllerDataSource>
 
@@ -62,8 +74,6 @@
 
 @property (nonatomic, strong) KSAudioPlayAnimationView *playAnimationView;
 
-@property (nonatomic, strong) NSTimer *timer;
-
 // 远端资源
 @property (nonatomic, strong) NSString *remoteVideoUrl;
 
@@ -84,7 +94,7 @@
 
 @property (nonatomic, strong) NSDictionary *preJsonDic;
 
-@property (nonatomic, strong) KSNewAlertView *alertView;
+@property (nonatomic, strong) MergeTipsAlert *alertView;
 
 @property (nonatomic, assign) BOOL hasSave;
 
@@ -108,12 +118,33 @@
 
 @property (nonatomic, assign) NSInteger defaultDelay;
 
+@property (nonatomic, assign) CGFloat bgPlayerRate;
+
+
 @property (nonatomic, assign) NSInteger evaluateDelay;
 
 @property (nonatomic, assign) BOOL fromDraftPage; // 是否从草稿页面进入
 
 @property (nonatomic, strong) KSUMShareManager *shareManager;
 
+@property (nonatomic, strong) UIView *webViewContainer; // 单行谱
+
+@property (nonatomic, strong) UIView *colorView; // 视频时渐变色
+
+@property (nonatomic, strong) MergeMusicStaffView *staffView;
+
+@property (nonatomic, assign) BOOL hasSaveDraft;
+
+@property (nonatomic, strong) KSBaseGuideManager *guideManager;
+
+@property (nonatomic, assign) BOOL needShowGuide;
+
+@property (nonatomic, strong) KSSpectrumView *spectrumView;
+
+@property (nonatomic, assign) CGFloat spectrumViewWidth;
+
+@property (nonatomic, assign) BOOL isCancel;
+
 @end
 
 @implementation KSMediaMergeView
@@ -125,7 +156,6 @@
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appEnterBackground) name:@"appEnterBackground" object:nil];
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherLogin) name:@"otherLogin" object:nil];
         [self configAudioSession];
-
     }
     return self;
 }
@@ -169,15 +199,69 @@
     self.originalOffset = 0;
     [self.contrlView configWithOffsetTime:0];
     self.contrlView.hideBackView = NO;
-    [self configUI];
-    [LOADING_MANAGER showCustomLoading:@"资源下载中"];
-    [self modifyBgSpeed:self.bgAudioUrl callback:^{
+    
+    [self prepareSource];
+}
+
+- (void)cancelTask {
+    [LOADING_MANAGER removeCustomLoading];
+    self.isCancel = YES;
+    if (self.mergeCallback) {
+        [self removeViewTips:NO];
+    }
+    else {
+        [self removeView];
+    }
+}
+
+- (void)prepareSource {
+    [LOADING_MANAGER showCancelCustomLoading:@"资源加载中,请稍等…" cancel:^{
+        [self cancelTask];
+    }];
+
+    [self requestSource];
+    [self checkGuide];
+    [self modifySpeed];
+    dispatch_group_notify(self.requestGroup, dispatch_get_main_queue(), ^{
+        [self configUI];
         [self configPlayer];
+    });
+}
+
+- (void)modifySpeed {
+    dispatch_group_enter(self.requestGroup);
+    [self modifyBgSpeed:self.bgAudioUrl callback:^{
+        dispatch_group_leave(self.requestGroup);
     }];
 }
 
-- (void)configRemoteVideoUrl:(NSString *)remoteVideoUrl bgAudioUrl:(NSString *)remoteBgAudioUrl recordUrl:(NSString *)remoteRecrodUrl jsonConfig:(NSString *)jsonConfig callback:(DraftEditCallback)callback {
+- (void)requestSource {
+    dispatch_group_enter(self.requestGroup);
+    [KSNetworkingManager musicPracticeRecordRequest:KS_GET recordId:self.recordId success:^(NSDictionary * _Nonnull dic) {
+        if (dic) {
+            NSDictionary *data = [dic ks_dictionaryValueForKey:@"data"];
+            if (data == nil) {
+                self.hasSaveDraft = NO;
+            }
+            else {
+                self.hasSaveDraft = YES;
+            }
+        }
+        dispatch_group_leave(self.requestGroup);
+    } faliure:^(NSError * _Nonnull error) {
+        dispatch_group_leave(self.requestGroup);
+    }];
+}
 
+- (void)checkGuide {
+    
+    if (UserDefaultBoolForKey(MERGE_GUIDEKEY) == NO) {
+        self.needShowGuide = YES;
+    }
+}
+
+- (void)configRemoteVideoUrl:(NSString *)remoteVideoUrl bgAudioUrl:(NSString *)remoteBgAudioUrl recordUrl:(NSString *)remoteRecrodUrl jsonConfig:(NSString *)jsonConfig callback:(DraftEditCallback)callback {
+    
     if (callback) {
         self.draftCallback = callback;
     }
@@ -207,15 +291,21 @@
     self.offsetTime = self.originalOffset;
     [self.contrlView configWithOffsetTime:self.originalOffset];
     self.contrlView.hideBackView = YES;
+    // 草稿列表表示已保存了
+    self.hasSaveDraft = YES;
     [self downloadFileSource];
 }
 
+
 - (void)downloadFileSource {
-    [LOADING_MANAGER showCustomLoading:@"资源下载中"];
+    [LOADING_MANAGER showCancelCustomLoading:@"资源加载中,请稍等…" cancel:^{
+        [self cancelTask];
+    }];
     
     [self downloadVideo];
     [self downloadAccompany];
     [self downloadAudio];
+    [self checkGuide];
     dispatch_group_notify(self.requestGroup, dispatch_get_main_queue(), ^{
         [self configUI];
         [self configPlayer];
@@ -228,6 +318,9 @@
         [KSNetworkingManager downloadFileRequestWithFileUrl:self.remoteVideoUrl progress:^(int64_t bytesRead, int64_t totalBytes) {
             
         } success:^(NSURL * _Nonnull fileUrl) {
+            if (self.isCancel) {
+                return;
+            }
             dispatch_group_leave(self.requestGroup);
             self.videoUrl = fileUrl;
         } faliure:^(NSError * _Nonnull error) {
@@ -258,6 +351,9 @@
         [KSNetworkingManager downloadFileRequestWithFileUrl:self.remoteRecrodUrl progress:^(int64_t bytesRead, int64_t totalBytes) {
             
         } success:^(NSURL * _Nonnull fileUrl) {
+            if (self.isCancel) {
+                return;
+            }
             dispatch_group_leave(self.requestGroup);
             self.recordUrl = fileUrl;
         } faliure:^(NSError * _Nonnull error) {
@@ -272,6 +368,9 @@
         [KSNetworkingManager downloadFileRequestWithFileUrl:self.remoteBgAudioUrl progress:^(int64_t bytesRead, int64_t totalBytes) {
             
         } success:^(NSURL * _Nonnull fileUrl) {
+            if (self.isCancel) {
+                return;
+            }
             self.bgAudioUrl = fileUrl;
             [self modifyBgSpeed:self.bgAudioUrl callback:^{
                 dispatch_group_leave(self.requestGroup);
@@ -283,13 +382,19 @@
         }];
     }
 }
-
 - (void)configPlayer {
     
+    if (self.isCancel) {
+        return;
+    }
+    BOOL needAnalyzer = YES;
     if (self.isVideoPlay) {
         [self.videoView preparePlayNativeVideoWithPath:self.videoUrl];
         self.videoView.isMute = YES;
+        needAnalyzer = NO;
     }
+    // 视频不需要分析频谱
+    self.mergePlayer.needAnalyzer = needAnalyzer;
     if (self.bgAudioUrl && self.recordUrl) {
         [self.mergePlayer prepareNativeSongWithUrl:self.recordUrl bgMusic:self.bgAudioUrl];;
     }
@@ -311,12 +416,46 @@
     [self.contrlView configRecordVolume:originalVolume bgVolume:accompanyVolume];
 }
 
+- (void)configAnalyzerDefault:(CGFloat)spectrumViewWidth {
+    if (_spectrumView) {
+        CGFloat barSpace = spectrumViewWidth / (CGFloat)(self.mergePlayer.analyzer.frequencyBands * 3 - 1);
+        self.spectrumView.barWidth = barSpace * 1.5;
+        self.spectrumView.space = barSpace * 1.5;
+    }
+}
+
 - (void)configUI {
+    CGFloat leftSpace = STATUS_GAP;
+    
+    if (self.isCancel) {
+        return;
+    }
+    
     self.backgroundColor = [UIColor whiteColor];
     if (self.isVideoPlay) {
         [self addSubview:self.videoView];
+        // 添加顶部阴影
+        UIView *headLayerView = [[UIView alloc] init];
+        [self addSubview:headLayerView];
+        [headLayerView mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.left.right.top.mas_equalTo(self);
+            make.height.mas_equalTo(70);
+        }];
+        CAGradientLayer *headLayer = [UIView createGradientLayerFromColor:HexRGBAlpha(0x000000, 0.7f) startPoint:CGPointMake(0.5, 0) endColor:HexRGBAlpha(0x000000, 0.0f) endPoint:CGPointMake(0.5, 1) bounds:CGRectMake(0, 0, KLandscapeWidth, 70)];
+        [headLayerView.layer addSublayer:headLayer];
+        // 添加底部阴影
+        UIView *bottomLayerView = [[UIView alloc] init];
+        [self addSubview:bottomLayerView];
+        [bottomLayerView mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.left.right.bottom.mas_equalTo(self);
+            make.height.mas_equalTo(150);
+        }];
+        
+        CAGradientLayer *bottomLayer = [UIView createGradientLayerFromColor:HexRGBAlpha(0x000000, 0.0f) startPoint:CGPointMake(0.5, 0) endColor:HexRGBAlpha(0x000000, 0.7f) endPoint:CGPointMake(0.5, 1) bounds:CGRectMake(0, 0, KLandscapeWidth, 150)];
+        [bottomLayerView.layer addSublayer:bottomLayer];
     }
     else {
+        // 动效
         [self addSubview:self.playerView];
     }
     
@@ -341,31 +480,36 @@
         UIImageView *shadowImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"merge_music_bg"]];
         [self.playerView addSubview:shadowImage];
         
+        // 音波动画
+        [self.playerView addSubview:self.spectrumView];
         
-        [self.playerView addSubview:self.playAnimationView];
-        [self.playAnimationView mas_makeConstraints:^(MASConstraintMaker *make) {
-            make.centerX.mas_equalTo(self.playerView.mas_centerX);
-            make.centerY.mas_equalTo(self.playerView.mas_centerY);
-            make.width.mas_equalTo(424);
-            make.height.mas_equalTo(80);
-        }];
+        CGFloat ratio = IS_IPAD ? 1.5 : 1.2;
         
         [self.playerView mas_makeConstraints:^(MASConstraintMaker *make) {
             make.left.top.bottom.mas_equalTo(self);
             make.right.mas_equalTo(self.contrlView.mas_left).offset(20);
         }];
         
+        
         [self.playerView addSubview:self.animationView];
         [self.animationView mas_makeConstraints:^(MASConstraintMaker *make) {
             make.centerX.mas_equalTo(self.playerView.mas_centerX);
-            make.centerY.mas_equalTo(self.playerView.mas_centerY);
-            make.width.height.mas_equalTo(200);
+            make.centerY.mas_equalTo(self.playerView.mas_centerY).offset(-20);
+            make.width.height.mas_equalTo(180);
         }];
+        
+        [self.spectrumView mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.centerX.mas_equalTo(self.animationView.mas_centerX);
+            make.centerY.mas_equalTo(self.animationView.mas_centerY);
+            make.width.mas_equalTo(320 * ratio);
+            make.height.mas_equalTo(100 * ratio);
+        }];
+        
         [shadowImage mas_makeConstraints:^(MASConstraintMaker *make) {
-            make.width.mas_equalTo(284);
-            make.height.mas_equalTo(151);
+            make.width.mas_equalTo(284*0.9);
+            make.height.mas_equalTo(151*0.9);
             make.centerX.mas_equalTo(self.playerView.mas_centerX);
-            make.top.mas_equalTo(self.animationView.mas_top).offset(86);
+            make.top.mas_equalTo(self.animationView.mas_top).offset(86*0.9);
         }];
         [self.animationView configWithImageWithUrl:self.coverImage];
         
@@ -373,35 +517,93 @@
     
     [self addSubview:self.playControlView];
     [self.playControlView mas_makeConstraints:^(MASConstraintMaker *make) {
-        make.left.mas_equalTo(self.mas_left);
+        make.left.mas_equalTo(self.mas_left).offset(leftSpace);
         make.right.mas_equalTo(self.contrlView.mas_left).offset(-20);
         make.height.mas_equalTo(44);
         make.bottom.mas_equalTo(self.mas_bottom).offset(-30);
     }];
     
     [self.playControlView configViewDisplayColor:self.isVideoPlay];
-    NSString *imgName = self.isVideoPlay ? @"back_button_white" : @"back_black";
+    NSString *imgName = self.isVideoPlay ? @"merge_back_white" : @"merge_back_black";
     UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
     [backButton setImage:[UIImage imageNamed:imgName] forState:UIControlStateNormal];
     [backButton addTarget:self action:@selector(backAction) forControlEvents:UIControlEventTouchUpInside];
     [self addSubview:backButton];
+    
     [backButton mas_makeConstraints:^(MASConstraintMaker *make) {
         make.width.height.mas_equalTo(44);
-        make.left.mas_equalTo(self.mas_left).offset(10);
-        make.top.mas_equalTo(self.mas_top).offset(10);
+        make.left.mas_equalTo(self.mas_left).offset(10.5);
+        make.top.mas_equalTo(self.mas_top).offset(10.5);
     }];
-        
-    UILabel *songLabel = [[UILabel alloc] init];
+    
+    CBAutoScrollLabel *songLabel = [[CBAutoScrollLabel alloc] init];
     songLabel.font = [UIFont systemFontOfSize:16.0f weight:UIFontWeightSemibold];
     songLabel.text = [NSString returnNoNullStringWithString:self.songName];
     songLabel.textColor = self.isVideoPlay ? HexRGB(0xffffff) : HexRGB(0x131415);
+    
+    songLabel.labelSpacing = 30;
+    songLabel.pauseInterval = 1.5;
+    songLabel.scrollDirection = CBAutoScrollDirectionLeft;
+    [songLabel observeApplicationNotifications];
+
     [self addSubview:songLabel];
     [songLabel mas_makeConstraints:^(MASConstraintMaker *make) {
-        make.left.mas_equalTo(backButton.mas_right);
-        make.centerY.mas_equalTo(backButton.mas_centerY);
+        make.left.mas_equalTo(backButton.mas_right).offset(4);
+        make.top.mas_equalTo(self.mas_top).offset(20);
         make.height.mas_equalTo(22);
-        make.right.mas_greaterThanOrEqualTo(self.contrlView.mas_left).offset(20);
+        make.width.mas_equalTo(200);
+    }];
+    
+    UILabel *singerLabel = [[UILabel alloc] init];
+    singerLabel.font = [UIFont systemFontOfSize:12.0f weight:UIFontWeightRegular];
+    singerLabel.textColor = self.isVideoPlay ? HexRGB(0xffffff) : HexRGB(0x333333);
+    [self addSubview:singerLabel];
+    [singerLabel mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.left.mas_equalTo(songLabel.mas_left);
+        make.top.mas_equalTo(songLabel.mas_bottom).offset(4);
+        make.height.mas_equalTo(18);
+        make.right.mas_equalTo(songLabel.mas_right);
     }];
+    NSString *userName = [NSString returnNoNullStringWithString:UserDefault(NicknameKey)];
+    if (userName.length > 8) {
+        userName = [NSString stringWithFormat:@"%@...", [userName substringWithRange:NSMakeRange(0, 8)]];
+    }
+    singerLabel.text = [NSString stringWithFormat:@"演奏:%@",userName];
+    
+    // 添加曲谱占位
+    if (![NSString isEmptyString:self.musicSheetId]) {
+        
+        [self addSubview:self.webViewContainer];
+        [self.webViewContainer mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.left.mas_equalTo(self.mas_left);
+            make.bottom.mas_equalTo(self.playControlView.mas_top);
+            make.right.mas_equalTo(self.contrlView.mas_left);
+            make.height.mas_equalTo(68+WEB_TOP);
+        }];
+        
+        if (self.isVideoPlay) {
+            self.colorView = [[UIView alloc] initWithFrame:CGRectZero];
+            self.colorView.backgroundColor = HexRGBAlpha(0xffffff, 0.7);
+            [self.webViewContainer addSubview:self.colorView];
+            [self.colorView mas_makeConstraints:^(MASConstraintMaker *make) {
+                make.left.right.top.bottom.mas_equalTo(self.webViewContainer);
+            }];
+        }
+        [self.webViewContainer addSubview:self.staffView];
+//        NSString *rendMode = self.isVideoPlay ? @"video" : @"audio";
+        NSString *rendMode = @"audio";
+        [self.staffView loadWebViewWithSongID:self.musicSheetId renderType:self.musicRenderType partIndex:self.partIndex backgroundMode:rendMode];
+        [self.staffView mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.left.mas_equalTo(self.webViewContainer.mas_left).offset(leftSpace);
+            make.right.mas_equalTo(self.webViewContainer.mas_right).offset(-20);
+            make.top.mas_equalTo(self.webViewContainer.mas_top).offset(WEB_TOP);
+            make.bottom.mas_equalTo(self.webViewContainer.mas_bottom);
+        }];
+        self.webViewContainer.hidden = YES;
+    }
+    //将按钮提前
+    [self bringSubviewToFront:self.showButton];
+    [self bringSubviewToFront:backButton];
 }
 
 - (void)refreshTotalTime {
@@ -413,12 +615,14 @@
 
 - (void)backAction {
     [self stopPlay];
-    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
-        if (self.hasModify) {
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        // 如果修改过或者没有保存过草稿
+        if (self.hasModify || self.hasSaveDraft == NO) {
             MJWeakSelf;
-            [self.alertView configTitle:@"提示" descMessage:@"是否将本次录制的作品保存为草稿?" leftButtonTitle:@"取消" rightButtonTitle:@"确认" leftButtonAction:^{
+            [self.alertView configWithDesc:@"是否将本次录制的作品存为草稿?" leftTitle:@"取消" rightTitle:@"确认"];
+            [self.alertView actionCallbackCancel:^{
                 [weakSelf removeViewTips:NO];
-            } rightButtonAction:^{
+            } sure:^{
                 [weakSelf saveCurrentDraft:YES];
             }];
             [self.alertView showAlert];
@@ -434,6 +638,26 @@
     });
 }
 
+// 重新评测
+- (void)retryEvaluatingAlert {
+    MJWeakSelf;
+    [self.alertView configWithDesc:@"是否重新录制作品?" leftTitle:@"取消" rightTitle:@"确认"];
+    [self.alertView actionCallbackCancel:^{
+        
+    } sure:^{
+        [weakSelf retryEvaluating];
+    }];
+    [self.alertView showAlert];
+}
+
+- (void)retryEvaluating {
+    [self freePlayer];
+    [self removeFromSuperview];
+    if (self.mergeCallback) {
+        self.mergeCallback(MERGEBACK_RETRY);
+    }
+}
+
 #pragma mark ----- lazying
 - (KSAudioAnimationView *)animationView {
     if (!_animationView) {
@@ -473,9 +697,9 @@
     if (self.isVideoPlay) {
         [self.videoView seekToTimePlay:playPosition];
     }
-    else {
-        [self.playAnimationView startAnimation];
-        [self startTimer];
+
+    if (_staffView) {
+        [self.staffView staffPageStartPlay];
     }
 }
 
@@ -487,9 +711,11 @@
     if (self.isVideoPlay) {
         [self.videoView puasePlay];
     }
-    else {
-        [self.playAnimationView stopAnimation];
-        [self stopTimer];
+    if (_staffView) {
+        [self.staffView staffPageStop:self.playControlView.playScheduleTime];
+    }
+    if (_spectrumView) {
+        [self.spectrumView resetLayer];
     }
 }
 
@@ -510,7 +736,11 @@
             if (self.mergePlayer.isPlaying) {
                 [self.mergePlayer seekToTimePlay:rate];
             }
-            
+            else {
+                if (_staffView) {
+                    [self.staffView updateSliderProgress:rate/1000.0f];
+                }
+            }
             if (self.isVideoPlay) {
 //                NSInteger realOffsetTime = self.offsetTime + self.evaluateDelay;
                 [self.videoView seekOffsetTime:rate];
@@ -523,15 +753,6 @@
 }
 
 
-
-- (void)startTimer {
-    [self.timer setFireDate:[NSDate distantPast]];
-}
-
-- (void)stopTimer {
-    [self.timer setFireDate:[NSDate distantFuture]];
-}
-
 - (KSMergeAudioControlView *)contrlView {
     if (!_contrlView) {
         _contrlView = [KSMergeAudioControlView shareIntance];
@@ -548,7 +769,8 @@
     switch (action) {
         case MERGEACTION_CANCLE:
         {
-            [self backAction];
+            [self stopPlay];
+            [self retryEvaluatingAlert];
         }
             break;
         case MERGEACTION_MODIFY:
@@ -585,6 +807,7 @@
         {
             [self stopPlay];
             [self showPublishAlert];
+            
         }
             break;
         case MERGEACTION_HIDEVIEW:
@@ -612,6 +835,8 @@
     switch (type) {
         case PUBLISH_ACTION_PUBLISH:
         {
+            // 暂停播放
+            [self stopPlay];
             self.desc = [NSString isEmptyString:self.publishAlert.publishContainView.textView.text] ? @"我发布了一首演奏作品,快来听听吧~" :self.publishAlert.publishContainView.textView.text;
             if (self.settingImage || self.videoCoverImage) { // 上传图片
                 [self updateWithCoverImage];
@@ -717,7 +942,7 @@
         NSData *imgData = [UIImage turnsImaegDataByImage:self.settingImage];
         NSString *fileName = @"musicCoverImg";
         [UPLOAD_MANAGER configWithfilePath:@"/user/"];
-        [[KSUploadManager shareInstance] uploadImage:imgData fileName:fileName successCallback:^(NSMutableArray * _Nonnull fileUrlArray) {
+        [UPLOAD_MANAGER uploadImage:imgData fileName:fileName successCallback:^(NSMutableArray * _Nonnull fileUrlArray) {
             NSString *avatarUrl = [fileUrlArray lastObject];
             self.coverImage = avatarUrl;
             [self uploadVideoCover];
@@ -738,7 +963,7 @@
         NSData *imgData = [UIImage turnsImaegDataByImage:self.videoCoverImage];
         NSString *fileName = @"musicCoverImg";
         [UPLOAD_MANAGER configWithfilePath:@"/user/"];
-        [[KSUploadManager shareInstance] uploadImage:imgData fileName:fileName successCallback:^(NSMutableArray * _Nonnull fileUrlArray) {
+        [UPLOAD_MANAGER uploadImage:imgData fileName:fileName successCallback:^(NSMutableArray * _Nonnull fileUrlArray) {
             NSString *avatarUrl = [fileUrlArray lastObject];
             self.videoCoverUrl = avatarUrl;
             [self publishMusic];
@@ -903,10 +1128,32 @@
 #pragma mark ----- player delegate
 
 - (void)videoPlayerIsReadyPlay:(AVPlayer *)player {
+    if (self.isCancel) {
+        return;
+    }
     if (self.mergePlayer.isReady) {
-        [LOADING_MANAGER removeCustomLoading];
-        [self startPlay];
+        if (_staffView) {
+            if (self.staffView.isReady) {
+                if (self.needShowGuide) {
+                    [self configGuideView];
+                }
+                else {
+                    [LOADING_MANAGER removeCustomLoading];
+                    [self startPlay];
+                }
+            }
+        }
+        else {
+            if (self.needShowGuide) {
+                [self configGuideView];
+            }
+            else {
+                [LOADING_MANAGER removeCustomLoading];
+                [self startPlay];
+            }
+        }
     }
+
 }
 
 
@@ -935,27 +1182,71 @@
 
 #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);
-     */
+    //    NSLog(@"-------- playTime ---%zd", playTime);
     dispatch_main_async_safe(^{
         self.playControlView.playScheduleTime = playTime;
+        if (self->_staffView) {
+            [self.staffView updatePlayProgress:playTime/1000.0];
+        }
     });
+    
 }
 
 - (void)enginePlayerIsReadyPlay:(KSMergeEnginePlayer *)player {
+    if (self.isCancel) {
+        return;
+    }
     dispatch_main_async_safe(^{
-        [LOADING_MANAGER removeCustomLoading];
+        
+        CGFloat ratio = IS_IPAD ? 1.5 : 1.2;
+        [self configAnalyzerDefault:(320 * ratio)];
+
         [self refreshTotalTime];
         if (self.isVideoPlay) {
             if (self.videoView.isReady) {
-                [self startPlay];
+                if (self->_staffView) {
+                    if (self.staffView.isReady) {
+                        if (self.needShowGuide) {
+                            [self configGuideView];
+                        }
+                        else {
+                            [LOADING_MANAGER removeCustomLoading];
+                            [self startPlay];
+                        }
+                    }
+                }
+                else {
+                    if (self.needShowGuide) {
+                        [self configGuideView];
+                    }
+                    else {
+                        [LOADING_MANAGER removeCustomLoading];
+                        [self startPlay];
+                    }
+                }
             }
         }
         else {
-            [self startPlay];
+            if (self->_staffView) {
+                if (self.staffView.isReady) {
+                    if (self.needShowGuide) {
+                        [self configGuideView];
+                    }
+                    else {
+                        [LOADING_MANAGER removeCustomLoading];
+                        [self startPlay];
+                    }
+                }
+            }
+            else {
+                if (self.needShowGuide) {
+                    [self configGuideView];
+                }
+                else {
+                    [LOADING_MANAGER removeCustomLoading];
+                    [self startPlay];
+                }
+            }
         }
     });
     
@@ -966,16 +1257,19 @@
         [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];
         }
-        
+        if (self->_staffView) {
+            [self.staffView staffPageStop:0];
+            [self.staffView updateSliderProgress:0];
+        }
+        if (self->_spectrumView) {
+            [self.spectrumView resetLayer];
+        }
     });
 }
 
@@ -1001,7 +1295,24 @@
             
         }];
     }
+}
+
+- (void)player:(KSMergeEnginePlayer *)player didGenerateSpectrum:(NSArray<NSArray<NSNumber *> *> *)spectra {
     
+    dispatch_main_async_safe(^{
+        if (self->_spectrumView) {
+            if (self.spectrumView.isModify == NO) {
+                self.spectrumView.spectra = spectra;
+            }
+        }
+    });
+}
+
+- (KSSpectrumView *)spectrumView {
+    if (!_spectrumView) {
+        _spectrumView = [[KSSpectrumView alloc] initWithFrame:CGRectMake(0, 0, 320, 100)];
+    }
+    return _spectrumView;
 }
 
 - (KSVideoPlayerView *)videoView {
@@ -1061,7 +1372,10 @@
     [parm setValue:@(self.accompanyVolume) forKey:@"accompanyVolume"];
     [parm setValue:@(self.defaultDelay) forKey:@"defaultDelay"];
     [parm setValue:@(self.evaluateDelay) forKey:@"evaluateDelay"];
-
+    [parm setValue:@(self.musicSpeed) forKey:@"speedRate"];
+    [parm setValue:self.musicRenderType forKey:@"musicRenderType"];
+    [parm setValue:@(self.partIndex) forKey:@"part-index"];
+    
     self.jsonConfig = [parm mj_JSONString];
     [KSNetworkingManager saveMusicMessage:KS_POST jsonConfig:self.jsonConfig img:self.coverImage videoUrl:fileUrl accompanyUrl:self.remoteBgAudioUrl desc:self.desc type:type musicPracticeRecordId:self.recordId videoImg:self.videoCoverUrl success:^(NSDictionary * _Nonnull dic) {
         if ([dic ks_integerValueForKey:@"code"] == 200) {
@@ -1108,6 +1422,7 @@
     [shareView shareOutCallback:^(MINESHARETYPE type) {
         [weakSelf shareAction:type desc:desc productId:productId];
     }];
+    
 }
 
 - (void)shareAction:(MINESHARETYPE)type desc:(NSString *)desc productId:(NSString *)productId {
@@ -1117,7 +1432,6 @@
     UIImage *image = [UIImage new];
     NSString *descMessage = desc;
     id thumImage = [NSString isEmptyString:self.coverImage] ? [UIImage imageNamed:@"pub_music_placeholder"] : [self.coverImage getUrlEndcodeString];
-    
     switch (type) {
         case MINESHARETYPE_FRIEND:
         {
@@ -1152,18 +1466,20 @@
 
 }
 
+
 - (void)showSaveDraftTipsAlert {
     MJWeakSelf;
-    [self.alertView configTitle:@"提示" descMessage:@"已成功保存到草稿,草稿7天未发布将自动删除。" leftButtonTitle:@"确认" rightButtonTitle:@"查看草稿" leftButtonAction:^{
+    [self.alertView configWithDesc:@"已成功保存到草稿,草稿7天未发布将自动删除。" leftTitle:@"我知道了" rightTitle:@"查看草稿"];
+    [self.alertView actionCallbackCancel:^{
         
-    } rightButtonAction:^{
+    } sure:^{
         [weakSelf displayDraft];
     }];
     [self.alertView showAlert];
 }
 
 - (void)displayDraft {
-    
+    [self freePlayer];
     // 跳转到我的作品
     UIViewController *baseCtrl = [self findViewController];
     [baseCtrl.navigationController popToRootViewControllerAnimated:YES];
@@ -1179,7 +1495,7 @@
     NSData *fileData = [NSData dataWithContentsOfURL:fileUrl];
     NSString *suffix = [NSString stringWithFormat:@".%@",[fileUrl pathExtension]];
     [UPLOAD_MANAGER configWithfilePath:@"/user/"];
-    [[KSUploadManager shareInstance] videoUpload:fileData fileName:@"video" fileSuffix:suffix progress:^(int64_t bytesWritten, int64_t totalBytes) {
+    [UPLOAD_MANAGER videoUpload:fileData fileName:@"video" fileSuffix:suffix progress:^(int64_t bytesWritten, int64_t totalBytes) {
         // 显示进度
         float progress = (bytesWritten*1.0 / totalBytes) * rate + beginProgress;
         dispatch_main_async_safe(^{
@@ -1248,40 +1564,14 @@
     return _playAnimationView;
 }
 
-- (NSTimer *)timer{
-    
-    if (!_timer) {
-        MJWeakSelf;
-        _timer = [NSTimer scheduledTimerWithTimeInterval:0.5f repeats:YES block:^(NSTimer * _Nonnull timer) {
-            [weakSelf timerAction];
-        }];
-        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
-        [_timer setFireDate:[NSDate distantFuture]];
-    }
-    return _timer;
-}
+
 
 - (void)timerAction {
     float value = drand48();
     [self.playAnimationView setSoundsValue:value];
 }
 
-#pragma mark -- 重置定时器
-- (void)resetTimer{
-    [self.timer setFireDate:[NSDate distantPast]];
-    [_timer invalidate];
-    _timer = nil;
-}
-
-- (void)removeAll{
-    
-    if (_timer) {
-        [_timer invalidate];
-        _timer = nil;
-    }
-}
 - (void)dealloc {
-    [self removeAll];
     [[NSNotificationCenter defaultCenter] removeObserver:self];
 }
 
@@ -1293,10 +1583,9 @@
     return _requestGroup;
 }
 
-- (KSNewAlertView *)alertView {
+- (MergeTipsAlert *)alertView {
     if (!_alertView) {
-        _alertView = [KSNewAlertView shareInstance];
-        
+        _alertView = [MergeTipsAlert shareInstance];
     }
     return _alertView;
 }
@@ -1375,6 +1664,157 @@
     [controller dismissViewControllerAnimated:YES completion:nil];
 }
 
+#pragma mark ----- 谱面
+- (UIView *)webViewContainer {
+    if (!_webViewContainer) {
+        _webViewContainer = [[UIView alloc] initWithFrame:CGRectZero];
+        _webViewContainer.backgroundColor = [UIColor clearColor];
+    }
+    return _webViewContainer;
+}
+
+- (MergeMusicStaffView *)staffView {
+    if (!_staffView) {
+        _staffView = [MergeMusicStaffView shareIntance];
+        MJWeakSelf;
+        [_staffView webActionCallback:^(CGFloat pageHeight) {
+            [weakSelf refreshContainerHeight:pageHeight];
+        }];
+    }
+    return _staffView;
+}
+
+- (void)refreshContainerHeight:(CGFloat)height {
+    [self.webViewContainer mas_updateConstraints:^(MASConstraintMaker *make) {
+        make.height.mas_equalTo(height+WEB_TOP);
+    }];
+    self.webViewContainer.hidden = NO;
+    if (self.isVideoPlay) {
+        
+        if (self.videoView.isReady && self.mergePlayer.isReady) {
+            if (self.needShowGuide) {
+                [self configGuideView];
+            }
+            else {
+                [LOADING_MANAGER removeCustomLoading];
+                [self startPlay];
+            }
+        }
+    }
+    else {
+        if (self.mergePlayer.isReady) {
+            if (self.needShowGuide) {
+                [self configGuideView];
+            }
+            else {
+                [LOADING_MANAGER removeCustomLoading];
+                [self startPlay];
+            }
+        }
+    }
+}
+
+#pragma mark ------- 功能引导
+- (void)configGuideView {
+    [self.guideManager clearAllGuideModel];
+    UIWindow *window = [NSObject getKeyWindow];
+    NSInteger totalCount = 7;
+    if (self.contrlView.hideBackView) {
+        totalCount = 6;
+    }
+    
+    NSInteger currentIndex = 0;
+    // 音量引导
+    CGRect volumeGuideFrame = [self.contrlView.volumeGuideBg convertRect:self.contrlView.volumeGuideBg.bounds toView:window];
+    CGRect volumeImageFrame = CGRectMake(CGRectGetMinX(volumeGuideFrame) - 270 - 8, CGRectGetMinY(volumeGuideFrame) + 10, 270, 161);
+    CGRect volumeButtonFrame = CGRectMake(CGRectGetMinX(volumeImageFrame) + 16, CGRectGetMaxY(volumeImageFrame) + 17, 100, 33);
+    KSBaseGuideModel *volumeGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_volume" guideViewFrame:volumeGuideFrame imageFrame:volumeImageFrame buttonFrame:volumeButtonFrame currentIndex:currentIndex totalCount:totalCount];
+    [self.guideManager addGuideModel:volumeGuideModel];
+    currentIndex++;
+    
+    // 演奏后移
+    CGRect delayGuideFrame = [self.contrlView.delayGuideBg convertRect:self.contrlView.delayGuideBg.bounds toView:window];
+    CGRect delayImageFrame = CGRectMake(CGRectGetMinX(delayGuideFrame) - 270 - 5, CGRectGetMidY(delayGuideFrame) - 207, 270, 207);
+    CGRect delaybuttonFrame = CGRectMake(CGRectGetMinX(delayImageFrame) + 16, CGRectGetMaxY(delayImageFrame) - 33 + 5, 100, 33);
+    KSBaseGuideModel *delayGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_delay" guideViewFrame:delayGuideFrame imageFrame:delayImageFrame buttonFrame:delaybuttonFrame currentIndex:currentIndex totalCount:totalCount];
+    [self.guideManager addGuideModel:delayGuideModel];
+    currentIndex++;
+    
+    // 演奏提前
+    CGRect forwardGuideFrame = [self.contrlView.forwardGuideBg convertRect:self.contrlView.forwardGuideBg.bounds toView:window];
+    CGRect forwardImageFrame = CGRectMake(CGRectGetMinX(forwardGuideFrame) - 270 - 5, CGRectGetMidY(forwardGuideFrame) - 209, 270, 209);
+    CGRect forwardbuttonFrame = CGRectMake(CGRectGetMinX(forwardImageFrame) + 16, CGRectGetMaxY(forwardImageFrame) - 33 + 5, 100, 33);
+    KSBaseGuideModel *forwardGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_forward" guideViewFrame:forwardGuideFrame imageFrame:forwardImageFrame buttonFrame:forwardbuttonFrame currentIndex:currentIndex totalCount:totalCount];
+    [self.guideManager addGuideModel:forwardGuideModel];
+    currentIndex++;
+    
+    // 对齐slider调整
+    CGRect offsetGuideFrame = [self.contrlView.offsetSliderBg convertRect:self.contrlView.offsetSliderBg.bounds toView:window];
+    CGRect offsetImageFrame = CGRectMake(CGRectGetMinX(offsetGuideFrame) - 270 - 5, CGRectGetMidY(offsetGuideFrame) - 206, 270, 206);
+    CGRect offsetbuttonFrame = CGRectMake(CGRectGetMinX(offsetImageFrame) + 16, CGRectGetMaxY(offsetImageFrame) - 33 + 5, 100, 33);
+    KSBaseGuideModel *offsetGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_offset" guideViewFrame:offsetGuideFrame imageFrame:offsetImageFrame buttonFrame:offsetbuttonFrame currentIndex:currentIndex totalCount:totalCount];
+    [self.guideManager addGuideModel:offsetGuideModel];
+    currentIndex++;
+    
+    // 重新录制
+    if (self.contrlView.hideBackView == NO) {
+        CGRect retryGuideFrame = [self.contrlView.retryGuideBg convertRect:self.contrlView.retryGuideBg.bounds toView:window];
+        CGRect retryImageFrame = CGRectMake(CGRectGetMidX(retryGuideFrame) - 260, CGRectGetMinY(retryGuideFrame) - 228 - 5, 260, 228);
+        CGRect retrybuttonFrame = CGRectMake(CGRectGetMinX(retryImageFrame) + 16, CGRectGetMaxY(retryImageFrame) - 33 - 15, 100, 33);
+        KSBaseGuideModel *retryGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_retry" guideViewFrame:retryGuideFrame imageFrame:retryImageFrame buttonFrame:retrybuttonFrame currentIndex:currentIndex totalCount:totalCount];
+        [self.guideManager addGuideModel:retryGuideModel];
+        currentIndex++;
+    }
+    
+    // 发布作品
+    CGRect publishGuideFrame = [self.contrlView.publishGuideBg convertRect:self.contrlView.publishGuideBg.bounds toView:window];
+    CGRect publishImageFrame = CGRectMake(CGRectGetMidX(publishGuideFrame) - 270 - 5, CGRectGetMinY(publishGuideFrame) - 227 - 5, 270, 227);
+    CGRect publishbuttonFrame = CGRectMake(CGRectGetMinX(publishImageFrame) + 16, CGRectGetMaxY(publishImageFrame) - 33 - 15, 100, 33);
+    KSBaseGuideModel *publishGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_publish" guideViewFrame:publishGuideFrame imageFrame:publishImageFrame buttonFrame:publishbuttonFrame currentIndex:currentIndex totalCount:totalCount];
+    [self.guideManager addGuideModel:publishGuideModel];
+    currentIndex++;
+    
+    // 保存草稿
+    CGRect draftGuideFrame = [self.contrlView.saveGuideBg convertRect:self.contrlView.saveGuideBg.bounds toView:window];
+    CGRect draftImageFrame = CGRectMake(CGRectGetMidX(draftGuideFrame) - 260, CGRectGetMinY(draftGuideFrame) - 246 - 5, 260, 246);
+    CGRect draftButtonFrame = CGRectMake(CGRectGetMinX(draftImageFrame) + 16, CGRectGetMaxY(draftImageFrame) - 33 - 15, 82, 33);
+    KSBaseGuideModel *draftGuideModel = [self createGuideModelWithDisplayImage:@"merge_guide_saveDraft" guideViewFrame:draftGuideFrame imageFrame:draftImageFrame buttonFrame:draftButtonFrame currentIndex:currentIndex totalCount:totalCount];
+    [self.guideManager addGuideModel:draftGuideModel];
+    
+    [LOADING_MANAGER removeCustomLoading];
+    [self.guideManager beginGuide];
+    self.needShowGuide = NO;
+    UserDefaultSetBoolForKey(YES, MERGE_GUIDEKEY);
+}
+
+
+- (KSBaseGuideModel *)createGuideModelWithDisplayImage:(NSString *)guideImage guideViewFrame:(CGRect)guideViewFrame imageFrame:(CGRect)imageFrame buttonFrame:(CGRect)buttonFrame currentIndex:(NSInteger)currentIndex totalCount:(NSInteger)totalCount {
+    KSBaseGuideModel *model = [[KSBaseGuideModel alloc] init];
+    model.displayImageName = guideImage;
+    model.currentIndex = currentIndex;
+    model.totalIndex = totalCount;
+    model.guideViewFrame = guideViewFrame;
+    model.buttonFrame = buttonFrame;
+    model.imageFrame = imageFrame;
+    
+    return model;
+}
+
+- (KSBaseGuideManager *)guideManager {
+    if (!_guideManager) {
+        _guideManager = [[KSBaseGuideManager alloc] init];
+        [_guideManager configskipButtonImage:@"merge_guide_skip" nextButtonImage:@"merge_guide_nextBg" replayButtonImage:@"merge_guide_retryBg" finishButtonImage:@"merge_guide_finishBg"];
+        MJWeakSelf;
+        [_guideManager GuideFinish:^{
+            [weakSelf guideFinishAction];
+        }];
+    }
+    return _guideManager;
+}
+
+- (void)guideFinishAction {
+    [self startPlay];
+}
 /*
 // Only override drawRect: if you perform custom drawing.
 // An empty implementation adversely affects performance during animation.

+ 14 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/KSMergeAudioControlView.h

@@ -22,6 +22,20 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface KSMergeAudioControlView : UIView
 
+@property (weak, nonatomic) IBOutlet UIView *volumeGuideBg;
+
+@property (weak, nonatomic) IBOutlet UIView *delayGuideBg;
+
+@property (weak, nonatomic) IBOutlet UIView *forwardGuideBg;
+
+@property (weak, nonatomic) IBOutlet UIView *offsetSliderBg;
+
+@property (weak, nonatomic) IBOutlet UIView *retryGuideBg;
+
+@property (weak, nonatomic) IBOutlet UIView *publishGuideBg;
+
+@property (weak, nonatomic) IBOutlet UIView *saveGuideBg;
+
 @property (nonatomic, assign) BOOL hideBackView;
 
 + (instancetype)shareIntance;

+ 95 - 35
KulexiuForTeacher/KulexiuForTeacher/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="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" 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="22684"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
@@ -17,8 +17,36 @@
                 <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dNG-od-Opv">
                     <rect key="frame" x="0.0" y="0.0" width="289" height="388"/>
                     <subviews>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="unX-kF-GoX">
+                            <rect key="frame" x="11" y="52" width="277" height="158"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        </view>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kcU-rG-UWE">
+                            <rect key="frame" x="7.6666666666666643" y="260.66666666666669" width="63.999999999999993" height="68"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        </view>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zGK-Nm-bhv">
+                            <rect key="frame" x="227.66666666666663" y="260.66666666666669" width="64" height="68"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        </view>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4Yk-Bc-KTb">
+                            <rect key="frame" x="66.666666666666686" y="259" width="166" height="70"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        </view>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4uQ-6L-UfR">
+                            <rect key="frame" x="8.6666666666666643" y="321" width="56.999999999999993" height="57"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        </view>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="beO-nV-nu5">
+                            <rect key="frame" x="64" y="324" width="161" height="53"/>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        </view>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="NBl-15-e5U">
+                            <rect key="frame" x="223.66666666666666" y="321" width="56.999999999999972" height="57"/>
+                            <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="ZEM-tW-WX9">
-                            <rect key="frame" x="111.66666666666669" y="14" width="66" height="22"/>
+                            <rect key="frame" x="112.66666666666667" y="14" width="63.666666666666671" height="22"/>
                             <constraints>
                                 <constraint firstAttribute="height" constant="22" id="CkE-ww-wZM"/>
                             </constraints>
@@ -34,7 +62,7 @@
                             </constraints>
                         </view>
                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="演奏音量" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oyf-C4-Pe3">
-                            <rect key="frame" x="26.000000000000004" y="62" width="57.333333333333343" height="17"/>
+                            <rect key="frame" x="25.999999999999996" y="62" width="55.666666666666657" height="17"/>
                             <fontDescription key="fontDescription" type="system" pointSize="14"/>
                             <color key="textColor" red="0.074509803920000006" green="0.078431372550000003" blue="0.08235294118" alpha="1" colorSpace="calibratedRGB"/>
                             <nil key="highlightedColor"/>
@@ -46,19 +74,19 @@
                             <color key="thumbTintColor" red="0.1764705882" green="0.78039215689999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                         </slider>
                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="伴奏音量" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="if2-R5-Tul">
-                            <rect key="frame" x="26" y="145" width="58" height="17"/>
+                            <rect key="frame" x="25.999999999999996" y="152" width="55.666666666666657" height="17"/>
                             <fontDescription key="fontDescription" type="system" pointSize="14"/>
                             <color key="textColor" red="0.074509803920000006" green="0.078431372550000003" blue="0.08235294118" alpha="1" colorSpace="calibratedRGB"/>
                             <nil key="highlightedColor"/>
                         </label>
                         <slider opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="100" minValue="0.0" maxValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="ucS-Gd-cx3">
-                            <rect key="frame" x="24" y="178" width="251" height="31"/>
+                            <rect key="frame" x="24" y="185" width="251" height="31"/>
                             <color key="minimumTrackTintColor" red="0.1764705882" green="0.78039215689999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                             <color key="maximumTrackTintColor" red="0.90980392156862744" green="0.92549019607843142" blue="0.94509803921568625" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                             <color key="thumbTintColor" red="0.1764705882" green="0.78039215689999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                         </slider>
                         <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="DAF-iV-Mgw">
-                            <rect key="frame" x="26" y="173" width="247" height="40"/>
+                            <rect key="frame" x="26" y="180" width="247" height="40"/>
                             <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         </view>
                         <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="VTj-cX-p66">
@@ -94,7 +122,7 @@
                             </constraints>
                         </imageView>
                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="重录" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f5Q-wh-6l8">
-                            <rect key="frame" x="26.666666666666664" y="356" width="20.666666666666664" height="12"/>
+                            <rect key="frame" x="27" y="356" width="20" height="12"/>
                             <fontDescription key="fontDescription" type="system" pointSize="10"/>
                             <color key="textColor" red="0.46666666666666667" green="0.46666666666666667" blue="0.46666666666666667" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                             <nil key="highlightedColor"/>
@@ -110,7 +138,7 @@
                             </connections>
                         </button>
                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="保存草稿" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YP9-QE-4Lm">
-                            <rect key="frame" x="231.66666666666666" y="356" width="40.999999999999972" height="12"/>
+                            <rect key="frame" x="232" y="356" width="40" height="12"/>
                             <fontDescription key="fontDescription" type="system" pointSize="10"/>
                             <color key="textColor" red="0.46666666670000001" green="0.46666666670000001" blue="0.46666666670000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                             <nil key="highlightedColor"/>
@@ -133,13 +161,13 @@
                             </connections>
                         </button>
                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="演奏对齐" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XPO-nW-Uox">
-                            <rect key="frame" x="26" y="228" width="58" height="17"/>
+                            <rect key="frame" x="25.999999999999996" y="242" width="55.666666666666657" height="17"/>
                             <fontDescription key="fontDescription" type="system" pointSize="14"/>
                             <color key="textColor" red="0.074509803920000006" green="0.078431372550000003" blue="0.08235294118" alpha="1" colorSpace="calibratedRGB"/>
                             <nil key="highlightedColor"/>
                         </label>
                         <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="96g-7r-vnm">
-                            <rect key="frame" x="26" y="248.66666666666663" width="27" height="40"/>
+                            <rect key="frame" x="26" y="262.66666666666669" width="27" height="40"/>
                             <constraints>
                                 <constraint firstAttribute="height" constant="40" id="Ggw-dF-h9G"/>
                             </constraints>
@@ -150,7 +178,7 @@
                             </connections>
                         </button>
                         <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"/>
+                            <rect key="frame" x="89.666666666666657" y="304" width="109.66666666666666" height="15"/>
                             <constraints>
                                 <constraint firstAttribute="height" constant="15" id="To3-OM-9PS"/>
                             </constraints>
@@ -159,7 +187,7 @@
                             <nil key="highlightedColor"/>
                         </label>
                         <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="slK-h7-P25">
-                            <rect key="frame" x="59" y="257" width="181" height="23"/>
+                            <rect key="frame" x="59" y="271" width="181" height="23"/>
                             <subviews>
                                 <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="aligningSlider_bg" translatesAutoresizingMaskIntoConstraints="NO" id="ppH-Ks-AgY">
                                     <rect key="frame" x="0.0" y="6" width="181" height="11"/>
@@ -174,7 +202,7 @@
                             </constraints>
                         </view>
                         <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="imR-Qo-Jpf">
-                            <rect key="frame" x="246" y="248.66666666666663" width="27" height="40"/>
+                            <rect key="frame" x="246" y="262.66666666666669" width="27" height="40"/>
                             <constraints>
                                 <constraint firstAttribute="height" constant="40" id="6gg-eX-Lfg"/>
                             </constraints>
@@ -185,7 +213,7 @@
                             </connections>
                         </button>
                         <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KFO-kx-bI9">
-                            <rect key="frame" x="146" y="252" width="8" height="24"/>
+                            <rect key="frame" x="146" y="266" width="8" height="24"/>
                             <subviews>
                                 <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pointing_arrow" translatesAutoresizingMaskIntoConstraints="NO" id="Wt9-xF-pLo">
                                     <rect key="frame" x="0.0" y="0.0" width="8" height="6"/>
@@ -232,8 +260,8 @@
                                 <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="slider_bubble" translatesAutoresizingMaskIntoConstraints="NO" id="ahK-NK-SMi">
                                     <rect key="frame" x="0.0" y="0.0" width="34" height="34"/>
                                 </imageView>
-                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oYa-cu-i4y">
-                                    <rect key="frame" x="0.0" y="4" width="34" height="17"/>
+                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oYa-cu-i4y">
+                                    <rect key="frame" x="4" y="4" width="26" height="17"/>
                                     <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
                                     <color key="textColor" red="0.074509803921568626" green="0.078431372549019607" blue="0.082352941176470587" alpha="1" colorSpace="calibratedRGB"/>
                                     <nil key="highlightedColor"/>
@@ -243,22 +271,20 @@
                             <constraints>
                                 <constraint firstItem="ahK-NK-SMi" firstAttribute="leading" secondItem="nyI-H7-gwi" secondAttribute="leading" id="EMl-Ys-Vv1"/>
                                 <constraint firstAttribute="trailing" secondItem="ahK-NK-SMi" secondAttribute="trailing" id="H4j-le-wjD"/>
-                                <constraint firstItem="oYa-cu-i4y" firstAttribute="leading" secondItem="nyI-H7-gwi" secondAttribute="leading" id="MPY-GZ-0K3"/>
                                 <constraint firstAttribute="bottom" secondItem="ahK-NK-SMi" secondAttribute="bottom" id="Xgf-qc-1hG"/>
                                 <constraint firstItem="oYa-cu-i4y" firstAttribute="centerX" secondItem="nyI-H7-gwi" secondAttribute="centerX" id="ejz-pz-k8f"/>
                                 <constraint firstItem="ahK-NK-SMi" firstAttribute="top" secondItem="nyI-H7-gwi" secondAttribute="top" id="eku-h5-9mJ"/>
                                 <constraint firstItem="oYa-cu-i4y" firstAttribute="top" secondItem="nyI-H7-gwi" secondAttribute="top" constant="4" id="lH8-xm-u7O"/>
-                                <constraint firstAttribute="trailing" secondItem="oYa-cu-i4y" secondAttribute="trailing" id="xZP-kT-yES"/>
                             </constraints>
                         </view>
                         <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Z4q-fo-efi">
-                            <rect key="frame" x="250" y="137" width="34" height="34"/>
+                            <rect key="frame" x="250" y="144" width="34" height="34"/>
                             <subviews>
                                 <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="slider_bubble" translatesAutoresizingMaskIntoConstraints="NO" id="qaQ-ih-J2u">
                                     <rect key="frame" x="0.0" y="0.0" width="34" height="34"/>
                                 </imageView>
-                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TSI-LH-Hac">
-                                    <rect key="frame" x="0.0" y="4" width="34" height="17"/>
+                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TSI-LH-Hac">
+                                    <rect key="frame" x="4" y="4" width="26" height="17"/>
                                     <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
                                     <color key="textColor" red="0.074509803920000006" green="0.078431372550000003" blue="0.08235294118" alpha="1" colorSpace="calibratedRGB"/>
                                     <nil key="highlightedColor"/>
@@ -269,25 +295,23 @@
                                 <constraint firstAttribute="bottom" secondItem="qaQ-ih-J2u" secondAttribute="bottom" id="LPG-Hp-6Nw"/>
                                 <constraint firstItem="qaQ-ih-J2u" firstAttribute="top" secondItem="Z4q-fo-efi" secondAttribute="top" id="STk-ay-vnl"/>
                                 <constraint firstItem="qaQ-ih-J2u" firstAttribute="leading" secondItem="Z4q-fo-efi" secondAttribute="leading" id="V5S-da-cyV"/>
-                                <constraint firstAttribute="trailing" secondItem="TSI-LH-Hac" secondAttribute="trailing" id="ZNT-5e-22G"/>
                                 <constraint firstItem="TSI-LH-Hac" firstAttribute="top" secondItem="Z4q-fo-efi" secondAttribute="top" constant="4" id="jM5-5i-DJN"/>
                                 <constraint firstItem="TSI-LH-Hac" firstAttribute="centerX" secondItem="Z4q-fo-efi" secondAttribute="centerX" id="pzq-an-1hO"/>
                                 <constraint firstAttribute="trailing" secondItem="qaQ-ih-J2u" secondAttribute="trailing" id="vFJ-Hx-o68"/>
-                                <constraint firstItem="TSI-LH-Hac" firstAttribute="leading" secondItem="Z4q-fo-efi" secondAttribute="leading" id="xQH-jo-Qaa"/>
                             </constraints>
                         </view>
                         <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="DzT-go-dxS">
                             <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="hcG-ht-1dk">
-                            <rect key="frame" x="17" y="290.66666666666669" width="45" height="14"/>
+                        <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.666666666666671" y="304.66666666666669" width="44" 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="oLm-QY-ktj">
-                            <rect key="frame" x="237" y="290.66666666666669" width="45" height="14"/>
+                        <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.66666666666663" y="304.66666666666669" width="44" 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"/>
@@ -295,22 +319,32 @@
                     </subviews>
                     <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                     <constraints>
+                        <constraint firstItem="53S-gG-Fsg" firstAttribute="bottom" secondItem="beO-nV-nu5" secondAttribute="bottom" constant="-9" id="0I9-PU-jGs"/>
                         <constraint firstItem="Q7z-0G-roa" firstAttribute="leading" secondItem="53S-gG-Fsg" secondAttribute="trailing" constant="20" id="3Cw-bb-fyr"/>
                         <constraint firstAttribute="trailing" secondItem="Q7z-0G-roa" secondAttribute="trailing" constant="25" id="3Tk-kO-5hd"/>
+                        <constraint firstItem="Mqk-Mg-Pni" firstAttribute="leading" secondItem="NBl-15-e5U" secondAttribute="leading" constant="8.5" id="48d-p6-1wF"/>
                         <constraint firstItem="DAF-iV-Mgw" firstAttribute="top" secondItem="ucS-Gd-cx3" secondAttribute="top" constant="-5" id="5Uc-4c-awL"/>
+                        <constraint firstItem="4Yk-Bc-KTb" firstAttribute="top" secondItem="XPO-nW-Uox" secondAttribute="bottom" id="5Uc-8p-g4a"/>
                         <constraint firstItem="ffV-Sq-WQc" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" id="5b1-03-HFl"/>
                         <constraint firstItem="DAF-iV-Mgw" firstAttribute="trailing" secondItem="ucS-Gd-cx3" secondAttribute="trailing" id="5wr-ml-Vww"/>
                         <constraint firstItem="KFO-kx-bI9" firstAttribute="bottom" secondItem="slK-h7-P25" secondAttribute="bottom" constant="-4" id="63b-Bu-bya"/>
+                        <constraint firstItem="ses-18-GMw" firstAttribute="bottom" secondItem="4Yk-Bc-KTb" secondAttribute="bottom" constant="-10" id="66L-0W-a85"/>
+                        <constraint firstItem="96g-7r-vnm" firstAttribute="top" secondItem="kcU-rG-UWE" secondAttribute="top" constant="2" id="6mk-Pq-dQg"/>
                         <constraint firstItem="Mqk-Mg-Pni" firstAttribute="top" secondItem="Q7z-0G-roa" secondAttribute="top" id="77v-Ks-NYn"/>
                         <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="hcG-ht-1dk" firstAttribute="centerX" secondItem="96g-7r-vnm" secondAttribute="centerX" id="9AK-XJ-GIK"/>
+                        <constraint firstItem="xzg-Xa-du5" firstAttribute="trailing" secondItem="kcU-rG-UWE" secondAttribute="trailing" constant="-10" id="8fA-uQ-cET"/>
                         <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="xzg-Xa-du5" firstAttribute="leading" secondItem="kcU-rG-UWE" secondAttribute="leading" constant="10" id="CuR-49-Ew1"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="leading" secondItem="zGK-Nm-bhv" secondAttribute="leading" constant="10" id="DaB-rw-Huz"/>
                         <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="oLm-QY-ktj" firstAttribute="centerX" secondItem="imR-Qo-Jpf" secondAttribute="centerX" id="GJk-IW-91X"/>
+                        <constraint firstItem="imR-Qo-Jpf" firstAttribute="top" secondItem="zGK-Nm-bhv" secondAttribute="top" constant="2" id="FhE-Kf-3Oq"/>
                         <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"/>
                         <constraint firstItem="VTj-cX-p66" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" id="Juc-95-lKd"/>
@@ -320,48 +354,67 @@
                         <constraint firstItem="imR-Qo-Jpf" firstAttribute="centerY" secondItem="96g-7r-vnm" secondAttribute="centerY" id="Mff-dk-R7V"/>
                         <constraint firstItem="YP9-QE-4Lm" firstAttribute="centerX" secondItem="Q7z-0G-roa" secondAttribute="centerX" id="MjW-OK-XDC"/>
                         <constraint firstItem="YP9-QE-4Lm" firstAttribute="bottom" secondItem="Mqk-Mg-Pni" secondAttribute="bottom" id="NPw-x6-w1G"/>
+                        <constraint firstItem="GuK-qa-jwH" firstAttribute="top" secondItem="4uQ-6L-UfR" secondAttribute="top" constant="10" id="Oj3-R4-8dR"/>
                         <constraint firstItem="Jqd-bC-HWT" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="25" id="QHn-11-YHg"/>
                         <constraint firstAttribute="trailing" secondItem="ffV-Sq-WQc" secondAttribute="trailing" id="QYz-xj-o4P"/>
                         <constraint firstItem="Jqd-bC-HWT" firstAttribute="top" secondItem="VTj-cX-p66" secondAttribute="bottom" constant="10" id="Qii-R1-RO0"/>
                         <constraint firstItem="pQK-tY-S1I" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="26" id="QmU-HT-h32"/>
                         <constraint firstItem="ZEM-tW-WX9" firstAttribute="centerX" secondItem="dNG-od-Opv" secondAttribute="centerX" id="QoR-XG-IZJ"/>
-                        <constraint firstItem="oLm-QY-ktj" firstAttribute="centerY" secondItem="hcG-ht-1dk" secondAttribute="centerY" id="Tab-W5-EDU"/>
                         <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="if2-R5-Tul" firstAttribute="top" secondItem="pQK-tY-S1I" secondAttribute="bottom" constant="27" 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="ses-18-GMw" firstAttribute="centerY" secondItem="hcG-ht-1dk" secondAttribute="centerY" id="Xdh-Uf-rO4"/>
+                        <constraint firstItem="Mqk-Mg-Pni" firstAttribute="trailing" secondItem="NBl-15-e5U" secondAttribute="trailing" constant="-8.5" id="XBE-xc-Kes"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="centerX" secondItem="imR-Qo-Jpf" secondAttribute="centerX" id="YAs-zN-DUP"/>
+                        <constraint firstItem="unX-kF-GoX" firstAttribute="leading" secondItem="oyf-C4-Pe3" secondAttribute="leading" constant="-15" id="Ybw-HP-U4G"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="leading" secondItem="4Yk-Bc-KTb" secondAttribute="trailing" constant="5" id="ZFH-D6-gAr"/>
                         <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="53S-gG-Fsg" firstAttribute="trailing" secondItem="beO-nV-nu5" secondAttribute="trailing" constant="-5" id="aUt-lF-11M"/>
+                        <constraint firstItem="DAF-iV-Mgw" firstAttribute="bottom" secondItem="unX-kF-GoX" secondAttribute="bottom" constant="10" id="cqL-cM-9bX"/>
+                        <constraint firstItem="GuK-qa-jwH" firstAttribute="leading" secondItem="4uQ-6L-UfR" secondAttribute="leading" constant="8.5" id="dAg-hx-VdR"/>
                         <constraint firstItem="ZEM-tW-WX9" firstAttribute="top" secondItem="dNG-od-Opv" secondAttribute="top" constant="14" id="ddY-o0-jeB"/>
                         <constraint firstAttribute="trailing" secondItem="VTj-cX-p66" secondAttribute="trailing" id="dw9-31-S22"/>
                         <constraint firstItem="MX8-jr-euo" firstAttribute="centerY" secondItem="dNG-od-Opv" secondAttribute="centerY" id="e5b-Qo-rqf"/>
                         <constraint firstItem="53S-gG-Fsg" firstAttribute="top" secondItem="VTj-cX-p66" secondAttribute="bottom" constant="12" id="e5p-cL-oZ8"/>
                         <constraint firstItem="DzT-go-dxS" firstAttribute="trailing" secondItem="pQK-tY-S1I" secondAttribute="trailing" id="eDg-VV-OiA"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="trailing" secondItem="zGK-Nm-bhv" secondAttribute="trailing" constant="-10" id="f9S-Uu-isw"/>
                         <constraint firstItem="oyf-C4-Pe3" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="26" id="fSo-c2-9fa"/>
+                        <constraint firstItem="unX-kF-GoX" firstAttribute="trailing" secondItem="DzT-go-dxS" secondAttribute="trailing" constant="15" id="g52-cp-eYI"/>
                         <constraint firstItem="Jqd-bC-HWT" firstAttribute="centerX" secondItem="GuK-qa-jwH" secondAttribute="centerX" id="gAv-2s-iSN"/>
                         <constraint firstItem="slK-h7-P25" firstAttribute="leading" secondItem="96g-7r-vnm" secondAttribute="trailing" constant="6" id="gWj-rN-OUA"/>
                         <constraint firstItem="XPO-nW-Uox" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="26" id="h5x-Vo-RBz"/>
                         <constraint firstItem="MX8-jr-euo" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" id="hPK-xS-fSR"/>
+                        <constraint firstItem="unX-kF-GoX" firstAttribute="top" secondItem="oyf-C4-Pe3" secondAttribute="top" constant="-10" id="hPS-T9-LHg"/>
+                        <constraint firstItem="Mqk-Mg-Pni" firstAttribute="top" secondItem="NBl-15-e5U" secondAttribute="top" constant="10" id="hSM-Po-KXc"/>
+                        <constraint firstItem="53S-gG-Fsg" firstAttribute="top" secondItem="beO-nV-nu5" secondAttribute="top" constant="9" id="hnc-Bj-geW"/>
+                        <constraint firstItem="xzg-Xa-du5" firstAttribute="bottom" secondItem="kcU-rG-UWE" secondAttribute="bottom" constant="-10" id="hr2-nQ-JRi"/>
                         <constraint firstItem="gDH-uC-2DO" firstAttribute="leading" secondItem="dNG-od-Opv" secondAttribute="leading" constant="8" id="hut-3H-gU3"/>
                         <constraint firstAttribute="bottom" secondItem="VTj-cX-p66" secondAttribute="bottom" constant="67" id="iPG-s9-hwk"/>
                         <constraint firstItem="imR-Qo-Jpf" firstAttribute="leading" secondItem="slK-h7-P25" secondAttribute="trailing" constant="6" id="iTt-Wy-aYO"/>
                         <constraint firstItem="nyI-H7-gwi" firstAttribute="centerX" secondItem="dNG-od-Opv" secondAttribute="leading" constant="267" id="jdI-be-CFN"/>
                         <constraint firstItem="DzT-go-dxS" firstAttribute="top" secondItem="pQK-tY-S1I" secondAttribute="top" constant="-5" id="kMt-a5-3io"/>
                         <constraint firstItem="Q7z-0G-roa" firstAttribute="top" secondItem="VTj-cX-p66" secondAttribute="bottom" constant="10" id="kNw-jw-Ek9"/>
+                        <constraint firstItem="53S-gG-Fsg" firstAttribute="leading" secondItem="beO-nV-nu5" secondAttribute="leading" constant="5" id="kzd-BB-SzU"/>
+                        <constraint firstItem="GuK-qa-jwH" firstAttribute="trailing" secondItem="4uQ-6L-UfR" secondAttribute="trailing" constant="-8.5" id="lE8-KF-PEf"/>
                         <constraint firstItem="pQK-tY-S1I" firstAttribute="top" secondItem="oyf-C4-Pe3" secondAttribute="bottom" constant="16" id="lye-Zg-T75"/>
+                        <constraint firstItem="gDU-D3-OZv" firstAttribute="bottom" secondItem="zGK-Nm-bhv" secondAttribute="bottom" constant="-10" id="mfD-xI-XhQ"/>
                         <constraint firstItem="slK-h7-P25" firstAttribute="centerY" secondItem="96g-7r-vnm" secondAttribute="centerY" id="n4k-sg-aoH"/>
                         <constraint firstItem="ses-18-GMw" firstAttribute="top" secondItem="slK-h7-P25" secondAttribute="bottom" constant="10" id="nwQ-Zl-n0Z"/>
                         <constraint firstItem="YP9-QE-4Lm" firstAttribute="top" secondItem="Q7z-0G-roa" secondAttribute="bottom" constant="1" id="pNI-cJ-kcy"/>
+                        <constraint firstItem="Mqk-Mg-Pni" firstAttribute="bottom" secondItem="NBl-15-e5U" secondAttribute="bottom" constant="-10" id="pcU-C4-c4P"/>
                         <constraint firstItem="Q7z-0G-roa" firstAttribute="centerX" secondItem="Mqk-Mg-Pni" secondAttribute="centerX" id="r2q-WP-Qet"/>
                         <constraint firstItem="GuK-qa-jwH" firstAttribute="top" secondItem="Jqd-bC-HWT" secondAttribute="top" id="rZ4-6e-EoW"/>
                         <constraint firstItem="DzT-go-dxS" firstAttribute="bottom" secondItem="pQK-tY-S1I" secondAttribute="bottom" constant="5" id="s20-jc-4Hn"/>
                         <constraint firstItem="DzT-go-dxS" firstAttribute="leading" secondItem="pQK-tY-S1I" secondAttribute="leading" id="sW8-PU-ecw"/>
                         <constraint firstAttribute="trailing" secondItem="ucS-Gd-cx3" secondAttribute="trailing" constant="16" id="sh2-B9-nHe"/>
+                        <constraint firstItem="4Yk-Bc-KTb" firstAttribute="leading" secondItem="xzg-Xa-du5" secondAttribute="trailing" constant="5" id="uXE-R6-fAK"/>
                         <constraint firstItem="KFO-kx-bI9" firstAttribute="leading" secondItem="slK-h7-P25" secondAttribute="leading" constant="87" id="vNR-7w-pR5"/>
-                        <constraint firstItem="XPO-nW-Uox" firstAttribute="top" secondItem="ucS-Gd-cx3" secondAttribute="bottom" constant="20" id="vQT-M9-wQP"/>
+                        <constraint firstItem="XPO-nW-Uox" firstAttribute="top" secondItem="ucS-Gd-cx3" secondAttribute="bottom" constant="27" id="vQT-M9-wQP"/>
                         <constraint firstItem="pQK-tY-S1I" firstAttribute="top" secondItem="nyI-H7-gwi" secondAttribute="bottom" constant="7" id="wzO-w2-Rcm"/>
                         <constraint firstItem="f5Q-wh-6l8" firstAttribute="top" secondItem="Jqd-bC-HWT" secondAttribute="bottom" constant="1" id="xpQ-65-ym0"/>
+                        <constraint firstItem="GuK-qa-jwH" firstAttribute="bottom" secondItem="4uQ-6L-UfR" secondAttribute="bottom" constant="-10" id="zrA-b7-0nZ"/>
                     </constraints>
                 </view>
             </subviews>
@@ -382,9 +435,13 @@
                 <outlet property="bgSlider" destination="ucS-Gd-cx3" id="VCY-3N-zgV"/>
                 <outlet property="bgView" destination="dNG-od-Opv" id="1DR-aZ-txJ"/>
                 <outlet property="bgVolumeBubble" destination="TSI-LH-Hac" id="MMM-Pk-mfI"/>
+                <outlet property="delayGuideBg" destination="kcU-rG-UWE" id="zie-pA-G3a"/>
+                <outlet property="forwardGuideBg" destination="zGK-Nm-bhv" id="WfH-2M-Hxb"/>
+                <outlet property="offsetSliderBg" destination="4Yk-Bc-KTb" id="qs2-or-CeA"/>
                 <outlet property="offsetTipsLabel" destination="ses-18-GMw" id="HI9-Fn-LA7"/>
                 <outlet property="pointLeft" destination="vNR-7w-pR5" id="Th0-hj-S4p"/>
                 <outlet property="pointView" destination="KFO-kx-bI9" id="HqS-ma-hGF"/>
+                <outlet property="publishGuideBg" destination="beO-nV-nu5" id="3uq-H9-d1M"/>
                 <outlet property="reRecordImage" destination="Jqd-bC-HWT" id="sff-6V-G78"/>
                 <outlet property="recordBubble" destination="nyI-H7-gwi" id="biq-QL-T1A"/>
                 <outlet property="recordBubbleLabel" destination="oYa-cu-i4y" id="OWU-Kh-jDU"/>
@@ -395,7 +452,10 @@
                 <outlet property="recordImgWidth" destination="GDO-qk-rQt" id="WPb-Fx-8yt"/>
                 <outlet property="recordSlider" destination="pQK-tY-S1I" id="kTK-c2-yi8"/>
                 <outlet property="recordTitle" destination="f5Q-wh-6l8" id="loJ-19-rjA"/>
+                <outlet property="retryGuideBg" destination="4uQ-6L-UfR" id="p1E-jS-73O"/>
+                <outlet property="saveGuideBg" destination="NBl-15-e5U" id="hEZ-nx-E4K"/>
                 <outlet property="sliderView" destination="slK-h7-P25" id="cTD-pY-jtL"/>
+                <outlet property="volumeGuideBg" destination="unX-kF-GoX" id="Onv-cs-DTa"/>
             </connections>
             <point key="canvasLocation" x="80.152671755725194" y="-176.05633802816902"/>
         </view>
@@ -403,7 +463,7 @@
     <resources>
         <image name="align_left_bg" width="27" height="23"/>
         <image name="align_right_bg" width="27" height="23"/>
-        <image name="aligningSlider_bg" width="180.66667175292969" height="11.333333015441895"/>
+        <image name="aligningSlider_bg" width="181" height="11"/>
         <image name="cancleMerge_image" width="24" height="24"/>
         <image name="merge_next" width="14" height="14"/>
         <image name="merge_upload" width="24" height="24"/>

+ 6 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeAudioAnimation/KSRealtimeAnalyzer.m

@@ -20,6 +20,8 @@
 
 @property (nonatomic, strong) NSArray<NSNumber *> *cachedFrequencyWeights;
 
+@property (nonatomic, assign) BOOL isCanceled;
+
 @end
 
 @implementation KSRealtimeAnalyzer
@@ -43,6 +45,7 @@
 }
 
 - (void)dealloc {
+    self.isCanceled = YES;
     vDSP_destroy_fftsetup(_fftSetup);
     free(_realp);
     free(_imagp);
@@ -64,6 +67,9 @@
 }
 
 - (NSArray<NSArray<NSNumber *> *> *)analyseWithBuffer:(AVAudioPCMBuffer *)buffer {
+    if (self.isCanceled) {
+        return [NSArray array];
+    }
     self.isAnalise = YES;
     NSArray<NSArray<NSNumber *> *> *channelsAmplitudes = [self fftWithBuffer:buffer];
     NSArray<NSNumber *> *aWeights = self.cachedFrequencyWeights;

+ 2 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeAudioAnimation/KSSpectrumView.h

@@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface KSSpectrumView : UIView
 
+@property (nonatomic, assign) BOOL isModify;
+
 @property (nonatomic, assign) CGFloat barWidth;
 @property (nonatomic, assign) CGFloat space;
 

+ 41 - 34
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeAudioAnimation/KSSpectrumView.m

@@ -38,8 +38,8 @@
 
     self.combinedGradientLayer = [CAGradientLayer layer];
     self.combinedGradientLayer.colors = @[
-        (__bridge id)HexRGBAlpha(0xffffff, 0.32f).CGColor,
-        (__bridge id)HexRGBAlpha(0xffffff, 0.32f).CGColor
+        (__bridge id)HexRGBAlpha(0xffffff, 1.0f).CGColor,
+        (__bridge id)HexRGBAlpha(0xffffff, 1.0f).CGColor
     ];
     self.combinedGradientLayer.locations = @[@0.6, @1.0];
     [self.layer addSublayer:self.combinedGradientLayer];
@@ -52,41 +52,48 @@
 - (void)setSpectra:(NSArray<NSArray<NSNumber *> *> *)spectra {
     _spectra = spectra;
     if (spectra) {
-        NSUInteger spectraCount = [spectra[0] count];
-        NSMutableArray<NSNumber *> *combinedSpectrum = [NSMutableArray arrayWithCapacity:spectraCount];
-        // 取两个声道数据中的最大值
-        for (NSUInteger i = 0; i < spectraCount; i++) {
-            NSNumber *leftAmplitude = spectra[0][i];
-            NSNumber *rightAmplitude = spectra.count > 1 ? spectra[1][i] : @0;
-            CGFloat maxAmplitude = MAX(leftAmplitude.floatValue, rightAmplitude.floatValue);
-            [combinedSpectrum addObject:@(maxAmplitude)];
-        }
-        
         CGFloat viewHeight = self.bounds.size.height;
         CGFloat viewWidth = self.bounds.size.width;
-        CGFloat middleY = viewHeight / 2.0;
-        CGFloat barHeight = (viewHeight) / 2.0;
-        CGFloat cornerRadius = viewWidth / 2.0f;
-        CGFloat xIncrement = self.barWidth + self.space;
-        UIBezierPath *combinedPath = [UIBezierPath bezierPath];
-
-        // Left channel
-        for (NSUInteger i = 0; i < spectraCount; i++) {
-            CGFloat x = i * xIncrement + self.space;
-            CGFloat amplitudeValue = combinedSpectrum[i].floatValue;
-            CGFloat height = amplitudeValue * barHeight;
-            CGFloat y = middleY - height/2.0; // Centered vertically
+        @weakObj(self);
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+            @strongObj(self);
+            self.isModify = YES;
+            NSUInteger spectraCount = [spectra[0] count];
+            NSMutableArray<NSNumber *> *combinedSpectrum = [NSMutableArray arrayWithCapacity:spectraCount];
+            // 取两个声道数据中的最大值
+            for (NSUInteger i = 0; i < spectraCount; i++) {
+                NSNumber *leftAmplitude = spectra[0][i];
+                NSNumber *rightAmplitude = spectra.count > 1 ? spectra[1][i] : @0;
+                CGFloat maxAmplitude = MAX(leftAmplitude.floatValue, rightAmplitude.floatValue);
+                [combinedSpectrum addObject:@(maxAmplitude)];
+            }
             
-            CGRect rect = CGRectMake(x, y, self.barWidth, height);
-            UIBezierPath *barPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius];
-            [combinedPath appendPath:barPath];
-        }
-        
-        CAShapeLayer *combinedMaskLayer = [CAShapeLayer layer];
-        combinedMaskLayer.path = combinedPath.CGPath;
-        self.combinedGradientLayer.frame = CGRectMake(0, 0, viewWidth, viewHeight);
-        self.combinedGradientLayer.mask = combinedMaskLayer;
-
+            CGFloat middleY = viewHeight / 2.0;
+            CGFloat barHeight = (viewHeight) / 2.0;
+            CGFloat cornerRadius = viewWidth / 2.0f;
+            CGFloat xIncrement = self.barWidth + self.space;
+            UIBezierPath *combinedPath = [UIBezierPath bezierPath];
+            
+            // Left channel
+            for (NSUInteger i = 0; i < spectraCount; i++) {
+                CGFloat x = i * xIncrement + self.space;
+                CGFloat amplitudeValue = combinedSpectrum[i].floatValue;
+                CGFloat height = amplitudeValue * barHeight;
+                CGFloat y = middleY - height/2.0; // Centered vertically
+                
+                CGRect rect = CGRectMake(x, y, self.barWidth, height);
+                UIBezierPath *barPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius];
+                [combinedPath appendPath:barPath];
+            }
+            
+            dispatch_async(dispatch_get_main_queue(), ^{
+                CAShapeLayer *combinedMaskLayer = [CAShapeLayer layer];
+                combinedMaskLayer.path = combinedPath.CGPath;
+                self.combinedGradientLayer.frame = CGRectMake(0, 0, viewWidth, viewHeight);
+                self.combinedGradientLayer.mask = combinedMaskLayer;
+                self.isModify = NO;
+            });
+        });
     }
 }
 

+ 36 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideManager.h

@@ -0,0 +1,36 @@
+//
+//  KSBaseGuideManager.h
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/26.
+//
+
+#import <Foundation/Foundation.h>
+#import "KSBaseGuideModel.h"
+
+typedef void(^BaseGuideFinishCallback)(void);
+
+#define MERGE_GUIDEKEY (@"MergeGuideKey")  // 作品引导
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KSBaseGuideManager : NSObject
+
+@property (nonatomic, assign) BOOL hideSkipButton;
+
+- (void)configskipButtonImage:(NSString *)skipButtonImage nextButtonImage:(NSString *)nextButtonImage replayButtonImage:(NSString *)replayButtonImage finishButtonImage:(NSString *)finishButtonImage;
+
+- (void)addGuideModel:(KSBaseGuideModel *)model;
+
+- (void)clearAllGuideModel;
+
+- (void)getNextGuideModel;
+
+- (void)beginGuide;
+
+- (BOOL)canShowNext;
+
+- (void)GuideFinish:(BaseGuideFinishCallback)callback;
+@end
+
+NS_ASSUME_NONNULL_END

+ 125 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideManager.m

@@ -0,0 +1,125 @@
+//
+//  KSBaseGuideManager.m
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/26.
+//
+
+#import "KSBaseGuideManager.h"
+#import "KSBaseGuideView.h"
+
+@interface KSBaseGuideManager ()
+
+@property (nonatomic, strong) NSMutableArray<KSBaseGuideModel *> *guideSourceArray;
+
+// 记录当前显示索引
+@property (nonatomic, assign) NSInteger showIndex;
+
+@property (nonatomic, strong) KSBaseGuideView *guideView;
+
+@property (nonatomic, copy) BaseGuideFinishCallback callback;
+
+@property (nonatomic, strong) NSString *skipButtonImage;
+
+@property (nonatomic, strong) NSString *nextButtonImage;
+
+@property (nonatomic, strong) NSString *replayButtonImage;
+
+@property (nonatomic, strong) NSString *finishButtonImage;
+@end
+
+
+@implementation KSBaseGuideManager
+
+- (void)GuideFinish:(BaseGuideFinishCallback)callback {
+    if (callback) {
+        self.callback = callback;
+    }
+}
+
+- (void)clearAllGuideModel {
+    self.showIndex = 0;
+    [self.guideSourceArray removeAllObjects];
+}
+
+- (void)configskipButtonImage:(NSString *)skipButtonImage nextButtonImage:(NSString *)nextButtonImage replayButtonImage:(NSString *)replayButtonImage finishButtonImage:(NSString *)finishButtonImage {
+    self.skipButtonImage = skipButtonImage;
+    self.nextButtonImage = nextButtonImage;
+    self.replayButtonImage = replayButtonImage;
+    self.finishButtonImage = finishButtonImage;
+}
+
+- (void)addGuideModel:(KSBaseGuideModel *)model {
+    [self.guideSourceArray addObject:model];
+}
+
+- (void)beginGuide {
+    if (self.guideSourceArray.count) {
+        if (!self.guideView) {
+            KSBaseGuideView *guideView = [[KSBaseGuideView alloc] initWithFrame:[NSObject getKeyWindow].frame];
+            [guideView configskipButtonImage:self.skipButtonImage nextButtonImage:self.nextButtonImage replayButtonImage:self.replayButtonImage finishButtonImage:self.finishButtonImage];
+            guideView.hideSkipButton = self.hideSkipButton;
+            @weakObj(self);
+            guideView.callback = ^(KS_NEXTSHOW showType) {
+                @strongObj(self);
+                if (showType == KS_NEXTSHOW_NEXT) {
+                    [self getNextGuideModel];
+                }
+                else if (showType == KS_NEXTSHOW_REPLAY) {
+                    self.showIndex = 0;
+                    [self beginGuide];
+                }
+                else {
+                    [self clearAllGuideModel];
+                    [self.guideView removeFromSuperview];
+                    self.guideView = nil;
+                    if (self.callback) {
+                        self.callback();
+                    }
+                }
+            };
+            // 移除之前存在的guide view
+            [self removeOtherGuideView];
+            self.guideView = guideView;
+            [[NSObject getKeyWindow] addSubview:self.guideView];
+        }
+        // 展示第一个
+        [self getNextGuideModel];
+    }
+}
+
+- (void)removeOtherGuideView {
+    UIView *displayView = [NSObject getKeyWindow];
+    for (UIView *subView in displayView.subviews) {
+        if ([subView isKindOfClass:[KSBaseGuideView class]]) {
+            [subView removeFromSuperview];
+            break;
+        }
+    }
+}
+
+- (void)getNextGuideModel {
+    if (self.showIndex < self.guideSourceArray.count) {
+        if (self.guideView) {
+            [self.guideView addGuideLayerWithModel:self.guideSourceArray[self.showIndex]];
+        }
+    }
+    self.showIndex++;
+}
+
+- (BOOL)canShowNext {
+    if (self.showIndex == self.guideSourceArray.count -1) {
+        return NO;
+    }
+    else {
+        return YES;
+    }
+}
+
+- (NSMutableArray<KSBaseGuideModel *> *)guideSourceArray {
+    if (!_guideSourceArray) {
+        _guideSourceArray = [NSMutableArray array];
+    }
+    return _guideSourceArray;
+}
+@end

+ 28 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideModel.h

@@ -0,0 +1,28 @@
+//
+//  KSBaseGuideModel.h
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/26.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KSBaseGuideModel : NSObject
+
+@property (nonatomic, strong) NSString *displayImageName;
+
+@property (nonatomic, assign) NSInteger currentIndex;
+
+@property (nonatomic, assign) NSInteger totalIndex;
+
+@property (nonatomic, assign) CGRect guideViewFrame;
+
+@property (nonatomic, assign) CGRect imageFrame;
+
+@property (nonatomic, assign) CGRect buttonFrame;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 12 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideModel.m

@@ -0,0 +1,12 @@
+//
+//  KSBaseGuideModel.m
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/26.
+//
+
+#import "KSBaseGuideModel.h"
+
+@implementation KSBaseGuideModel
+
+@end

+ 34 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideView.h

@@ -0,0 +1,34 @@
+//
+//  KSBaseGuideView.h
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/26.
+//
+
+#import <UIKit/UIKit.h>
+#import "KSBaseGuideModel.h"
+
+
+typedef NS_ENUM(NSInteger, KS_NEXTSHOW) {
+    KS_NEXTSHOW_NEXT,
+    KS_NEXTSHOW_SKIP,
+    KS_NEXTSHOW_REPLAY,
+};
+
+typedef void(^BaseNextShowBlock)(KS_NEXTSHOW showType);
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KSBaseGuideView : UIView
+
+@property (nonatomic, assign) BOOL hideSkipButton;
+
+@property (nonatomic, copy) BaseNextShowBlock callback;
+
+- (void)configskipButtonImage:(NSString *)skipButtonImage nextButtonImage:(NSString *)nextButtonImage replayButtonImage:(NSString *)replayButtonImage finishButtonImage:(NSString *)finishButtonImage;
+
+- (void)addGuideLayerWithModel:(KSBaseGuideModel *)guideModel;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 249 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeGuide/KSBaseGuideView.m

@@ -0,0 +1,249 @@
+//
+//  KSBaseGuideView.m
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/26.
+//
+
+#import "KSBaseGuideView.h"
+
+@interface KSBaseGuideView ()
+
+@property (nonatomic, strong) UIImageView *displayImageView;
+
+// 跳过引导
+@property (nonatomic, strong) UIButton *skipButton;
+
+// 下一步
+@property (nonatomic, strong) UIButton *nextButton;
+
+// 再看一遍
+@property (nonatomic, strong) UIButton *replayButton;
+
+// 完成按钮
+@property (nonatomic, strong) UIButton *finishButton;
+
+@property (nonatomic, strong) KSBaseGuideModel *guideModel;
+
+@property (nonatomic, assign) BOOL isLastPage;
+
+@property (nonatomic, strong) NSString *skipButtonImage;
+
+@property (nonatomic, strong) NSString *nextButtonImage;
+
+@property (nonatomic, strong) NSString *replayButtonImage;
+
+@property (nonatomic, strong) NSString *finishButtonImage;
+
+@end
+
+@implementation KSBaseGuideView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (self) {
+        self.backgroundColor = [UIColor clearColor];
+        [self addTapGesture];
+    }
+    return self;
+}
+
+- (void)addTapGesture {
+    UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
+    [self addGestureRecognizer:gesture];
+}
+
+- (void)tapAction {
+    if (self.guideModel.currentIndex == self.guideModel.totalIndex - 1) {
+        if (self.callback) {
+            self.callback(KS_NEXTSHOW_SKIP);
+        }
+    }
+    else {
+        if (self.callback) {
+            self.callback(KS_NEXTSHOW_NEXT);
+        }
+    }
+}
+
+- (void)configskipButtonImage:(NSString *)skipButtonImage nextButtonImage:(NSString *)nextButtonImage replayButtonImage:(NSString *)replayButtonImage finishButtonImage:(NSString *)finishButtonImage {
+    self.skipButtonImage = skipButtonImage;
+    self.nextButtonImage = nextButtonImage;
+    self.replayButtonImage = replayButtonImage;
+    self.finishButtonImage = finishButtonImage;
+}
+
+- (void)addGuideLayerWithModel:(KSBaseGuideModel *)guideModel {
+    if (guideModel.currentIndex == guideModel.totalIndex - 1) {
+        self.isLastPage = YES;
+    }
+    else {
+        self.isLastPage = NO;
+    }
+    [self clear];
+    [self drawMaskWithModel:guideModel];
+    [self addExtendBubble];
+    [self addShowViews];
+}
+
+- (void)clear {
+    [self removeAllSubViews];
+    [self removeallSubLayers];
+}
+
+- (void)removeallSubLayers {
+    [self.layer.sublayers makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
+}
+
+- (void)drawMaskWithModel:(KSBaseGuideModel *)guideModel {
+    self.guideModel = guideModel;
+    CGFloat addSize = 0;
+    CGRect guideFrame = guideModel.guideViewFrame;
+    CGRect guideViewFrame = CGRectMake(CGRectGetMinX(guideFrame) - addSize, CGRectGetMinY(guideFrame) - addSize , CGRectGetWidth(guideFrame) + addSize * 2, CGRectGetHeight(guideFrame) + addSize * 2);
+    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.frame cornerRadius:0];
+    // 镂空
+    UIBezierPath *carvedPath = [UIBezierPath bezierPathWithRoundedRect:guideViewFrame cornerRadius:10.0f];
+    [path appendPath:carvedPath];
+    [path setUsesEvenOddFillRule:YES];
+    
+    CAShapeLayer *fillLayer = [CAShapeLayer layer];
+    fillLayer.path = path.CGPath;
+    // 中间镂空 填充规则
+    fillLayer.fillRule = kCAFillRuleEvenOdd;
+    fillLayer.fillColor = HexRGBAlpha(0x000000, 0.68f).CGColor;
+    [self.layer addSublayer:fillLayer];
+}
+
+- (void)addExtendBubble {
+    UIImage *displayImage = [UIImage imageNamed:self.guideModel.displayImageName];
+    self.displayImageView = [[UIImageView alloc] initWithImage:displayImage];
+    self.displayImageView.frame = self.guideModel.imageFrame;
+    [self addSubview:self.displayImageView];
+}
+
+
+- (void)addShowViews {
+    if (self.isLastPage == NO) {
+        if (self.hideSkipButton == NO) {
+            // 跳过
+            [self addSubview:self.skipButton];
+            CGFloat leftSpace = 20;
+            [self.skipButton mas_makeConstraints:^(MASConstraintMaker *make) {
+                make.top.mas_equalTo(self.mas_top).offset(20);
+                make.width.mas_equalTo(48);
+                make.height.mas_equalTo(24);
+                make.left.mas_equalTo(self.mas_left).offset(leftSpace);
+            }];
+        }
+        
+        [self addSubview:self.nextButton];
+        [self.nextButton setTitle:[NSString stringWithFormat:@"下一步 (%zd/%zd)", self.guideModel.currentIndex+1, self.guideModel.totalIndex] forState:UIControlStateNormal];
+        self.nextButton.frame = self.guideModel.buttonFrame;
+    }
+    else {
+        // 显示跳过按钮
+        [self addSubview:self.skipButton];
+        CGFloat leftSpace = 20;
+        [self.skipButton mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.top.mas_equalTo(self.mas_top).offset(20);
+            make.width.mas_equalTo(48);
+            make.height.mas_equalTo(24);
+            make.left.mas_equalTo(self.mas_left).offset(leftSpace);
+        }];
+        
+        // 完成
+        [self addSubview:self.finishButton];
+        self.finishButton.frame = self.guideModel.buttonFrame;
+        
+        // 再来一次
+        [self addSubview:self.replayButton];
+        [self.replayButton mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.left.mas_equalTo(self.finishButton.mas_right).offset(14);
+            make.width.mas_equalTo(82);
+            make.height.mas_equalTo(32);
+            make.centerY.mas_equalTo(self.finishButton.mas_centerY);
+        }];
+        
+    }
+}
+
+- (void)skipGuideAction {
+    if (self.callback) {
+        self.callback(KS_NEXTSHOW_SKIP);
+    }
+}
+
+- (void)replayGuideAction {
+    if (self.callback) {
+        self.callback(KS_NEXTSHOW_REPLAY);
+    }
+}
+
+- (void)nextGuideAction {
+    if (self.callback) {
+        self.callback(KS_NEXTSHOW_NEXT);
+    }
+}
+
+- (UIButton *)skipButton {
+    if (!_skipButton) {
+        _skipButton = [UIButton buttonWithType:UIButtonTypeCustom];
+        [_skipButton setBackgroundColor:[UIColor clearColor]];
+        [_skipButton setBackgroundImage:[UIImage imageNamed:self.skipButtonImage] forState:UIControlStateNormal];
+        [_skipButton addTarget:self action:@selector(skipGuideAction) forControlEvents:UIControlEventTouchUpInside];
+        _skipButton.adjustsImageWhenHighlighted = NO;
+    }
+    return _skipButton;
+}
+
+- (UIButton *)replayButton {
+    if (!_replayButton) {
+        _replayButton = [UIButton buttonWithType:UIButtonTypeCustom];
+        [_replayButton setTitle:@"再看一遍" forState:UIControlStateNormal];
+        [_replayButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
+        [_replayButton setTitleColor:HexRGB(0xFFFFFF) forState:UIControlStateNormal];
+        [_replayButton.titleLabel setFont:[UIFont systemFontOfSize:13.0f weight:UIFontWeightSemibold]];
+        [_replayButton setBackgroundColor:[UIColor clearColor]];
+        [_replayButton setBackgroundImage:[UIImage imageNamed:self.replayButtonImage] forState:UIControlStateNormal];
+        _replayButton.adjustsImageWhenHighlighted = NO;
+        [_replayButton addTarget:self action:@selector(replayGuideAction) forControlEvents:UIControlEventTouchUpInside];
+    }
+    return _replayButton;
+}
+
+- (UIButton *)nextButton {
+    if (!_nextButton) {
+        _nextButton = [UIButton buttonWithType:UIButtonTypeCustom];
+        [_nextButton setTitleColor:HexRGB(0x00807A) forState:UIControlStateNormal];
+        [_nextButton.titleLabel setFont:[UIFont systemFontOfSize:13.0f weight:UIFontWeightSemibold]];
+        [_nextButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
+        [_nextButton setBackgroundImage:[UIImage imageNamed:self.nextButtonImage] forState:UIControlStateNormal];
+        _nextButton.adjustsImageWhenHighlighted = NO;
+        [_nextButton addTarget:self action:@selector(nextGuideAction) forControlEvents:UIControlEventTouchUpInside];
+    }
+    return _nextButton;
+}
+
+- (UIButton *)finishButton {
+    if (!_finishButton) {
+        _finishButton = [UIButton buttonWithType:UIButtonTypeCustom];
+        [_finishButton setTitle:@"完成" forState:UIControlStateNormal];
+        [_finishButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
+        [_finishButton setTitleColor:HexRGB(0x00807A) forState:UIControlStateNormal];
+        [_finishButton.titleLabel setFont:[UIFont systemFontOfSize:13.0f weight:UIFontWeightSemibold]];
+        [_finishButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
+        [_finishButton setBackgroundImage:[UIImage imageNamed:self.finishButtonImage] forState:UIControlStateNormal];
+        _finishButton.adjustsImageWhenHighlighted = NO;
+        [_finishButton addTarget:self action:@selector(skipGuideAction) forControlEvents:UIControlEventTouchUpInside];
+    }
+    return _finishButton;
+}
+/*
+// Only override drawRect: if you perform custom drawing.
+// An empty implementation adversely affects performance during animation.
+- (void)drawRect:(CGRect)rect {
+    // Drawing code
+}
+*/
+
+@end

+ 33 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeMusicStaffView.h

@@ -0,0 +1,33 @@
+//
+//  MergeMusicStaffView.h
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/25.
+//
+
+#import <UIKit/UIKit.h>
+
+typedef void(^StaffPageCallback)(CGFloat pageHeight);
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MergeMusicStaffView : UIView
+
+@property (nonatomic, assign) BOOL isReady;
+
++ (instancetype)shareIntance;
+// backgroundMode video audio
+- (void)loadWebViewWithSongID:(NSString *)songID renderType:(NSString *)renderType partIndex:(NSInteger)partIndex backgroundMode:(NSString *)backgroundMode;
+
+- (void)webActionCallback:(StaffPageCallback)callback;
+
+- (void)staffPageStartPlay;
+
+- (void)staffPageStop:(NSTimeInterval)currentProgress;
+
+- (void)updatePlayProgress:(NSTimeInterval)currentProgress;
+
+- (void)updateSliderProgress:(NSTimeInterval)sliderProgress;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 276 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeMusicStaffView.m

@@ -0,0 +1,276 @@
+//
+//  MergeMusicStaffView.m
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/25.
+//
+
+#import "MergeMusicStaffView.h"
+#import <WebKit/WebKit.h>
+#import "WeakWebViewScriptMessageDelegate.h"
+#import "AuthChallengeManager.h"
+
+
+@interface MergeMusicStaffView ()<WKUIDelegate,WKNavigationDelegate,WKScriptMessageHandler,UIScrollViewDelegate>
+
+@property (nonatomic, strong) WKWebView *myWebView;
+
+@property (nonatomic, strong) NSString *url;
+
+@property (nonatomic, copy) StaffPageCallback callback;
+@end
+
+@implementation MergeMusicStaffView
+
++ (instancetype)shareIntance {
+    MergeMusicStaffView *view = [[MergeMusicStaffView alloc] initWithFrame:CGRectZero];
+    return view;
+}
+
+- (void)loadWebViewWithSongID:(NSString *)songID renderType:(NSString *)renderType partIndex:(NSInteger)partIndex backgroundMode:(NSString *)backgroundMode {
+    if (![self.subviews containsObject:self.myWebView]) {
+        [self addSubview:self.myWebView];
+        [_myWebView mas_makeConstraints:^(MASConstraintMaker *make) {
+            make.left.right.top.bottom.mas_equalTo(self);
+        }];
+    }
+    self.url = [NSString stringWithFormat:@"%@/#/simple-detail?id=%@&musicRenderType=%@&part-index=%zd&backgroundRendMode=%@", CLOUD_URL, songID, renderType,partIndex, backgroundMode];
+    [self loadRequest];
+}
+
+- (void)webActionCallback:(StaffPageCallback)callback {
+    if (callback) {
+        self.callback = callback;
+    }
+}
+
+- (void)staffPageStartPlay {
+    NSMutableDictionary *parm = [NSMutableDictionary dictionary];
+    [parm setValue:@"api_play" forKey:@"api"];
+    [parm setValue:@{} forKey:@"content"];
+    [self postMessage:parm];
+}
+
+- (void)staffPageStop:(NSTimeInterval)currentProgress {
+    NSMutableDictionary *parm = [NSMutableDictionary dictionary];
+    [parm setValue:@"api_paused" forKey:@"api"];
+    NSMutableDictionary *content = [NSMutableDictionary dictionary];
+    [content setValue:@(currentProgress) forKey:@"currentTime"];
+    [parm setValue:content forKey:@"content"];
+    [self postMessage:parm];
+}
+
+- (void)updatePlayProgress:(NSTimeInterval)currentProgress {
+    NSMutableDictionary *parm = [NSMutableDictionary dictionary];
+    [parm setValue:@"api_playProgress" forKey:@"api"];
+    NSMutableDictionary *content = [NSMutableDictionary dictionary];
+    [content setValue:@(currentProgress) forKey:@"currentTime"];
+    [parm setValue:content forKey:@"content"];
+    [self postMessage:parm];
+}
+
+- (void)updateSliderProgress:(NSTimeInterval)sliderProgress {
+    NSMutableDictionary *parm = [NSMutableDictionary dictionary];
+    [parm setValue:@"api_updateProgress" forKey:@"api"];
+    NSMutableDictionary *content = [NSMutableDictionary dictionary];
+    [content setValue:@(sliderProgress) forKey:@"currentTime"];
+    [parm setValue:content forKey:@"content"];
+    [self postMessage:parm];
+}
+
+- (void)loadRequest {
+    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0f];
+    [self.myWebView loadRequest:request];
+}
+
+- (void)configUserAgent:(WKWebViewConfiguration *)config {
+    NSString *oldUserAgent = config.applicationNameForUserAgent;
+    NSString *newAgent = [NSString stringWithFormat:@"%@ %@ %@",oldUserAgent,AGENT_NAME,AGENT_DOMAIN];
+    config.applicationNameForUserAgent = newAgent;
+}
+
+- (NSDictionary *)convertJsonStringToNSDictionary:(NSString *)jsonString {
+    if (jsonString == nil) {
+        return nil;
+    }
+    NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
+    NSError *error;
+    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
+    if (error) {
+        NSLog(@"jsonString解析失败:%@", error);
+        return nil;
+    }
+    return json;
+}
+
+
+#pragma mark - WKScriptMessageHandler
+- (void)userContentController:(WKUserContentController *)userContentController
+      didReceiveScriptMessage:(WKScriptMessage *)message {
+    
+    if ([message.name isEqualToString:SCRIPT_NAME]) {
+        NSDictionary *parm = [self convertJsonStringToNSDictionary:message.body];
+        NSLog(@"-----parm desc : %@ ", parm);
+        // 回到主线程
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self handleScriptMessageSource:parm];
+        });
+    }
+}
+
+- (void)handleScriptMessageSource:(NSDictionary *)parm {
+    if ([[parm ks_stringValueForKey:@"api"] isEqualToString:@"api_musicPage"]) {
+        self.isReady = YES;
+        CGFloat height = [[parm ks_dictionaryValueForKey:@"content"] ks_floatValueForKey:@"height"];
+        // 回调高度
+        if (self.callback) {
+            self.callback(height);
+        }
+    }
+}
+
+
+- (void)postMessage:(NSDictionary *)parm {
+    if (_myWebView) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+            NSString *jsString = [parm mj_JSONString];
+            [self.myWebView evaluateJavaScript:[NSString stringWithFormat:@"postMessage(%@,'*')", jsString] completionHandler:nil];
+        });
+    }
+}
+
+- (WKWebView *)myWebView {
+    if (!_myWebView) {
+        WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
+        config.selectionGranularity = WKSelectionGranularityDynamic;
+        config.allowsInlineMediaPlayback = YES;
+        config.mediaTypesRequiringUserActionForPlayback = NO;
+        config.processPool = [MergeMusicStaffView singleWkProcessPool];
+        config.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
+        [self configUserAgent:config];
+        
+        //自定义的WKScriptMessageHandler 是为了解决内存不释放的问题
+        WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];
+        //这个类主要用来做native与JavaScript的交互管理
+        WKUserContentController * wkUController = [[WKUserContentController alloc] init];
+        [wkUController addScriptMessageHandler:weakScriptMessageDelegate name:SCRIPT_NAME];
+        config.userContentController = wkUController;
+        
+        WKPreferences *preferences = [WKPreferences new];
+        // 是否支出javaScript
+        preferences.javaScriptEnabled = YES;
+        //不通过用户交互,是否可以打开窗口
+        preferences.javaScriptCanOpenWindowsAutomatically = YES;
+        config.preferences = preferences;
+        _myWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
+        _myWebView.UIDelegate = self;
+        _myWebView.navigationDelegate = self;
+        _myWebView.scrollView.bounces = NO;
+        _myWebView.opaque = NO;
+        _myWebView.scrollView.alwaysBounceVertical = NO;
+        _myWebView.scrollView.alwaysBounceHorizontal = NO;
+        _myWebView.scrollView.showsVerticalScrollIndicator = NO;
+        _myWebView.scrollView.showsHorizontalScrollIndicator = NO;
+        _myWebView.scrollView.delegate = self;
+        _myWebView.backgroundColor = [UIColor clearColor];
+        _myWebView.scrollView.backgroundColor = [UIColor clearColor];
+#ifdef DEBUG
+            if (@available(iOS 16.4, *)) {
+                _myWebView.inspectable = YES;
+            }
+#endif
+    }
+    return _myWebView;
+}
+
+- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler
+{
+    dispatch_queue_t queue =  dispatch_queue_create("webViewChallengeQueue", NULL);
+    dispatch_async(queue, ^{
+        if (SSL_AUTH) {
+
+            NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
+            NSURLCredential *customCredential = nil;
+            
+            if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+                // 默认信任
+                customCredential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
+                disposition = NSURLSessionAuthChallengeUseCredential;
+            }
+            else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) {
+                // client authentication
+                SecIdentityRef identity = NULL;
+                SecTrustRef trust = NULL;
+                if ([AuthChallengeManager extractIdentity:&identity andTrust:&trust filePath:CERT_PATH]) {
+                    SecCertificateRef certificate = NULL;
+                    SecIdentityCopyCertificate(identity, &certificate);
+                    const void*certs[] = {certificate};
+                    CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs,1,NULL);
+                    customCredential =[NSURLCredential credentialWithIdentity:identity certificates:(__bridge  NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
+                    disposition = NSURLSessionAuthChallengeUseCredential;
+                    // 释放 CFArrayRef 和 SecCertificateRef
+                    CFRelease(certArray);
+                    CFRelease(certificate);
+                }
+            }
+            
+            if (completionHandler) {
+                completionHandler(disposition, customCredential);
+            }
+        }
+        else {
+            if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+                if (challenge.previousFailureCount == 0) {
+                    NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
+                    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
+                } else {
+                    completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
+                }
+            } else {
+                completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
+            }
+        }
+    });
+}
+
++ (WKProcessPool*)singleWkProcessPool {
+    static WKProcessPool *sharedPool;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedPool = [[WKProcessPool alloc] init];
+    });
+    return sharedPool;
+}
+
+- (void)removeMemoryCache {
+    WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
+    NSSet *websiteDataTypes = [NSSet setWithArray:@[
+        WKWebsiteDataTypeMemoryCache,
+    ]];
+
+    [dataStore removeDataOfTypes:websiteDataTypes
+                   modifiedSince:[NSDate dateWithTimeIntervalSince1970:0]
+               completionHandler:^{
+        NSLog(@"Memory cache, disk cache, and cookies cleared");
+    }];
+}
+
+- (void)dealloc {
+    NSLog(@"---- web dealloc");
+    [self removeMemoryCache];
+    [[_myWebView configuration].userContentController removeScriptMessageHandlerForName:SCRIPT_NAME];
+    [_myWebView loadHTMLString:@"" baseURL:nil];
+    [_myWebView removeFromSuperview];
+    _myWebView = nil;
+
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+/*
+// Only override drawRect: if you perform custom drawing.
+// An empty implementation adversely affects performance during animation.
+- (void)drawRect:(CGRect)rect {
+    // Drawing code
+}
+*/
+
+@end

+ 4 - 1
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.h

@@ -38,7 +38,10 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface KSMergeEnginePlayer : NSObject
 
-@property (nonatomic, strong) KSRealtimeAnalyzer *analyzer;
+// 是否需要分析音频
+@property (nonatomic, assign) BOOL needAnalyzer;
+
+@property (nonatomic, strong, nullable) KSRealtimeAnalyzer *analyzer;
 
 @property (nonatomic, weak) id <KSMergeEnginePlayerDelegate>delegate;
 

+ 74 - 22
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergePlayer/KSMergeEnginePlayer.m

@@ -41,6 +41,8 @@
 
 @property (nonatomic, assign) double sampleRate;
 
+@property (nonatomic, assign) BOOL isInterrupt; // 是否被打断
+
 @property (nonatomic, assign) NSInteger timeCount;
 
 @property (nonatomic, assign) BOOL isCanceled; // 是否在取消
@@ -96,6 +98,9 @@
     [self.audioEngine attachNode:self.nodePlayer];
     AVAudioFormat *outputFormat = [self.audioEngine.mainMixerNode outputFormatForBus:0];
     [self.audioEngine connect:self.nodePlayer to:self.audioEngine.mainMixerNode format:outputFormat];
+    if (self.needAnalyzer && self.analyzer) {
+        [self installTapIfNeeded];
+    }
 }
 
 - (void)startEngine {
@@ -105,7 +110,10 @@
     // 启动engine
     NSError *error = nil;
     @try {
-        [self.audioEngine startAndReturnError:&error];
+        BOOL status =  [self.audioEngine startAndReturnError:&error];
+        if (status == NO) {
+            NSLog(@"-----");
+        }
     } @catch (NSException *exception) {
         NSLog(@"--------Exception: %@", exception);
     } @finally {
@@ -133,6 +141,10 @@
         
         AVAudioFormat *outputFormat = [self.audioEngine.mainMixerNode outputFormatForBus:0];
         [self.audioEngine connect:self.nodePlayer to:self.audioEngine.mainMixerNode format:outputFormat];
+        if (self.needAnalyzer) {
+            [self createAnalyzer];
+            [self installTapIfNeeded];
+        }
         [self startEngine];
         
         if (self.audioEngine && self.audioEngine.isRunning) {
@@ -141,34 +153,41 @@
     });
 }
 
-- (void)addTapBus {
-    BOOL delegateRespondsToDidGenerateSpectrum = [self.delegate respondsToSelector:@selector(player:didGenerateSpectrum:)];
+- (void)createAnalyzer {
     self.analyzer = [[KSRealtimeAnalyzer alloc] initWithFFTSize:BUFFER_SIZE];
+}
+
+- (void)installTapIfNeeded {
+    BOOL delegateRespondsToDidGenerateSpectrum = [self.delegate respondsToSelector:@selector(player:didGenerateSpectrum:)];
     AVAudioFormat *outputFormat = [self.audioEngine.mainMixerNode outputFormatForBus:0];
-    @weakObj(self);
     [self.audioEngine.mainMixerNode removeTapOnBus:0];
+    @weakObj(self);
     [self.audioEngine.mainMixerNode installTapOnBus:0 bufferSize:BUFFER_SIZE format:outputFormat block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
         @strongObj(self);
-        if (!self || !self.nodePlayer.isPlaying) {
+        if (!self || !self.nodePlayer.isPlaying || self.isCanceled) {
             return;
         }
         // 将频谱分析任务提交到后台队列
-            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
-                if (self.analyzer.isAnalise == NO) {
-                    // 分析音频缓冲区
-                    NSArray<NSArray<NSNumber *> *> *spectra = [self.analyzer analyseWithBuffer:buffer];
-                    
-                    // 回到主线程更新 UI 或调用委托方法
-                    dispatch_async(dispatch_get_main_queue(), ^{
-                        if (delegateRespondsToDidGenerateSpectrum) {
-                            [self.delegate player:self didGenerateSpectrum:spectra];
-                        }
-                    });
-                }
-            });
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            if (self.analyzer.isAnalise == NO) {
+                // 分析音频缓冲区
+                NSArray<NSArray<NSNumber *> *> *spectra = [self.analyzer analyseWithBuffer:buffer];
+                
+                // 回到主线程更新 UI 或调用委托方法
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    if (delegateRespondsToDidGenerateSpectrum) {
+                        [self.delegate player:self didGenerateSpectrum:spectra];
+                    }
+                });
+            }
+        });
     }];
 }
 
+- (void)removeTap {
+    [self.audioEngine.mainMixerNode removeTapOnBus:0];
+}
+
 - (void)loadAuidoFile:(NSURL *)audioFileUrl isBgm:(BOOL)isBgm {
     NSError *error = nil;
     AVAudioFile *audioFile = nil;
@@ -438,6 +457,10 @@
 }
 
 - (void)seekToTime:(NSInteger)time {
+    if (self.isInterrupt) {
+        [self sendInterruptError:nil];
+        return;
+    }
     if (self.audioEngine.isRunning == NO) {
         [self startEngine];
     }
@@ -477,6 +500,9 @@
 - (void)freePlayer {
     self.isCanceled = YES;
     [self stopPlay];
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+        [self removeTap];
+    });
 }
 
 - (void)startTimer {
@@ -585,12 +611,38 @@
     return NO;
 }
 
+- (void)releaseAudioResources {
+    if (self.audioEngine.isRunning) {
+        [self.audioEngine stop];
+        [self.audioEngine reset];
+    }
+    // 停止并移除 player node
+    if (self.nodePlayer) {
+        [self.nodePlayer stop];
+        [self.audioEngine detachNode:self.nodePlayer];
+        self.nodePlayer = nil;
+    }
+    if (self.analyzer) {
+        self.analyzer = nil;
+    }
+    
+    // 释放音频文件
+    self.audioFile = nil;
+    self.audioFormat = nil;
+    self.bgAudioFile = nil;
+    self.bgAudioFormat = nil;
+    self.mixBuffer = nil;
+    
+    // 释放音频引擎
+    self.audioEngine = nil;
+    
+    // 打印确认释放日志
+    NSLog(@"Audio resources successfully released.");
+}
+
 - (void)dealloc {
     NSLog(@"---- KSMergeEnginePlayer dealloc");
-    if (_audioEngine) {
-        [_audioEngine disconnectNodeInput:self.nodePlayer];
-        [_audioEngine detachNode:self.nodePlayer];
-    }
+    [self releaseAudioResources];
     // 停止并清理定时器
     if (_timer) {
         [_timer invalidate];

+ 26 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeTipsAlert.h

@@ -0,0 +1,26 @@
+//
+//  MergeTipsAlert.h
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/15.
+//
+
+#import <UIKit/UIKit.h>
+
+typedef void(^MergeTipsCallback)(void);
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MergeTipsAlert : UIView
+
++ (instancetype)shareInstance;
+
+- (void)configWithDesc:(NSString *)desc leftTitle:(NSString *)leftTitle rightTitle:(NSString *)rightTitle;
+
+- (void)actionCallbackCancel:(MergeTipsCallback)cancel sure:(MergeTipsCallback)sure;
+
+- (void)showAlert;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 102 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeTipsAlert.m

@@ -0,0 +1,102 @@
+//
+//  MergeTipsAlert.m
+//  KulexiuSchoolStudent
+//
+//  Created by 王智 on 2024/7/15.
+//
+
+#import "MergeTipsAlert.h"
+
+@interface MergeTipsAlert ()
+
+@property (weak, nonatomic) IBOutlet UIView *colorView;
+
+@property (weak, nonatomic) IBOutlet UILabel *tipsLabel;
+
+@property (weak, nonatomic) IBOutlet UIButton *leftButton;
+
+@property (weak, nonatomic) IBOutlet UIButton *rightButton;
+
+@property (nonatomic, copy) MergeTipsCallback cancelCallback;
+
+@property (nonatomic, copy) MergeTipsCallback sureCallback;
+
+@end
+
+@implementation MergeTipsAlert
+
+- (void)awakeFromNib {
+    [super awakeFromNib];
+}
+
+
++ (instancetype)shareInstance {
+    MergeTipsAlert *view = [[[NSBundle mainBundle] loadNibNamed:@"MergeTipsAlert" owner:nil options:nil] firstObject];
+    return view;
+}
+
+- (void)configWithDesc:(NSString *)desc leftTitle:(NSString *)leftTitle rightTitle:(NSString *)rightTitle {
+    if ([NSString isEmptyString:desc]) {
+        desc = @"";
+    }
+    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
+    [paragraphStyle setLineSpacing:4];//调整行间距
+    paragraphStyle.alignment = NSTextAlignmentCenter;
+    paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
+    NSMutableAttributedString *tipsAttrs = [[NSMutableAttributedString alloc] initWithString:desc attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16.0f weight:UIFontWeightRegular], NSForegroundColorAttributeName:HexRGB(0x777777),NSParagraphStyleAttributeName:paragraphStyle}];
+    self.tipsLabel.attributedText = tipsAttrs;
+    if (![NSString isEmptyString:leftTitle]) {
+        [self.leftButton setTitle:leftTitle forState:UIControlStateNormal];
+    }
+    if (![NSString isEmptyString:rightTitle]) {
+        [self.rightButton setTitle:rightTitle forState:UIControlStateNormal];
+    }
+    [self layoutIfNeeded];
+}
+
+- (void)actionCallbackCancel:(MergeTipsCallback)cancel sure:(MergeTipsCallback)sure {
+    
+    if (cancel) {
+        self.cancelCallback = cancel;
+    }
+    if (sure) {
+        self.sureCallback = sure;
+    }
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+}
+
+
+- (void)showAlert {
+    UIView *displayView = [NSObject getKeyWindow];
+    [displayView addSubview:self];
+    [self mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.left.right.top.bottom.mas_equalTo(displayView);
+    }];
+}
+
+- (IBAction)cancelAction:(id)sender {
+    [self removeFromSuperview];
+    if (self.cancelCallback) {
+        self.cancelCallback();
+    }
+}
+
+- (IBAction)sureAction:(id)sender {
+    [self removeFromSuperview];
+    if (self.sureCallback) {
+        self.sureCallback();
+    }
+}
+
+/*
+// Only override drawRect: if you perform custom drawing.
+// An empty implementation adversely affects performance during animation.
+- (void)drawRect:(CGRect)rect {
+    // Drawing code
+}
+*/
+
+@end

+ 127 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/MediaMerge/AudioMerge/MergeTipsAlert.xib

@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" 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="23084"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="MergeTipsAlert">
+            <rect key="frame" x="0.0" y="0.0" width="936" height="939"/>
+            <autoresizingMask key="autoresizingMask"/>
+            <subviews>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zfV-ZE-Fdh">
+                    <rect key="frame" x="324.66666666666669" y="386" width="287.00000000000006" height="167.33333333333337"/>
+                    <subviews>
+                        <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lAI-zq-Mkc">
+                            <rect key="frame" x="0.0" y="0.0" width="287" height="167.33333333333334"/>
+                            <subviews>
+                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="是否将本次录制的作品保存为草稿?" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wF9-Pp-hku">
+                                    <rect key="frame" x="20" y="65" width="247" height="19.333333333333329"/>
+                                    <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                                    <color key="textColor" red="0.46666666666666667" green="0.46666666666666667" blue="0.46666666666666667" alpha="1" colorSpace="calibratedRGB"/>
+                                    <nil key="highlightedColor"/>
+                                </label>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pzh-ao-8sJ">
+                                    <rect key="frame" x="20" y="110.33333333333331" width="118" height="37"/>
+                                    <fontDescription key="fontDescription" type="system" pointSize="15"/>
+                                    <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                    <state key="normal" title="取消">
+                                        <color key="titleColor" red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="calibratedRGB"/>
+                                    </state>
+                                    <userDefinedRuntimeAttributes>
+                                        <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                            <integer key="value" value="18"/>
+                                        </userDefinedRuntimeAttribute>
+                                        <userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
+                                            <integer key="value" value="1"/>
+                                        </userDefinedRuntimeAttribute>
+                                        <userDefinedRuntimeAttribute type="color" keyPath="borderColor">
+                                            <color key="value" red="0.85882352941176465" green="0.85882352941176465" blue="0.85882352941176465" alpha="1" colorSpace="calibratedRGB"/>
+                                        </userDefinedRuntimeAttribute>
+                                    </userDefinedRuntimeAttributes>
+                                    <connections>
+                                        <action selector="cancelAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="zjV-XP-5Jx"/>
+                                    </connections>
+                                </button>
+                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="b2S-9q-b3D">
+                                    <rect key="frame" x="149" y="110.33333333333331" width="118" height="37"/>
+                                    <color key="backgroundColor" red="0.1764705882352941" green="0.7803921568627451" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                    <constraints>
+                                        <constraint firstAttribute="height" constant="37" id="sDc-En-Pny"/>
+                                    </constraints>
+                                    <fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
+                                    <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                    <state key="normal" title="确认"/>
+                                    <userDefinedRuntimeAttributes>
+                                        <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                            <integer key="value" value="18"/>
+                                        </userDefinedRuntimeAttribute>
+                                    </userDefinedRuntimeAttributes>
+                                    <connections>
+                                        <action selector="sureAction:" destination="iN0-l3-epB" eventType="touchUpInside" id="xRk-1I-NzG"/>
+                                    </connections>
+                                </button>
+                                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="温馨提示" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="82h-Qq-aX6">
+                                    <rect key="frame" x="107.33333333333331" y="20" width="72" height="25"/>
+                                    <constraints>
+                                        <constraint firstAttribute="height" constant="25" id="Sd9-Pp-b4E"/>
+                                    </constraints>
+                                    <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
+                                    <color key="textColor" red="0.074509803921568626" green="0.078431372549019607" blue="0.082352941176470587" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                    <nil key="highlightedColor"/>
+                                </label>
+                            </subviews>
+                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            <constraints>
+                                <constraint firstItem="wF9-Pp-hku" firstAttribute="leading" secondItem="lAI-zq-Mkc" secondAttribute="leading" constant="20" id="1Ag-sb-aFn"/>
+                                <constraint firstAttribute="bottom" secondItem="pzh-ao-8sJ" secondAttribute="bottom" constant="20" id="2X9-wO-OdY"/>
+                                <constraint firstItem="b2S-9q-b3D" firstAttribute="width" secondItem="pzh-ao-8sJ" secondAttribute="width" id="CyX-20-Xeb"/>
+                                <constraint firstAttribute="trailing" secondItem="wF9-Pp-hku" secondAttribute="trailing" constant="20" id="DY3-tb-OpF"/>
+                                <constraint firstItem="82h-Qq-aX6" firstAttribute="centerX" secondItem="lAI-zq-Mkc" secondAttribute="centerX" id="Inw-Fz-U1x"/>
+                                <constraint firstItem="wF9-Pp-hku" firstAttribute="top" secondItem="82h-Qq-aX6" secondAttribute="bottom" constant="20" id="JGb-K8-jH5"/>
+                                <constraint firstItem="b2S-9q-b3D" firstAttribute="height" secondItem="pzh-ao-8sJ" secondAttribute="height" id="MC9-1F-uzt"/>
+                                <constraint firstItem="82h-Qq-aX6" firstAttribute="top" secondItem="lAI-zq-Mkc" secondAttribute="top" constant="20" id="NIz-g5-L2n"/>
+                                <constraint firstItem="pzh-ao-8sJ" firstAttribute="leading" secondItem="lAI-zq-Mkc" secondAttribute="leading" constant="20" id="YMY-Ry-u8h"/>
+                                <constraint firstItem="b2S-9q-b3D" firstAttribute="leading" secondItem="pzh-ao-8sJ" secondAttribute="trailing" constant="11" id="YXh-yi-Rgf"/>
+                                <constraint firstItem="b2S-9q-b3D" firstAttribute="bottom" secondItem="pzh-ao-8sJ" secondAttribute="bottom" id="ZXJ-de-UFf"/>
+                                <constraint firstItem="pzh-ao-8sJ" firstAttribute="top" secondItem="wF9-Pp-hku" secondAttribute="bottom" constant="26" id="e5j-ng-3lp"/>
+                                <constraint firstAttribute="trailing" secondItem="b2S-9q-b3D" secondAttribute="trailing" constant="20" id="iGa-mh-nd2"/>
+                            </constraints>
+                        </view>
+                    </subviews>
+                    <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <constraints>
+                        <constraint firstAttribute="bottom" secondItem="lAI-zq-Mkc" secondAttribute="bottom" id="IGC-KN-LiG"/>
+                        <constraint firstItem="lAI-zq-Mkc" firstAttribute="top" secondItem="zfV-ZE-Fdh" secondAttribute="top" id="lvr-Oy-zpI"/>
+                        <constraint firstItem="lAI-zq-Mkc" firstAttribute="leading" secondItem="zfV-ZE-Fdh" secondAttribute="leading" id="trB-S2-zpd"/>
+                        <constraint firstAttribute="trailing" secondItem="lAI-zq-Mkc" secondAttribute="trailing" id="ubI-zL-muv"/>
+                        <constraint firstAttribute="width" constant="287" id="yBZ-fb-lM1"/>
+                    </constraints>
+                    <userDefinedRuntimeAttributes>
+                        <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                            <integer key="value" value="16"/>
+                        </userDefinedRuntimeAttribute>
+                    </userDefinedRuntimeAttributes>
+                </view>
+            </subviews>
+            <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
+            <constraints>
+                <constraint firstItem="zfV-ZE-Fdh" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="5kr-sK-306"/>
+                <constraint firstItem="zfV-ZE-Fdh" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="iWR-Iv-EpW"/>
+            </constraints>
+            <nil key="simulatedTopBarMetrics"/>
+            <nil key="simulatedBottomBarMetrics"/>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <connections>
+                <outlet property="leftButton" destination="pzh-ao-8sJ" id="Q2r-p7-m4e"/>
+                <outlet property="rightButton" destination="b2S-9q-b3D" id="OuJ-J8-YHL"/>
+                <outlet property="tipsLabel" destination="wF9-Pp-hku" id="dsE-Dp-cZe"/>
+            </connections>
+            <point key="canvasLocation" x="-85.496183206106863" y="207.3943661971831"/>
+        </view>
+    </objects>
+</document>

+ 82 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/ThirdPart/CBAutoScrollLabel/CBAutoScrollLabel.h

@@ -0,0 +1,82 @@
+//
+//  CBAutoScrollLabel.h
+//  CBAutoScrollLabel
+//
+//  Created by Brian Stormont on 10/21/09.
+//  Updated/Modernized by Christopher Bess on 2/5/12
+//
+//  Copyright 2009 Stormy Productions. All rights reserved.
+//
+//  Originally from: http://blog.stormyprods.com/2009/10/simple-scrolling-uilabel-for-iphone.html
+//
+//  Permission is granted to use this code free of charge for any project.
+//
+
+#import <UIKit/UIKit.h>
+
+/// Specifies the direction of the scroll
+typedef NS_ENUM(NSInteger, CBAutoScrollDirection) {
+    CBAutoScrollDirectionRight,
+    CBAutoScrollDirectionLeft
+};
+
+@interface CBAutoScrollLabel : UIView <UIScrollViewDelegate>
+
+@property (nonatomic) CBAutoScrollDirection scrollDirection;
+/// Scroll speed in pixels per second, defaults to 30
+@property (nonatomic) float scrollSpeed;
+@property (nonatomic) NSTimeInterval pauseInterval; // defaults to 1.5
+@property (nonatomic) NSInteger labelSpacing; // pixels, defaults to 20
+
+/**
+ * The animation options used when scrolling the UILabels.
+ * @discussion UIViewAnimationOptionAllowUserInteraction is always applied to the animations.
+ */
+@property (nonatomic) UIViewAnimationOptions animationOptions;
+
+/**
+ * Returns YES, if it is actively scrolling, NO if it has paused or if text is within bounds (disables scrolling).
+ */
+@property (nonatomic, readonly) BOOL scrolling;
+@property (nonatomic) CGFloat fadeLength; // defaults to 7
+
+// UILabel properties
+@property (nonatomic, strong, nonnull) UIFont *font;
+@property (nonatomic, copy, nullable) NSString *text;
+@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
+@property (nonatomic, strong, nonnull) UIColor *textColor;
+@property (nonatomic) NSTextAlignment textAlignment; // only applies when not auto-scrolling
+@property (nonatomic, strong, nullable) UIColor *shadowColor;
+@property (nonatomic) CGSize shadowOffset;
+
+/**
+ * Lays out the scrollview contents, enabling text scrolling if the text will be clipped.
+ * @discussion Uses [scrollLabelIfNeeded] internally.
+ */
+- (void)refreshLabels;
+
+/**
+ * Set the text to the label and refresh labels, if needed.
+ * @discussion Useful when you have a situation where you need to layout the scroll label after it's text is set.
+ */
+- (void)setText:(nullable NSString *)text refreshLabels:(BOOL)refresh;
+
+/**
+ Set the attributed text and refresh labels, if needed.
+ */
+- (void)setAttributedText:(nullable NSAttributedString *)theText refreshLabels:(BOOL)refresh;
+
+/**
+ * Initiates auto-scroll, if the label width exceeds the bounds of the scrollview.
+ */
+- (void)scrollLabelIfNeeded;
+
+/**
+ * Observes UIApplication state notifications to auto-restart scrolling and watch for 
+ * orientation changes to refresh the labels.
+ * @discussion Must be called to observe the notifications. Calling multiple times will still only
+ * register the notifications once.
+ */
+- (void)observeApplicationNotifications;
+
+@end

+ 422 - 0
KulexiuForTeacher/KulexiuForTeacher/Common/ThirdPart/CBAutoScrollLabel/CBAutoScrollLabel.m

@@ -0,0 +1,422 @@
+//
+//  CBAutoScrollLabel.m
+//  CBAutoScrollLabel
+//
+//  Created by Brian Stormont on 10/21/09.
+//  Updated by Christopher Bess on 2/5/12
+//
+//  Copyright 2009 Stormy Productions.
+//
+//  Permission is granted to use this code free of charge for any project.
+//
+
+#import "CBAutoScrollLabel.h"
+#import <QuartzCore/QuartzCore.h>
+
+#define kLabelCount 2
+#define kDefaultFadeLength 7.f
+// pixel buffer space between scrolling label
+#define kDefaultLabelBufferSpace 20
+#define kDefaultPixelsPerSecond 30
+#define kDefaultPauseTime 1.5f
+
+// shortcut method for NSArray iterations
+static void each_object(NSArray *objects, void (^block)(id object)) {
+    for (id obj in objects) {
+        block(obj);
+    }
+}
+
+// shortcut to change each label attribute value
+#define EACH_LABEL(ATTR, VALUE) each_object(self.labels, ^(UILabel *label) { label.ATTR = VALUE; });
+
+@interface CBAutoScrollLabel ()
+
+@property (nonatomic, strong) NSArray<UILabel *> *labels;
+@property (nonatomic, strong, readonly) UILabel *mainLabel;
+@property (nonatomic, strong) UIScrollView *scrollView;
+
+@end
+
+@implementation CBAutoScrollLabel
+
+- (id)initWithCoder:(NSCoder *)aDecoder {
+    if ((self = [super initWithCoder:aDecoder])) {
+        [self commonInit];
+    }
+    return self;
+}
+
+- (id)initWithFrame:(CGRect)frame {
+    if ((self = [super initWithFrame:frame])) {
+        [self commonInit];
+    }
+    return self;
+}
+
+- (void)commonInit {
+    // create the labels
+    NSMutableSet<UILabel *> *labelSet = [[NSMutableSet alloc] initWithCapacity:kLabelCount];
+
+    for (int index = 0; index < kLabelCount; ++index) {
+        UILabel *label = [[UILabel alloc] init];
+        label.backgroundColor = [UIColor clearColor];
+        label.autoresizingMask = self.autoresizingMask;
+
+        // store labels
+        [self.scrollView addSubview:label];
+        [labelSet addObject:label];
+    }
+
+    self.labels = [labelSet.allObjects copy];
+
+    // default values
+    _scrollDirection = CBAutoScrollDirectionLeft;
+    _scrollSpeed = kDefaultPixelsPerSecond;
+    self.pauseInterval = kDefaultPauseTime;
+    self.labelSpacing = kDefaultLabelBufferSpace;
+    self.textAlignment = NSTextAlignmentLeft;
+    self.animationOptions = UIViewAnimationOptionCurveLinear;
+    self.scrollView.showsVerticalScrollIndicator = NO;
+    self.scrollView.showsHorizontalScrollIndicator = NO;
+    self.scrollView.scrollEnabled = NO;
+    self.userInteractionEnabled = NO;
+    self.backgroundColor = [UIColor clearColor];
+    self.clipsToBounds = YES;
+    self.fadeLength = kDefaultFadeLength;
+}
+
+- (void)dealloc {
+    [NSObject cancelPreviousPerformRequestsWithTarget:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)setFrame:(CGRect)frame {
+    [super setFrame:frame];
+
+    [self didChangeFrame];
+}
+
+// For autolayout
+- (void)setBounds:(CGRect)bounds {
+    [super setBounds:bounds];
+
+    [self didChangeFrame];
+}
+
+- (void)didMoveToWindow {
+    [super didMoveToWindow];
+    
+    if (self.window) {
+        [self scrollLabelIfNeeded];
+    }
+}
+
+#pragma mark - Properties
+
+- (UIScrollView *)scrollView {
+    if (_scrollView == nil) {
+        _scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
+        _scrollView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
+        _scrollView.backgroundColor = [UIColor clearColor];
+
+        [self addSubview:_scrollView];
+    }
+    return _scrollView;
+}
+
+- (void)setFadeLength:(CGFloat)fadeLength {
+    if (_fadeLength != fadeLength) {
+        _fadeLength = fadeLength;
+
+        [self refreshLabels];
+        [self applyGradientMaskForFadeLength:fadeLength enableFade:NO];
+    }
+}
+
+- (UILabel *)mainLabel {
+    return [self.labels firstObject];
+}
+
+- (void)setText:(NSString *)theText {
+    [self setText:theText refreshLabels:YES];
+}
+
+- (void)setText:(NSString *)theText refreshLabels:(BOOL)refresh {
+    // ignore identical text changes
+    if ([theText isEqualToString:self.text])
+        return;
+
+    EACH_LABEL(text, theText)
+
+    if (refresh)
+        [self refreshLabels];
+}
+
+- (NSString *)text {
+    return self.mainLabel.text;
+}
+
+- (void)setAttributedText:(NSAttributedString *)theText {
+    [self setAttributedText:theText refreshLabels:YES];
+}
+
+- (void)setAttributedText:(NSAttributedString *)theText refreshLabels:(BOOL)refresh {
+    // ignore identical text changes
+    if ([theText.string isEqualToString:self.attributedText.string])
+        return;
+
+    EACH_LABEL(attributedText, theText)
+
+    if (refresh)
+        [self refreshLabels];
+}
+
+- (NSAttributedString *)attributedText {
+    return self.mainLabel.attributedText;
+}
+
+- (void)setTextColor:(UIColor *)color {
+    EACH_LABEL(textColor, color)
+}
+
+- (UIColor *)textColor {
+    return self.mainLabel.textColor;
+}
+
+- (void)setFont:(UIFont *)font {
+    if (self.mainLabel.font == font)
+        return;
+
+    EACH_LABEL(font, font)
+
+    [self refreshLabels];
+    [self invalidateIntrinsicContentSize];
+}
+
+- (UIFont *)font {
+    return self.mainLabel.font;
+}
+
+- (void)setScrollSpeed:(float)speed {
+    _scrollSpeed = speed;
+
+    [self scrollLabelIfNeeded];
+}
+
+- (void)setScrollDirection:(CBAutoScrollDirection)direction {
+    _scrollDirection = direction;
+
+    [self scrollLabelIfNeeded];
+}
+
+- (void)setShadowColor:(UIColor *)color {
+    EACH_LABEL(shadowColor, color)
+}
+
+- (UIColor *)shadowColor {
+    return self.mainLabel.shadowColor;
+}
+
+- (void)setShadowOffset:(CGSize)offset {
+    EACH_LABEL(shadowOffset, offset)
+}
+
+- (CGSize)shadowOffset {
+    return self.mainLabel.shadowOffset;
+}
+
+#pragma mark - Autolayout
+
+- (CGSize)intrinsicContentSize {
+    return CGSizeMake(0, [self.mainLabel intrinsicContentSize].height);
+}
+
+#pragma mark - Misc
+
+- (void)observeApplicationNotifications {
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+    // restart scrolling when the app has been activated
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(scrollLabelIfNeeded)
+                                                 name:UIApplicationWillEnterForegroundNotification
+                                               object:nil];
+
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(scrollLabelIfNeeded)
+                                                 name:UIApplicationDidBecomeActiveNotification
+                                               object:nil];
+
+#ifndef TARGET_OS_TV
+    // refresh labels when interface orientation is changed
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(onUIApplicationDidChangeStatusBarOrientationNotification:)
+                                                 name:UIApplicationDidChangeStatusBarOrientationNotification
+                                               object:nil];
+#endif
+
+}
+
+- (void)enableShadow {
+    _scrolling = YES;
+    [self applyGradientMaskForFadeLength:self.fadeLength enableFade:YES];
+}
+
+- (void)scrollLabelIfNeeded {
+    if (!self.text.length)
+        return;
+
+    CGFloat labelWidth = CGRectGetWidth(self.mainLabel.bounds);
+    if (labelWidth <= CGRectGetWidth(self.bounds))
+        return;
+
+    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(scrollLabelIfNeeded) object:nil];
+    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(enableShadow) object:nil];
+
+    [self.scrollView.layer removeAllAnimations];
+
+    BOOL doScrollLeft = (self.scrollDirection == CBAutoScrollDirectionLeft);
+    self.scrollView.contentOffset = (doScrollLeft ? CGPointZero : CGPointMake(labelWidth + self.labelSpacing, 0));
+
+    // Add the left shadow after delay
+    [self performSelector:@selector(enableShadow) withObject:nil afterDelay:self.pauseInterval];
+
+    // animate the scrolling
+    NSTimeInterval duration = labelWidth / self.scrollSpeed;
+    [UIView animateWithDuration:duration delay:self.pauseInterval options:self.animationOptions | UIViewAnimationOptionAllowUserInteraction animations:^{
+         // adjust offset
+         self.scrollView.contentOffset = (doScrollLeft ? CGPointMake(labelWidth + self.labelSpacing, 0) : CGPointZero);
+     } completion:^(BOOL finished) {
+         self->_scrolling = NO;
+
+         // remove the left shadow
+         [self applyGradientMaskForFadeLength:self.fadeLength enableFade:NO];
+
+         // setup pause delay/loop
+         if (finished) {
+             [self performSelector:@selector(scrollLabelIfNeeded) withObject:nil];
+         }
+     }];
+}
+
+- (void)refreshLabels {
+    __block float offset = 0;
+
+    each_object(self.labels, ^(UILabel *label) {
+        [label sizeToFit];
+
+        CGRect frame = label.frame;
+        frame.origin = CGPointMake(offset, 0);
+        frame.size.height = CGRectGetHeight(self.bounds);
+        label.frame = frame;
+
+        // Recenter label vertically within the scroll view
+        label.center = CGPointMake(label.center.x, roundf(self.center.y - CGRectGetMinY(self.frame)));
+
+        offset += CGRectGetWidth(label.bounds) + self.labelSpacing;
+    });
+
+    self.scrollView.contentOffset = CGPointZero;
+    [self.scrollView.layer removeAllAnimations];
+
+    // if the label is bigger than the space allocated, then it should scroll
+    if (CGRectGetWidth(self.mainLabel.bounds) > CGRectGetWidth(self.bounds)) {
+        CGSize size;
+        size.width = CGRectGetWidth(self.mainLabel.bounds) + CGRectGetWidth(self.bounds) + self.labelSpacing;
+        size.height = CGRectGetHeight(self.bounds);
+        self.scrollView.contentSize = size;
+
+        EACH_LABEL(hidden, NO)
+
+        [self applyGradientMaskForFadeLength:self.fadeLength enableFade:self.scrolling];
+
+        [self scrollLabelIfNeeded];
+    } else {
+        // Hide the other labels
+        EACH_LABEL(hidden, (self.mainLabel != label))
+
+        // adjust the scroll view and main label
+        self.scrollView.contentSize = self.bounds.size;
+        self.mainLabel.frame = self.bounds;
+        self.mainLabel.hidden = NO;
+        self.mainLabel.textAlignment = self.textAlignment;
+
+        // cleanup animation
+        [self.scrollView.layer removeAllAnimations];
+
+        [self applyGradientMaskForFadeLength:0 enableFade:NO];
+    }
+}
+
+// bounds or frame has been changed
+- (void)didChangeFrame {
+    [self refreshLabels];
+    [self applyGradientMaskForFadeLength:self.fadeLength enableFade:self.scrolling];
+}
+
+#pragma mark - Gradient
+
+// ref: https://github.com/cbpowell/MarqueeLabel
+- (void)applyGradientMaskForFadeLength:(CGFloat)fadeLength enableFade:(BOOL)fade {
+    CGFloat labelWidth = CGRectGetWidth(self.mainLabel.bounds);
+
+    if (labelWidth <= CGRectGetWidth(self.bounds))
+        fadeLength = 0;
+
+    if (fadeLength) {
+        // Recreate gradient mask with new fade length
+        CAGradientLayer *gradientMask = [CAGradientLayer layer];
+
+        gradientMask.bounds = self.layer.bounds;
+        gradientMask.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
+
+        gradientMask.shouldRasterize = YES;
+        gradientMask.rasterizationScale = [UIScreen mainScreen].scale;
+
+        gradientMask.startPoint = CGPointMake(0, CGRectGetMidY(self.frame));
+        gradientMask.endPoint = CGPointMake(1, CGRectGetMidY(self.frame));
+
+        // setup fade mask colors and location
+        id transparent = (id)[UIColor clearColor].CGColor;
+        id opaque = (id)[UIColor blackColor].CGColor;
+        gradientMask.colors = @[transparent, opaque, opaque, transparent];
+
+        // calcluate fade
+        CGFloat fadePoint = fadeLength / CGRectGetWidth(self.bounds);
+        NSNumber *leftFadePoint = @(fadePoint);
+        NSNumber *rightFadePoint = @(1 - fadePoint);
+        if (!fade) switch (self.scrollDirection) {
+                case CBAutoScrollDirectionLeft:
+                    leftFadePoint = @0;
+                    break;
+
+                case CBAutoScrollDirectionRight:
+                    leftFadePoint = @0;
+                    rightFadePoint = @1;
+                    break;
+            }
+
+        // apply calculations to mask
+        gradientMask.locations = @[@0, leftFadePoint, rightFadePoint, @1];
+
+        // don't animate the mask change
+        [CATransaction begin];
+        [CATransaction setDisableActions:YES];
+        self.layer.mask = gradientMask;
+        [CATransaction commit];
+    } else {
+        // Remove gradient mask for 0.0f length fade length
+        self.layer.mask = nil;
+    }
+}
+
+#pragma mark - Notifications
+
+- (void)onUIApplicationDidChangeStatusBarOrientationNotification:(NSNotification *)notification {
+    // delay to have it re-calculate on next runloop
+    [self performSelector:@selector(refreshLabels) withObject:nil afterDelay:.1f];
+    [self performSelector:@selector(scrollLabelIfNeeded) withObject:nil afterDelay:.1f];
+}
+
+@end