一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
AVFoundation 是Apple iOS和OS X系统中用于处理基于时间的媒体数据的高级框架,通过开发所需的工具提供了强大的功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序,其针对64位处理器设计,充分利用了多核硬件优势,会自动提供硬件加速操作,确保大部分设备能以最佳性能运行,是iOS开发接触音视频开发必学的框架之一。
参与掘金日新计划,持续记录AVFoundation学习,Demo学习地址,这篇文章主要讲述利用AVAudioPlayer实现音频播放,以及播放时的中断、线路改变处理,其他类的相关用法可查看我的其他文章。
AVAudioSession 音频会话
-
在学习音频之前,先理解音频会话是很有必要的,例如,当我们播放音乐时,接到电话音乐会暂停,拔掉耳机音乐暂停,插入耳机音乐继续播放,这是一个复杂的步骤,在iOS开发中,音频会话帮我们把这一切都处理了,我们只需要配置合适的音频会话即可
-
AVFoundation定义了7种分类来描述应用所使用的音频行为
分类 | 作用 | 是否允许混音 | 音频输入 | 音频输出 |
---|---|---|---|---|
Ambient | 游戏、效率应用 | √ | - | √ |
Solo Ambient(默认) | 游戏、效率应用 | - | - | √ |
Playback | 游戏、效率应用 | 可选 | - | √ |
Record | 录音机、音频捕捉 | - | √ | - |
Play And Record | VoIP、语音聊天 | 可选 | √ | √ |
Audio Processing | 离线会话和处理 | - | - | - |
Multi-Route | 使用外部硬件的高级A/V应用程序 | - | √ | √ |
多数情况使用以上几种分类就可以满足大部分应用所需,若需要更复杂功能,其中一些可以通过使用options和mods的方法进一步自定义开发,例如如果选择了Play And Record则需将options设置为defaultToSpeaker,否则播放音乐是从听筒不是扬声器。
- 音频会话在App生命周期中可以根据需要配置多次,但一般在应用启动时配置
private func confitAudioSession() {
let audioSession = AVAudioSession.sharedInstance()
do {
// 配置会话分类
_ = try audioSession.setCategory(.playAndRecord)
} catch {
CQLog(error)
}
do {
// 激活音频会话
_ = try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
CQLog(error)
}
}
AVAudioPlayer
- 诞生于iOS2.2,至今已经14年,但仍然是最常用的几个类之一,音频播放时很多App的常见需求,AVAudioPlayer让这一功能的实现变的非常简单
- AVAudioPlayer构建于CoreAudio中的C-based Audio Queue Services的最顶层,所以它可以提供所有你在Audio Queue Services中所能找到的核心功能,比如播放、循环甚至音频计量,使用的是非常友好简单的OC/Swift接口,除非你需要从网络流中播放音频、需要访问原始音频样本或者需要非常低的延迟,否则AVAudioPlayer都能胜任。
- 创建AVAudioPlayer,可以用URL创建,也可以用NSData创建
private var audioPlayer: AVAudioPlayer!
let fileUrl: URL = Bundle.main.url(forResource: "kenengfou", withExtension: "mp3")!
let musicData: Data = try! Data(contentsOf: fileUrl)
audioPlayer = try! AVAudioPlayer(data: musicData)
audioPlayer = try! AVAudioPlayer(contentsOf: fileUrl)
- 开始加载,不调用也会隐性调用,但会增加play和听到之间的延时
audioPlayer.prepareToPlay()
- 播放
audioPlayer.play()
- 暂停,play会继续播放
audioPlayer.pause()
- 停止,play同样会继续播放,和pause的区别是,stop会撤销调用prepareToPlay时所作的设置,pause则不会
audioPlayer.stop()
- 修改音量,独立于系统音量,可以实现很多有趣的效果,例如渐隐,0.0-1.0
audioPlayer.volume = 1
- pan值,允许使用立体声,pan的范围-1.0(左)-1.0(右),默认0.0居中
audioPlayer.pan = 0
- 速率,在不改变音调的情况下调整播放速率,0.5(半速)-2.0(2倍速),1.0正常速度,部分资源可能无效
audioPlayer.rate = 1.0
- 循环次数,-1无限循环
audioPlayer.numberOfLoops = -1
音频中断处理
- 注册中断通知
NotificationCenter.default.addObserver(self, selector: #selector(self.handleInterruptionNotification(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
- 判断中断类型业务处理 从userInfo字典里,检索AVAudioSessionInterruptionTypeKey判断中断类型,在中断结束后,检索AVAudioSessionInterruptionOptionKey判断是否已经重新激活以及是否可以再次播放
/// 处理中断
@objc private func handleInterruptionNotification(_ notification: Notification) {
// 接收一个字典 AVAudioSessionInterruptionTypeKey确定中断类型
let info: Dictionary = notification.userInfo!
// CQLog(info)
let interruptionType: AVAudioSession.InterruptionType = AVAudioSession.InterruptionType(rawValue: info[AVAudioSessionInterruptionTypeKey]! as! UInt)!
switch interruptionType {
case .began:
CQLog("中断开始")
case .ended:
CQLog("中断结束")
let options: AVAudioSession.InterruptionOptions = AVAudioSession.InterruptionOptions(rawValue: info[AVAudioSessionInterruptionOptionKey]! as! UInt)
// 是否已经重新激活,是否可以再次播放
if options == .shouldResume {
CQLog("中断结束,已经重新激活,可以再次播放")
} else {
CQLog("中断结束,尚未激活,无法播放")
}
@unknown default:
fatalError("未知的音频中断类型")
}
}
线路改变处理
- 注册线路改变通知
NotificationCenter.default.addObserver(self, selector: #selector(self.handleRouteChangeNotification(_:)), name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance())
- 判断线路改变类型业务处理 可以通过AVAudioSessionRouteChangeReasonKey查找userInfo判断线路变化原因,可以通过AVAudioSessionRouteChangePreviousRouteKey检索判断前一个端口类型
/// 处理线路变化
@objc private func handleRouteChangeNotification(_ notification: Notification) {
// 判断线路变化原因
let info: Dictionary = notification.userInfo!
// CQLog(info)
let routeChangeReason: AVAudioSession.RouteChangeReason = AVAudioSession.RouteChangeReason(rawValue: info[AVAudioSessionRouteChangeReasonKey]! as! UInt)!
switch routeChangeReason {
case .unknown:
CQLog("线路变化原因-未知")
case .newDeviceAvailable:
CQLog("线路变化原因-接入的新设备可用(例如,插入了耳机)")
case .oldDeviceUnavailable:
CQLog("线路变化原因-旧设备无法使用(例如,耳机被拔下)")
case .categoryChange:
CQLog("线路变化原因-音频类别发生了变化(例如重新设定了AVAudioSession.sharedInstance().setCategory)")
case .override:
CQLog("线路变化原因-这条线路已被重写(例如音频会话分类是AVAudioSessionCategoryPlayAndRecord时,改变了options)")
case .wakeFromSleep:
CQLog("线路变化原因-装置唤醒")
case .noSuitableRouteForCategory:
CQLog("线路变化原因-当没有当前类别路由时,例如音频会话分类是AVAudioSessionCategoryRecord时,却没有输入设备可用")
case .routeConfigurationChange:
CQLog("线路变化原因-输入/输出端口的集合没有改变,但某些方面他们的配置已经改变(例如,端口选择的数据源发生了变化)")
@unknown default:
fatalError("未知的音频线路变化类型")
}
// 获取前一个线路的描述
let previousRoute: AVAudioSessionRouteDescription = info[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription
// 输出端口描述
let previousRouteOutputs: [AVAudioSessionPortDescription] = previousRoute.outputs
let previousOutput = previousRoute.outputs[0]
// 获取输出端口类型
let portType: AVAudioSession.Port = previousOutput.portType
// 判断端口类型
if portType == AVAudioSession.Port.lineIn {
CQLog("端口类型-停靠连接器上的线电平输入")
} else if portType == AVAudioSession.Port.builtInMic {
CQLog("端口类型-iOS设备内置麦克风")
} else if portType == AVAudioSession.Port.headsetMic {
CQLog("端口类型-有线耳机上的麦克风")
} else if portType == AVAudioSession.Port.lineOut {
CQLog("端口类型-停靠连接器上的线电平输出")
} else if portType == AVAudioSession.Port.headphones {
CQLog("端口类型-耳机")
} else if portType == AVAudioSession.Port.bluetoothA2DP {
CQLog("端口类型-蓝牙A2DP输出")
} else if portType == AVAudioSession.Port.builtInReceiver {
CQLog("端口类型-打电话时贴在耳朵上的扬声器")
} else if portType == AVAudioSession.Port.builtInSpeaker {
CQLog("端口类型-iOS设备内置扬声器")
} else if portType == AVAudioSession.Port.HDMI {
CQLog("端口类型-高清多媒体接口输出")
} else if portType == AVAudioSession.Port.airPlay {
CQLog("端口类型-airPlay设备输出")
} else if portType == AVAudioSession.Port.bluetoothLE {
CQLog("端口类型-低功耗蓝牙设备输出")
} else if portType == AVAudioSession.Port.bluetoothHFP {
CQLog("端口类型-蓝牙免提配置文件设备上的输入或输出")
} else if portType == AVAudioSession.Port.usbAudio {
CQLog("端口类型-usb设备上的输入或输出")
} else if portType == AVAudioSession.Port.carAudio {
CQLog("端口类型-通过汽车音频输入或输出")
} else if #available(iOS 14.0, *) {
if portType == AVAudioSession.Port.virtual {
CQLog("端口类型-与实际音频硬件不对应的输入或输出")
} else if portType == AVAudioSession.Port.PCI {
CQLog("端口类型-通过PCI(外围组件互连)总线连接的输入或输出")
} else if portType == AVAudioSession.Port.fireWire {
CQLog("端口类型-通过火线连接输入或输出")
} else if portType == AVAudioSession.Port.displayPort {
CQLog("端口类型-displayPort输入输出")
} else if portType == AVAudioSession.Port.AVB {
CQLog("端口类型-通过AVB(音视频桥接)连接输入或输出")
} else if portType == AVAudioSession.Port.thunderbolt {
CQLog("端口类型-通过雷电接口连接输入或输出")
}
} else {
CQLog("端口类型-未知类型")
}
}