版本记录
版本号 | 时间 |
---|---|
V1.0 | 2021.05.18 星期二 |
前言
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之基本概览(一)
19. AVFoundation框架解析(十九)—— AVAudioEngine之详细说明和一个简单示例(二)
20. AVFoundation框架解析(二十)—— AVAudioEngine之详细说明和一个简单示例源码(三)
21. AVFoundation框架解析(二十一)—— 一个简单的视频流预览和播放示例之解析(一)
22. AVFoundation框架解析(二十二)—— 一个简单的视频流预览和播放示例之源码(二)
23. AVFoundation框架解析(二十三) —— 向视频层添加叠加层和动画(一)
24. AVFoundation框架解析(二十四) —— 向视频层添加叠加层和动画(二)
25. AVFoundation框架解析(二十五) —— 播放、录制和合并视频简单示例(一)
26. AVFoundation框架解析(二十六) —— 播放、录制和合并视频简单示例(二)
开始
首先看下主要内容:
了解如何使用
AVAudioEngine
构建下一个最佳播客应用程序! 实施音频功能以暂停,跳过,加快,放慢速度并更改应用程序中音频的音调。内容来自翻译。
下面就是写作环境:
Swift 5, iOS 14, Xcode 12
接着就是正文了。
向大多数iOS开发人员提及音频处理,他们会给您带来恐惧的感觉。 这是因为,在iOS 8
之前,这意味着要深入研究底层的Core Audio
框架 —— 只有少数勇敢的人敢于这样做。 值得庆幸的是,随着iOS 8
和AVAudioEngine
的发布,这一切都在2014
年发生了变化。 该AVAudioEngine
教程将向您展示如何使用Apple
的新的高级音频工具包来制作音频处理应用程序,而无需深入研究Core Audio
。
这是正确的! 您不再需要搜索模糊的,基于指针的C / C ++
结构和内存缓冲区来收集原始音频数据。 如果您了解基本的Swift
代码,则本教程将指导您完成向应用程序添加音频功能的过程。
在本教程中,您将使用AVAudioEngine
构建下一个出色的播客应用:Raycast
。
您将在此应用中实现的功能包括:
- 播放本地音频文件。
- 查看播放进度。
- 用
VU
表观察音频信号电平。 - 向前或向后跳过。
- 更改播放速率和音调。
完成后,您将拥有一个出色的应用程序,可以收听播客和音频文件。
下载入门项目,在Xcode
中构建并运行您的项目,您将看到基本的用户界面:
控件尚无任何作用。 实际上,由于音频尚未准备好播放,因此暂时将其禁用。 但是,将控件设置为调用将要实现的各自的视图模型方法。
1. Understanding iOS Audio Frameworks
在进入项目之前,这里是iOS
音频框架的快速概述:
-
CoreAudio
和AudioToolbox
是底层C
框架。 -
AVFoundation
是一个Objective-C / Swift
框架。 -
AVAudioEngine
是AVFoundation
的一部分。
AVAudioEngine
是一个类,它定义一组连接的音频节点。 您将在项目中添加两个节点:AVAudioPlayerNode
和AVAudioUnitTimePitch
。
通过利用这些框架,您可以避免深入研究音频信息的底层处理,而专注于要添加到应用程序中的高级功能。
2. Setting up Audio
打开Models / PlayerViewModel.swift
并查看内部。 在顶部的Public properties
下,您将看到视图中用于布置音频播放器的所有属性。 提供了用于制作播放器的方法供您填写。
将以下代码添加到setupAudio()
:
// 1
guard let fileURL = Bundle.main.url(
forResource: "Intro",
withExtension: "mp3")
else {
return
}
do {
// 2
let file = try AVAudioFile(forReading: fileURL)
let format = file.processingFormat
audioLengthSamples = file.length
audioSampleRate = format.sampleRate
audioLengthSeconds = Double(audioLengthSamples) / audioSampleRate
audioFile = file
// 3
configureEngine(with: format)
} catch {
print("Error reading the audio file: \(error.localizedDescription)")
}
仔细看看发生了什么:
- 1) 这将获取应用程序捆绑包中包含的音频文件的
URL
。 - 2) 音频文件将转换为
AVAudioFile
,并从文件的元数据中提取一些属性。 - 3) 准备要播放的音频文件的最后一步是设置
audio engine
。
将此代码添加到configureEngine(with :)
:
// 1
engine.attach(player)
engine.attach(timeEffect)
// 2
engine.connect(
player,
to: timeEffect,
format: format)
engine.connect(
timeEffect,
to: engine.mainMixerNode,
format: format)
engine.prepare()
do {
// 3
try engine.start()
scheduleAudioFile()
isPlayerReady = true
} catch {
print("Error starting the player: \(error.localizedDescription)")
}
详细看下:
- 1) 将播放器节点附加到引擎,在连接其他节点之前必须执行此操作。 这些节点将产生,处理或输出音频。
audio engine
提供了一个主要的混音器节点,以将其连接到播放器节点。 默认情况下,主混音器连接到引擎默认输出节点iOS设备扬声器。 - 2) 将播放器和时间效果连接到引擎。
prepare()
预分配所需的资源。 - 3) 启动引擎,这将使设备准备播放音频。 状态也将更新以准备可视界面。
接下来,将以下内容添加到scheduleAudioFile()
:
guard
let file = audioFile,
needsFileScheduled
else {
return
}
needsFileScheduled = false
seekFrame = 0
player.scheduleFile(file, at: nil) {
self.needsFileScheduled = true
}
这样可以安排播放整个音频文件。 at:
的参数是时间 —— AVAudioTime
—— 将来您要播放音频。 将其设置为nil
立即开始播放。 该文件仅调度播放一次。 再次点击播放不会从头开始重新播放。 您需要重新调度才能再次播放。 音频文件结束播放后,在完成block
中设置了标记needsFileScheduled
。
调度音频播放的其他方式包括:
-
scheduleBuffer(_:completionHandler :)
:这提供了一个预加载了音频数据的缓冲区。 -
scheduleSegment(_:startingFrame:frameCount:at:completionHandler :)
:类似于scheduleFile(_:at :)
,不同之处在于您指定从哪个音频帧开始播放以及要播放多少帧。
接下来,您将解决用户交互。 将以下内容添加到playOrPause()
中:
// 1
isPlaying.toggle()
if player.isPlaying {
// 2
player.pause()
} else {
// 3
if needsFileScheduled {
scheduleAudioFile()
}
player.play()
}
这是在做什么:
- 1)
isPlaying
属性切换到下一个状态,该状态会更改Play/Pause
按钮图标。 - 2) 如果播放器当前正在播放,则暂停。
- 3) 如果播放器已经暂停,它将继续播放。 如果
needsFileScheduled
为true
,则需要重新调度音频。
构建并运行。
轻按播放,您应该会听到Ray
的The raywenderlich.com Podcast播客精彩介绍。但是,没有UI反馈-您不知道文件有多长时间或文件在其中。
Adding Progress Feedback
现在您可以听到音频了,如何去看呢? 嗯,本教程未涵盖内容。 但是,您当然可以查看音频文件的进度!
在Models / PlayerViewModel.swift
的底部,将以下内容添加到setupDisplayLink()
中:
displayLink = CADisplayLink(target: self, selector: #selector(updateDisplay))
displayLink?.add(to: .current, forMode: .default)
displayLink?.isPaused = true
提示:您可以通过按
Control-6
并键入要查找的名称的一部分,在更长的文件(如PlayerViewModel.swift
)中找到方法和属性!
CADisplayLink
是一个计时器对象,可与显示器的刷新率同步。 您可以使用selector
updateDisplay
实例化它。 然后,将其添加到run loop
中(在本例中为默认run loop
)。 最后,它不需要开始运行,因此请将isPaused
设置为true
。
用以下代码替换playOrPause()
的实现:
isPlaying.toggle()
if player.isPlaying {
displayLink?.isPaused = true
disconnectVolumeTap()
player.pause()
} else {
displayLink?.isPaused = false
connectVolumeTap()
if needsFileScheduled {
scheduleAudioFile()
}
player.play()
}
此处的关键是通过在播放器状态更改时设置displayLink?.isPaused
来暂停或启动displayLink
。 您将在下面的VU
计量器部分中了解connectVolumeTap()
和disconnectVolumeTap()
。
现在,您需要实现关联的UI更新。 将以下内容添加到updateDisplay()
:
// 1
currentPosition = currentFrame + seekFrame
currentPosition = max(currentPosition, 0)
currentPosition = min(currentPosition, audioLengthSamples)
// 2
if currentPosition >= audioLengthSamples {
player.stop()
seekFrame = 0
currentPosition = 0
isPlaying = false
displayLink?.isPaused = true
disconnectVolumeTap()
}
// 3
playerProgress = Double(currentPosition) / Double(audioLengthSamples)
let time = Double(currentPosition) / audioSampleRate
playerTime = PlayerTime(
elapsedTime: time,
remainingTime: audioLengthSeconds - time
)
这是怎么回事:
- 1) 属性
seekFrame
是一个偏移量offset
,该偏移量最初设置为零,从currentFrame中加或者减去。 确保currentPosition
不超出文件范围。 - 2) 如果
currentPosition
位于文件的末尾,则:- 停止播放器。
- 重置
seek
和当前位置属性。 - 暂停
display link
并重置isPlaying
。 - 断开
volume tap
。
- 3) 将
playerProgress
更新到音频文件中的当前位置。 通过将currentPosition
除以音频文件的audioSampleRate
来计算时间。 更新playerTime
,这是一个将两个进度值作为输入的结构体。
该接口已经连接到显示playerProgress
,elapsedTime
和remainingTime
。
构建并运行,然后点击播放/暂停。 您会再次听到Ray
的介绍,但是这次进度条和计时器标签会提供缺少的状态信息。
Implementing the VU Meter
现在是时候添加VU Meter
功能了。 VU Meter
通过根据音频的音量描绘跳动图形来指示实时音频。
您将使用定位在暂停图标栏之间的视图。 播放音频的平均功率决定了视图的高度。 这是您进行音频处理的第一个机会。
您将在1k
音频样本缓冲区上计算平均功率。 确定音频样本缓冲区平均功率的一种常用方法是计算样本的Root Mean Square (RMS)
。
平均功率是音频样本数据范围的平均值的分贝表示。 您还应该注意峰值功率,它是样本数据范围内的最大值。
用以下代码替换scaledPower(power :)
中的代码:
// 1
guard power.isFinite else {
return 0.0
}
let minDb: Float = -80
// 2
if power < minDb {
return 0.0
} else if power >= 1.0 {
return 1.0
} else {
// 3
return (abs(minDb) - abs(power)) / abs(minDb)
}
scaledPower(power :)
将负功率分贝值转换为正值,以调整meterLevel
值。 它的作用是:
- 1)
power.isFinite
检查以确保power
是有效值(即不是NaN
),如果不是,则返回0.0
。 - 2) 这会将
VU
表的动态范围[dynamic range(https://en.wikipedia.org/wiki/Dynamic_range#Audio)
设置为80db
。 对于低于-80.0
的任何值,返回0.0
。 iOS上的分贝值的范围是-160db
,接近无声,最大功率为0db
。minDb
设置为-80.0
,动态范围为80db
。80
提供了足够的分辨率以像素为单位绘制界面。 更改此值以查看它如何影响VU
表。 - 3) 计算介于
0.0
和1.0
之间的缩放比例值(scaled value)
。
现在,将以下内容添加到connectVolumeTap()
:
// 1
let format = engine.mainMixerNode.outputFormat(forBus: 0)
// 2
engine.mainMixerNode.installTap(
onBus: 0,
bufferSize: 1024,
format: format
) { buffer, _ in
// 3
guard let channelData = buffer.floatChannelData 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 {
return $0 * $0
}
.reduce(0, +) / Float(buffer.frameLength))
// 6
let avgPower = 20 * log10(rms)
// 7
let meterLevel = self.scaledPower(power: avgPower)
DispatchQueue.main.async {
self.meterLevel = self.isPlaying ? meterLevel : 0
}
}
这里发生了很多事情,所以这里是细分:
- 1) 获取
mainMixerNode
输出的数据格式。 - 2)
installTap(onBus:0,bufferSize:1024,format:format)
使您可以访问mainMixerNode
的输出总线上的音频数据。您请求的缓冲区大小为1024
个字节,但不能保证所请求的大小,尤其是当您请求的缓冲区太小或太大时。Apple
的文档没有指定这些限制。completion block
接收AVAudioPCMBuffer
和AVAudioTime
作为参数。您可以检查buffer.frameLength
以确定实际的缓冲区大小。 - 3)
buffer.floatChannelData
为您提供了一个指向每个样本数据的指针数组。channelDataValue
是UnsafeMutablePointer <Float>
的数组。 - 4) 从
UnsafeMutablePointer <Float>
数组转换为Float
数组将使以后的计算更加容易。为此,请使用stride(from:to:by :)
创建一个到channelDataValue
的索引数组。然后,map {channelDataValue [$ 0]}
访问数据值并将其存储在channelDataValueArray
中。 - 5) 用均方根
(Root Mean Square)
计算功效涉及map/reduce/divide
运算。首先,map
操作对数组中的所有值求平方,reduce
操作将这些值求和。将平方和除以缓冲区大小,然后取平方根,在缓冲区中生成音频样本数据的RMS
。该值应介于0.0
到1.0
之间,但是在某些极端情况下,它可能是负值。 - 6) 将
RMS
转换为分贝。如果需要,这是声学分贝参考acoustic decibel reference。分贝值应在-160
到0
之间,但是如果RMS
为负,则该分贝值为NaN
。 - 7) 将分贝缩放为适合您的
VU
仪表的值。
最后,将以下内容添加到disconnectVolumeTap()
:
engine.mainMixerNode.removeTap(onBus: 0)
meterLevel = 0
AVAudioEngine
每条总线仅允许单击一次。 最好在不使用时将其删除。
构建并运行,然后点击播放/暂停:
VU
表现在处于活动状态,提供音频数据的平均功率反馈。 播放音频时,您应用的用户将可以轻松地从视觉上辨别。
Implementing Skip
是时候实现向前和向后跳过按钮了。 在此应用程序中,每个按钮向前或向后搜索10
秒钟。
将以下内容添加到seek(to :)
:
guard let audioFile = audioFile else {
return
}
// 1
let offset = AVAudioFramePosition(time * audioSampleRate)
seekFrame = currentPosition + offset
seekFrame = max(seekFrame, 0)
seekFrame = min(seekFrame, audioLengthSamples)
currentPosition = seekFrame
// 2
let wasPlaying = player.isPlaying
player.stop()
if currentPosition < audioLengthSamples {
updateDisplay()
needsFileScheduled = false
let frameCount = AVAudioFrameCount(audioLengthSamples - seekFrame)
// 3
player.scheduleSegment(
audioFile,
startingFrame: seekFrame,
frameCount: frameCount,
at: nil
) {
self.needsFileScheduled = true
}
// 4
if wasPlaying {
player.play()
}
}
这是逐一播放:
- 1) 通过将时间乘以
audioSampleRate
,将时间(以秒为单位)转换为帧位置,并将其添加到currentPosition
。 然后,确保seekFrame
不在文件的开头之前,也不在文件的结尾之后。 - 2)
player.stop()
不仅停止播放,而且清除所有先前安排的事件。 调用updateDisplay()
将UI设置为新的currentPosition
值。 - 3)
player.scheduleSegment(_:startingFrame:frameCount:at :)
调度从音频文件的seekFrame
位置开始播放。frameCount
是要播放的帧数。 您要播放到文件末尾,因此将其设置为audioLengthSamples-seekFrame
。 最后,at:nil
指定立即开始播放,而不是在将来的某个时间开始播放。 - 4) 如果在调用跳过之前正在播放音频,请调用
player.play()
以继续播放。
是时候用这种方法去seek
了。 添加以下内容到skip(forwards:)
:
let timeToSeek: Double
if forwards {
timeToSeek = 10
} else {
timeToSeek = -10
}
seek(to: timeToSeek)
视图中的两个跳过按钮均调用此方法。 如果forwards
参数为true
,则音频会向前跳过10
秒钟。 相反,如果参数为false
,则音频向后跳。
构建并运行,然后点击播放/暂停。 点击向前跳过和向后跳过按钮以向前和向后跳过。 观察progressBar
和计数标签的变化。
Implementing Rate Change
下一个要添加的功能是任何音频应用程序的良好质量。 如今,以高于1x
倍的速度收听播客是一种流行的功能。
将以下内容添加到updateForRateSelection()
:
let selectedRate = allPlaybackRates[playbackRateIndex]
timeEffect.rate = Float(selectedRate.value)
在界面中,用户将点击分段选择器以选择播放速度。 您将选定的选项转换为乘法器以发送到音频播放器。
构建并运行,然后播放音频。 调整速率控制,以听取Ray
和Dru
咖啡过多或过少时的声音。
Implementing Pitch Change
要实现的最后一件事是更改播放的音调。 尽管音调控制不如改变播放速率实用,但仍然很有趣。
将以下内容添加到updateForPitchSelection()
:
let selectedPitch = allPlaybackPitches[playbackPitchIndex]
timeEffect.pitch = 1200 * Float(selectedPitch.value)
根据AVAudioUnitTimePitch.pitch的文档,该值以cents
为单位。 一个八
度等于1200 cents
。 在文件顶部声明的allPlaybackPitches
的值为-0.5、0、0.5
。 将音调改变半个八
度可以使音频保持完整,因此您仍然可以听到每个单词。 随意玩这个数量或多或少会使声音失真。
构建并运行。 调整音调以听到令人毛骨悚然和/或松鼠的声音。
回顾一下AVAudioEngine
的简介,主要关注点是:
- 从文件创建
AVAudioFile
。 - 将
AVAudioPlayer
连接到AVAudioEngine
。 - 调度
AVAudioFile
通过·AVAudioPlayer·播放。
有了这些,您就可以在设备上播放音频。在创建自己的播放器时有用的其他关键主题是:
- 使用音频单元
(audio units)
(例如AVAudioUnitTimePitch
)向引擎添加效果。 - 连接
volume tap
以使用来自AVAudioPCMBuffer
的数据创建VU
表。 - 使用
AVAudioFramePosition
在音频文件中seek
位置。
要了解有关AVAudioEngine
和相关的iOS音频主题的更多信息,请查看:
- What's New in AVAudioEngine - WWDC 2019 - Videos - Apple Developer
- Apple's "Working with Audio"
- Beginning Audio with AVFoundation: Audio Effects
- Audio Tutorial for iOS: File and Data Formats
有关媒体播放的更多信息,请参阅Apple
在AVFoundation上的文档。
后记
本篇主要讲述了基于
AVAudioEngine
的简单使用示例,感兴趣的给个赞或者关注~~~
网友评论