iOS - 录制音频,给音频添加背景音乐(音频的合并与剪辑)

虽然因为很多程序员不再用发表文章了,但是仅作为记录来用的我,在成为能写干货的大牛前,反正也是单机..

因为自己很喜欢玩"抖音"这个APP,音视频的技术也是iOS程序员该去学习的..这个是没有视频信息的抖音版哈哈...顺便练习一下Swift 4.0


iOS - 录制音频,给音频添加背景音乐(音频的合并与剪辑)_第1张图片
image.png

长按麦克风按钮开始语音输入,松开停止,再长按接着录制..点击试听会将录制过的多段录音合并.. 为什么合并,而不是暂停了再继续呢? 因为有一个回撤的按钮删除最近的一段,所以每次松开按钮都会是新的一段音频文件.录制的时候如果有背景音乐会加入配乐的声音.嗯,看图就知道业务逻辑是什么了..但是实现起来还是花了些时间的.

1. 添加配乐

iOS - 录制音频,给音频添加背景音乐(音频的合并与剪辑)_第2张图片
添加配乐.png

主要是音频的剪辑还有下面"选取范围"视图的逻辑,

/// 剪辑一段视频
 ///
    /// - Parameters:
    ///   - audioPath: 音频源的路径
    ///   - fromTime: 截取的起始时间
    ///   - toTime: 截取到哪个时间点
    ///   - outputPath: 剪辑完新音频的路径
    ///   - completed: 结束的回调block
    class func cutAudio(_ audioPath: String, fromTime: CGFloat, toTime: CGFloat, outputPath: String, completed:@escaping () -> ()) {
        /// 音频源.
        let asset = AVURLAsset(url: URL(fileURLWithPath: audioPath))
        /// 输出相关设置
        let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A)!
        exportSession.outputFileType = AVFileType.m4a
        exportSession.outputURL = URL(fileURLWithPath: outputPath)
        let startTime = CMTimeMake(Int64(fromTime), 1)
        let endTime = CMTimeMake(Int64(toTime), 1)
        /// 截取的范围
        exportSession.timeRange = CMTimeRangeFromTimeToTime(startTime, endTime)
        exportSession.exportAsynchronously {
            if exportSession.status == .completed {
                DispatchQueue.main.async {
                    completed()
                }
            }
        }
        
    }

嗯,,这一个函数就搞定了这个功能了.

2. 开始录音

点击录音按钮后,判断有没有添加配乐,如果有就用播放器播放配乐同时录音.(还要根据当前录制的时间,跳转到配乐对应的时间)

  @IBAction func startRecord(_ sender: UIButton) {
        recorder.startRecoder()
        if let backMusicPath = backMusicPath {
            //如果有配乐,播放.
            player.playLocalAudio(URL(fileURLWithPath: backMusicPath))
            player.playToTimeOffset(recorder.totalRecorderTime)
        }
    }
 /// 开始录音
    func startRecoder() {
        let recorder = try! AVAudioRecorder(url: fileTool.createOneStageRecordPath(recorders.count), settings: recordSettings)
        recorder.isMeteringEnabled = true
        recorders.append(recorder)
        recorders.last!.record()
    }
fileprivate var recordSettings:[String: Any] = {
        // 2. 设置录音参数
        var recordSettings = [String:Any]()
        recordSettings[AVFormatIDKey] = kAudioFormatMPEG4AAC // 编码格式
        recordSettings[AVSampleRateKey] = 11025.0 // 采样率
        recordSettings[AVNumberOfChannelsKey] = 1 // 通道数
        recordSettings[AVEncoderAudioQualityKey] = kRenderQuality_Min // 音频质量
        return recordSettings
    }()

因为是多段录音,每次按住录音按钮时候都会进入这个方法.在录制的类中创建了一个数组来装每一段的录音器.并且每一段录音的路径也要不同,这样做的好处还有可以判断录音段的个数来提供给UI,获取每一段的时间等.

// 录某一段的路径
  // 根据录制的第几段来设置不同路径.
     func createOneStageRecordPath(_ stageNum: Int) -> URL{
        let path = tempRecoderPath + "/temp\(stageNum).m4a"
        return URL(fileURLWithPath: path)
    }
    /// 结束录音
    func endRecoder() {
        // 记录时间
        recordersDuration.append(recorders.last!.currentTime)
        // 停止
        recorders.last!.stop()
    }

如果想让功能更加健全可以给recorder设置个代理设置一下打断或者什么的,这里只是基础功能所以没有遵循代理.

