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

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

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


image.png

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

1. 添加配乐

添加配乐.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()
            }
        }
    }

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

image.png

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

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

4. 删除上一段

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

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

demo链接:
github

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容