|
@@ -0,0 +1,825 @@
|
|
|
+//
|
|
|
+// TSEditAudioVC.swift
|
|
|
+// AIRingtone
|
|
|
+//
|
|
|
+// Created by 100Years on 2025/3/30.
|
|
|
+//
|
|
|
+import AVFoundation
|
|
|
+class TSEditAudioVC: TSEditAudioVideoBaseVC , ZHCroppedDelegate, ZHWaveformViewDelegate {
|
|
|
+ //#################################### 数据区域 ####################################//
|
|
|
+ override var titleName:String{
|
|
|
+ "Edit Ringtone".localized
|
|
|
+ }
|
|
|
+
|
|
|
+ var ringModel: TSRingModel //请传递副本过来,会修改数据的
|
|
|
+
|
|
|
+ init(ringModel: TSRingModel,editOriginalURL:URL) {
|
|
|
+ self.ringModel = ringModel
|
|
|
+ super.init(editOriginalURL: editOriginalURL)
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ lazy var ringDuration:Double = Double(ringModel.duration)
|
|
|
+ //#################################### 名字 ####################################//
|
|
|
+ lazy var nameLabel: UILabel = {
|
|
|
+ let nameLabel = UILabel.createLabel(text: ringModel.title, font: .font(size: 14),textColor: .white.withAlphaComponent(0.8),textAlignment: .center)
|
|
|
+ return nameLabel
|
|
|
+ }()
|
|
|
+
|
|
|
+ weak var nameInputTextField: UITextField?
|
|
|
+ lazy var nameButton: UIButton = {
|
|
|
+ let nameButton = UIButton.createButton(image: UIImage(named: "edit_field")) { [weak self] in
|
|
|
+ guard let self = self else { return }
|
|
|
+
|
|
|
+ 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
|
|
|
+ guard let self = self else { return }
|
|
|
+ nameLabel.text = nameInputTextField?.text
|
|
|
+
|
|
|
+ if let text = nameLabel.text{
|
|
|
+ ringModel.title = text
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let cancel = UIAlertAction(title: "Cancel".localized, style: .cancel)
|
|
|
+ alertVC.addAction(cancel)
|
|
|
+ alertVC.addAction(ok)
|
|
|
+ present(alertVC, animated: true) {
|
|
|
+ self.nameInputTextField?.becomeFirstResponder()
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ return nameButton
|
|
|
+ }()
|
|
|
+
|
|
|
+ lazy var nameView: UIView = {
|
|
|
+ let nameView = UIView()
|
|
|
+
|
|
|
+ let nameContentView = UIView()
|
|
|
+ nameView.addSubview(nameContentView)
|
|
|
+ nameContentView.snp.makeConstraints { make in
|
|
|
+ make.center.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ nameContentView.addSubview(nameLabel)
|
|
|
+ nameContentView.addSubview(nameButton)
|
|
|
+
|
|
|
+ nameLabel.snp.makeConstraints { make in
|
|
|
+ make.leading.equalToSuperview()
|
|
|
+ make.width.lessThanOrEqualTo(220*kDesignScale)
|
|
|
+ make.height.equalTo(23)
|
|
|
+ make.top.bottom.equalTo(0)
|
|
|
+ }
|
|
|
+ nameButton.snp.makeConstraints { make in
|
|
|
+ make.trailing.equalToSuperview()
|
|
|
+ make.leading.equalTo(nameLabel.snp.trailing)
|
|
|
+ make.width.height.equalTo(23)
|
|
|
+ make.top.bottom.equalTo(0)
|
|
|
+ }
|
|
|
+
|
|
|
+ return nameView
|
|
|
+ }()
|
|
|
+
|
|
|
+ //#################################### track编辑视频 ####################################//\
|
|
|
+ let trackContentViewLeft:CGFloat = 24.0
|
|
|
+ let trackViewH:CGFloat = 210.0
|
|
|
+ lazy var trackViewW:CGFloat = k_ScreenWidth - trackContentViewLeft * 2
|
|
|
+ lazy var trackView: ZHWaveformView = {
|
|
|
+ let waveform = ZHWaveformView(frame: CGRect(x: 0, y: 0, width: trackViewW, height:trackViewH),fileURL: editOriginalURL)
|
|
|
+ waveform.backgroundColor = .clear
|
|
|
+ waveform.beginningPartColor = .white.withAlphaComponent(0.2) // color
|
|
|
+ waveform.endPartColor = .white.withAlphaComponent(0.2)
|
|
|
+ waveform.wavesColor = "#7E57F4".uiColor
|
|
|
+ waveform.trackScale = 0.2// 0 ~ 1
|
|
|
+ waveform.waveformDelegate = self
|
|
|
+ waveform.croppedDelegate = self
|
|
|
+
|
|
|
+ return waveform
|
|
|
+ }()
|
|
|
+
|
|
|
+ lazy var trackContentView: UIView = {
|
|
|
+ let trackContentView = UIView(frame: CGRectMake(24, 0, k_ScreenWidth-48, trackViewH+12))
|
|
|
+
|
|
|
+ trackContentView.addSubview(trackView)
|
|
|
+ trackView.snp.makeConstraints { make in
|
|
|
+ make.top.centerX.equalToSuperview()
|
|
|
+ make.width.equalTo(trackView.width)
|
|
|
+ make.height.equalTo(trackView.height)
|
|
|
+ }
|
|
|
+ return trackContentView
|
|
|
+ }()
|
|
|
+
|
|
|
+
|
|
|
+ func creatDragView()->UIView{
|
|
|
+ let bgView = UIView()
|
|
|
+
|
|
|
+ let view1 = UIView.creatColor(color: .white)
|
|
|
+ bgView.addSubview(view1)
|
|
|
+ let view2 = UIView.creatColor(color: .white)
|
|
|
+ view2.cornerRadius = 6
|
|
|
+ bgView.addSubview(view2)
|
|
|
+
|
|
|
+ view1.snp.makeConstraints { make in
|
|
|
+ make.width.equalTo(2)
|
|
|
+ make.height.equalTo(trackViewH)
|
|
|
+ make.centerX.equalToSuperview()
|
|
|
+ make.top.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ view2.snp.makeConstraints { make in
|
|
|
+ make.top.equalTo(view1.snp.bottom)
|
|
|
+ make.width.height.equalTo(12)
|
|
|
+ make.bottom.equalToSuperview()
|
|
|
+ make.leading.equalTo(12)
|
|
|
+ make.trailing.equalTo(-12)
|
|
|
+ }
|
|
|
+ return bgView
|
|
|
+ }
|
|
|
+ lazy var dragLeftView: UIView = creatDragView()
|
|
|
+ lazy var dragRightView: UIView = creatDragView()
|
|
|
+ var progressLine: UIView = UIView.creatColor(color: .themeColor)
|
|
|
+
|
|
|
+
|
|
|
+ lazy var startTimeLabel: UILabel = {
|
|
|
+ let label = UILabel.createLabel(text: "00:00s",font: .font(size: 14.0),textColor: .lesserText)
|
|
|
+ return label
|
|
|
+ }()
|
|
|
+ var endTimeLabel: UILabel = {
|
|
|
+ let label = UILabel.createLabel(text: "10:00s",font: .font(size: 14.0),textColor: .lesserText)
|
|
|
+ return label
|
|
|
+ }()
|
|
|
+ var timeLabel: UILabel = {
|
|
|
+ let label = UILabel.createLabel(text: "00:00",font: .font(size: 26.0),textColor: .lesserText)
|
|
|
+ return label
|
|
|
+ }()
|
|
|
+
|
|
|
+
|
|
|
+ lazy var timeView: UIView = {
|
|
|
+ let timeView = UIView()
|
|
|
+
|
|
|
+ timeView.addSubview(startTimeLabel)
|
|
|
+ timeView.addSubview(endTimeLabel)
|
|
|
+ timeView.addSubview(timeLabel)
|
|
|
+
|
|
|
+ startTimeLabel.snp.makeConstraints { make in
|
|
|
+ make.leading.equalTo(16)
|
|
|
+ make.top.equalTo(12)
|
|
|
+ make.height.equalTo(20)
|
|
|
+ }
|
|
|
+
|
|
|
+ endTimeLabel.snp.makeConstraints { make in
|
|
|
+ make.trailing.equalTo(-16)
|
|
|
+ make.top.equalTo(12)
|
|
|
+ make.height.equalTo(20)
|
|
|
+ }
|
|
|
+
|
|
|
+ timeLabel.snp.makeConstraints { make in
|
|
|
+ make.centerX.equalToSuperview()
|
|
|
+ make.top.equalTo(45)
|
|
|
+ make.height.equalTo(26)
|
|
|
+ }
|
|
|
+
|
|
|
+ return timeView
|
|
|
+ }()
|
|
|
+
|
|
|
+ lazy var trackBgView: UIView = {
|
|
|
+ let trackBgView = UIView()
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ let gradientImageView = UIImageView.createImageView(imageName: "trackBg_gradient",contentMode: .scaleToFill)
|
|
|
+ trackBgView.addSubview(gradientImageView)
|
|
|
+ gradientImageView.snp.makeConstraints { make in
|
|
|
+ make.leading.top.trailing.equalToSuperview()
|
|
|
+ make.height.equalTo(trackViewH)
|
|
|
+ }
|
|
|
+
|
|
|
+ trackBgView.addSubview(trackContentView)
|
|
|
+ trackContentView.snp.makeConstraints { make in
|
|
|
+ make.top.equalToSuperview()
|
|
|
+ make.leading.equalTo(trackContentView.x)
|
|
|
+ make.trailing.equalTo(-trackContentView.x)
|
|
|
+ make.height.equalTo(trackViewH)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ trackBgView.addSubview(dragLeftView)
|
|
|
+ dragLeftView.snp.makeConstraints { make in
|
|
|
+ make.top.leading.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ trackBgView.addSubview(dragRightView)
|
|
|
+ dragRightView.snp.makeConstraints { make in
|
|
|
+ make.top.equalToSuperview()
|
|
|
+ make.trailing.equalToSuperview()
|
|
|
+ }
|
|
|
+ progressLine.isHidden = true
|
|
|
+ trackBgView.addSubview(progressLine)
|
|
|
+ progressLine.snp.makeConstraints { make in
|
|
|
+ make.top.leading.equalToSuperview()
|
|
|
+ make.width.equalTo(1)
|
|
|
+ make.height.equalTo(trackViewH)
|
|
|
+ }
|
|
|
+
|
|
|
+ trackBgView.addSubview(timeView)
|
|
|
+ timeView.snp.makeConstraints { make in
|
|
|
+ make.top.equalTo(trackContentView.bottom)
|
|
|
+ make.trailing.leading.equalToSuperview()
|
|
|
+ make.height.equalTo(71)
|
|
|
+ make.bottom.equalTo(0)
|
|
|
+ }
|
|
|
+
|
|
|
+ return trackBgView
|
|
|
+ }()
|
|
|
+
|
|
|
+
|
|
|
+ //#################################### Fade in ####################################//
|
|
|
+ lazy var fadeinSliderView: TSEditAudioSliderView = {
|
|
|
+ let fadeinSliderView = TSEditAudioSliderView()
|
|
|
+ fadeinSliderView.leftLabel.text = "Fade in".localized
|
|
|
+ fadeinSliderView.rightLabel.text = "0s"
|
|
|
+ fadeinSliderView.slider.maximumValue = 5
|
|
|
+// fadeinSliderView.slider.value = 3
|
|
|
+ return fadeinSliderView
|
|
|
+ }()
|
|
|
+ lazy var fadeinSlider: UISlider = {
|
|
|
+ let slider = fadeinSliderView.slider
|
|
|
+ slider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
|
|
|
+ slider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
|
|
|
+ slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
|
|
|
+ return slider
|
|
|
+ }()
|
|
|
+ //#################################### Fade out ####################################//
|
|
|
+
|
|
|
+ lazy var fadeoutSliderView: TSEditAudioSliderView = {
|
|
|
+ let fadeoutSliderView = TSEditAudioSliderView()
|
|
|
+ fadeoutSliderView.leftLabel.text = "Fade out".localized
|
|
|
+ fadeoutSliderView.rightLabel.text = "0s"
|
|
|
+ fadeoutSliderView.slider.maximumValue = 5
|
|
|
+// fadeoutSliderView.slider.value = 3
|
|
|
+ return fadeoutSliderView
|
|
|
+ }()
|
|
|
+ lazy var fadeoutSlider: UISlider = {
|
|
|
+ let slider = fadeoutSliderView.slider
|
|
|
+ slider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
|
|
|
+ slider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
|
|
|
+ slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
|
|
|
+ return slider
|
|
|
+ }()
|
|
|
+ //#################################### Output Volume ####################################//
|
|
|
+ lazy var outputVolumeSliderView: TSEditAudioSliderView = {
|
|
|
+ let outputVolumeSliderView = TSEditAudioSliderView()
|
|
|
+ outputVolumeSliderView.leftLabel.text = "Output Volume".localized
|
|
|
+ outputVolumeSliderView.rightLabel.text = "100%"
|
|
|
+ outputVolumeSliderView.slider.minimumValue = 0.2
|
|
|
+ outputVolumeSliderView.slider.maximumValue = 2.0
|
|
|
+ outputVolumeSliderView.slider.value = 1.0
|
|
|
+ return outputVolumeSliderView
|
|
|
+ }()
|
|
|
+ lazy var outputVolumeSlider: UISlider = {
|
|
|
+ let slider = outputVolumeSliderView.slider
|
|
|
+ slider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
|
|
|
+ slider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
|
|
|
+ slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
|
|
|
+ return slider
|
|
|
+ }()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ //#################################### viewDidLoad####################################//
|
|
|
+ override func createData() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ override func createView() {
|
|
|
+ super.createView()
|
|
|
+ setUpCusStackView()
|
|
|
+ }
|
|
|
+
|
|
|
+ func setUpCusStackView() {
|
|
|
+ cusStackView.addSubviewToStack(nameView)
|
|
|
+ nameView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(56)
|
|
|
+ make.width.equalTo(k_ScreenWidth)
|
|
|
+ }
|
|
|
+ cusStackView.addSubviewToStack(trackBgView)
|
|
|
+ trackBgView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(300)
|
|
|
+ make.width.equalTo(k_ScreenWidth)
|
|
|
+ }
|
|
|
+
|
|
|
+ cusStackView.addSubviewToStack(playButtonView)
|
|
|
+ playButtonView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(playButtonView.height)
|
|
|
+ make.width.equalTo(playButtonView.width)
|
|
|
+ }
|
|
|
+
|
|
|
+ cusStackView.addSubviewToStack(fadeinSliderView)
|
|
|
+ fadeinSliderView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(fadeinSliderView.height)
|
|
|
+ }
|
|
|
+
|
|
|
+ let spaceView = UIView()
|
|
|
+ cusStackView.addSubviewToStack(spaceView)
|
|
|
+ spaceView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(8)
|
|
|
+ }
|
|
|
+
|
|
|
+ cusStackView.addSubviewToStack(fadeoutSliderView)
|
|
|
+ fadeoutSliderView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(fadeinSliderView.height)
|
|
|
+ }
|
|
|
+
|
|
|
+ let spaceView1 = UIView()
|
|
|
+ cusStackView.addSubviewToStack(spaceView1)
|
|
|
+ spaceView1.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(8)
|
|
|
+ }
|
|
|
+
|
|
|
+ cusStackView.addSubviewToStack(outputVolumeSliderView)
|
|
|
+ outputVolumeSliderView.snp.makeConstraints { make in
|
|
|
+ make.height.equalTo(fadeinSliderView.height)
|
|
|
+ make.bottom.equalToSuperview()
|
|
|
+ }
|
|
|
+
|
|
|
+ _ = fadeinSlider
|
|
|
+ _ = fadeoutSlider
|
|
|
+ _ = outputVolumeSlider
|
|
|
+ }
|
|
|
+
|
|
|
+ override func dealThings() {
|
|
|
+
|
|
|
+ dePrint("TSEditAudioVC ringModel = \(String(describing: ringModel.toJSONString())),editOriginalURL = \(editOriginalURL)")
|
|
|
+
|
|
|
+ if player.duration != 0 {
|
|
|
+ ringDuration = player.duration
|
|
|
+ if ringModel.duration == 0{
|
|
|
+ ringModel.duration = Int(player.duration)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ operationCache.append(ringModel)
|
|
|
+ reloadTrackView()
|
|
|
+ }
|
|
|
+
|
|
|
+ private lazy var player:TSBusinessAudioPlayer = {
|
|
|
+ let player = TSBusinessAudioPlayer()
|
|
|
+ player.loadLoactionURL(url: self.editOriginalURL)
|
|
|
+ player.currentTimeChangedHandle = { [weak self] current,total in
|
|
|
+ guard let self = self else { return }
|
|
|
+ handlePlayer(progressChanged: current, total: total)
|
|
|
+ }
|
|
|
+
|
|
|
+ player.stateChangedHandle = { [weak self] state in
|
|
|
+ guard let self = self else { return }
|
|
|
+ handlePlayer(state: state)
|
|
|
+ }
|
|
|
+
|
|
|
+ return player
|
|
|
+ }()
|
|
|
+ private lazy var audioTool = AudioTool()
|
|
|
+ private var maxVolume:Float{
|
|
|
+ return outputVolume * 0.5
|
|
|
+ }
|
|
|
+
|
|
|
+ func reloadTrackView() {
|
|
|
+
|
|
|
+ tempMp3Path = editOriginalURL.path
|
|
|
+
|
|
|
+ startCropRate = 0
|
|
|
+ endCropRate = 1.0
|
|
|
+
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ self.dragLeftView.centerX = self.trackContentView.x
|
|
|
+ self.dragRightView.centerX = self.trackContentView.frame.maxX
|
|
|
+ self.previousRightX = self.dragRightView.centerX
|
|
|
+ self.trackView.updateLeftCroppedPosition(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ timeLabel.text = ringDuration.mmss
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //#################################### UI 储存属性 ####################################//
|
|
|
+ lazy var fadeInDuration: Int = 0 {
|
|
|
+ didSet {
|
|
|
+ fadeinSliderView.rightLabel.text = "\(fadeInDuration)s"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ lazy var fadeOutDuration: Int = 0 {
|
|
|
+ didSet {
|
|
|
+ fadeoutSliderView.rightLabel.text = "\(fadeOutDuration)s"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ lazy var outputVolume: Float = 1.0 {
|
|
|
+ didSet {
|
|
|
+ outputVolumeSliderView.rightLabel.text = "\(Int(outputVolume*100.0))%"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ lazy var previousLeftX: CGFloat = 0
|
|
|
+ lazy var startCropRate: CGFloat = 0 {
|
|
|
+ didSet {
|
|
|
+ let time = startDuration
|
|
|
+
|
|
|
+ // print("---start: \(time)")
|
|
|
+ startTimeLabel.text = time.mmss
|
|
|
+ timeLabel.text = (endDuration - startDuration).mmss
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 拖拽时被暂停,拖拽结束后,继续播放
|
|
|
+ lazy var isSuspendByAction = false
|
|
|
+ lazy var endCropRate: CGFloat = 1.0 {
|
|
|
+ didSet {
|
|
|
+ let time = endDuration
|
|
|
+ endTimeLabel.text = time.mmss
|
|
|
+ timeLabel.text = (endDuration - startDuration).mmss
|
|
|
+ }
|
|
|
+ }
|
|
|
+ lazy var previousRightX: CGFloat = 0
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ override func startPlay() {
|
|
|
+ playButton.isSelected = !playButton.isSelected
|
|
|
+ if player.isPlaying {
|
|
|
+ player.pause()
|
|
|
+ } else {
|
|
|
+ player.playUrlString(ringModel.audioUrl,localURL: editOriginalURL)
|
|
|
+ player.playUrlString(ringModel.audioUrl,localURL: editOriginalURL)
|
|
|
+ self.player.seek(to:self.startDuration)
|
|
|
+ player.setVolume(volume: fadeInDuration > 0 ? 0 : maxVolume)
|
|
|
+
|
|
|
+ if UIApplication.getSystemVolume() == 0.0 {
|
|
|
+ TSToastShared.showToast(text:"Please turn up the volume".localized,duration: 2.0)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func saveButtonClick() {
|
|
|
+ handleSaveCutAudio()
|
|
|
+ }
|
|
|
+
|
|
|
+ override func setButtonClick() {
|
|
|
+ handleSaveCutAudio { model in
|
|
|
+ if let ringModel = model {
|
|
|
+ kDelayMainShort {
|
|
|
+ let path = ringModel.documentPath.fillDocumentURL
|
|
|
+ if let vc = WindowHelper.topViewController(){
|
|
|
+ _ = kPurchaseToolShared.kshareBand(needVip: false, vc: vc, fileURL: path, fileName: ringModel.title)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func navBarClickLeftAction() {
|
|
|
+ if isThereAnyChange(){
|
|
|
+ super.navBarClickLeftAction()
|
|
|
+ }else{
|
|
|
+ pop()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+extension TSEditAudioVC {
|
|
|
+
|
|
|
+ @objc func sliderBeginTap(_ slider: UISlider) {
|
|
|
+ suspendPlay()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func sliderEndTap(_ slider: UISlider) {
|
|
|
+ autoPlayAfterMove()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func sliderValueChanged(_ slider: UISlider) {
|
|
|
+ // print("---\(slider.value)")
|
|
|
+ if player.isPlaying {
|
|
|
+ player.pause()
|
|
|
+ }
|
|
|
+ if slider == fadeinSlider {
|
|
|
+ fadeInDuration = Int(slider.value.rounded(.toNearestOrEven))
|
|
|
+ } else if slider == fadeoutSlider {
|
|
|
+ fadeOutDuration = Int(slider.value.rounded(.toNearestOrEven))
|
|
|
+ } else if slider == outputVolumeSlider {
|
|
|
+ outputVolume = slider.value
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 裁剪起始时间
|
|
|
+ var startDuration: Double {
|
|
|
+ return startCropRate * CGFloat(ringDuration)
|
|
|
+ }
|
|
|
+
|
|
|
+ var startMinCenterX: CGFloat {
|
|
|
+ return trackContentView.x
|
|
|
+ }
|
|
|
+
|
|
|
+ var startMaxCenterX: CGFloat {
|
|
|
+ let ratio = (endCropRate * CGFloat(ringDuration) - 10) / CGFloat(ringDuration)
|
|
|
+ return ratio * trackContentView.width + trackContentView.x
|
|
|
+ }
|
|
|
+
|
|
|
+ var endMinCenterX: CGFloat {
|
|
|
+ let ratio = (startCropRate * CGFloat(ringDuration) + 10) / CGFloat(ringDuration)
|
|
|
+ return ratio * trackContentView.width + trackContentView.x
|
|
|
+ }
|
|
|
+
|
|
|
+ var endMaxCenterX: CGFloat {
|
|
|
+ return trackContentView.frame.maxX
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func leftPanRecognizer(sender: UIPanGestureRecognizer) {
|
|
|
+
|
|
|
+ dePrint("TSEditAudioVC leftPanRecognizer=\(sender)")
|
|
|
+ let limitMinCenterX: CGFloat = startMinCenterX
|
|
|
+ let limitMaxCenterX: CGFloat = startMaxCenterX
|
|
|
+ guard limitMaxCenterX > limitMinCenterX else {
|
|
|
+ if sender.state == .began {
|
|
|
+ dePrint("TSEditAudioVC rightPanRecognizer No less than 10s")
|
|
|
+ TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
|
|
|
+ }
|
|
|
+ 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 {
|
|
|
+ TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
|
|
|
+ }
|
|
|
+ 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
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ var endDuration: Double {
|
|
|
+ return endCropRate * CGFloat(CGFloat(ringDuration))
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @objc private func rightPanRecognizer(sender: UIPanGestureRecognizer) {
|
|
|
+// dePrint("TSEditAudioVC rightPanRecognizer=\(sender)")
|
|
|
+ let limitMinCenterX: CGFloat = endMinCenterX
|
|
|
+ let limitMaxCenterX: CGFloat = endMaxCenterX
|
|
|
+
|
|
|
+
|
|
|
+ guard limitMaxCenterX > limitMinCenterX else {
|
|
|
+ if sender.state == .began {
|
|
|
+ dePrint("TSEditAudioVC rightPanRecognizer No less than 10s")
|
|
|
+ // 越界,拖不动了
|
|
|
+ TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if sender.state == .began {
|
|
|
+ suspendPlay()
|
|
|
+ } else if sender.state == .changed {
|
|
|
+ let newPoint = sender.translation(in: trackBgView)
|
|
|
+ //dePrint("rightPanRecognizer newPoint = \(newPoint)")
|
|
|
+ var center = dragRightView.center
|
|
|
+ //dePrint("rightPanRecognizer dragRightView.center = \(dragRightView.center)")
|
|
|
+ //dePrint("rightPanRecognizer previousRightX = \(previousRightX)")
|
|
|
+ center.x = previousRightX + newPoint.x
|
|
|
+ //dePrint("rightPanRecognizer center.x = \(center.x)")
|
|
|
+ //dePrint("rightPanRecognizer limitMinCenterX = \(limitMinCenterX),limitMaxCenterX = \(limitMaxCenterX)")
|
|
|
+ guard center.x > limitMinCenterX, center.x < limitMaxCenterX else {
|
|
|
+ // 越界,拖不动了, 最少10s
|
|
|
+ if endDuration - startDuration < 11 {
|
|
|
+ TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ dragRightView.center = center
|
|
|
+ //dePrint("rightPanRecognizer center = \(center)")
|
|
|
+ } else if sender.state == .ended || sender.state == .failed {
|
|
|
+ previousRightX = dragRightView.centerX
|
|
|
+ autoPlayAfterMove()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 边界校验
|
|
|
+ if dragRightView.centerX > limitMaxCenterX {
|
|
|
+ dragRightView.centerX = limitMaxCenterX
|
|
|
+ //dePrint("rightPanRecognizer limitMaxCenterX = \(limitMaxCenterX)")
|
|
|
+ }
|
|
|
+ if dragRightView.centerX < limitMinCenterX {
|
|
|
+ dragRightView.centerX = limitMinCenterX
|
|
|
+ //dePrint("rightPanRecognizer limitMinCenterX = \(limitMinCenterX)")
|
|
|
+ }
|
|
|
+
|
|
|
+ let position = dragRightView.centerX - trackContentView.x
|
|
|
+ //dePrint("rightPanRecognizer position = \(position)")
|
|
|
+ trackView.updateRightCroppedPosition(position)
|
|
|
+ endCropRate = position / trackContentView.width
|
|
|
+ //dePrint("rightPanRecognizer endCropRate = \(endCropRate)")
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拖拽,暂停
|
|
|
+ func suspendPlay() {
|
|
|
+ if player.isPlaying {
|
|
|
+ isSuspendByAction = true
|
|
|
+ player.pause()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止拖拽,继续播放
|
|
|
+ func autoPlayAfterMove() {
|
|
|
+ startPlay()
|
|
|
+ isSuspendByAction = false
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ func handleSaveCutAudio(completion:((TSRingModel?)->Void)? = nil){
|
|
|
+ player.pause()
|
|
|
+ // 保存音频
|
|
|
+ startCutAudio { result, errMsg,savePath in
|
|
|
+ if let ringModel = result {
|
|
|
+ // 裁剪的音频,使用本地文件播放,清空网络url
|
|
|
+ dePrint("TSEditAudioVC saveModel ringModel = \(String(describing: ringModel.toJSONString()))")
|
|
|
+ TSMineRintoneHistory.shared.saveModel(model: ringModel)
|
|
|
+
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ self.pop()
|
|
|
+ if let window = WindowHelper.getKeyWindow() {
|
|
|
+ kSaveSuccesswShared.show(atView:window,text: "Saved in “My Ringtone”".localized) {
|
|
|
+ if let vc = WindowHelper.topViewController(){
|
|
|
+ if vc is TSRingDownVC {
|
|
|
+ dePrint("vc 是 TSRingDownVC 类型")
|
|
|
+ }else{
|
|
|
+ kPushVC(target: vc, modelVC: TSRingDownVC())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ completion?(ringModel)
|
|
|
+ } else {
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ TSToastShared.showToast(text: errMsg ?? "Sorry, Save Failure".localized)
|
|
|
+ }
|
|
|
+ completion?(nil)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 裁剪
|
|
|
+ func startCutAudio(completion: ((TSRingModel?, String?,URL?) -> Void)?) {
|
|
|
+
|
|
|
+ let copyModel = ringModel
|
|
|
+ let savePath = TSDownloadManager.generateRingSaveLocalURL(name: copyModel.title)
|
|
|
+ TSFileManagerTool.checkFolderAndCreate(from: savePath)
|
|
|
+
|
|
|
+ //未做任何改变,直接返回原音频
|
|
|
+ if isThereAnyChange() == false {
|
|
|
+ TSFileManagerTool.copyFileWithOverwrite(from: editOriginalURL, to: savePath)
|
|
|
+ copyModel.documentPath = savePath.path.documentLastURLString
|
|
|
+ //如果列表中有重名的,就加上时间戳
|
|
|
+ if let _ = TSMineRintoneHistory.shared.listModels.first(where: {$0.title == copyModel.title}){
|
|
|
+ copyModel.title = getModelNewTitle(title: copyModel.title)
|
|
|
+ }
|
|
|
+ completion?(copyModel, nil, savePath)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ copyModel.duration = Int(endDuration - startDuration)
|
|
|
+ TSRingLoadingView.shared.showWindow()
|
|
|
+ audioTool.startTansformAudio(url:editOriginalURL.path, from: startDuration, to: endDuration, fadeIn: Double(fadeInDuration), fadeOut: Double(fadeOutDuration),addVolume: Double(outputVolume) ,savePath: savePath.path) { [weak self] filePath, errMsg in
|
|
|
+ guard let self = self else { return }
|
|
|
+ DispatchQueue.main.async {
|
|
|
+ TSRingLoadingView.shared.remove()
|
|
|
+ }
|
|
|
+ if let filePath = filePath {
|
|
|
+ let url = URL(fileURLWithPath: filePath)
|
|
|
+ copyModel.documentPath = filePath.documentLastURLString
|
|
|
+
|
|
|
+ if let fileInfo = TSBusinessAudioPlayer.getAudioFileInfo(path: url.path) {
|
|
|
+ if let size = fileInfo.sizeInBytes {
|
|
|
+ copyModel.size = Int(size)
|
|
|
+ }
|
|
|
+ if let duration = fileInfo.durationInSeconds {
|
|
|
+ copyModel.duration = Int(duration)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //如果列表中有重名的,就加上时间戳
|
|
|
+ if let _ = TSMineRintoneHistory.shared.listModels.first(where: {$0.title == copyModel.title}){
|
|
|
+ copyModel.title = self.getModelNewTitle(title: copyModel.title)
|
|
|
+ }
|
|
|
+
|
|
|
+ completion?(copyModel, errMsg, savePath)
|
|
|
+ } else {
|
|
|
+ completion?(nil, errMsg, nil)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func getModelNewTitle(title:String)->String{
|
|
|
+ return title.removeTimestampFromEnd() + "_" + Date.timestampString
|
|
|
+ }
|
|
|
+
|
|
|
+ //是否有改动,调整过编辑参数
|
|
|
+ func isThereAnyChange() -> Bool{
|
|
|
+ if startDuration == 0,
|
|
|
+ endDuration == ringDuration,
|
|
|
+ fadeInDuration == 0,
|
|
|
+ fadeOutDuration == 0,
|
|
|
+ outputVolume == 1.0{
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ }
|
|
|
+}
|
|
|
+extension TSEditAudioVC {
|
|
|
+ func handlePlayer(state: TSBusinessAudioPlayer.PlayerState) {
|
|
|
+ playButton.isSelected = player.isPlaying
|
|
|
+ progressLine.isHidden = !playButton.isSelected
|
|
|
+ if player.currentPlayerState == .play {
|
|
|
+ progressLine.centerX = dragLeftView.centerX
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func handlePlayer(progressChanged current: Double, total: Double) {
|
|
|
+ dePrint("TSEditAudioVC progressChanged=\(current),total=\(total)")
|
|
|
+ let range = endDuration - startDuration
|
|
|
+ let rangeWidth = trackContentView.width * range / total
|
|
|
+ let progress = max(0, current - startDuration) / range
|
|
|
+
|
|
|
+ if player.isPlaying {
|
|
|
+ progressLine.centerX = dragLeftView.centerX + progress * rangeWidth
|
|
|
+ }
|
|
|
+
|
|
|
+ if current >= endDuration {
|
|
|
+ player.pause()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 淡入
|
|
|
+ if fadeInDuration > 0, current - startDuration < Double(fadeInDuration) {
|
|
|
+ let fadeProgress = Float(current - startDuration) / Float(fadeInDuration)
|
|
|
+ let newVolume = min(fadeProgress * maxVolume * 100, 100.0)
|
|
|
+ print("---volume: \(newVolume)")
|
|
|
+ player.setVolume(volume:newVolume / 100)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 淡出
|
|
|
+ if fadeOutDuration > 0, current > (endDuration - Double(fadeOutDuration)) {
|
|
|
+ let fadeProgress = Float(endDuration - current) / Float(fadeOutDuration)
|
|
|
+ let newVolume = max(fadeProgress * maxVolume * 100, 0.0)
|
|
|
+ print("---volume: \(newVolume)")
|
|
|
+ player.setVolume(volume: newVolume / 10)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func sanitizeFilePath(_ path: String) -> String {
|
|
|
+ let illegalFileNameCharacters = CharacterSet(charactersIn: "/\\?%*|\"<>: ")
|
|
|
+ let sanitizedPath = path.components(separatedBy: illegalFileNameCharacters).joined(separator: "_")
|
|
|
+ return sanitizedPath
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|