Parcourir la source

剪辑音频,把关键代码拉过来了

100Years il y a 3 semaines
Parent
commit
1c9afc9105

+ 40 - 0
AIRingtone.xcodeproj/project.pbxproj

@@ -58,6 +58,12 @@
 		A840A7FA2D916D9B0044B8B9 /* TSGeneratePosterOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A7F92D916D9A0044B8B9 /* TSGeneratePosterOperation.swift */; };
 		A840A7FA2D916D9B0044B8B9 /* TSGeneratePosterOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A7F92D916D9A0044B8B9 /* TSGeneratePosterOperation.swift */; };
 		A840A7FE2D916DB50044B8B9 /* TSGeneratePhotoOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A7FD2D916DB40044B8B9 /* TSGeneratePhotoOperation.swift */; };
 		A840A7FE2D916DB50044B8B9 /* TSGeneratePhotoOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A7FD2D916DB40044B8B9 /* TSGeneratePhotoOperation.swift */; };
 		A840A8032D91945A0044B8B9 /* TSGenerateBaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8022D9194590044B8B9 /* TSGenerateBaseOperation.swift */; };
 		A840A8032D91945A0044B8B9 /* TSGenerateBaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8022D9194590044B8B9 /* TSGenerateBaseOperation.swift */; };
+		A840A8082D94044E0044B8B9 /* TSEditAudioVideoBaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8072D9404480044B8B9 /* TSEditAudioVideoBaseVC.swift */; };
+		A840A80F2D94057B0044B8B9 /* ZHAudioProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8092D94057B0044B8B9 /* ZHAudioProcessing.swift */; };
+		A840A8102D94057B0044B8B9 /* ZHWaveformViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80A2D94057B0044B8B9 /* ZHWaveformViewDelegate.swift */; };
+		A840A8112D94057B0044B8B9 /* ZHWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80C2D94057B0044B8B9 /* ZHWaveformView.swift */; };
+		A840A8122D94057B0044B8B9 /* ZHCroppedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80D2D94057B0044B8B9 /* ZHCroppedDelegate.swift */; };
+		A840A8132D94057B0044B8B9 /* ZHTrackProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80B2D94057B0044B8B9 /* ZHTrackProcessing.swift */; };
 		A868A89A2D75505E00F6D884 /* TSThemeBannerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A8992D75505800F6D884 /* TSThemeBannerCell.swift */; };
 		A868A89A2D75505E00F6D884 /* TSThemeBannerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A8992D75505800F6D884 /* TSThemeBannerCell.swift */; };
 		A868A89C2D75506C00F6D884 /* TSThemeContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A89B2D75506500F6D884 /* TSThemeContentCell.swift */; };
 		A868A89C2D75506C00F6D884 /* TSThemeContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A89B2D75506500F6D884 /* TSThemeContentCell.swift */; };
 		A868A8A22D7560B900F6D884 /* TSPageNullView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A89D2D7560B900F6D884 /* TSPageNullView.swift */; };
 		A868A8A22D7560B900F6D884 /* TSPageNullView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A89D2D7560B900F6D884 /* TSPageNullView.swift */; };
@@ -197,6 +203,12 @@
 		A840A7F92D916D9A0044B8B9 /* TSGeneratePosterOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneratePosterOperation.swift; sourceTree = "<group>"; };
 		A840A7F92D916D9A0044B8B9 /* TSGeneratePosterOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneratePosterOperation.swift; sourceTree = "<group>"; };
 		A840A7FD2D916DB40044B8B9 /* TSGeneratePhotoOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneratePhotoOperation.swift; sourceTree = "<group>"; };
 		A840A7FD2D916DB40044B8B9 /* TSGeneratePhotoOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneratePhotoOperation.swift; sourceTree = "<group>"; };
 		A840A8022D9194590044B8B9 /* TSGenerateBaseOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGenerateBaseOperation.swift; sourceTree = "<group>"; };
 		A840A8022D9194590044B8B9 /* TSGenerateBaseOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGenerateBaseOperation.swift; sourceTree = "<group>"; };
+		A840A8072D9404480044B8B9 /* TSEditAudioVideoBaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSEditAudioVideoBaseVC.swift; sourceTree = "<group>"; };
+		A840A8092D94057B0044B8B9 /* ZHAudioProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHAudioProcessing.swift; sourceTree = "<group>"; };
+		A840A80A2D94057B0044B8B9 /* ZHWaveformViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHWaveformViewDelegate.swift; sourceTree = "<group>"; };
+		A840A80B2D94057B0044B8B9 /* ZHTrackProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHTrackProcessing.swift; sourceTree = "<group>"; };
+		A840A80C2D94057B0044B8B9 /* ZHWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHWaveformView.swift; sourceTree = "<group>"; };
+		A840A80D2D94057B0044B8B9 /* ZHCroppedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHCroppedDelegate.swift; sourceTree = "<group>"; };
 		A868A8992D75505800F6D884 /* TSThemeBannerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeBannerCell.swift; sourceTree = "<group>"; };
 		A868A8992D75505800F6D884 /* TSThemeBannerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeBannerCell.swift; sourceTree = "<group>"; };
 		A868A89B2D75506500F6D884 /* TSThemeContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeContentCell.swift; sourceTree = "<group>"; };
 		A868A89B2D75506500F6D884 /* TSThemeContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeContentCell.swift; sourceTree = "<group>"; };
 		A868A89D2D7560B900F6D884 /* TSPageNullView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSPageNullView.swift; sourceTree = "<group>"; };
 		A868A89D2D7560B900F6D884 /* TSPageNullView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSPageNullView.swift; sourceTree = "<group>"; };
@@ -368,6 +380,7 @@
 		A80EDE632D718B19003CD332 /* Business */ = {
 		A80EDE632D718B19003CD332 /* Business */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				A840A8062D9404060044B8B9 /* TSEditAudioVideoVC */,
 				A83F871B2D79408300D29B1B /* Data */,
 				A83F871B2D79408300D29B1B /* Data */,
 				A899D3822D88303300AB9C1C /* TSDiscoverVC */,
 				A899D3822D88303300AB9C1C /* TSDiscoverVC */,
 				A899D34C2D82C61C00AB9C1C /* TSPurchaseMembershipVC */,
 				A899D34C2D82C61C00AB9C1C /* TSPurchaseMembershipVC */,
@@ -656,6 +669,26 @@
 			path = TSGenerateBaseOperation;
 			path = TSGenerateBaseOperation;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		A840A8062D9404060044B8B9 /* TSEditAudioVideoVC */ = {
+			isa = PBXGroup;
+			children = (
+				A840A8072D9404480044B8B9 /* TSEditAudioVideoBaseVC.swift */,
+			);
+			path = TSEditAudioVideoVC;
+			sourceTree = "<group>";
+		};
+		A840A80E2D94057B0044B8B9 /* ZHWaveform */ = {
+			isa = PBXGroup;
+			children = (
+				A840A8092D94057B0044B8B9 /* ZHAudioProcessing.swift */,
+				A840A80A2D94057B0044B8B9 /* ZHWaveformViewDelegate.swift */,
+				A840A80B2D94057B0044B8B9 /* ZHTrackProcessing.swift */,
+				A840A80C2D94057B0044B8B9 /* ZHWaveformView.swift */,
+				A840A80D2D94057B0044B8B9 /* ZHCroppedDelegate.swift */,
+			);
+			path = ZHWaveform;
+			sourceTree = "<group>";
+		};
 		A868A8982D75505100F6D884 /* View */ = {
 		A868A8982D75505100F6D884 /* View */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -766,6 +799,7 @@
 				A868A8EC2D76FE5D00F6D884 /* tutorial-ring.mp4 */,
 				A868A8EC2D76FE5D00F6D884 /* tutorial-ring.mp4 */,
 				A868A8DA2D76F00800F6D884 /* TSBandRingTool.swift */,
 				A868A8DA2D76F00800F6D884 /* TSBandRingTool.swift */,
 				A868A8EA2D76FD9800F6D884 /* placeholder.band */,
 				A868A8EA2D76FD9800F6D884 /* placeholder.band */,
+				A840A80E2D94057B0044B8B9 /* ZHWaveform */,
 			);
 			);
 			path = TSBandRingTool;
 			path = TSBandRingTool;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1190,6 +1224,7 @@
 				A868A9002D77E55800F6D884 /* TSPromptStyleView.swift in Sources */,
 				A868A9002D77E55800F6D884 /* TSPromptStyleView.swift in Sources */,
 				A83F87202D794FF000D29B1B /* TSAIPhotoImageCell.swift in Sources */,
 				A83F87202D794FF000D29B1B /* TSAIPhotoImageCell.swift in Sources */,
 				A899D3932D8924A300AB9C1C /* TSDiscoverListVC.swift in Sources */,
 				A899D3932D8924A300AB9C1C /* TSDiscoverListVC.swift in Sources */,
