虽然因为很多程序员不再用发表文章了,但是仅作为记录来用的我,在成为能写干货的大牛前,反正也是单机..
因为自己很喜欢玩"抖音"这个APP,音视频的技术也是iOS程序员该去学习的..这个是没有视频信息的抖音版哈哈...顺便练习一下Swift 4.0
长按麦克风按钮开始语音输入,松开停止,再长按接着录制..点击试听会将录制过的多段录音合并.. 为什么合并,而不是暂停了再继续呢? 因为有一个回撤的按钮删除最近的一段,所以每次松开按钮都会是新的一段音频文件.录制的时候如果有背景音乐会加入配乐的声音.嗯,看图就知道业务逻辑是什么了..但是实现起来还是花了些时间的.
1. 添加配乐
主要是音频的剪辑还有下面"选取范围"视图的逻辑,
/// 剪辑一段视频
///
/// - 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()
}
}
}
上面类很多,看起来有点乱,它们互相的关系是这样的:
两个音频轨道,一个往里面添加录音的声音,一个添加录制的声音,,然后用给ExportSession配置AudioMix就好了.
其中每一个AVMutableAudioMixInputParameters混音素材都可以设置其声音大小.
合成成功后返回路径用播放器进行播放就好了..
注意: 因为是多段录音,每次试听根据录音的段数不同都会合成新的音频文件,怎样处理好这些文件,存放和删除需要些讲究.
4. 删除上一段
因为我们用数组装了每一段的录音Recorder,删除上一段就比较方便了.
func deleteLastRecord() {
guard recordersDuration.count > 0 else {return}
recordersDuration.removeLast() // 时间数组
fileTool.deletePreviousAudio(recorders) // 删除本地文件
recorders.removeLast() // 录音器数组移除最后一个
}
demo链接:
github