AVFoundation(一)概览
AVFoundation(二)音频播放与录制
1. 音频会话AVAudioSession
iOS系统提供了一个可管理的音频环境(managed audio environment),可以带给所有iOS用户非常好的体验,这一神奇的过程就是通过音频会话(audio session)来实现的。
音频会话在应用程序和操作系统之间扮演者中间人的角色,我们可以指明应用程序的一般行为,并可以把对该行为的管理委托给音频会话,这样系统就可以对用户使用音频的体验进行最适当的管理。
所有iOS应用程序都具有音频会话,无论其是否使用。默认音频会话来自于以下一些预配置:
- 激活了音频播放,但是音频录制未激活
- 当用户切换响铃/静音开关到"静音"模式时,应用程序播放的所有音频都会消失
- 当设备显示解锁屏幕时,应用程序的音频处于静音状态
- 当应用程序播放音频时,所有后台播放的音频都会处于静音状态
默认音频会话提供了很多实用功能,我们也可以使用"分类"的功能,来很容易的定制我们的特殊需求。
1.1 音频会话分类
AVFoundation定义了7种分类来描述应用程序所使用的音频行为,如下图
可以看到,其实默认的就是“AVAudioSessionCategorySoloAmbient”类别。从表中我们可以总结如下:
-
AVAudioSessionCategoryAmbient
: 只用于播放音乐时,并且可以和QQ音乐同时播放,比如玩游戏的时候还想听QQ音乐的歌,那么把游戏播放背景音就设置成这种类别。同时,当用户锁屏或者静音时也会随着静音,这种类别基本使用所有App的背景场景。 -
AVAudioSessionCategorySoloAmbient
: 也是只用于播放,但是和AVAudioSessionCategoryAmbient
不同的是,用了它就别想听QQ音乐了,比如不希望QQ音乐干扰的App,类似节奏大师。同样当用户锁屏或者静音时也会随着静音,锁屏了就玩不了节奏大师了。 -
AVAudioSessionCategoryPlayback
: 如果锁屏了还想听声音怎么办?用这个类别,比如App本身就是播放器,同时当App播放时,其他类似QQ音乐就不能播放了。所以这种类别一般用于播放器类App -
AVAudioSessionCategoryRecord
: 有了播放器,肯定要录音机,比如微信语音的录制,就要用到这个类别,既然要安静的录音,肯定不希望有QQ音乐了,所以其他播放声音会中断。想想微信语音的场景,就知道什么时候用他了。 -
AVAudioSessionCategoryPlayAndRecord
: 如果既想播放又想录制该用什么模式呢?比如VoIP,打电话这种场景,PlayAndRecord就是专门为这样的场景设计的 。 -
AVAudioSessionCategoryMultiRoute
: 想象一个DJ用的App,手机连着HDMI到扬声器播放当前的音乐,然后耳机里面播放下一曲,这种常人不理解的场景,这个类别可以支持多个设备输入输出。 -
AVAudioSessionCategoryAudioProcessing
: 主要用于音频格式处理,一般可以配合AudioUnit进行使用
上述分类提供的几种常见行为可以满足大部分应用程序的需要,不过如果开发者需要更复杂的功能,其中一些分类可以通过使用options
和modes
方法进一步定义开发,options
可以让开发者使用一些附加行为,如使用Playback分类后,应用程序允许将输出音频和背景声音进行混合。modes
可以通过引入被定制的行为进一步对分类进行修改以满足一些特殊需求。
1.2 配置音频会话
音频会话在应用程序的生命周期中是可以修改的,但通常我们只对其配置一次,就是在应用程序启动时,也就是在func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
方法中
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback)
} catch let error {
print(error)
}
do {
try session.setActive(true)
} catch let error {
print(error)
}
return true
}
AVAudioSession提供了与应用程序音频会话交互的接口,所以开发者需要取得指向该单例的指针。通过设置合适的分类,开发者可为音频的播放指定需要的音频会话,在其中定制一些行为。最后告知该音频会话激活该配置。
2 使用AVAudioPlayer播放音频
2.1 创建AVAudioPlayer
AVAudioPlayer
可以播放内存版本的Data,或者本地音频文件的URL,如果基于iOS系统,URL必须在应用程序沙盒之内或者该URL一定是用户iPod库中的一个元素。
class LWAudioViewController: BaseViewController {
private var player: AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
let fileUR = Bundle.main.url(forResource: "音乐", withExtension: "mp3")
guard let fileURL = fileUR else {
return
}
player = try! AVAudioPlayer(contentsOf: fileURL)
player.prepareToPlay()
}
}
如上所示,我们最好使用prepareToPlay()
,因为这个方法会取得需要的音频硬件并预加载Audio Queue的缓存区。如果直接使用payer()
,它会隐形激活prepareToPlay()
方法,但是我们会感觉到开始点击播放和实际播放之间会有一个延时。
2.2 AVAudioPlayer的属性与方法
-
volume
:播放器音量,值在0.0(静音)到1.0(最大音量)之间 -
pan
:立体音,取值范围-1.0到1.0,-1.0表示极左,0.0表示中间,1.0表示极右 -
rate
:播放速度,取值范围0.5到2.0,0.5表示半速,2.0表示倍速,rate
要起作用,必须设置enableRate
为true -
numberOfLoops
:设置0表示单次不循环,设置大于0的数n,表示执行n次循环,设置-1表示无限循环 -
open func prepareToPlay() -> Bool
:将音频预加载到Audio Queue缓存区 -
open func play() -> Bool
:播放音频 -
open func play(atTime time: TimeInterval) -> Bool
:延时播放音频 -
open func pause()
:暂停播放 -
open func stop()
:停止播放
2.3 配置后台播放
首先,在target->signing&Capabilities中,选中后台播放
然后,在这个位置添加如下代码设置音频会话
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback)
} catch let error {
print(error)
}
do {
try session.setActive(true)
} catch let error {
print(error)
}
return true
}
这样就设置后,在APP退入后台或者设备锁屏后,音频依然可以根据之前代码中的预设播放了
2.4 处理中断事件
中断事件在iOS设备中经常出现,如电话呼入、闹钟响起以及谈起FaceTime请求等,这时会出现音频播放中断,而且之前事件结束后音频也没有再次播放。我们可以如下操作:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback)
} catch let error {
print(error)
}
do {
try session.setActive(true)
} catch let error {
print(error)
}
//添加观察者
NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
return true
}
//处理打断事件
@objc private func handleInterreption(_ noti: Notification){
guard let userInfo = noti.userInfo,
userInfo.keys.contains(AVAudioSessionInterruptionTypeKey),
let type = userInfo[AVAudioSessionInterruptionTypeKey] as? AVAudioSession.InterruptionType else {
return
}
switch type {
case .began:
/*其实到这里播放已经被中断了,我们切换显示的状态即可*/
print("打断开始,暂停播放,切换状态")
case .ended:
/*
当打断结束,通知中会返回一个InterruptionOptions来表明
音频会话是否已经重新激活以及是否可以再次播放
*/
if let option = userInfo[AVAudioSessionInterruptionOptionKey] as? AVAudioSession.InterruptionOptions,
option == AVAudioSession.InterruptionOptions.shouldResume {
print("打断结束,恢复播放,切换状态")
}
@unknown default:
break
}
}
3.线路改变
在iOS设备上添加或移除音频输入、输出线路时,会发生线路改变。有多重原因可以导致,如用户插入耳机或断开USB麦克风。当这些事件发生时,音频会根据情况改变输入或输出线路,同时AVAudioSession会广播一个描述该变化的通知给所有相关的侦听器。我们也可以通过注册相关的通知来处理该事件:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback)
} catch let error {
print(error)
}
do {
try session.setActive(true)
} catch let error {
print(error)
}
//添加观察者,观察打断事件
NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
//添加观察者,观察线路改变事件
NotificationCenter.default.addObserver(self, selector: #selector(hanleRouteChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil)
return true
}
//处理音频
@objc private func hanleRouteChange(_ noti: Notification){
guard let userInfo = noti.userInfo,
userInfo.keys.contains(AVAudioSessionRouteChangeReasonKey),
let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason else {
return
}
switch reason {
case .unknown :
print("未知原因")
case .oldDeviceUnavailable:
print("旧设备不可用,如拔掉耳机事件")
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription,let previousOutput = previousRoute.outputs.first {
let portType = previousOutput.portType
if portType == .headphones {
//headphones是有线耳机
print("停止播放")
}
}
case .newDeviceAvailable:
print("有可用的新设备,如插入耳机")
case .categoryChange:
break
case .override:
break
case .wakeFromSleep:
break
case .noSuitableRouteForCategory:
break
case .routeConfigurationChange:
print("设备配置改变")
@unknown default:
break
}
}
4. 使用AVAudioRecorder录制音频
4.1 创建AVAudioRecorder
AVAudioRecorder
也是构建于Audio Queue Service之上的,我们创建AVAudioRecorder
实例时需要为其提供数据的一些信息,分别是:
- 用于表示音频流写入文件的本地文件URL
- 包含用于配置录音会话键值信息的字典对象
private func recordSound(){
let filePath = kPathTemp + "sound.m4a"
let url = URL(fileURLWithPath: filePath)
let setting = [AVFormatIDKey: kAudioFormatMPEG4AAC,AVSampleRateKey:22050.0,AVNumberOfChannelsKey:1] as [String : Any]
self.recorder = try! AVAudioRecorder(url: url, settings: setting)
self.recorder.prepareToRecord()
}
和AVPlayer的prepareToPlay方法类似,这个方法执行底层Audio Queue初始化的必要过程。该方法还在URL参数指定的位置创建一个文件,将录制启动时的延时降到最小。
字典设置的key都定义在AVFoundation->AVFAudio->AVAudioSettings
文件中,包含了音频格式、采样率、通道数、指定格式
4.1.1 音频格式
AVFormatIDKey
定义了写入内容的音频格式,下面的常量都是音频格式所支持的值:
CF_ENUM(AudioFormatID)
{
kAudioFormatLinearPCM = 'lpcm',
kAudioFormatAC3 = 'ac-3',
kAudioFormat60958AC3 = 'cac3',
kAudioFormatAppleIMA4 = 'ima4',
kAudioFormatMPEG4AAC = 'aac ',
kAudioFormatMPEG4CELP = 'celp',
kAudioFormatMPEG4HVXC = 'hvxc',
kAudioFormatMPEG4TwinVQ = 'twvq',
kAudioFormatMACE3 = 'MAC3',
kAudioFormatMACE6 = 'MAC6',
kAudioFormatULaw = 'ulaw',
kAudioFormatALaw = 'alaw',
kAudioFormatQDesign = 'QDMC',
kAudioFormatQDesign2 = 'QDM2',
kAudioFormatQUALCOMM = 'Qclp',
kAudioFormatMPEGLayer1 = '.mp1',
kAudioFormatMPEGLayer2 = '.mp2',
kAudioFormatMPEGLayer3 = '.mp3',
kAudioFormatTimeCode = 'time',
kAudioFormatMIDIStream = 'midi',
kAudioFormatParameterValueStream = 'apvs',
kAudioFormatAppleLossless = 'alac',
kAudioFormatMPEG4AAC_HE = 'aach',
kAudioFormatMPEG4AAC_LD = 'aacl',
kAudioFormatMPEG4AAC_ELD = 'aace',
kAudioFormatMPEG4AAC_ELD_SBR = 'aacf',
kAudioFormatMPEG4AAC_ELD_V2 = 'aacg',
kAudioFormatMPEG4AAC_HE_V2 = 'aacp',
kAudioFormatMPEG4AAC_Spatial = 'aacs',
kAudioFormatAMR = 'samr',
kAudioFormatAMR_WB = 'sawb',
kAudioFormatAudible = 'AUDB',
kAudioFormatiLBC = 'ilbc',
kAudioFormatDVIIntelIMA = 0x6D730011,
kAudioFormatMicrosoftGSM = 0x6D730031,
kAudioFormatAES3 = 'aes3',
kAudioFormatEnhancedAC3 = 'ec-3'
};
指定kAudioFormatLinearPCM
会将未压缩的音频流写入到文件中。这种格式的保真度最高,不过相应的文件也最大。选择AAC(kAudioFormatMPEG4AAC)
或AppleIMA4(kAudioFormatAppleIMA4)
的压缩格式会显著缩小文件,还能保证高质量的音频内容。
注意:
你所指定的音频格式一定要和URL参数定义的文件类型兼容。比如,如果录制一个名为test.wav
的文件,隐含的意思就是录制的音频必须满足Waveform Audio File Format(WAVE)
的格式要求,即低字节序、Linear PCM
。为AVFormatIDKey
值指定除kAudioFormatLinearPCM
之外的值会导致错误。
4.1.2 采样率
AVSampleRateKey
用于定义录音器的采样率。采样率定义了对输入的模拟音频信号每一秒的采样数。在录制音频的质量及最终文件大小方面,采样率扮演者至关重要的角色。使用低采样率,比如8kHz,会导致粗粒度、AM广播类型的录制效果,不过文件会比较小;使用44.1kHz的采样率(CD质量的采样率)会得到非常高质量的内容,不过文件就比较大。对于使用什么采样率最好没有一个明确的定义,不过开发者应该尽量使用标准的采样率,比如8kHz、16kHz、22050Hz、44100Hz。
4.1.3 通道数
AVNumberOfChannelsKey
用于定义记录音频内容的通道数。指定默认值1意味着使用单声道录制,设置2表示使用立体声录制。除非使用外部硬件进行录制,否则通常应该创建单声道录音。
4.1.4 指定格式的键
处理Linear PCM
或压缩音频格式时,可以定义一些其他指定格式的键。可在AVFoundation->AVFAudio->AVAudioSettings
中找到完整的列表。
4.2 控制录音过程
AVAudioRecorder
包含一些方法可以支持无限时长的录制,比如在未来某一时间点开始录制或录制指定时长的内容等。开发者可以暂停录音并在停止的地方继续录制。
5.一个简单的录音控制器
5.1 配置音频会话
音频会话默认是AVAudioSession.Category.soloAmbient
,这个会话只支持播放,并且在锁屏等时候会静音,单独需要录音的话,我们可以使用.record
分类,不过我们既想播放音频也想录音的话,使用.playAndRecord
是个很好的选择
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord)
} catch let error {
print(error)
}
do {
try session.setActive(true)
} catch let error {
print(error)
}
return true
}
5.2 音频录制器代码
import AVFoundation
struct LWMemo {
var name: String
var url: URL
init(_ name: String,_ url: URL) {
self.name = name
self.url = url
}
}
class LWAudioRecorderController: NSObject {
//MARK: 音频录制的属性与方法
/*
格式化时间
*/
var formattedCurrentTime: String {
///currentTime是音频文件从开始的时间
let time = UInt(recorder?.currentTime ?? 0)
let hours = time/3600
let minutes = (time/60)%60
let seconds = time%60
var formatString = ""
if hours > 0 {
formatString += String(format: "%02i:", hours)
}
if minutes > 0 {
formatString += String(format: "%02i", minutes)
}
if seconds > 0 {
formatString += String(format: "%02i", seconds)
}
if formatString.count == 0 {
formatString = "00:00"
}
return formatString
}
func record () -> Bool {
recorder?.record() ?? false
}
func pause(){
recorder?.pause()
}
func stop(_ completion: @escaping (Bool) -> Void){
completionHanlder = completion
/*
调用stop之后会触发协议的audioRecorderDidFinishRecording
方法
*/
recorder?.stop()
}
func saveRecording(_ name: String,_ completion: ((Bool,Any?) -> Void)?){
/*
该方法中完成录音保存的功能
*/
let timeStamp = Date.timeIntervalSinceReferenceDate
let fileName = name+"-"+"\(timeStamp)"+".caf"
let destinationPath = kPathDoucument + "/\(fileName)"
guard let sourceUrl = recorder?.url else {
return
}
let destionationUrl = URL(fileURLWithPath: destinationPath)
do {
try FileManager.default.copyItem(at: sourceUrl, to: destionationUrl)
completion?(true,LWMemo(name, destionationUrl))
} catch let error {
completion?(false,error)
}
}
private var recorder: AVAudioRecorder?
private var completionHanlder: ((Bool)->Void)?
override init() {
super.init()
self.configInit()
}
private func configInit() {
/*
1.将录音存放到tmp目录中名为memo.caf的文件
2.使用Core Audio Format(CAF)作为容器格式
因为它和内容无关并可以保存Core Audo支持的
任何音频格式
3.使用AppleIMA4作为音频格式
4.采样率设置为44.1kHz
5.位深设置为16位
6.单声道录制
*/
let filePath = kPathTemp + "memo.caf"
let url = URL(fileURLWithPath: filePath)
let settings = [AVFormatIDKey: kAudioFormatAppleIMA4,AVSampleRateKey: 44100.0,AVNumberOfChannelsKey:1,AVEncoderBitDepthHintKey: 16,AVEncoderAudioQualityKey: AVAudioQuality.medium] as [String: Any]
do {
try recorder = AVAudioRecorder(url: url, settings: settings)
} catch let error {
print(error)
}
recorder?.delegate = self
recorder?.prepareToRecord()
}
//MARK: 音频播放的属性与方法
var player: AVAudioPlayer?
func playback(_ memo: LWMemo) -> Bool{
player?.stop()
player = try? AVAudioPlayer(contentsOf: memo.url)
if player?.prepareToPlay() ?? false {
player?.play()
}
return player != nil
}
}
extension LWAudioRecorderController: AVAudioRecorderDelegate{
//录制结束
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool){
completionHanlder?(flag)
}
//发生编码错误的回调
func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?){
}
//录音被打断
func audioRecorderBeginInterruption(_ recorder: AVAudioRecorder){
}
//录音结束被打断
func audioRecorderEndInterruption(_ recorder: AVAudioRecorder, withOptions flags: Int){
}
}
以上播放器代码注意点:
我们使用formattedCurrentTime
的只读计算属性返回格式化的录音时间,如果需要实时更新展示,需要我们自定义一个每秒定时器,实时的展示时间
6. 音频测量
AVAudioRecorder
和AVAudioPlayer
最强大和最实用的功能就是对音频进行测量。Audio Metering
可让开发者读取音频的平均分贝和峰值分贝数据。
两个类都是使用如下两个方法来返回分贝(dB)等级的浮点值,这个值的范围从表示最大分贝的0Db
(full scale)到表示最小分贝或静音的-160dB
。
//返回峰值分贝
open func peakPower(forChannel channelNumber: Int) -> Float
//返回平均分贝
open func averagePower(forChannel channelNumber: Int) -> Float
在读取这些值之前,我们需要首先将isMeteringEnabled
属性设置为true
才能支持对音频进行测量,然后我们调用updateMeters()
方法来获取最新的值。
//先设置允许测量
recorder?.isMeteringEnabled = true
//更新测量数据
recorder?.updateMeters()
//得到测量数据平均值
recorder?.averagePower(forChannel: 0)
//得到测量数据峰值
recorder?.peakPower(forChannel: 0)
声道索引都是以0开始的,由于我们单声道录制,只需要询问第一个声道即可
不断的读取音频强度值和5.2中一样,需要设置一个定时器,不断的获取音频强度数据,不过由于我们希望频繁更新用于展示计量值以保持动画效果比较平滑,所以可以改用CADisplayLink
作为解决方案。
关于声音计量展示需要注意的一点是,这么做会增加开销。启用计量功能会导致一些额外计算,会影响设备的耗电量
。所以如果录制长时间的音频内容,可能需要考虑禁用音频计量功能。默认isMeteringEnabled
就是false
,也就是默认就是禁用的。