+				A840A8082D94044E0044B8B9 /* TSEditAudioVideoBaseVC.swift in Sources */,
 				A868A8DB2D76F00C00F6D884 /* TSBandRingTool.swift in Sources */,
 				A868A8DB2D76F00C00F6D884 /* TSBandRingTool.swift in Sources */,
 				A840A7F42D8EA0340044B8B9 /* TSGeneralPhotoBrowseVC.swift in Sources */,
 				A840A7F42D8EA0340044B8B9 /* TSGeneralPhotoBrowseVC.swift in Sources */,
 				A8D776FA2D8D345C007EAB35 /* TSBaseOperation.swift in Sources */,
 				A8D776FA2D8D345C007EAB35 /* TSBaseOperation.swift in Sources */,
@@ -1261,6 +1296,11 @@
 				A8272EBB2D7AFD0F00F1C814 /* TSBusinessAudioPlayer.swift in Sources */,
 				A8272EBB2D7AFD0F00F1C814 /* TSBusinessAudioPlayer.swift in Sources */,
 				A868A8A42D7560B900F6D884 /* TSCommonloadingView.swift in Sources */,
 				A868A8A42D7560B900F6D884 /* TSCommonloadingView.swift in Sources */,
 				A868A89A2D75505E00F6D884 /* TSThemeBannerCell.swift in Sources */,
 				A868A89A2D75505E00F6D884 /* TSThemeBannerCell.swift in Sources */,
+				A840A80F2D94057B0044B8B9 /* ZHAudioProcessing.swift in Sources */,
+				A840A8102D94057B0044B8B9 /* ZHWaveformViewDelegate.swift in Sources */,
+				A840A8112D94057B0044B8B9 /* ZHWaveformView.swift in Sources */,
+				A840A8122D94057B0044B8B9 /* ZHCroppedDelegate.swift in Sources */,
+				A840A8132D94057B0044B8B9 /* ZHTrackProcessing.swift in Sources */,
 				A80EDF182D7193EE003CD332 /* TSTutorialsVC.swift in Sources */,
 				A80EDF182D7193EE003CD332 /* TSTutorialsVC.swift in Sources */,
 				A899D3A52D89786200AB9C1C /* TSAudioAVPlayer.swift in Sources */,
 				A899D3A52D89786200AB9C1C /* TSAudioAVPlayer.swift in Sources */,
 				A80EDEEA2D718CEA003CD332 /* PaddedLabel.swift in Sources */,
 				A80EDEEA2D718CEA003CD332 /* PaddedLabel.swift in Sources */,

+ 601 - 0
AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioVideoBaseVC.swift

