引言
当我们在前两篇博客中成功地构建了一个媒体组合,并且略过了音频部分时,我们意识到了我们需要对这个项目进行更详细的探讨。在本篇博客中,我们将会展示如何创建一个包含视频轨道、配音音频轨道以及背景音频轨道的完整媒体组合。更进一步,我们将探讨当两个音频轨道同时竞争空间时的音频混合方案。
构建双音轨组合
在前面博客的基础上我们需要进行一些调整以构建新的媒体组合。
资源选择
首先是资源选择器的调整,原本的资源选择器只支持单一的视频媒体资源选择,现在我们将其分为三组,分别为视频资源,配音音频资源,背景音乐资源。
代码如下:
修改数据源为二维数组
/// 数据
var dataArray:[[PHResource]] = [[PHResource]]()
初始化数据
override func initData() {
super.initData()
//添加视频资源
var videoArray = [PHResource]()
let resource_breckiehill = PHResource(resource_name: "01_nebula", resource_ext: "mp4", resource_type: .video)
videoArray.append(resource_breckiehill)
let resource_dkglitch = PHResource(resource_name: "04_quasar", resource_ext: "mp4", resource_type: .video)
videoArray.append(resource_dkglitch)
dataArray.append(videoArray)
//添加音频资源
var audioArray = [PHResource]()
let resource_john_kennedy = PHResource(resource_name: "John F. Kennedy", resource_ext: "m4a", resource_type: .audio)
audioArray.append(resource_john_kennedy)
let resource_ronald_reagen = PHResource(resource_name: "Ronald Reagan", resource_ext: "m4a", resource_type: .audio)
audioArray.append(resource_ronald_reagen)
dataArray.append(audioArray)
//添加背景音乐资源
var bgmArray = [PHResource]()
let resource_keep_going = PHResource(resource_name: "02 Keep Going", resource_ext: "m4a", resource_type: .music)
bgmArray.append(resource_keep_going)
let resource_star_gazing = PHResource(resource_name: "01 Star Gazing", resource_ext: "m4a", resource_type: .music)
bgmArray.append(resource_star_gazing)
dataArray.append(bgmArray)
列表改为分组样式,显示结果如下图:
资源编辑
资源编辑页面需要将三个轨道都显示到页面上,为此我们需要创建三个不同的横向列表来显示不同的媒体资源轨道。
/// 数据
var timeLine = PHTimeLine()
/// 视频编辑视图
var videoCollectionView:UICollectionView?
/// 音频编辑视图
var audioCollectionView:UICollectionView?
/// 背景音乐编辑视图
var musicCollectionView:UICollectionView?
func addCollectionView() {
addVideoCollectionView()
addAudioCollectionView()
addMusicCollectionView()
}
func addVideoCollectionView() {
self.videoCollectionView = buildCollectionView(offsetY: 50.0)
}
func addAudioCollectionView() {
let offsetY = CGRectGetMaxY(self.videoCollectionView?.frame ?? CGRect.zero) + 8.0
self.audioCollectionView = buildCollectionView(offsetY: offsetY)
}
func addMusicCollectionView() {
let offsetY = CGRectGetMaxY(self.audioCollectionView?.frame ?? CGRect.zero) + 8.0
self.musicCollectionView = buildCollectionView(offsetY: offsetY)
}
/// 创建编辑视图
/// - Parameter :
/// - offsetY: 偏移量
///
/// - Returns: UICollectionView
func buildCollectionView(offsetY:CGFloat) -> UICollectionView {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .horizontal
flowLayout.itemSize = item_size
flowLayout.minimumInteritemSpacing = 0.0
flowLayout.minimumLineSpacing = 0.0
let collectionView = UICollectionView(frame: CGRect(x: 0.0, y: offsetY, width: self.bounds.size.width, height: item_size.height), collectionViewLayout: flowLayout)
collectionView.contentInset = UIEdgeInsets(top: 0.0, left: self.bounds.width*0.5, bottom: 0.0, right: self.bounds.width*0.5)
collectionView.delegate = self
collectionView.dataSource = self
self.addSubview(collectionView)
collectionView.register(PHEditorCell.self, forCellWithReuseIdentifier: NSStringFromClass(PHEditorCell.self))
return collectionView
}
每个collectionView加载timeline中的不同数据。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell:UICollectionViewCell?
var mediaItem:PHMediaItem?
var backViewColor:UIColor = .red
if collectionView == self.videoCollectionView {
mediaItem = timeLine.videoItmes[indexPath.section]
backViewColor = .red
}
if collectionView == self.audioCollectionView {
mediaItem = timeLine.audioItems[indexPath.section]
backViewColor = .blue
}
if collectionView == self.musicCollectionView {
mediaItem = timeLine.musicItem
backViewColor = .green
}
if let editorCell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(PHEditorCell.self), for: indexPath) as? PHEditorCell {
if let mediaItem = mediaItem {
editorCell.titleLabel.text = mediaItem.title
editorCell.backViewColor = backViewColor
cell = editorCell
}
}
if cell == nil {
assert(false, "cell is nil")
}
return cell!
}
最终的实现的页面显示效果如下:
混合音频
这次组合媒体看起来还不错,场景切换也十分清晰,但仔细感受我们会发现在音频上有一些小瑕疵,首先一个比较严重的问题是我们的两个音频轨道发生了冲突,在开始播放时几乎听不见画外音,背景音乐的音量已经完全覆盖了它。与其让着两个音频轨道发生冲突,倒不如使用一种名为闪避的处理方案,在画外音持续的时间内将背景音乐的音量调低,并保持这个音量直到画外音结束之后再恢复到原来的音量。
另外一个小问题时当播放结束时音乐戛然而止,如果声音可以渐渐减小,会带来更好的用户体验。
框架提供了一个AVAudioMix类处理上面的问题,它是用来在组合的音频轨道中进行自定义音频的处理。
AVAudioMix所具有的音频处理方法是由它的输入参数集定义的,它的参数是AVAudioMixInputParameters类型的对象。AVAudioMixInputParameters的实例关联组合中的单独音频轨道,并在添加到音频混合时定义基于轨道的处理方法。AVAudioMix和其相关联的AVAudioMixInputParameters集合都是不可变对象,意味着它们适用于为AVPlayerItem和AVAssetExportSession之类的客户端提供相关数据,不过它们不能操作其状态。当我们需要创建一个自定义音频混合时,需要改用它们在AVMutableAudioMix和AVMutableAudioMixInputParameters中的可变子类。
AVAudioMix及其相关类的示意图如下:
自动调节音量
当一个组合资源播放或导出时,默认行为是以最大音量或正常音量。只有一个单音轨道时这样的方法才可能比较容易接受,不过当一个组合资源包含多个音频源时就会出现问题。对于多音频轨道的情况,每个声音都在争夺空间,这就不可避免会导致一些声音可能无法被听到。
AV Foundation把音量定义为了一个标准化的浮点型数值,数值范围从0.0~1.0。音频轨道的默认音量为1.0,不过可以使用AVMutableAudioMixInputParameters实例修改这个值。这个对象允许在一个指定时间点或给定的时间范围自动调节音量。
AVMutableAudioMixInputParameters提供了两个方法来调节音量:
1.在指定时间点立即调节音量。音量在音轨持续时间内会保持不变,直到有另一个音量调节出现。
setVolume(_ volume: Float, at time: CMTime)
volume:表示目标音量。
time:起始时间点。
2.在一个给定时间范围内平滑地将音量从一个值调节到另外一个值。当需要在一个时间范围内调整音量时,音量会立即变为指定值的初始值音量并在持续时间内逐渐调整为指定的结束值。
setVolumeRamp(fromStartVolume startVolume: Float, toEndVolume endVolume: Float, timeRange: CMTimeRange)
starVolume:起始音量。
endVolume:目标音量。
timeRange:变化持续时间范围。
简单示例
我们来使用上面的知识来实现一下下面的小示例,8秒的音频资源,开始播放时默认音量为1.0,当播放到2秒的时候开始设置音量平滑的减小到3秒时音量为0.4,而到第5秒音量直接调整为0.6。
// 创建一个音频轨道
let composition = AVMutableComposition()
let compositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
// 设置时间
let zeroSeconds = CMTime.zero
let twoSeconds = CMTime(value: 2, timescale: 1)
let threeSeconds = CMTime(value: 3, timescale: 1)
let fiveSeconds = CMTime(value: 5, timescale: 1)
// 创建parameters
let parameters = AVMutableAudioMixInputParameters(track: compositionTrack)
// 设置初始音量 (即使不设置默认也是最大音量1.0)
parameters.setVolume(1.0, at: zeroSeconds)
// 2s时音量开始平滑减小3s减小至0.4
parameters.setVolumeRamp(fromStartVolume: 1.0, toEndVolume: 0.4, timeRange: CMTimeRange(start: twoSeconds, end: threeSeconds))
// 5s时直接设置音量为0.6
parameters.setVolume(0.6, at: fiveSeconds)
// 创建audioMix
let audioMix = AVMutableAudioMix()
audioMix.inputParameters = [parameters]
- 首先需要有一个音频轨道。
- 定义设置音量变化的时间。
- 创建一个新的与要操作的轨道关联的AVMutableAudioMixInputParameters实例。
- 默认音量为1.0,在2s时设置音量平滑过渡到0.4持续时间为1s,在5秒时直接设置音量为0.6。
- 定义好所有参数后就可以创建AVMutableAudioMix了,将参数添加到数组中,并将数组赋给音频混合对象的inputParameters属性。
示例中创建了一个全格式的音频混合,可以被设置为AVPlayerItem或AVAssetExportSession的audioMix属性进行播放或导出。
结语
在本文中,我们深入探讨了使用AVFoundation进行音频混合的相关类和方案。我们介绍了核心概念,包括AVAudioMix,AVMutableAudioMix,AVAudioMixInputParameters,AVMutableAudioMixInputParameters等类的使用场景。
通过提供一个简单的实例,我们演示了如何在实际项目中应用这些技术。
希望本文能为您提供清晰的指导,并激发您在自己的应用程序中尝试音频混合的想法。在下一篇博客中,我们将继续探讨如何将这些概念整合到实际项目中,为您展示更多高级技术和实用技巧。