版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.08.19 |
前言
AVFoundation
框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 视频音频的合成(一)
7. AVFoundation框架解析(七)—— 视频组合和音频混合调试
8. AVFoundation框架解析(八)—— 优化用户的播放体验
9. AVFoundation框架解析(九)—— AVFoundation的变化(一)
10. AVFoundation框架解析(十)—— AVFoundation的变化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的变化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的变化(四)
13. AVFoundation框架解析(十三)—— 构建基本播放应用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)
16. AVFoundation框架解析(十六)—— 一个简单示例之播放、录制以及混合视频(一)
17. AVFoundation框架解析(十七)—— 一个简单示例之播放、录制以及混合视频之源码及效果展示(二)
18. AVFoundation框架解析(十八)—— AVAudioEngine之基本概览(一)
开始
向大多数iOS开发人员提及音频处理,他们认为很困难甚至是恐惧。这是因为,在iOS 8之前,它意味着深入探讨低级Core Audio
框架的深度 - 只有少数勇敢的灵魂才能做到这一点。值得庆幸的是,随着iOS 8和AVAudioEngine
的发布,这一切都在2014年发生了变化。本文将向您展示如何使用Apple的新的更高级别的音频工具audio toolkit
包来制作音频处理应用程序,而无需深入研究Core Audio
。
那就对了!您不再需要搜索模糊的基于指针的C / C ++结构和内存缓冲区来收集原始音频数据。
在这个AVAudioEngine
教程中,您将使用AVAudioEngine
构建下一个优秀的播客应用程序。更具体地说,您将添加由UI控制的音频功能:播放/暂停按钮,跳过前进/后退按钮,进度条和播放速率选择器。当你完成后,你会有一个很棒的应用程序。
注意:写作本文的环境Swift 4, iOS 11, Xcode 9。
iOS Audio Framework Introduction - iOS音频框架介绍
在进入项目之前,首先看一下iOS音频框架的概述:
-
CoreAudio
和AudioToolbox
是低级C框架。 -
AVFoundation
是一个Objective-C / Swift框架。 -
AVAudioEngine
是AVFoundation
的一部分。
-
AVAudioEngine
是一个定义一组连接的音频节点的类。 您将向项目添加两个节点:AVAudioPlayerNode
和AVAudioUnitTimePitch
。
Setup Audio - 设置Audio
打开ViewController.swift
并查看内部。 在顶部,您将看到所有连接的outlets
和类变量。 actions
还连接到sb中的相应outlets
。
将以下代码添加到setupAudio()
:
// 1
audioFileURL = Bundle.main.url(forResource: "Intro", withExtension: "mp4")
// 2
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFormat)
engine.prepare()
do {
// 3
try engine.start()
} catch let error {
print(error.localizedDescription)
}
仔细看看发生了什么:
- 1)这将获取bundle音频文件的URL。 设置后,它将在上面变量声明部分的
audioFileURL
的didSet
块中实例化audioFile
。 - 2)将播放器节点附加到引擎,在连接其他节点之前必须执行此操作。 这些节点将生成,处理或输出音频。 音频引擎提供连接到播放器节点的主混音器节点。 默认情况下,主混音器连接到
engine
默认输出节点(iOS设备扬声器)。prepare()
预分配所需的资源。
接下来,将以下内容添加到scheduleAudioFile()
:
guard let audioFile = audioFile else { return }
skipFrame = 0
player.scheduleFile(audioFile, at: nil) { [weak self] in
self?.needsFileScheduled = true
}
这会调度播放整个audioFile
。 at:
是您希望音频播放的未来时间(AVAudioTime)
。 设置为nil
会立即开始播放。 该文件仅调度播放一次。 再次点击Play
按钮不会从头重新开始。 您需要重新调度再次播放。 播放完音频文件后,在完成块中设置标志needsFileScheduled
。
还有其他调度音频用于播放:
-
scheduleBuffer(AVAudioPCMBuffer,completionHandler:AVAudioNodeCompletionHandler?= nil)
:这提供了预先加载音频数据的缓冲区。 -
scheduleSegment(AVAudioFile,startingFrame:AVAudioFramePosition,frameCount:AVAudioFrameCount,at:AVAudioTime?,completionHandler:AVAudioNodeCompletionHandler?= nil)
:这就像scheduleFile
,除了你指定开始播放的音频帧和播放的帧数。
然后,将以下内容添加到playTapped(_ :)
:
// 1
sender.isSelected = !sender.isSelected
// 2
if player.isPlaying {
player.pause()
} else {
if needsFileScheduled {
needsFileScheduled = false
scheduleAudioFile()
}
player.play()
}
下面细分一下:
- 1)切换按钮的选择状态,这会更改sb中设置的按钮图像。
- 2)使用
player.isPlaying
确定当前播放器正在播放。 如果是这样,暂停它,如果不是,请播放。 您还可以检查needsFileScheduled
并根据需要重新调度文件。
Build并运行,然后点击playPauseButton
。 你应该听到声音。 但是,没有UI反馈,你不知道文件有多长或者你现在播放到哪里。
Add Progress Feedback - 增加进度反馈
在viewDidLoad()
中添加如下代码:
updater = CADisplayLink(target: self, selector: #selector(updateUI))
updater?.add(to: .current, forMode: .defaultRunLoopMode)
updater?.isPaused = true
CADisplayLink
是一个计时器对象,与显示器的刷新率同步。 您使用方法updateUI
实例化它。 然后,将其添加到运行循环中 - 在本例中为默认运行循环default run loop
。 最后,它不需要开始运行,因此将isPaused
设置为true
。
用以下内容替换playTapped(_ :)
的实现:
sender.isSelected = !sender.isSelected
if player.isPlaying {
disconnectVolumeTap()
updater?.isPaused = true
player.pause()
} else {
if needsFileScheduled {
needsFileScheduled = false
scheduleAudioFile()
}
connectVolumeTap()
updater?.isPaused = false
player.play()
}
这里的关键是当播放器暂停时使用updater.isPaused = true
暂停UI。 您将在下面的VU Meter
部分中了解connectVolumeTap()
和disconnectVolumeTap()
。
使用以下内容替换var currentFrame:AVAudioFramePosition = 0
:
var currentFrame: AVAudioFramePosition {
// 1
guard
let lastRenderTime = player.lastRenderTime,
// 2
let playerTime = player.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
// 3
return playerTime.sampleTime
}
currentFrame
返回播放器呈现的最后一个音频样本。 下面一步步的看:
- 1)
player.lastRenderTime
返回引擎启动时间的时间。 如果引擎未运行,则lastRenderTime
返回nil。 - 2)
player.playerTime(forNodeTime :)
将lastRenderTime
转换为相对于播放器开始时间的时间。 如果播放器没有播放,那么playerTime
将返回nil。 - 3)
sampleTime
是音频文件中的一些音频采样的时间。
现在进行UI更新。 将以下内容添加到updateUI()
:
// 1
currentPosition = currentFrame + skipFrame
currentPosition = max(currentPosition, 0)
currentPosition = min(currentPosition, audioLengthSamples)
// 2
progressBar.progress = Float(currentPosition) / Float(audioLengthSamples)
let time = Float(currentPosition) / audioSampleRate
countUpLabel.text = formatted(time: time)
countDownLabel.text = formatted(time: audioLengthSeconds - time)
// 3
if currentPosition >= audioLengthSamples {
player.stop()
updater?.isPaused = true
playPauseButton.isSelected = false
disconnectVolumeTap()
}
下面我们一步一步的看:
- 1)属性
skipFrame
是添加到currentFrame
或从currentFrame
中减去的偏移量,最初设置为零。 确保currentPosition
不超出文件范围。 - 2)将
progressBar.progress
更新为audioFile
中的currentPosition
。 通过将currentPosition
除以audioFile
的sampleRate
来计算时间。 将countUpLabel
和countDownLabel
文本更新为audioFile
中的当前时间。 - 3)如果
currentPosition
位于文件末尾,则:- 停止播放器。
- 暂停计时器。
- 重置
playPauseButton
选择状态。 - 断开音量tap。
Build并运行,然后点击playPauseButton
。 再次,您将听到声音,但这次progressBar
和计时器标签提供以前缺少的状态信息。
Implement the VU Meter - 实现VU Meter
现在是时候添加VU Meter
功能了。 这是一个UIView定位在暂停图标的栏之间。 视图的高度由播放音频的平均功率决定。 这是您进行某些音频处理的第一次机会。
您将计算1k音频样本缓冲区的平均功率。 确定音频样本缓冲器的平均功率的常用方法是计算样本的均方根(RMS)。
平均功率是以分贝表示的一系列音频样本数据的平均值。 还有峰值功率,这是一系列样本数据中的最大值。
在connectVolumeTap()
下面添加以下helper
方法:
func scaledPower(power: Float) -> Float {
// 1
guard power.isFinite else { return 0.0 }
// 2
if power < minDb {
return 0.0
} else if power >= 1.0 {
return 1.0
} else {
// 3
return (fabs(minDb) - fabs(power)) / fabs(minDb)
}
}
scaledPower(power :)
将负功率分贝值转换为正值,以适应调整上面的volumeMeterHeight.constant
值。 这是它的作用:
- 1)
power.isFinite
检查以确保功率是有效值 - 即,不是NaN
- 如果不是则返回0.0。 - 2)这将我们的
vuMeter
的dynamic range设置为80db。 对于低于-80.0的任何值,返回0.0。 iOS上的分贝值范围为-160db,接近静音,为0db,最大功率。minDb
设置为-80.0,动态范围为80db。 您可以更改此值以查看它如何影响vuMeter。 - 3)计算0.0到1.0之间的缩放值。
现在,将以下内容添加到connectVolumeTap()
:
// 1
let format = engine.mainMixerNode.outputFormat(forBus: 0)
// 2
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, when in
// 3
guard
let channelData = buffer.floatChannelData,
let updater = self.updater
else {
return
}
let channelDataValue = channelData.pointee
// 4
let channelDataValueArray = stride(from: 0,
to: Int(buffer.frameLength),
by: buffer.stride).map{ channelDataValue[$0] }
// 5
let rms = sqrt(channelDataValueArray.map{ $0 * $0 }.reduce(0, +) / Float(buffer.frameLength))
// 6
let avgPower = 20 * log10(rms)
// 7
let meterLevel = self.scaledPower(power: avgPower)
DispatchQueue.main.async {
self.volumeMeterHeight.constant = !updater.isPaused ?
CGFloat(min((meterLevel * self.pauseImageHeight), self.pauseImageHeight)) : 0.0
}
}
这里进行细分说明:
- 1)获取
mainMixerNode
输出的数据格式。 - 2)
installTap(onBus:0,bufferSize:1024,format:format)
使您可以访问mainMixerNode
输出总线上的音频数据。您请求1024字节的缓冲区大小,但不保证请求的大小,特别是如果您请求的缓冲区太小或太大。 Apple的文档没有说明这些限制是什么。完成block接收AVAudioPCMBuffer
和AVAudioTime
作为参数。您可以检查buffer.frameLength
以确定实际的缓冲区大小。when
提供缓冲区的捕获时间。 - 3)
buffer.floatChannelData
为您提供了指向每个样本数据的指针数组。channelDataValue
是UnsafeMutablePointer
的数组 - 4)从
UnsafeMutablePointer
数组转换为Float
数组会使以后的计算更容易。为此,请使用stride(from:to:by :)
在channelDataValue
中创建索引数组。然后map{channelDataValue [$ 0]}
以访问和存储channelDataValueArray
中的数据值。 - 5)计算RMS涉及映射/缩减/除法操作。首先,映射操作对数组中的所有值进行平方,reduce操作求和。将平方和除以缓冲区大小,然后取平方根,生成缓冲区中音频样本数据的RMS。这应该是介于0.0和1.0之间的值,但可能存在一些边缘情况,它是负值。
- 6)RMS转换为分贝(Acoustic Decibel reference)。这应该是-160和0之间的值,但如果
rms
为负,则该值为NaN
。 - 7)将分贝缩放为适合您的
vuMeter
的值。
最后,将以下内容添加到disconnectVolumeTap()
:
engine.mainMixerNode.removeTap(onBus: 0)
volumeMeterHeight.constant = 0
AVAudioEngine
每个总线只允许一次点击。 在不使用时将其删除是一种很好的做法。
Build并运行,然后点击playPauseButton
。 vuMeter
现在处于活动状态,提供音频数据的平均功率反馈。
Implementing Skip - 实现Skip
是时候实现跳过前进和后退按钮了。skipForwardButton
在音频文件中向前跳10秒,skipBackwardButton
跳回10秒。
添加以下内容到seek(to:)
:
guard
let audioFile = audioFile,
let updater = updater
else {
return
}
// 1
skipFrame = currentPosition + AVAudioFramePosition(time * audioSampleRate)
skipFrame = max(skipFrame, 0)
skipFrame = min(skipFrame, audioLengthSamples)
currentPosition = skipFrame
// 2
player.stop()
if currentPosition < audioLengthSamples {
updateUI()
needsFileScheduled = false
// 3
player.scheduleSegment(audioFile,
startingFrame: skipFrame,
frameCount: AVAudioFrameCount(audioLengthSamples - skipFrame),
at: nil) { [weak self] in
self?.needsFileScheduled = true
}
// 4
if !updater.isPaused {
player.play()
}
}
这是进行详细分解:
- 1)通过乘以
audioSampleRate
将时间(以秒为单位)转换为帧位置,并将其添加到currentPosition
。然后,确保skipFrame
不在文件开头之前,也不超过文件末尾。 - 2)
player.stop()
不仅停止播放,还清除所有先前调度的事件。调用updateUI()
将UI设置为新的currentPosition
值。 - 3)
player.scheduleSegment(_:startingFrame:frameCount:at :)
调度从audioFile
的skipFrame
位置开始播放。frameCount
是要播放的帧数。您想要播放到文件末尾,因此将其设置为audioLengthSamples - skipFrame
。最后,at:nil
指定立即开始播放,而不是在将来的某个时间开始播放。 - 4)如果在调用
skip
之前播放器正在播放,则调用player.play()
以恢复播放。updater.isPaused
可以方便地确定这一点,因为只有先前暂停了播放器才会生效。
Build并运行,然后点击playPauseButton
。点击skipBackwardButton
并使用skipForwardButton
跳过前进和后退。观察progressBar
和计数标签的变化。
Implementing Rate Change - 实现播放速率的改变
最后要实现的是改变播放速度。 如今,以超过1倍的速度收听播客是一项受欢迎的功能。
在setupAudio()
中,替换以下内容:
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFormat)
以及:
engine.attach(player)
engine.attach(rateEffect)
engine.connect(player, to: rateEffect, format: audioFormat)
engine.connect(rateEffect, to: engine.mainMixerNode, format: audioFormat)
这会将rateEffect
(AVAudioUnitTimePitch
节点)连接到音频图并将其连接起来。 此节点类型是效果节点,具体来说,它可以改变播放速率和音频音高。
didChangeRateValue()
action处理对rateSlider
的更改。 它计算rateSliderValues
数组的索引并设置rateValue
,它设置rateEffect.rate
。 rateSlider
的值范围为0.5x到3.0x
Build并运行,然后点击playPauseButton
。 调整rateSlider
就可以听一下效果声音了。
参考文章
- 1. WWDC 2014 Session 502: AVAudioEngine in Practice
- 2. Apple's "Working with Audio"
后记
本篇主要讲述了AVAudioEngine之详细说明和一个简单示例,感兴趣的给个赞或者关注~~~