@@ -0,0 +1,601 @@
+//
+//  TSEditAudioVideoBaseVC.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/26.
+//
+
+class TSEditAudioVideoBaseVC: TSBaseVC , ZHCroppedDelegate, ZHWaveformViewDelegate {
+    
+    var titleName:String = "Edit".localized
+    override func createView() {
+        
+        setPageTitle(titleName)
+        
+        
+        
+    }
+    
+    
+    var nameLabel: UILabel!
+    var nameButton: UIButton!
+
+    var trackContentView: UIView!
+    var trackBgView: UIView!
+
+    var timeLabel: UILabel!
+    var cutButton: UIButton!
+    var playButton: UIButton!
+    var undoButton: UIButton!
+
+    var fadeinSlider: UISlider!
+    var fadeinTimeLabel: UILabel!
+    var fadeoutSlider: UISlider!
+    var fadeoutTimeLabel: UILabel!
+
+    var doneButton: UIButton!
+
+    var dragLeftView: UIView!
+    var startTimeLabel: UILabel!
+    var dragRightView: UIView!
+    var endTimeLabel: UILabel!
+    var progressLine: UIView!
+    weak var nameInputTextField: UITextField?
+
+    var trackView: ZHWaveformView?
+
+    private lazy var player = TSBusinessAudioPlayer()
+    private lazy var audioTool = AudioTool()
+
+    var ringModel: TSRingModel?
+    lazy var operationCache: [TSRingModel] = []
+    var tempMp3Path: String?
+    var needClearTemp : Bool = false
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        view.backgroundColor = .clear
+//        saveButton.set { [weak self] in
+//            self?.saveButtonClick()
+//        }
+
+        if let ring = ringModel {
+            operationCache.append(ring)
+        }
+
+
+        setupUI()
+
+        let leftPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.leftPanRecognizer(sender:)))
+        dragLeftView.addGestureRecognizer(leftPanRecognizer)
+        dragLeftView.isUserInteractionEnabled = true
+
+        let rightPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.rightPanRecognizer(sender:)))
+        dragRightView.addGestureRecognizer(rightPanRecognizer)
+        dragRightView.isUserInteractionEnabled = true
+    }
+
+
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+
+        if needClearTemp {
+            deleteTempFiles()
+        }
+    }
+
+    func deleteTempFiles() {
+        if let tempMp3Path = tempMp3Path {
+            let mp3Dir = NSString(string: tempMp3Path).deletingLastPathComponent
+            if FileManager.default.isDeletableFile(atPath: mp3Dir) {
+                let fileUrl = URL(fileURLWithPath: mp3Dir)
+                do {
+                    try FileManager.default.removeItem(at: fileUrl)
+                } catch {
+                    dePrint("删除文件失败")
+                }
+            }
+        }
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+    }
+
+    lazy var previousBounds: CGRect = .zero
+    override func viewDidLayoutSubviews() {
+        super.viewDidLayoutSubviews()
+
+        if previousBounds == .zero {
+            previousBounds = view.bounds
+            DispatchQueue.main.async {
+                self.reloadTrackView()
+            }
+        }
+    }
+
+    deinit {
+        dePrint("RingEditViewController 销毁了")
+    }
+
+    func reloadTrackView() {
+        guard let current = ringModel else {
+            return
+        }
+        var ringFilePath: String?
+        if let fileName = current.fileName,
+           let filePath = RingDownloadManager.shared.filePath(with: fileName),
+           FileManager.default.fileExists(atPath: filePath) {
+            ringFilePath = filePath
+        } else if let filePath = RingDownloadManager.shared.filePath(for: current.fileUrl),
+                  FileManager.default.fileExists(atPath: filePath) {
+            ringFilePath = filePath
+        }
+        guard let ringFilePath = ringFilePath else {
+            return
+        }
+
+        tempMp3Path = ringFilePath
+
+        trackView?.removeFromSuperview()
+        startCropRate = 0
+        endCropRate = 1.0
+        dragLeftView.centerX = trackContentView.x
+        dragRightView.centerX = trackContentView.frame.maxX
+
+        let url = URL(fileURLWithPath: ringFilePath)
+
+        let waveform = ZHWaveformView(
+            frame: CGRect(x: 0, y: 20, width: trackContentView.width, height: trackContentView.height - 40),
+            fileURL: url
+        )
+        waveform.backgroundColor = .clear
+
+        // color
+        waveform.beginningPartColor = .white.withAlphaComponent(0.2)
+        waveform.endPartColor = .white.withAlphaComponent(0.2)
+        waveform.wavesColor = "#55A0E9".uiColor
+
+        // 0 ~ 1
+        waveform.trackScale = 0.2
+
+        waveform.waveformDelegate = self
+        waveform.croppedDelegate = self
+
+        trackContentView.insertSubview(waveform, at: 0)
+        trackView = waveform
+
+        timeLabel.text = current.duration.mmss
+        undoButton.isEnabled = operationCache.count > 1
+        cutButton.isEnabled = false
+    }
+
+    func setupUI() {
+        nameLabel.text = ringModel?.title ?? ""
+
+        doneButton.titleLabel?.numberOfLines = 2
+        doneButton.titleLabel?.textAlignment = .center
+        doneButton.setTitle("Export to GarageBand\nSet Ringtones".localized, for: .normal)
+        doneButton.setGradient(colors: AppTheme.gradientColors, index: 0)
+
+        fadeinSlider.setThumbImage(UIImage(named: "ic-ring-edit-slider"), for: .normal)
+        fadeinSlider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
+        fadeinSlider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
+        fadeinSlider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
+
+        fadeoutSlider.setThumbImage(UIImage(named: "ic-ring-edit-slider"), for: .normal)
+        fadeoutSlider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
+        fadeoutSlider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
+        fadeoutSlider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
+    }
+
+    lazy var fadeInDuration: Int = 0 {
+        didSet {
+            fadeinTimeLabel.text = "\(fadeInDuration)s"
+        }
+    }
+
+    lazy var fadeOutDuration: Int = 0 {
+        didSet {
+            fadeoutTimeLabel.text = "\(fadeOutDuration)s"
+        }
+    }
+
+    @objc func sliderBeginTap(_ slider: UISlider) {
+        suspendPlay()
+    }
+
+    @objc func sliderEndTap(_ slider: UISlider) {
+        autoPlayAfterMove()
+    }
+
+    @objc func sliderValueChanged(_ slider: UISlider) {
+//        print("---\(slider.value)")
+        if player.state == .playing {
+            player.pause()
+        }
+        if slider == fadeinSlider {
+            fadeInDuration = Int(slider.value.rounded(.toNearestOrEven))
+        } else if slider == fadeoutSlider {
+            fadeOutDuration = Int(slider.value.rounded(.toNearestOrEven))
+        }
+    }
+
+    lazy var startCropRate: CGFloat = 0 {
+        didSet {
+            let time = startDuration
+            cutButton.isEnabled = true
+
+//            print("---start: \(time)")
+            startTimeLabel.text = time.mmss
+            timeLabel.text = (endDuration - startDuration).mmss
+        }
+    }
+
+    // 裁剪起始时间
+    var startDuration: Double {
+        guard let model = ringModel else {
+            return 0
+        }
+        return startCropRate * model.duration
+    }
+
+    lazy var previousLeftX: CGFloat = 0
+
+    var startMinCenterX: CGFloat {
+        return trackContentView.x
+    }
+
+    var startMaxCenterX: CGFloat {
+        guard let model = ringModel else {
+            return 0
+        }
+        let ratio = (endCropRate * model.duration - 10) / model.duration
+        return ratio * trackContentView.width + trackContentView.x
+    }
+
+    var endMinCenterX: CGFloat {
+        guard let model = ringModel else {
+            return 0
+        }
+        let ratio = (startCropRate * model.duration + 10) / model.duration
+        return ratio * trackContentView.width + trackContentView.x
+    }
+
+    var endMaxCenterX: CGFloat {
+        return trackContentView.frame.maxX
+    }
+
+    // 拖拽时被暂停,拖拽结束后,继续播放
+    lazy var isSuspendByAction = false
+    @objc private func leftPanRecognizer(sender: UIPanGestureRecognizer) {
+        let limitMinCenterX: CGFloat = startMinCenterX
+        let limitMaxCenterX: CGFloat = startMaxCenterX
+        guard limitMaxCenterX > limitMinCenterX else {
+            if sender.state == .began {
+                THUD.toast("No less than 10s".localized())
+            }
+            return
+        }
+
+        if sender.state == .began {
+            suspendPlay()
+        } else if sender.state == .changed {
+            // 修改位置
+            let newPoint = sender.translation(in: trackBgView)
+            var center = dragLeftView.center
+            center.x = previousLeftX + newPoint.x
+            guard center.x > limitMinCenterX,
+                  center.x < limitMaxCenterX else {
+                // 越界,拖不动了, 最少10s
+                if endDuration - startDuration < 11 {
+                    THUD.toast("No less than 10s".localized())
+                }
+                return
+            }
+            dragLeftView.center = center
+
+        } else if sender.state == .ended || sender.state == .failed {
+            previousLeftX = dragLeftView.center.x
+            autoPlayAfterMove()
+        }
+
+        // 边界校验
+        if dragLeftView.centerX < limitMinCenterX {
+            dragLeftView.centerX = limitMinCenterX
+        }
+        if dragLeftView.centerX > limitMaxCenterX {
+            dragLeftView.centerX = limitMaxCenterX
+        }
+
+        let position = dragLeftView.centerX - trackContentView.x
+        trackView?.updateLeftCroppedPosition(position)
+        startCropRate = position / trackContentView.width
+        progressLine.centerX = dragLeftView.centerX
+    }
+
+    lazy var endCropRate: CGFloat = 1.0 {
+        didSet {
+            cutButton.isEnabled = true
+
+            let time = endDuration
+//            print("---end: \(time)")
+            endTimeLabel.text = time.mmss
+            timeLabel.text = (endDuration - startDuration).mmss
+        }
+    }
+
+    var endDuration: Double {
+        guard let model = ringModel else {
+            return 0
+        }
+        return endCropRate * model.duration
+    }
+
+    lazy var previousRightX: CGFloat = 0
+    @objc private func rightPanRecognizer(sender: UIPanGestureRecognizer) {
+        let limitMinCenterX: CGFloat = endMinCenterX
+        let limitMaxCenterX: CGFloat = endMaxCenterX
+        guard limitMaxCenterX > limitMinCenterX else {
+            if sender.state == .began {
+                // 越界,拖不动了
+                THUD.toast("No less than 10s".localized())
+            }
+            return
+        }
+
+        if sender.state == .began {
+            suspendPlay()
+        } else if sender.state == .changed {
+            let newPoint = sender.translation(in: trackBgView)
+            var center = dragRightView.center
+            center.x = previousRightX + newPoint.x
+            guard center.x > limitMinCenterX, center.x < limitMaxCenterX else {
+                // 越界,拖不动了, 最少10s
+                if endDuration - startDuration < 11 {
+                    THUD.toast("No less than 10s".localized())
+                }
+                return
+            }
+            dragRightView.center = center
+        } else if sender.state == .ended || sender.state == .failed {
+            previousRightX = dragRightView.centerX
+            autoPlayAfterMove()
+        }
+
+        // 边界校验
+        if dragRightView.centerX > limitMaxCenterX {
+            dragRightView.centerX = limitMaxCenterX
+        }
+        if dragRightView.centerX < limitMinCenterX {
+            dragRightView.centerX = limitMinCenterX
+        }
+
+        let position = dragRightView.centerX - trackContentView.x
+        trackView?.updateRightCroppedPosition(position)
+        endCropRate = position / trackContentView.width
+    }
+
+    @IBAction func buttonClick(_ sender: UIButton) {
+        guard let model = ringModel else { return }
+
+        switch sender {
+        case playButton:
+            startPlay()
+        case undoButton:
+            player.pause()
+            operationCache.removeLast()
+            ringModel = operationCache.last
+            reloadTrackView()
+        case cutButton:
+            startCutAudio { [weak self] newModel, errMsg in
+                DispatchQueue.main.async {
+                    if let newModel = newModel {
+                        self?.operationCache.append(newModel)
+                        self?.ringModel = newModel
+                        self?.reloadTrackView()
+                    } else {
+                        THUD.toast(errMsg ?? "Sorry, Edit Failure".localized())
+                    }
+                }
+            }
+        case doneButton:
+            player.pause()
+            startCutAudio { [weak self] result, errMsg in
+                DispatchQueue.main.async {
+                    if let ringModel = result {
+                        THUD.showLoading()
+                        RingDownloadManager.shared.shareBand(with: ringModel) { _ in
+                            DispatchQueue.main.async {
+                                THUD.hide()
+                            }
+                        }
+                    } else {
+                        THUD.toast(errMsg ?? "Sorry, Edit Failure".localized())
+                    }
+                }
+            }
+        case nameButton:
+            let alertVC = UIAlertController(title: nil, message: "Ringtone Name".localized(), preferredStyle: .alert)
+            alertVC.addTextField { textField in
+                textField.placeholder = "input name".localized()
+                textField.font = UIFont.systemFont(ofSize: 16)
+                textField.text = self.ringModel?.title
+                self.nameInputTextField = textField
+            }
+            let ok = UIAlertAction(title: "OK".localized(), style: .default) { [weak self] _ in
+                self?.ringModel?.title = self?.nameInputTextField?.text
+                self?.nameLabel.text = self?.ringModel?.title
+            }
+            let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel)
+            alertVC.addAction(cancel)
+            alertVC.addAction(ok)
+            present(alertVC, animated: true) {
+                self.nameInputTextField?.becomeFirstResponder()
+            }
+        default:
+            break
+        }
+    }
+
+    // 拖拽,暂停
+    func suspendPlay() {
+        if player.state == .playing {
+            isSuspendByAction = true
+            player.pause()
+        }
+    }
+
+    // 停止拖拽,继续播放
+    func autoPlayAfterMove() {
+//        if isSuspendByAction {
+        startPlay()
+        isSuspendByAction = false
+//        }
+    }
+
+    func startPlay() {
+        guard let model = ringModel else { return }
+
+        playButton.isSelected = !playButton.isSelected
+        if player.state == .playing {
+            player.pause()
+        } else {
+            if let playURL = model.playURL {
+                player.play(playURL)
+                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+                    self.player.seek(self.startDuration)
+                }
+                player.set(volum: fadeInDuration > 0 ? 0 : 1)
+
+                if UIApplication.getSystemVolume() < 0.1 {
+                    THUD.toast("Please turn up the volume".localized(), shake: true)
+                }
+            }
+        }
+    }
+
+    // 裁剪
+    func startCutAudio(completion: ((RingModel?, String?) -> Void)?) {
+        guard let model = ringModel,
+              let filePath = RingDownloadManager.shared.ringFilePath(for: model),
+              let copyModel = model.copy() as? RingModel else {
+            completion?(nil, nil)
+            return
+        }
+        let url = URL(fileURLWithPath: filePath)
+        let asset = AVAsset(url: url)
+
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyyMMddHHmmss"
+
+        var editName = model.title ?? formatter.string(from: Date())
+        editName = sanitizeFilePath(editName)
+        if !url.pathExtension.isEmpty {
+            editName.append(".\(url.pathExtension)")
+        }
+
+        guard let savePath = RingDownloadManager.shared.getCopyToPath(with: editName) else {
+            completion?(nil, nil)
+            return
+        }
+
+        copyModel.duration = endDuration - startDuration
+        THUD.showLoading()
+        audioTool.startTansformAudio(url: url.path, from: startDuration, to: endDuration, fadeIn: Double(fadeInDuration), fadeOut: Double(fadeOutDuration), savePath: savePath) { filePath, errMsg in
+            DispatchQueue.main.async {
+                THUD.hide()
+            }
+            if let filePath = filePath {
+                let url = URL(fileURLWithPath: filePath)
+                copyModel.title = url.deletingPathExtension().lastPathComponent.removingPercentEncoding
+                copyModel.fileName = url.lastPathComponent
+                copyModel.size = FileManager.default.getFileSize(url) ?? copyModel.size
+                completion?(copyModel, errMsg)
+            } else {
+                completion?(nil, errMsg)
+            }
+        }
+    }
+
+    func saveButtonClick() {
+        guard let current = ringModel else { return }
+//        guard operationCache.count > 1 else {
+//            // 只有原音频,意味着未进行裁剪操作,提示
+//            let alertVC = UIAlertController(title: nil, message: "No audio can be saved. Whether to save the original audio?", preferredStyle: .alert)
+//            alertVC.addAction(UIAlertAction(title: "Cancel", style: .cancel))
+//            alertVC.addAction(UIAlertAction(title: "Save", style: .default, handler: { [weak self] _ in
+//                RingDownloadManager.shared.saveEdited(ring: current)
+//                self?.dismiss(animated: true)
+//            }))
+//            present(alertVC, animated: true)
+//            return
+//        }
+
+        player.pause()
+        let dismissHandler: (() -> Void)? = { [weak self] in
+            self?.dismiss(animated: true, completion: {
+                /// 编辑完也需要小红点提示
+                RingDownloadManager.shared.hasUnreadRing = true
+                NotificationCenter.default.post(name: .downloadUnreadStateChanged, object: nil)
+
+                if let _ = UIApplication.topViewController as? MineRingsViewController {
+                    THUD.toast("Saved successfully".localized(), shake: true)
+                    return
+                }
+                SaveSuccessTipsView.show(at: UIApplication.rootViewController?.topController.view) {
+                    guard let topVC = UIApplication.topViewController else {
+                        return
+                    }
+                    let mineVC = MineRingsViewController()
+                    mineVC.currentType = .edited
+                    topVC.navigationController?.pushViewController(mineVC, animated: true)
+                }
+            })
+        }
+
+//        // 未编辑, 不需要裁剪
+//        guard startCropRate != 0 || endCropRate != 1.0
+//                || fadeInDuration != 0 || fadeOutDuration != 0 else {
+//            // 修改名称,直接保存
+//            RingDownloadManager.shared.saveEdited(ring: current)
+//            dismissHandler?()
+//            return
+//        }
+
+        // 保存音频
+        startCutAudio { result, errMsg in
+            if let ringModel = result {
+                // 裁剪的音频,使用本地文件播放,清空网络url
+                ringModel.fileUrl = nil
+                RingDownloadManager.shared.saveEdited(ring: ringModel)
+                DispatchQueue.main.async {
+                    dismissHandler?()
+                }
+            } else {
+                DispatchQueue.main.async {
+                    THUD.toast(errMsg ?? "Sorry, Save Failure".localized())
+                }
+            }
+        }
+    }
+
+    override func navigationBarItemClick(_ type: NavigationBarAction) {
+        if case .back = type {
+            guard operationCache.count <= 1 else {
+                let alertVC = UIAlertController(title: nil, message: "As you leave, any changes you have made will not be saved.".localized(), preferredStyle: .alert)
+                alertVC.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel))
+                alertVC.addAction(UIAlertAction(title: "Leave".localized(), style: .default, handler: { [weak self] _ in
+                    self?.dismiss(animated: true)
+                }))
+                present(alertVC, animated: true)
+                return
+            }
+            dismiss(animated: true)
+        }
+    }
+}

