引言
音频混合技术是一项强大的工具,可以为应用程序增添丰富的功能和用户体验。在前一篇博客中,我们深入探讨了AVFoundation框架中的音频混合基础知识和实现方法。现在,让我们进一步探索,看看如何将这些技术应用到实际项目中。
在本文中,我们将介绍一个实际应用场景,并展示如何利用音频混合技术来实现特定的功能。我们将从问题背景开始,阐述为什么需要音频混合,然后逐步介绍我们的解决方案。通过一个实际案例,我们将演示如何使用AVFoundation框架中的音频混合功能来实现我们的目标。
实现
下面我们就开始将音频混合应用到项目当中,来解决声音突兀,以及两个音频轨道争夺空间导致配音音频无法被听见的问题。
目标
首先我们需要在有配音音频的时候将背景音乐的音量尽量减小比如设置为0.1,当配音音频结束后背景音乐的音量恢复到1.0。而当资源播放即将结束的时候将音频逐渐减小到0.0。
数据
首先创建一个新的类用来描述音频的音量变化,包括音频的起始音量,结束音量,变化起始时间及持续时长。
PHVolumeAutomation:
import UIKit
import CoreMedia
class PHVolumeAutomation: NSObject {
/// 开始音量
var startVolume:Float = 0.0
/// 结束音量
var endVolume:Float = 0.0
/// timeRange
var timeRange:CMTimeRange = CMTimeRange.zero
/// 自定义初始化
/// - Parameters:
/// - startVolume: 开始音量
/// - endVolume: 结束音量
/// - timeRange: timeRange
init(startVolume:Float,endVolume:Float,timeRange:CMTimeRange) {
self.startVolume = startVolume
self.endVolume = endVolume
self.timeRange = timeRange
}
}
给PHMusicItem类增加特有属性,用来保存音量变化的数据。
import UIKit
class PHMusicItem: PHMediaItem {
/// 音频调节 数组
var volumeAutomations = [PHVolumeAutomation]()
}
在添加音频轨道数据以及背景音乐轨道数据时来调用构建音量自动闪避的数据。
resourcePickerView.addMediaItemBlock = { [weak self] mediaItem in
guard let self = self else { return }
if mediaItem.isKind(of: PHVideoItem.self) {
/// 视频
self.timeLine.videoItmes.append(mediaItem as! PHVideoItem)
self.videoCollectionView?.reloadData()
} else if mediaItem.isKind(of: PHAudioItem.self) {
/// 音频
self.timeLine.audioItems.append(mediaItem as! PHAudioItem)
self.audioCollectionView?.reloadData()
self.buildVolumeData()
} else if mediaItem.isKind(of: PHMusicItem.self) {
/// 音乐
self.timeLine.musicItem = mediaItem as? PHMusicItem
self.musicCollectionView?.reloadData()
self.buildVolumeData()
}
}
buildVolumeData方法实现如下:
/// 构建 音量调节数据
func buildVolumeData() {
let volumeBuilder = PHVolumeAutomationBuilder()
guard let volumeAutomations = volumeBuilder.buildVolumeData(timeLine: timeLine) else { return }
timeLine.musicItem?.volumeAutomations = volumeAutomations
}
其中volumeBuilder是我们自定义的一个构建器,代码实现如下:
// 音频音量调节数据模型构建者
import UIKit
import CoreMedia
class PHVolumeAutomationBuilder: NSObject {
/// 根据timeline构建音量调节数据
/// - Parameters:
/// - timeline: timeline
/// - Returns: 返回音量调节数据
func buildVolumeData(timeLine:PHTimeLine) -> [PHVolumeAutomation]? {
var volumeAutomations = [PHVolumeAutomation]()
guard let musicItem = timeLine.musicItem else { return nil}
// 闪避配音音频
if PHIsEmpty(array: timeLine.audioItems) {
return nil
}
// 变化持续时间
let transitionDuration = CMTime(value: 1, timescale: 1)
// 配音开始时间
let voiceStartTime = CMTime.zero
let startAutoVolumeAutomation = PHVolumeAutomation(startVolume: 0.1, endVolume: 0.1, timeRange: CMTimeRange(start: voiceStartTime, duration: transitionDuration))
timeLine.musicItem?.volumeAutomations.append(startAutoVolumeAutomation)
volumeAutomations.append(startAutoVolumeAutomation)
// 配音结束时间
// 计算总时长
var voiceEndTime = CMTime.zero
for audioItem in timeLine.audioItems {
voiceEndTime = CMTimeAdd(voiceEndTime, audioItem.timeRange.duration)
}
voiceEndTime = CMTimeSubtract(voiceEndTime, transitionDuration)
let endAutoVolumeAutomation = PHVolumeAutomation(startVolume: 0.1, endVolume: 1.0, timeRange: CMTimeRange(start: voiceEndTime, duration: transitionDuration))
volumeAutomations.append(endAutoVolumeAutomation)
// 结尾逐渐减小
let endTime = musicItem.timeRange.end
let startTime = CMTimeSubtract(endTime, CMTime(value: 2, timescale: 1))
let endVolumeAutomation = PHVolumeAutomation(startVolume: 1.0, endVolume: 0.0, timeRange: CMTimeRange(start: startTime, end: endTime))
volumeAutomations.append(endVolumeAutomation)
return volumeAutomations
}
}
上面的代码看起来内容很多,实际上都是CMTime的数据处理和计算,在前面我们已经说过音视频编辑的部分CMTime相关的知识非常重要,使用十分频繁。
AVAudioMix
将AVAudioMix应用到视频播放和导出中,需要对PHBaseComposition和PHBaseCompositionBuilder中的代码进行调整,我们将之前的两个类保留。
重新创建PHAudioMixComposition类用来创建包含音频混合的可播放和可导出版本,实现如下:
import UIKit
import AVFoundation
class PHAudioMixComposition: NSObject,PHComposition {
/// 私有composition
private var compostion:AVComposition?
/// 音频混合
private var audioMix:AVAudioMix?
/// 自定义初始化
/// - Parameters:
/// - compostion: AVComposition
/// - audioMix: AVAudioMix
init(compostion: AVComposition? = nil,audioMix:AVAudioMix? = nil) {
self.compostion = compostion
self.audioMix = audioMix
}
func makePlayerItem() -> AVPlayerItem? {
if let compostion = compostion {
let playerItem = AVPlayerItem(asset: compostion.copy() as! AVAsset)
playerItem.audioMix = audioMix
return playerItem
}
return nil
}
func makeAssetExportSession() -> AVAssetExportSession? {
var assetExportSession:AVAssetExportSession? = nil
if let compostion = compostion {
let prset = AVAssetExportPresetHighestQuality
assetExportSession = AVAssetExportSession(asset: compostion.copy() as! AVAsset, presetName: prset)
assetExportSession?.audioMix = audioMix
}
return assetExportSession
}
}
代码几乎和原来的实现相同,只是新增了AVAudioMix属性,并在播放和导出的时候进行了应用。
创建创建PHAudioMixCompositionBuilder类实现PHCompositionBuilder中定义的接口:
import UIKit
import AVFoundation
class PHAudioMixCompositionBuilder: NSObject, PHCompositionBuilder{
/// 时间线
var timeLine:PHTimeLine!
/// composition
private var composition = AVMutableComposition()
init(timeLine: PHTimeLine!) {
self.timeLine = timeLine
}
func buildComposition() -> PHComposition? {
// 添加视频轨道
let _ = addCompositionTrack(mediaType: .video, mediaItems: timeLine.videoItmes)
// 添加音频轨道
let _ = addCompositionTrack(mediaType: .audio, mediaItems: timeLine.audioItems)
// 添加背景音乐
var audioMix:AVAudioMix? = nil
if timeLine.musicItem != nil {
let musicCompositionTrack = addCompositionTrack(mediaType: .audio, mediaItems: [timeLine.musicItem!])
let musicAudioMix = buildAudioMixWithTrack(track: musicCompositionTrack)
audioMix = musicAudioMix
}
return PHAudioMixComposition(compostion: self.composition, audioMix: audioMix)
}
/// 私有方法-添加媒体资源轨道
/// - Parameters:
/// - mediaType: 媒体类型
/// - mediaItems: 媒体媒体资源数组
/// - Returns: 返回一个AVCompositionTrack
private func addCompositionTrack(mediaType:AVMediaType,mediaItems:[PHMediaItem]?) -> AVMutableCompositionTrack? {
if PHIsEmpty(array: mediaItems) {
return nil
}
let trackID = kCMPersistentTrackID_Invalid
guard let compositionTrack = composition.addMutableTrack(withMediaType: mediaType, preferredTrackID: trackID) else { return nil }
//设置起始时间
var cursorTime = CMTime.zero
guard let mediaItems = mediaItems else { return nil }
for item in mediaItems {
//这里默认时间都是从0开始
guard let asset = item.asset else { continue }
guard let assetTrack = asset.tracks(withMediaType: mediaType).first else { continue }
do {
try compositionTrack.insertTimeRange(item.timeRange, of: assetTrack, at: cursorTime)
} catch {
print("addCompositionTrack error")
}
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration)
}
return compositionTrack
}
/// 创建音频混合器
/// - Parameters:
/// - musicTrack: 音乐轨道
func buildAudioMixWithTrack(track:AVMutableCompositionTrack?) -> AVAudioMix? {
return nil
}
}
这里面的代码和上一篇博客中PHBaseCompositionBuilder的代码也是几乎相同,不同地方是为背景音乐轨道创建了一个AVAudioMix,接下来我们来看一下buildAudioMixWithTrack方法的实现:
/// 创建音频混合器
/// - Parameters:
/// - musicTrack: 音乐轨道
func buildAudioMixWithTrack(track:AVMutableCompositionTrack?) -> AVAudioMix? {
guard let track = track else { return nil }
guard let musicItem = timeLine.musicItem else { return nil }
let audioMix = AVMutableAudioMix()
let audioMixParam = AVMutableAudioMixInputParameters(track: track)
for volumeAutomaition in musicItem.volumeAutomations {
audioMixParam.setVolumeRamp(fromStartVolume: volumeAutomaition.startVolume, toEndVolume: volumeAutomaition.endVolume, timeRange: volumeAutomaition.timeRange)
}
audioMix.inputParameters = [audioMixParam]
return audioMix
}
图形呈现
截止到上面的代码,其实关于音频混合的功能就已经完成。只需要在点击播放构建播放资源时将原来的PHBaseCompositionBuilder替换为音频混合的PHAudioMixCompositionBuilder即可,代码如下:
func player() {
guard let delegate = self.delegate else { return }
let compositionBuilder = PHAudioMixCompositionBuilder(timeLine: timeLine)
let composition = compositionBuilder.buildComposition()
let playerItem = composition?.makePlayerItem()
delegate.replaceCurrentItem(playerItem: playerItem)
}
再次播放媒体组合,会发现当存在独白音频时,背景音乐的音量非常小,当独白结束后背景音乐音量开始增大到1,而当组合播放即将结束时,背景音乐的音量又会逐渐减小至0。
我们采用一种更直观的方式,将背景音乐的音量大小使用图像呈现出来。
定义一个PHMusicEditorCell继承自PHEditorCell,采用CAShaperLayer+UIBeizierPath方式来呈现音量。代码如下:
// 背景音乐编辑cell
import UIKit
import CoreMedia
class PHMusicEditorCell: PHEditorCell {
let shapLayer = CAShapeLayer()
let path = UIBezierPath()
var timeLine:PHTimeLine? {
didSet {
setNeedsDisplay()
}
}
override func setupView() {
super.setupView()
shapLayer.strokeColor = UIColor.red.cgColor
shapLayer.fillColor = UIColor.clear.cgColor
shapLayer.lineWidth = 6.0
contentView.layer.addSublayer(shapLayer)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
path.removeAllPoints()
guard let timeLine = timeLine else {
return
}
if let musicItem = timeLine.musicItem {
let volumeAutomations = musicItem.volumeAutomations
if volumeAutomations.count > 0 {
var pathY = 0.0
path.move(to: CGPoint(x: 0, y: pathY))
let height = rect.height
let width = rect.width
for volumeAutomation in volumeAutomations {
let start = volumeAutomation.timeRange.start.seconds / musicItem.timeRange.duration.seconds * width
let end = CGFloat(volumeAutomation.timeRange.end.seconds / musicItem.timeRange.duration.seconds) * width
let startVolume = CGFloat(volumeAutomation.startVolume) * height
let endVolume = CGFloat(volumeAutomation.endVolume) * height
/// y不变 x 变
path.addLine(to: CGPoint(x: start, y: pathY))
pathY = height - startVolume
path.addLine(to: CGPoint(x: start, y: pathY))
pathY = height - endVolume
path.addLine(to: CGPoint(x: end, y: pathY))
}
shapLayer.path = path.cgPath
}
}
}
}
- 定义shaplayer和path以及timeLine,shaplayer和path用于呈现图像,timeLine用于从中获取音量随时间轴变化的数据。
- 在给timeLine进行赋值时,触发draw方法进行重绘。
- 重写draw方法,根据musicItem中的volumeAutomations进行绘制。这里面的内容和AVAudioMix没有任何关联,就不过多介绍。
呈现结果如下。
结语
通过本文,我们深入探讨了音频混合技术在实际应用中的应用。我们从一个具体的应用场景出发,逐步介绍了如何利用AVFoundation框架中的音频混合功能来解决特定问题。
希望通过本文的阅读,您对音频混合技术有了更深入的了解,并能够将其应用到您自己的项目中。音频混合不仅可以为您的应用增添新的功能和特色,还可以提升用户体验,为用户带来更加丰富的视听享受。
下面一篇博客我们会介绍到关于媒体编辑最复杂的部分,创建视频过渡效果。
项目地址:PHEditorPlayer: AV Foundation 音视频编辑