Overview
Apple通过audio sessions管理app, app与其他app, app与外部音频硬件间的行为.使用audio session可以向系统传达你将如何使用音频.audio session充当着app与系统间的中介.这样我们无需了解硬件相关却可以操控硬件行为.
1.Audio session- 配置audio session类别与模式去告诉系统在app中你想怎么使用音频
- 激活audio session使配置的类别与模式可以工作
- 添加通知,响应重要的audio session通知,例如音频中断与硬件线路改变
- 配置音频采样率,声道数等信息
1.配置Audio Session
1.1. Audio Session管理Audio
audio session是应用程序与系统间的中介,用于配置音频行为,APP启动时,会自动获得一个audio session的单例对象,配置并且激活它以让音频按照期望开始工作.
1.2. Categories代表Audio作用
audio session category代表音频的主要行为.通过设置类别, 可以指明app是否使用的当前的输入或输出音频设备,以及当别的app中正在播放音频进入我们app时他们的音频是强制停止还是与我们的音频一起播放等等.
AVFoundation中定义了很多audio session categories, 你可以根据需要自定义音频行为,很多类别支持播放,录制,录制与播放同时进行,当系统了解了你定义的音频规则,它将提供给你合适的路径去访问硬件资源.系统也将确保别的app中的音频以适合你应用的方式运行.
一些categories可以根据Mode进一步定制,该模式用于专门指定类别的行为,例如当使用视频录制模式时,系统可能会选择一个不同于默认内置麦克风的麦克风,系统还可以针对录制调整麦克风的信号强度.
1.3. 中断处理
如果audio意外中断,系统会将aduio session置为停用状态,音频也会因此立即停止.当一个别的app的audio session被激活并且它的类别未设置与系统类别或你应用程序类别混合时,中断就会发生.你的应用程序在收到中断通知后应该保存当时的状态,以及更新用户界面等相关操作.通过注册AVAudioSessionInterruptionNotification可以观察中断的开始与结束点.
1.4. 音频线路改变
当用户做出连接,断开音频输入,输出设备时,(如:插拔耳机)音频线路发生变化,通过注册AVAudioSessionRouteChangeNotification
可以在音频线路发生变化时做出相应处理.
1.5. Audio Sessions控制设备配置
App不能直接控制设备的硬件,但是audio session提供了一些接口去获取或设置一些高级的音频设置,如采样率,声道数等等.
1.6. Audio Sessions保护用户隐私
App如果想使用音频录制功能必须请求用户授权,否则无法使用.
2. 激活Audio Session
在设置了audio session的category, options, mode后,我们可以激活它以启动音频.
2.1. 系统如何解决音频竞争
随着app的启动,内置的一些服务(短信,音乐,浏览器,电话等)也将在后台运行.前面的这些内置服务都可能产生音频,如有电话打来,有短信提示等等...
2.2. 激活,停用Audio Session
虽然AVFoundation中播放与录制可以自动激活你的audio session, 但你可以手动激活并且测试是否激活成功.
系统会停用你的audio session当有电话打进来,闹钟响了,或是日历提醒等消息介入.当处理完这些介入的消息后,系统允许我们手动重新激活audio sesseion.
let session = AVAudioSession.sharedInstance()
do {
// 1) Configure your audio session category, options, and mode
// 2) Activate your audio session to enable your custom configuration
try session.setActive(true)
} catch let error as NSError {
print("Unable to activate audio session: \(error.localizedDescription)")
}
如果我们使用AVFoundation对象(AVPlayer, AVAudioRecorder等),系统负责在中断结束时重新激活audio session.然而,如果你注册了通知去重新激活audio session,你可以验证是否激活成功并且更新用户界面.
- 确保在后台运行的VoIP应用程序的音频会话仅在应用程序处理呼叫时才处于激活状态。在后台,若未收到呼叫,VoIP应用程序的音频会话不应该是激活的。
- 确保使用录制类别的应用程序的音频会话仅在录制时处于激活状态。在录制开始和停止之前,请确保您的会话处于未激活状态,以允许播放其他声音,例如系统声音。
- 如果应用程序支持后台音频播放或录制,但在应用程序未主动使用音频(或准备使用音频)时,在进入后台时停用其音频会话。这样做允许系统释放音频资源,以便其他进程可以使用它们。
2.3. 检查别的Audio是否正在播放
当你的app被激活前,当前设备可能正在播放别的声音,如果你的app是一个游戏的app,知道别的声音来源显得十分重要,因为许多游戏允许同时播放别的音乐以增强用户体验.
在app进入前台前,我们可以通过applicationDidBecomeActive:
代理方法在其中使用secondaryAudioShouldBeSilencedHint
属性来确定音频是否正在播放.当别的app正在播放的audio session为不可混音配置时,该值为true. app可以使用此属性消除次要音频.
func setupNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleSecondaryAudio),
name: .AVAudioSessionSilenceSecondaryAudioHint,
object: AVAudioSession.sharedInstance())
}
func handleSecondaryAudio(notification: Notification) {
// Determine hint type
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt,
let type = AVAudioSessionSilenceSecondaryAudioHintType(rawValue: typeValue) else {
return
}
if type == .begin {
// Other app audio started playing - mute secondary audio
} else {
// Other app audio stopped playing - restart secondary audio
}
}
3. 响应中断
在app中断后可以通过代码做出响应.音频中断将会导致audio session停用,同时应用程序中音频立即终止.当一个来自其他app的竞争的audio session被激活且这个audio session类别不支持与你的app进行混音时,中断发生.注册通知后我们可以在得知音频中断后做出相应处理.
App会因为中断被暂停,当用户接到电话时,闹钟,或其他系统事件被触发时,当中断结束后,App会继续运行,但是需要我们手动重新激活audio session.
3.1. 中断的生命周期
下图简单展示了当收到facetime后app的audio session与系统的audio session间激活与未激活状态变化.
2.interruput_lifecycle
3.2. 中断处理方法
通过注册监听中断的通知可以在中断来的时候进行处理.处理中断取决于你当前正在执行的操作:播放,录制,音频格式转换,读取音频数据包等等.一般而言,我们应尽量避免中断并且做到中断后尽快恢复.
中断前
- 保存状态与上下文
- 更新用户界面
中断后
- 恢复状态与上下文
- 更新用户界面
- 重新激活audio session.
Audio technology | How interruptions work |
---|---|
AVFoundation framework | 系统在中断时会自动暂停录制与播放,当中断结束后重新激活audio session,恢复录制与播放 |
Audio Queue Services, I/O audio unit | 系统会发出中断通知,开发者可以保存播放与录制状态并且在中断结束后重新激活audio session |
System Sound Services | 使用系统声音服务在中断来临时保持静音,如果中断结束,声音自动播放. |
3.3. 处理Siri
当处理Siri时,与其他中断不同,我们在中断期间需要对Siri进行监听,如在中断期间,用户要求Siri去暂停开发者app中的音频播放,当app收到中断结束的通知时,不应该自动恢复播放.同时,用户界面需要跟Siri要求的保持一致.
3.4. 监听中断
注册AVAudioSessionInterruptionNotification
通知可以监听中断.
func registerForNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: AVAudioSession.sharedInstance())
}
func handleInterruption(_ notification: Notification) {
// Handle interruption
}
func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions (save state, update user interface)
}
else if type == .ended {
guard let optionsValue =
userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
return
}
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
}
}
}
注意: 无法确保在开始中断后一定有一个结束中断,所以,如果没有结束中断,我们在app重新播放音频时需要总是检查aduio session是否被激活.
3.5. 响应媒体服务器重置操作
media server通过一个共享服务器进程提供了音频和其他多媒体功能.尽管很少见,但是如果在你的app正在运行时收到一条重置命令,可以通过注册AVAudioSessionMediaServicesWereResetNotification
通知监听media server是否重置.收到通知后需要做如下操作.
- 销毁音频对象并且创建新的音频对象(如:players,recorders,converters,audio queues)
- 重置所有audio状态,包括AVAudioSession全部属性
- 在合适时机重新激活AVAudioSession对象.
注册AVAudioSessionMediaServicesWereLostNotification
可以在media server不可用时收到通知.
如果开发者的应用程序中需要重置功能,如设置中有重置选项,可以使用这个方法轻松重置.
4. 线路改变
audio hardware route指定的设备音频硬件线路发生改变.当用户插拔耳机,系统会自动改变硬件的线路.开发者可以注册AVAudioSessionRouteChangeNotification
通知在线路变化时作出相应调整.
如上图,系统在app启动时会确定一套音频线路,而后程序运行期间会继续监听当前活跃的音频线路,在录制期间,用户可能插拔耳机,系统会发送一份改变线路的通知告诉开发者同时音频停止,开发者可以通过代码决定是否重新激活.
播放与录制稍有不同,播放时如果用户拔掉耳机,默认暂停音频,如果插上耳机,默认继续播放.
4.1. 监听Audio线路变化
原因
- 插拔耳机
- 连接,断开蓝牙耳机
- 插拔USB音频设备
func setupNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleRouteChange),
name: .AVAudioSessionRouteChange,
object: AVAudioSession.sharedInstance())
}
func handleRouteChange(_ notification: Notification) {
}
userInfo
中提供了关于线路改变的详细信息.可以查询改变原因通过字典中的AVAudioSessionRouteChangeReason
,如当新的设备接入时,原因为AVAudioSessionRouteChangeReason
,移除时为AVAudioSessionRouteChangeReasonOldDeviceUnavailable
func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
// Handle new device available.
case .oldDeviceUnavailable:
// Handle old device removed.
default: ()
}
}
当有音频硬件插入时,你可以查询audio session的currentRoute
属性去确定当前音频输出的位置.它将返回一个AVAudioSessionRouteDescription
对象包含audio session全部的输入输出信息.当一个音频硬件被移除时,我们也可以从该对象中查询上一个线路.在以上两种情况中,我们都可以查询outputs
属性,通过返回的AVAudioSessionPortDescription
对象提供了音频输出的全部信息.
func handleRouteChange(notification: NSNotification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
headphonesConnected = true
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
headphonesConnected = false
}
}
default: ()
}
}
5. 配置设备硬件
使用audio session属性,可以在运行时优化硬件音频行为.这样可以让代码适配运行设备的特性.这样做同样适用于用户对音频硬件作出的更改.
5.1. 配置初始音频参数
使用audio session指定音频设备的设置,如采样率, I/O缓冲区时间.
Setting | Preferred sample rate | Preferred I/O buffer duration |
---|---|---|
High value | Example: 48 kHz, + High audio quality, – Large file or buffer size | Example: 500 mS, + Less-frequent file access, – Longer latency |
Low value | Example: 8 kHz, + Small file or buffer size, – Low audio quality | Example: 5 mS,+ Low latency, – Frequent file access |
Note: 默认音频输入输出缓冲时间(I/O buffer duration)为大多数应用提供足够的相应时间,如44.1kHz音频大概为20ms响应一次,你可以设置更低的延迟但相应数据量每次过来的也会降低,根据自己的需求进行选择.
5.2. 设置
在激活audio session前必须完成设置内容.如果你正在运行audio session, 先停用它,然后改变设置重新激活.
let session = AVAudioSession.sharedInstance()
// Configure category and mode
do {
try session.setCategory(AVAudioSessionCategoryRecord, mode: AVAudioSessionModeDefault)
} catch let error as NSError {
print("Unable to set category: \(error.localizedDescription)")
}
// Set preferred sample rate
do {
try session.setPreferredSampleRate(44_100)
} catch let error as NSError {
print("Unable to set preferred sample rate: \(error.localizedDescription)")
}
// Set preferred I/O buffer duration
do {
try session.setPreferredIOBufferDuration(0.005)
} catch let error as NSError {
print("Unable to set preferred I/O buffer duration: \(error.localizedDescription)")
}
// Activate the audio session
do {
try session.setActive(true)
} catch let error as NSError {
print("Unable to activate session. \(error.localizedDescription)")
}
// Query the audio session's ioBufferDuration and sampleRate properties
// to determine if the preferred values were set
print("Audio Session ioBufferDuration: \(session.ioBufferDuration), sampleRate: \(session.sampleRate)")
5.3. 选择,配置麦克风
一个设备可能有多个麦克风(内置,外接),iOS会根据当前使用的audio session mode自动选择一个.mode指定了输入数字信号处理(DSP)和可能的线路.输入线路针对每种模式的用例进行了优化,设置mode还可能影响正在使用的音频线路.
开发者可以手动选择麦克风,甚至可以设置polar pattern如果硬件支持.
在使用任何音频设备之前,请为您的应用设置音频会话类别和模式,然后激活音频会话。
- 设置Preferred Input
为了找到当前设备连接的音频输入设备,可以使用audio session的availableInputs
属性,该属性返回一个AVAudioSessionPortDescription
对象的数组,描述当前可用输入设备端口,端口用portType
进行标识.可以使用setPreferredInput:error:
设置可用的音频输入设备.
- 设置Preferred Data Source
部分端口如内置麦克风,USB等支持数据源(data source),应用程序可以通过查询端口的dataSources
属性发现可用的数据源.对于内置麦克风,返回的数据源描述对象代表每个单独的麦克风。不同的设备为内置麦克风返回不同的值。例如,iPhone 4和iPhone 4S有两个麦克风:底部和顶部。 iPhone 5有三个麦克风:底部,前部和后部。
可以通过数据源描述的location
属性(上,下)和orientation
属性(前,后等)的组合来识别各个内置麦克风。应用程序可以使用AVAudioSessionPortDescription对象的setPreferredDataSource:error:方法设置首选数据源。
- 设置 Preferred Polar Pattern
某些iOS设备支持为某些内置麦克风配置麦克风极性模式。麦克风的极性模式定义了其对声音相对于声源方向的灵敏度。使用supportedPolarPatterns
属性返回数据源是否支持此模式,此属性返回数据源支持的极坐标模式数组(如心形或全向),或者在没有可选模式时返回nil。如果数据源具有许多支持的极坐标模式,则可以使用数据源描述的setPreferredPolarPattern:error:方法设置首选极坐标模式。
- 选择特定麦克风并且设置polar pattern.
// Preferred Mic = Front, Preferred Polar Pattern = Cardioid
let preferredMicOrientation = AVAudioSessionOrientationFront
let preferredPolarPattern = AVAudioSessionPolarPatternCardioid
// Retrieve your configured and activated audio session
let session = AVAudioSession.sharedInstance()
// Get available inputs
guard let inputs = session.availableInputs else { return }
// Find built-in mic
guard let builtInMic = inputs.first(where: {
$0.portType == AVAudioSessionPortBuiltInMic
}) else { return }
// Find the data source at the specified orientation
guard let dataSource = builtInMic.dataSources?.first (where: {
$0.orientation == preferredMicOrientation
}) else { return }
// Set data source's polar pattern
do {
try dataSource.setPreferredPolarPattern(preferredPolarPattern)
} catch let error as NSError {
print("Unable to preferred polar pattern: \(error.localizedDescription)")
}
// Set the data source as the input's preferred data source
do {
try builtInMic.setPreferredDataSource(dataSource)
} catch let error as NSError {
print("Unable to preferred dataSource: \(error.localizedDescription)")
}
// Set the built-in mic as the preferred input
// This call will be a no-op if already selected
do {
try session.setPreferredInput(builtInMic)
} catch let error as NSError {
print("Unable to preferred input: \(error.localizedDescription)")
}
// Print Active Configuration
session.currentRoute.inputs.forEach { portDesc in
print("Port: \(portDesc.portType)")
if let ds = portDesc.selectedDataSource {
print("Name: \(ds.dataSourceName)")
print("Polar Pattern: \(ds.selectedPolarPattern ?? "[none]")")
}
}
Running this code on an iPhone 6s produces the following console output:
Port: MicrophoneBuiltIn
Name: Front
Polar Pattern: Cardioid
5.4. 模拟器运行
可以在模拟器或设备上运行您的应用。但是,Simulator不会模拟不同进程或音频线路更改中的音频会话之间的大多数交互。在Simulator中运行应用程序时,您不能:
- 调用中断
- 模拟插入或拔出耳机
- 更改静音开关的设置
- 模拟屏幕锁定
- 测试音频混合行为 - 即播放音频以及来自其他应用(例如音乐应用)的音频
#if arch(i386) || arch(x86_64)
// Execute subset of code that works in the Simulator
#else
// Execute device-only code as well as the other code
#endif
保护用户隐私
为了保护用户隐私,应用必须在录制音频之前询问并获得用户的许可。如果用户未授予许可,则仅记录静音。当您使用支持录制的类别并且应用程序尝试使用输入线路时,系统会自动提示用户获得权限。
您可以使用requestRecordPermission:
方法手动请求权限,而不是等待系统提示用户提供记录权限。使用此方法可以让您的应用获得权限,而不会中断应用的自然流动,从而获得更好的用户体验。
AVAudioSession.sharedInstance().requestRecordPermission { granted in
if granted {
// User granted access. Present recording interface.
} else {
// Present message to user indicating that recording
// can't be performed until they change their preference
// under Settings -> Privacy -> Microphone
}
}
从iOS 10开始,所有访问任何设备麦克风的应用都必须静态声明其意图。为此,应用程序现在必须在其Info.plist文件中包含NSMicrophoneUsageDescription键,并为此密钥提供目的字符串。当系统提示用户允许访问时,此字符串将显示为警报的一部分。如果应用程序尝试访问任何设备的麦克风而没有此键和值,则应用程序将终止。
网友评论