+ 55 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHAudioProcessing.swift

@@ -0,0 +1,55 @@
+//
+//  ZHAudioProcessing.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+struct ZHAudioProcessing {
+    
+    typealias BufferRefSuccessHandler = (NSMutableData) -> Swift.Void
+    typealias BufferRefFailureHandler = (Error?) -> Swift.Void
+    
+    public static func bufferRef(
+        asset: AVAsset,
+        track: AVAssetTrack,
+        success: BufferRefSuccessHandler?,
+        failure: BufferRefFailureHandler?
+        ) {
+        let data = NSMutableData()
+        let dict: [String: Any] = [AVFormatIDKey: kAudioFormatLinearPCM,
+                                   AVLinearPCMIsBigEndianKey: false,
+                                   AVLinearPCMIsFloatKey: false,
+                                   AVLinearPCMBitDepthKey: 16]
+        do {
+            let reader: AVAssetReader = try AVAssetReader(asset: asset)
+            let output: AVAssetReaderTrackOutput = AVAssetReaderTrackOutput(track: track, outputSettings: dict)
+            reader.add(output)
+            reader.startReading()
+            while reader.status == .reading {
+                if let sampleBuffer: CMSampleBuffer = output.copyNextSampleBuffer() {
+                    if let blockBuffer: CMBlockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) {
+                        let length: Int = CMBlockBufferGetDataLength(blockBuffer)
+                        var sampleBytes = [Int16](repeating: Int16(), count: length)
+                        CMBlockBufferCopyDataBytes(blockBuffer, atOffset: 0, dataLength: length, destination: &sampleBytes)
+//                        CMBlockBufferCopyDataBytes(blockBuffer, 0, length, &sampleBytes)
+                        data.append(&sampleBytes, length: length)
+                        CMSampleBufferInvalidate(sampleBuffer)
+                    }
+                }
+            }
+            if reader.status == .completed {
+                print("读取结束")
+                success?(data)
+            } else {
+                failure?(nil)
+            }
+        } catch let err {
+            failure?(err)
+        }
+    }
+}