3. 试听录音(合成音频)

 /// 合成并播放录音(逻辑判断部分)
    func createAudio(_ backMusicPath: String?, _ completion: @escaping ((_ outputUrl: URL?) -> ())) {
        guard recorders.count > 0 else { completion(nil);  return}
        // 如果只是录了一段并且没有背景音乐,直接返回这段录音
        if recorders.count == 1 && backMusicPath == nil{
            completion(recorders.first!.url)
      
        }else {
            let outputPath = fileTool.combineRecorderPath(recorders.count)
                fileTool.combineAllRecorder(recorders, backMusicPath, completed: {
                    completion(URL(fileURLWithPath: outputPath))
                }   
        }
    }

下面是合并多段录音和配乐的核心代码部分

/// 合并多段音频
    /// 生成一段包含多种轨道音乐的步骤:   exportsession -> AVAudioMix,AVMutableComposition -> AVMutableAudioMix -> [AVMutableAudioMixInputParameters] ->  AVMutableCompositionTrack.insert -> [AVAssetTrack] -> asset
    func combineAllRecorder(_ recoders: [AVAudioRecorder],_ backMusicPath: String?, completed:@escaping() -> ()) {
        let outputPath = tempRecoderPath + "/combine\(recoders.count).m4a"
        
        // 存放音频混合参数的数组
        var mixParams = [AVMutableAudioMixInputParameters]()
        
        // 用来添加track轨道的混合素材.
        let composition = AVMutableComposition()
        // 录音的轨道
        let recordMutableTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!
        var insertTime = kCMTimeZero // 下一次插入录音段的起点
        // 往录音轨道中添加所有录制的音频
        for recorder in recoders {
            let asset = AVURLAsset(url: recorder.url)
            // 取出资源中的音频素材
            let track = asset.tracks(withMediaType: .audio)
            // 将音频素材插入到创建的录音轨道当中.
            try?recordMutableTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: track.first!, at: insertTime)
            insertTime = insertTime + asset.duration
        }
        // 从录音轨道中生成一个混音素材,添加到数组中.
        let recorderMix = AVMutableAudioMixInputParameters(track: recordMutableTrack)
        mixParams.append(recorderMix)
        
        // 插入背景音乐
        if let backMusicPath = backMusicPath {
            let backMutableTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!
            let asset = AVURLAsset(url: URL(fileURLWithPath: backMusicPath))
            let track = asset.tracks(withMediaType: .audio).first!
            
            let duration = asset.duration > insertTime ? insertTime : asset.duration
            try?backMutableTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, duration), of: track, at: kCMTimeZero)
            let backTrackMix = AVMutableAudioMixInputParameters(track: backMutableTrack)
            // 背景音乐的音量
            backTrackMix.setVolume(0.4, at: CMTimeMake(0, 1))
            mixParams.append(backTrackMix)
        }
        // 创建一个可变的音频混音
        let audioMix = AVMutableAudioMix()
        // 将两个混音素材添加到混音对象中.
        audioMix.inputParameters = mixParams
        
        
        let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)!
        exportSession.outputFileType = AVFileType.m4a
        // 如果有混音就设置这个参数.
        exportSession.audioMix = audioMix
        exportSession.outputURL = URL(fileURLWithPath: outputPath)
        exportSession.exportAsynchronously {
            if exportSession.status == .completed {
                completed()
            }
        }
    }

上面类很多,看起来有点乱,它们互相的关系是这样的:

iOS - 录制音频,给音频添加背景音乐(音频的合并与剪辑)_第3张图片
image.png

两个音频轨道,一个往里面添加录音的声音,一个添加录制的声音,,然后用给ExportSession配置AudioMix就好了.
其中每一个AVMutableAudioMixInputParameters混音素材都可以设置其声音大小.

合成成功后返回路径用播放器进行播放就好了..
注意: 因为是多段录音,每次试听根据录音的段数不同都会合成新的音频文件,怎样处理好这些文件,存放和删除需要些讲究.

4. 删除上一段

因为我们用数组装了每一段的录音Recorder,删除上一段就比较方便了.

   func deleteLastRecord() {
        guard recordersDuration.count > 0 else {return}
        recordersDuration.removeLast() // 时间数组
        fileTool.deletePreviousAudio(recorders) // 删除本地文件
        recorders.removeLast() // 录音器数组移除最后一个
    }

demo链接:
github

你可能感兴趣的:(iOS - 录制音频,给音频添加背景音乐(音频的合并与剪辑))