AVAudioSession简介
音频在iOS,tvOS是一种托管服务。系统通过使用音频会话管理应用程序、应用程序间和设备级别的音频行为。
我们使用AVAudioSession来与系统沟通在我们的应用程序中使用音频。我们可以做的交互如下
- 配置音频会话类别(
Category
)和模式(mode
),以便与系统通信,了解您打算如何在应用程序中使用音频 - 激活应用程序的音频会话,将类别(
Category
)和模式(mode
)配置放入操作中 - 订阅和响应重要的音频会话通知,例如音频中断(
AVAudioSessionInterruptionNotification
)和路由更改(AVAudioSessionRouteChangeNotification
) - 执行高级音频设备配置,如设置采样速率、I/O缓冲区持续时间和通道数量
AVAudioSession
是App和System之间的中介,在App启动的时候,应用程序会自动提供一个AVAudioSession
单例,你通过设置这个单例来实现音频配置。
音频的激活
音频竞争
当我们的App启动时,内置的应用程序或者其他App(消息、音乐、Safari、手机)可能正在后台运行。它们中的每一个都可能产生音频:一条文本消息到达,音乐后台播放。
Q1:当QQ音乐后台播放时,你的App需要播放声音,如何处理这种竞争关系呢?
系统处理音频竞争图如下:
背景:Music App正在占用音频播放。此时我们的MyApp需要进行音频播放
1-2.MyApp请求CoreAudio控制中心,我请求激活音频会话
//get your app's audioSession singleton object
AVAudioSession* session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker |AVAudioSessionCategoryOptionAllowBluetooth
error:nil];
[session setMode:AVAudioSessionModeDefault error:&error];
[session setActive:YES error:&error];
3-4.通过MyApp的设置代码,CoreAudio考虑其类别,对其他音频进行处理
如果,您的应用程序使用了一个类别(不是AVAudioSessionCategoryOptionMixWithOthers),该类别要求对其他音频进行静音,系统将关闭音乐应用程序的音频会话,停止其音频播放。
5.系统(CoreAudio)激活您的应用程序的音频会话和播放可以开始。
音频打断恢复
Q2:当你的App正在播放,此时来了一个电话,接听电话后,如何让App恢复音频播放?
AVAudioSessionInterruptionNotification
当电话打断,短信打断时,此类音频打断我们都会受到一个AVAudioSessionInterruptionNotification
通知,通过监听这个通知,我们处理App的音频恢复播放
AVAudioSession *session = [AVAudioSession sharedInstance];
NSNotificationCenter *noficationCenter = [NSNotificationCenter defaultCenter];
[noficationCenter addObserver: self
selector: @selector(handleRouteChange:)
name: AVAudioSessionRouteChangeNotification
object: session]; //这是route改变通知,例如耳机插拔
[noficationCenter addObserver: self
selector: @selector(handleInterruption:)
name: AVAudioSessionInterruptionNotification
object: session]; //这是音频打断通知
以下是我的项目中处理打断通知的模板,
_isAudioInteruptBegan
表示打断开始,当进入前台时,我们会重置音频,所以需要这个标识。即打断开始后,App又自己进入前台了。
_isBackground
是在后台模式下的处理,此时应区分App是否支持Audio BackgroudModes
,否则在App不支持(即在后台挂起情况),那么会影响我们的音频处理。
_isBackgroundAudioMode
表示App是否支持Audio BackgroudModes
,通过获取plist中的字段,我们可以知道。
上面
_isBackgroundAudioMode _isBackground _isAudioInteruptBegan
是我自己添加的
[AVAudioSession sharedInstance].secondaryAudioShouldBeSilencedHint
指示另一应用程序是否正在播放音频,与otherAudioPlaying
一样,只是在iOS 8之后,其判断更加严格
- (void)handleInterruption:(NSNotification *)notification {
NSInteger reason = 0;
NSString *reasonStr = @"";
if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) {
//Posted when an audio interruption occurs.
if (@available(iOS 10.3, *)) { //bug https://blog.csdn.net/shengpeng3344/article/details/83617979
BOOL isSuspend = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionWasSuspendedKey] boolValue];
if (isSuspend) {
NSLog(@"AVAudioSessionInterruptionWasSuspendedKey is YES");
return;
}
} else {
// Fallback on earlier versions
}
reason = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] integerValue];
if (reason == AVAudioSessionInterruptionTypeBegan ) { //在非后台模式下 打断处理 - 后台打断处理会导致底层线程卡死
reasonStr = @"AVAudioSessionInterruptionTypeBegan";
if (_isBackground) { //如果后台
}else{
}
_isAudioInteruptBegan = YES;
}
if (reason == AVAudioSessionInterruptionTypeEnded) {
_isAudioInteruptBegan = NO;
reasonStr = @"AVAudioSessionInterruptionTypeEnded";
NSNumber *seccondReason = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];
switch ([seccondReason integerValue]) {
case AVAudioSessionInterruptionOptionShouldResume:{ //这里需要恢复音频,即Active:NO方法设置了withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation选项,则会有该值
reasonStr = @"AVAudioSessionInterruptionTypeEnded - AVAudioSessionInterruptionOptionShouldResume ";
if (_isBackground) {
if (_isBackgroundAudioMode && ![AVAudioSession sharedInstance].secondaryAudioShouldBeSilencedHint) {
}else{
}
}else{
//if UITextView / UITextField audio entry interruped , need reset openal
}
}
// Indicates that the audio session is active and immediately ready to be used. Your app can resume the audio operation that was interrupted.
break;
default:
break;
}
}
}
NSLog(@"handleInterruption: %@ reason %@", [notification name], reasonStr);
}
其他App调用[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
,自己的App会收到AVAudioSessionInterruptionNotification
,且在音频打断通知AVAudioSessionInterruptionNotification
的AVAudioSessionInterruptionOptionKey
设置为AVAudioSessionInterruptionOptionShouldResume
,这就是当接入电话后,QQ音乐会自动恢复播放的原因。如果不设置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
,则无法做到这点。
AVAudioSessionRouteChangeNotification
另附上handleRouteChange
的通知处理,在我的项目中并没有过多涉及。
- (void)handleRouteChange:(NSNotification *)notification {
AVAudioSession *session = [AVAudioSession sharedInstance];
NSString *seccReason = @"";
NSInteger reason = [[[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
// AVAudioSessionRouteDescription* prevRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey];
switch (reason) {
case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
seccReason = @"The route changed because no suitable route is now available for the specified category.";
break;
case AVAudioSessionRouteChangeReasonWakeFromSleep:
seccReason = @"The route changed when the device woke up from sleep.";
break;
case AVAudioSessionRouteChangeReasonOverride:
seccReason = @"The output route was overridden by the app.";
break;
case AVAudioSessionRouteChangeReasonCategoryChange:
{
seccReason = @"The category of the session object changed.";
if (![session.category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) {
_isNeedResetCategory = YES;
seccReason = @"The category of the session object changed. not 13";
}
}
break;
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: {
seccReason = @"The previous audio output path is no longer available.";
AVAudioSessionRouteDescription *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey];
for (AVAudioSessionPortDescription *output in previousRoute.outputs) {
if (output.portType == AVAudioSessionPortHeadphones) { //耳机
headphonesConnected = NO;
}
}
}
break;
case AVAudioSessionRouteChangeReasonNewDeviceAvailable: {
seccReason = @"A preferred new audio output path is now available.";
for (AVAudioSessionPortDescription *output in session.currentRoute.outputs) {
if (output.portType == AVAudioSessionPortHeadphones) { //耳机
headphonesConnected = YES;
}
}
}
break;
case AVAudioSessionRouteChangeReasonUnknown:
default:
seccReason = @"The reason for the change is unknown.";
break;
}
GSLog(@"handleRouteChange reason is %@", seccReason);
AVAudioSessionPortDescription *input = [[session.currentRoute.inputs count] ? session.currentRoute.inputs : nil objectAtIndex:0];
AVAudioSessionPortDescription *output = [[session.currentRoute.outputs count] ? session.currentRoute.outputs : nil objectAtIndex:0];
if (input.portType == AVAudioSessionPortHeadsetMic) {
}
GSLog(@"inport port type is %@", input.portType);
GSLog(@"output port type is %@", output.portType);
}
远程控制的监听
另外远程控制的监听需要 MPRemoteCommandCenter
相关的知识 https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter
Media Server Reset 媒体服务器重置
AVAudioSessionMediaServicesWereResetNotification
媒体服务器通过共享服务器进程提供音频和其他多媒体功能。虽然很少见,但媒体服务器可以在您的应用程序处于活动状态时重置。注册AVAudioSessionMediaServicesWereResetNotification
通知,以监视媒体服务器重置。收到通知后,您的app需要做以下操作:
- 处理孤立的音频对象(如播放器、录音机、转换器或音频队列)并创建新对象
- 重置任何被跟踪的内部音频状态,包括AVAudioSession的所有属性
- 在适当的时候,使用setActive:error:方法重新激活AVAudioSession实例
重要提示:应用程序不需要重新注册任何AVAudioSession通知或重置AVAudioSession属性上的key-value observer。
AVAudioSessionMediaServicesWereLostNotification
如果您想知道媒体服务器何时首次不可用,还可以注册AVAudioSessionMediaServicesWereLostNotification
通知。然而,大多数应用程序只需要响应重置通知。只有当应用程序必须响应在媒体服务器丢失后但在媒体服务器重置之前发生的用户事件时,才使用丢失的通知。
如何测试:您可以通过在“设置”->“开发者”->Reset Media Services模拟。使用此实用程序可以方便地确保您的应用程序在重置媒体服务时作出适当的响应。
Mode的影响
关于Category和Mode的配置参数,官方文档说明了
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10-SW3
对于AVAudioSessionModeDefault
和AVAudioSessionModeVoiceChat
两种模式,AVAudioSessionModeVoiceChat
会隐式的降低声音大小,同样在AVAudioSessionModeDefault
上播放的音频声音,在AVAudioSessionModeVoiceChat
会有明显的缩小需要开发者注意,并在合适的时机判断Mode
是否符合预期,不符合预期则重置,例如:
if (session.mode != AVAudioSessionModeDefault) {
success = [session setMode:AVAudioSessionModeDefault error:&error];
if (!success) MLog(@"AVAudioSession setMode error: %@",error);
}
为AirPlay选择类别和模式
只有特定的类别和模式支持AirPlay。以下类别同时支持AirPlay的镜像和非镜像版本
- AVAudioSessionCategorySoloAmbient
- AVAudioSessionCategoryAmbient
- AVAudioSessionCategoryPlayback
AVAudioSessionCategoryPlayAndRecord类别和以下模式只支持AirPlay的镜像版本:
- AVAudioSessionModeDefault
- AVAudioSessionModeVideoChat
- AVAudioSessionModeGameChat
权限获取-录制音频的许可
为了保护用户隐私,您的应用程序在录制音频之前必须征得用户的同意。如果用户不授予权限,则只记录静默。当你使用一个支持记录的类别,而应用程序试图使用输入路径时,系统会自动提示用户获得许可。
您可以使用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开始,所有访问设备麦克风的应用程序都必须静态声明它们的意图。要做到这一点,应用程序现在必须在其信息中包含NSMicrophoneUsageDescription键。并为该键提供一个目的字符串。当系统提示用户允许访问时,此字符串将作为警告的一部分显示。
如果一个应用程序试图在没有该密钥和值的情况下访问设备的任何麦克风,该应用程序将终止。
即应该是在plist中需要进行设置键值对
如果一个应用程序试图在没有该密钥和值的情况下访问设备的任何麦克风,该应用程序将终止。
关于如何打开后台Audio模式
OS和tvOS应用程序要求您为某些后台操作启用某些功能。回放应用程序需要的一个常见功能是播放背景音频。启用此功能后,当用户切换到其他应用程序或锁定iOS设备时,应用程序的音频可以继续。这一功能也需要支持先进的播放功能,如AirPlay流媒体和图片中的图片在iOS中播放。
配置这些功能的最简单方法是使用Xcode。在Xcode中选择应用程序的目标并选择capability选项卡。在capability选项卡下,将背景模式切换为ON,并从可用模式列表中选择“Audio, AirPlay, and Picture in Picture”选项。
官方指南
音频指南按应用程序类型
以下内容都摘自官方文档
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioGuidelinesByAppType/AudioGuidelinesByAppType.html#//apple_ref/doc/uid/TP40007875-CH11-SW1
本人加上了自己的理解,翻译水平有限
游戏类应用程序
大多数游戏都需要用户交互才能让游戏中发生任何事情。在设计游戏时使用AVAudioSessionCategoryAmbient或AVAudioSessionCategorySoloAmbient
类别。当用户打开另一个应用程序或锁定屏幕时,他们不希望该应用程序继续播放。通常他们希望在游戏程序播放时,来自另一个应用程序的音频能够继续播放。
以下是官方文档的指南:
- 在应用程序委托的
applicationDidBecomeActive:
方法中激活音频会话。 - 播放应用程序的音效,同时允许其他应用程序的音频播放。
Tips: 一般使用AVAudioSessionCategoryOptionDuckOthers
- 当其他音频不播放时,播放应用程序原声音频,否则允许前一个音频播放。
- 在结束中断事件后,始终尝试重新激活和恢复声音效果的播放。(
AVAudioSessionInterruptionNotification
) - 查询音频会话的
secondaryAudioShouldBeSilencedHint
属性,以确定是否应该重新播放游戏的原声带。 - 忽略所有路由更改,除非应用程序特别需要注意它们。
- 在应用程序启动显示视频闪屏前设置音频类别。
播放和录制类应用程序
播放和录制类App,应该选用AVAudioSessionCategoryRecord、AVAudioSessionCategoryPlayAndRecord或AVAudioSessionCategoryPlayback
类别,当它们的音频会话被激活时,通常会中断其他系统音频。
以下是官方文档的指南:
- 当应用程序进入前台时,等待用户按下播放或录制按钮,然后激活音频会话。
- 当应用程序在前台时,保持音频会话活动,除非它被中断。
- 如果App在切换到后台时没有积极地播放或录制音频,则禁用其音频会话。这可以防止它的音频会话被另一个不可混合的App或系统在App被挂起时中断。
当你的app是非后台模式时,你需要在进入后台时,显式的关闭AudioSession,否则会收到不必要的打断通知,详情见: https://blog.csdn.net/shengpeng3344/article/details/83617979
- 更新UI,以指示回放或录制在中断时已暂停。不要停用音频会话。
- 观察
AVAudioSessionInterruptionNotification
类型的通知,以便在音频会话中断时得到通知。当中断结束时,不要再开始播放或录制音频,除非应用程序在中断之前已经这样做了 - 如果路由更改是由拔出事件引起的,则暂停播放或录制,但要保持音频会话处于活动状态。
- 假设应用程序的音频会话在从暂停状态过渡到前台状态时处于非活动状态。当用户按下播放或录制按钮时,重新激活音频会话。
- 确保设置了
audio
UIBackgroundModes
标志,即后台允许音频播放。 - 注册远程控制事件(请参阅
[MPRemoteCommandCenter](https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter)
),并为您的媒体提供适当的NowPlaying信息(请参阅MPNowPlayingInfoCenter
)。 - 使用MPVolumeView对象来显示系统音量调节和路由选择器。
- 使用后台任务而不是流媒体静音来防止应用程序被暂停。
- 通过
requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。 - 对于录制应用程序,使用
AVAudioSessionCategoryPlayAndRecord
类别而不是AVAudioSessionCategoryRecord
类别。只记录的类别实际上会使所有系统输出静默,而且通常对大多数应用程序来说限制太大。
VoIP和聊天应用程序
VoIP和聊天应用程序要求输入和输出路由(input and output routes
)都可用。这类应用程序使用AVAudioSessionCategoryPlayAndRecord
类别,不与其他应用程序混合。
- 只有当用户接听或发起呼叫时,才激活音频会话。
- 在收到中断通知之后更新UI以反映调用的音频已被中断。
- 在中断之后,不要激活音频会话,直到用户回答或发起一个调用。
- 在调用结束后,使用
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
常量禁用音频会话。
Tips
:意为调用[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];,会告诉CoreAudio自己的App音频关闭了,CoreAudio会恢复其他需要播放的App,这就是当接入电话后,QQ音乐会自动恢复播放的原因。如果不设置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation,则无法做到这点。
- 忽略所有路由更改,除非应用程序特别需要注意它们。例如,路由更改可能导致对会话的采样率、缓冲区持续时间或延迟的更改。如果这些值与您的应用程序相关,请在路由更改后查询它们,以获得它们的最新状态。
- 对于VoIP应用程序,使用苹果的语音处理I/O音频单元。
- 确保设置了
audio UIBackgroundModes
标志。 - 使用
MPVolumeView
对象来显示系统音量调节和路由选择器。 - 通过
requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。
从iOS 10开始,要构建与内置电话应用(电话和FaceTime应用)具有相同功能的VoIP应用程序,请使用CallKit框架
。有关更多信息,请参见CallKit框架参考。
计量的应用程序
计量应用程序需要对输入和输出路由应用最少的系统提供的信号处理。设置AVAudioSessionCategoryPlayAndRecord
类别和测量模式,以最小化信号处理。此外,这类应用程序不会与其他应用程序混合。
- 总是尝试在结束中断事件后重新激活并恢复播放。
- 忽略所有路由更改,除非应用程序特别需要注意它们。
- 在应用程序启动显示视频闪屏前设置音频类别。
- 通过
requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。
类似浏览器的应用程序
社交媒体或其他类似浏览器的应用程序经常播放短视频。它们使用AVAudioSessionCategoryPlayback
类别,不服从铃声开关。这些应用程序也不与其他应用程序混合。
- 总是等待用户开始回放。
- 在视频结束后,使用AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation常量禁用音频会话。
- 由于断开事件导致路由更改,请暂停音频会话,但要保持音频会话处于活动状态。
- 视频播放时注册远程控制事件,视频结束时注销。
- 当应用程序收到开始中断事件时更新UI。
- 在接收到结束中断事件后,等待用户开始回放。
导航和健身应用
导航和锻炼应用程序使用AVAudioSessionCategoryPlayback
或AVAudioSessionCategoryPlayAndRecord
类别。这些应用程序的音频通常由简短的语音提示组成。当播放时,这些提示会中断语音,比如播客或有声书,并与(和鸭子)其他音频混合,比如音乐应用程序的播放。
- 使用
AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
和AVAudioSessionCategoryOptionDuckOthers
选项激活音频会话。即开启混合,或者使得其他App的音频变小
- 在需要提示之前不要激活音频会话。
- 总是在播放提示后禁用音频会话。
- 不要试图重新播放被中断的提示符。
合作音乐应用程序
合作音乐应用程序是为在其他应用程序播放时播放而设计的。这些类型的应用程序使用AVAudioSessionCategoryPlayback
或AVAudioSessionCategoryPlayAndRecord`类别,并与其他应用程序混合使用。
- 使用AVAudioSessionCategoryOptionMixWithOthers选项激活音频会话。
- 如果应用程序的UI没有提供开始/停止按钮,请遵循游戏应用程序指南。
- 如果应用程序的UI提供了一个start/stop按钮,那么只在用户按下play按钮时激活音频会话。
- 不要注册远程控制事件。
- 确保设置了audio UIBackgroundModes标志。
- 通过
requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。
学习不断,有错误请指出,如果对你有帮助,点个👍