+ 37 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHCroppedDelegate.swift

@@ -0,0 +1,37 @@
+//
+//  ZHCroppedDelegate.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol ZHCroppedDelegate {
+    /**
+     start cropped
+     */
+    @objc optional func waveformView(startCropped: UIView, progress rate: CGFloat)
+    
+    /**
+     end cropped
+     */
+    @objc optional func waveformView(endCropped: UIView, progress rate: CGFloat)
+    
+    /**
+     will
+     */
+    @objc optional func waveformView(croppedStartDragging cropped: UIView)
+    
+    /**
+     ing
+     */
+    @objc optional func waveformView(croppedDragIn cropped: UIView)
+    
+    /**
+     ed
+     */
+    @objc optional func waveformView(croppedDragFinish cropped: UIView)
+    
+}

+ 36 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHTrackProcessing.swift

@@ -0,0 +1,36 @@
+//
+//  ZHTrackProcessing.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+struct ZHTrackProcessing {
+    public static func cutAudioData(size: CGSize, recorder data: NSMutableData, scale: CGFloat) -> [CGFloat] {
+        var filteredSamplesMA: [CGFloat] = []
+        let sampleCount = data.length / MemoryLayout<Int>.size
+        let binSize = CGFloat(sampleCount) / (size.width * scale)
+        var i = 0
+        while i < sampleCount {
+            let rangeData = data.subdata(with: NSRange(location: i, length: 1))
+            let item = rangeData.withUnsafeBytes({ (ptr: UnsafePointer<Int>) -> Int in
+                return ptr.pointee
+            })
+            filteredSamplesMA.append(CGFloat(item))
+            i += Int(binSize)
+        }
+        return trackScale(size: size, source: filteredSamplesMA)
+    }
+    
+    private static func trackScale(size: CGSize, source: [CGFloat]) -> [CGFloat] {
+        if let max = source.max() {
+            let k = size.height / max
+            return source.map{ $0 * k }
+        }
+        return source
+    }
+}

+ 311 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHWaveformView.swift

@@ -0,0 +1,311 @@
+//
+//  ZHWaveformView.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+public class ZHWaveformView: UIView {
+    
+    /** waves color */
+    public var wavesColor: UIColor = .red {
+        didSet {
+            DispatchQueue.main.async {
+                _ = self.trackLayer.map({ [unowned self] in
+                    $0.strokeColor = self.wavesColor.cgColor
+                })
+            }
+        }
+    }
+    
+    /** Cut off the beginning part color */
+    public var beginningPartColor: UIColor = .gray
+    
+    /** Cut out the end part color */
+    public var endPartColor: UIColor = .gray
+    
+    /** Track Scale normal 0.5, max 1*/
+    public var trackScale: CGFloat = 0.5 {
+        didSet {
+            if let `assetMutableData` = assetMutableData {
+                croppedViewZero()
+                trackProcessingCut = ZHTrackProcessing.cutAudioData(size: self.frame.size, recorder: assetMutableData, scale: trackScale)
+                drawTrack(
+                    with: CGRect(x: (startCroppedView?.bounds.width ?? 0),
+                                 y: 0,
+                                 width: self.frame.width - (startCroppedView?.bounds.width ?? 0) - (endCroppedView?.bounds.width ?? 0),
+                                 height: self.frame.height),
+                    filerSamples: trackProcessingCut ?? []
+                )
+            }
+        }
+    }
+    
+    public weak var croppedDelegate: ZHCroppedDelegate? {
+        didSet { layoutIfNeeded() }
+    }
+    
+    public weak var waveformDelegate: ZHWaveformViewDelegate?
+    
+    private var fileURL: URL
+    
+    private var asset: AVAsset?
+    
+    private var track: AVAssetTrack?
+    
+    private var trackLayer: [CAShapeLayer] = []
+    
+    private var startCroppedView: UIView?
+    
+    private var endCroppedView: UIView?
+    
+    private var leftCorppedCurrentX: CGFloat = 0
+    
+    private var rightCorppedCurrentX: CGFloat = 0
+    
+    private var trackWidth: CGFloat = 0
+    
+    private var startCorppedIndex: Int = 0
+    
+    private var endCorppedIndex: Int = 0
+    
+    private var trackProcessingCut: [CGFloat]?
+    
+    private var assetMutableData: NSMutableData?
+    
+    private(set) var startCroppedRate: CGFloat = 0
+    private(set) var endCroppedRate: CGFloat = 0
+    
+    public init(frame: CGRect, fileURL: URL) {
+        self.fileURL = fileURL
+        super.init(frame: frame)
+        waveformDelegate?.waveformViewStartDrawing?(waveformView: self)
+        backgroundColor = .white
+        
+        let asset = AVAsset(url: fileURL)
+        guard let track = asset.tracks(withMediaType: .audio).first else {
+            return
+        }
+        self.asset = asset
+        self.track = track
+        ZHAudioProcessing.bufferRef(asset: asset, track: track, success: { [unowned self] (data) in
+            self.assetMutableData = data
+            self.trackProcessingCut = ZHTrackProcessing.cutAudioData(size: frame.size, recorder: data, scale: self.trackScale)
+            self.drawTrack(with: CGRect(origin: .zero, size: frame.size), filerSamples: self.trackProcessingCut ?? [])
+            self.waveformDelegate?.waveformViewDrawComplete?(waveformView: self)
+        }) { (error) in
+            assert(true, error?.localizedDescription ?? "Error, AudioProcessing.bufferRef")
+        }
+    }
+    
+    override public func layoutIfNeeded() {
+        super.layoutIfNeeded()
+        if let samples = trackProcessingCut {
+            creatCroppedView()
+            drawTrack(
+                with: CGRect(x: startCroppedView?.bounds.width ?? 0,
+                             y: 0,
+                             width: frame.width - (startCroppedView?.bounds.width ?? 0) - (endCroppedView?.bounds.width ?? 0),
+                             height: frame.height),
+                filerSamples: samples
+            )
+        }
+    }
+    
+    private func drawTrack(with rect: CGRect, filerSamples: [CGFloat]) {
+        _ = trackLayer.map{ $0.removeFromSuperlayer() }
+        trackLayer.removeAll()
+        startCroppedView?.removeFromSuperview()
+        endCroppedView?.removeFromSuperview()
+        // bezier width
+        trackWidth = rect.width / (CGFloat(filerSamples.count - 1) + CGFloat(filerSamples.count))
+        endCorppedIndex = filerSamples.count
+        for t in 0..<filerSamples.count {
+            let layer = CAShapeLayer()
+            layer.frame = CGRect(
+                x: CGFloat(t) * trackWidth * 2 + (startCroppedView?.bounds.width ?? 0),
+                y: 0,
+                width: trackWidth,
+                height: rect.height
+            )
+            layer.lineCap = CAShapeLayerLineCap.butt
+            layer.lineJoin = CAShapeLayerLineJoin.round
+            layer.lineWidth = trackWidth
+            layer.strokeColor = wavesColor.cgColor
+            self.layer.addSublayer(layer)
+            self.trackLayer.append(layer)
+        }
+        
+        for i in 0..<filerSamples.count {
+            let itemLinePath = UIBezierPath()
+            let y: CGFloat = (rect.height - filerSamples[i]) / 2
+            let height: CGFloat = filerSamples[i] + y
+            itemLinePath.move(to: CGPoint(x: 0, y: y))
+            itemLinePath.addLine(to: CGPoint(x: 0, y: height))
+            itemLinePath.close()
+            itemLinePath.lineWidth = trackWidth
+            let itemLayer = trackLayer[i]
+            itemLayer.path = itemLinePath.cgPath
+        }
+        if let l = startCroppedView {
+            addSubview(l)
+        }
+        if let r = endCroppedView {
+            addSubview(r)
+        }
+        
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+}
+
+extension ZHWaveformView {
+    
+    private func croppedViewZero() {
+        if let leftCropped = startCroppedView {
+            leftCropped.frame = CGRect(x: 0, y: leftCropped.frame.origin.y, width: leftCropped.bounds.width, height: leftCropped.bounds.height)
+        }
+        if let rightCropped = endCroppedView {
+            rightCropped.frame = CGRect(x: bounds.width - rightCropped.bounds.width, y: rightCropped.frame.origin.y, width: rightCropped.bounds.width, height: rightCropped.bounds.height)
+        }
+        
+    }
+    
+    private func creatCroppedView() {
+//        if let leftCropped = croppedDelegate?.waveformView(startCropped: self) {
+//            leftCropped.frame = CGRect(x: 0, y: leftCropped.frame.origin.y, width: leftCropped.bounds.width, height: leftCropped.bounds.height)
+//            leftCorppedCurrentX = 0
+//            let leftPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.leftCroppedPanRecognizer(sender:)))
+//            leftCropped.addGestureRecognizer(leftPanRecognizer)
+//            leftCropped.isUserInteractionEnabled = true
+//            startCroppedView = leftCropped
+//        }
+        
+//        if let rightCropped = croppedDelegate?.waveformView(endCropped: self) {
+//            rightCropped.frame = CGRect(x: bounds.width - rightCropped.bounds.width, y: rightCropped.frame.origin.y, width: rightCropped.bounds.width, height: rightCropped.bounds.height)
+//            rightCorppedCurrentX = bounds.width - rightCropped.bounds.width
+//            let rightPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.rightCroppedPanRecognizer(sender:)))
+//            rightCropped.addGestureRecognizer(rightPanRecognizer)
+//            rightCropped.isUserInteractionEnabled = true
+//            endCroppedView = rightCropped
+//        }
+    }
+    
+    @objc private func leftCroppedPanRecognizer(sender: UIPanGestureRecognizer) {
+        let limitMinX: CGFloat = frame.minX
+        let limitMaxX: CGFloat = endCroppedView?.frame.minX ?? bounds.width
+        if sender.state == .began {
+            croppedDelegate?.waveformView?(croppedDragIn: startCroppedView ?? UIView())
+        } else if sender.state == .changed {
+            croppedDelegate?.waveformView?(croppedDragIn: startCroppedView ?? UIView())
+            let newPoint = sender.translation(in: self)
+            var center = startCroppedView?.center
+            center?.x = leftCorppedCurrentX + newPoint.x
+            guard (center?.x ?? 0) > limitMinX && (center?.x ?? 0) < limitMaxX else { return }
+            startCroppedView?.center = center ?? .zero
+        } else if sender.state == .ended || sender.state == .failed {
+            croppedDelegate?.waveformView?(croppedDragFinish: startCroppedView ?? UIView())
+            leftCorppedCurrentX = startCroppedView?.center.x ?? 0
+        }
+        if (startCroppedView?.frame.minX ?? 0) < 0 {
+            var leftFrame = startCroppedView?.frame
+            leftFrame?.origin.x = 0
+            startCroppedView?.frame = leftFrame ?? .zero
+        }
+        
+        if (startCroppedView?.frame.maxX ?? 0) > limitMaxX {
+            var leftFrame = startCroppedView?.frame
+            leftFrame?.origin.x = limitMaxX - (startCroppedView?.bounds.width ?? 0)
+            startCroppedView?.frame = leftFrame ?? .zero
+        } // floorf ceilf
+        let lenght = ceilf(Float((((startCroppedView?.frame.maxX ?? 0) - (startCroppedView?.bounds.width ?? 0)) / trackWidth)))
+        let bzrLenght = ceilf(lenght/2)
+        startCorppedIndex = Int(bzrLenght) > trackLayer.count ? trackLayer.count : Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width - (startCroppedView?.frame.width ?? 0) - (endCroppedView?.frame.width ?? 0)
+        croppedDelegate?.waveformView?(startCropped: startCroppedView ?? UIView(), progress: ((startCroppedView?.frame.maxX ?? 0) - 20)/bezierWidth)
+    }
+    
+    func updateLeftCroppedPosition(_ position: CGFloat) {
+        let lenght = ceilf(Float((position / trackWidth)))
+        let bzrLenght = ceilf(lenght/2)
+        startCorppedIndex = Int(bzrLenght) > trackLayer.count ? trackLayer.count : Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width
+        self.startCroppedRate = position/bezierWidth
+    }
+    
+    func updateRightCroppedPosition(_ position: CGFloat) {
+        let lenght = ceilf(Float(position / trackWidth))
+        let bzrLenght = floorf(lenght/2) < 0 ? 0 : ceilf(lenght/2)
+        endCorppedIndex = Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width
+        self.endCroppedRate = position/bezierWidth
+    }
+    
+    @objc private func rightCroppedPanRecognizer(sender: UIPanGestureRecognizer) {
+        let limitMinX: CGFloat = startCroppedView?.frame.maxX ?? 0
+        let limitMaxX: CGFloat = frame.maxX
+        if sender.state == .began {
+            croppedDelegate?.waveformView?(croppedStartDragging: endCroppedView ?? UIView())
+        } else if sender.state == .changed {
+            croppedDelegate?.waveformView?(croppedDragIn: endCroppedView ?? UIView())
+            let newPoint = sender.translation(in: self)
+            var center = endCroppedView?.center
+            center?.x = rightCorppedCurrentX + newPoint.x
+            guard (center?.x ?? 0) > limitMinX && (center?.x ?? 0) < limitMaxX else { return }
+            endCroppedView?.center = center ?? .zero
+        } else if sender.state == .ended || sender.state == .failed {
+            croppedDelegate?.waveformView?(croppedDragFinish: endCroppedView ?? UIView())
+            rightCorppedCurrentX = endCroppedView?.center.x ?? (bounds.width - (endCroppedView?.bounds.width ?? 0))
+        }
+        if (endCroppedView?.frame.maxX ?? 0) > frame.maxX {
+            var rightFrame = endCroppedView?.frame
+            rightFrame?.origin.x = frame.maxX - (endCroppedView?.bounds.width ?? 0)
+            endCroppedView?.frame = rightFrame ?? .zero
+        }
+        if (endCroppedView?.frame.minX ?? 0) < limitMinX {
+            var rightFrame = endCroppedView?.frame
+            rightFrame?.origin.x = limitMinX
+            endCroppedView?.frame = rightFrame ?? .zero
+        }
+        let lenght = ceilf(Float(((endCroppedView?.frame.minX ?? 0) - (startCroppedView?.bounds.width ?? 0)) / trackWidth))
+        let bzrLenght = floorf(lenght/2) < 0 ? 0 : ceilf(lenght/2)
+        endCorppedIndex = Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width - (startCroppedView?.frame.width ?? 0) - (endCroppedView?.frame.width ?? 0)
+        croppedDelegate?.waveformView?(endCropped: endCroppedView ?? UIView(), progress: ((endCroppedView?.frame.minX ?? 0)-20)/bezierWidth)
+    }
+    
+    typealias TrackIndex = Int
+    
+    func croppedWaveform(start: TrackIndex, end: TrackIndex) {
+        guard start > 0, start < trackLayer.count,
+              end > start, end <= trackLayer.count else {
+            return
+        }
+        let beginLayers = trackLayer[0..<start]
+        let wavesLayers = trackLayer[start..<end]
+        let endLayers = trackLayer[end..<trackLayer.count]
+        DispatchQueue.main.async {
+            _ = beginLayers.map({ [unowned self] in
+                $0.strokeColor = self.beginningPartColor.cgColor
+            })
+            _ = wavesLayers.map({ [unowned self] in
+                $0.strokeColor = self.wavesColor.cgColor
+            })
+            _ = endLayers.map({ [unowned self] in
+                $0.strokeColor = self.endPartColor.cgColor
+            })
+        }
+    }
+    
+}

+ 19 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHWaveformViewDelegate.swift

@@ -0,0 +1,19 @@
+//
+//  ZHWaveformViewDelegate.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol ZHWaveformViewDelegate {
+    
+    // start
+    @objc optional func waveformViewStartDrawing(waveformView: ZHWaveformView)
+    
+    // complete
+    @objc optional func waveformViewDrawComplete(waveformView: ZHWaveformView)
+    
+}