本篇为《iOS音频播放》系列的第二篇。
本篇主要介绍关于AudioSession使用、期间需要注意的地方以及可能面临的坑。
AudioSession简介
在iOS的音视频开发中,使用具体的API之前都会先创建一个会话,这里也不例外。但在这之前,先来认识一下音频会话(AudioSession),AudioSession这个玩意的主要功能包括以下几点:
- 1.确定你的app如何使用音频(是播放?还是录音?)
- 2.为你的app选择合适的输入输出设备(比如输入用的麦克风,输出是耳机、手机功放或者airplay)
- 3.协调你的app的音频播放和系统以及其他app行为(例如有电话时需要打断,电话结束时需要恢复,按下静音按钮时是否歌曲也要静音等)
初始化AudioSession
使用AudioSession类首先需要调用初始化方法:
extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop,
CFStringRef inRunLoopMode,
AudioSessionInterruptionListener inInterruptionListener,
void *inClientData);
前两个参数一般填NULL表示AudioSession运行在主线程上(但并不代表音频的相关处理运行在主线程上,只是AudioSession),第三个参数需要传入一个AudioSessionInterruptionListener类型的方法,作为AudioSession被打断时的回调,第四个参数则是代表打断回调时需要附带的对象(即回到方法中的inClientData,如下所示,可以理解为UIView animation中的context)。
typedef void (*AudioSessionInterruptionListener)(void * inClientData, UInt32 inInterruptionState);
这才刚开始,坑就来了。这里会有两个问题:
第一,AudioSessionInitialize可以被多次执行,但AudioSessionInterruptionListener只能被设置一次,这就意味着这个打断回调方法是一个静态方法,一旦初始化成功以后所有的打断都会回调到这个方法,即便下一次再次调用AudioSessionInitialize并且把另一个静态方法作为参数传入,当打断到来时还是会回调到第一次设置的方法上。
这种场景并不少见,例如你的app既需要播放歌曲又需要录音,当然你不可能知道用户会先调用哪个功能,所以你必须在播放和录音的模块中都调用AudioSessionInitialize注册打断方法,但最终打断回调只会作用在先注册的那个模块中,所以对于AudioSession的使用最好的方法是生成一个类单独进行管理,统一接收打断回调并发送自定义的打断通知,在需要用到AudioSession的模块中接收通知并做相应的操作。
第二,AudioSessionInitialize方法的第四个参数inClientData,也就是回调方法的第一个参数。上面已经说了打断回调是一个静态方法,而这个参数的目的是为了能让回调时拿到context(上下文信息),所以这个inClientData需要是一个有足够长生命周期的对象(当然前提是你确实需要用到这个参数),如果这个对象被dealloc了,那么回调时拿到的inClientData会是一个野指针。就这一点来说构造一个单独管理AudioSession的类也是有必要的,因为这个类的生命周期和AudioSession一样长,我们可以把context保存在这个类中。
监听RouteChange事件
如果想要实现类似于“拔掉耳机就把歌曲暂停”的功能就需要监听RouteChange事件:
extern OSStatus AudioSessionAddPropertyListener(AudioSessionPropertyID inID,
AudioSessionPropertyListener inProc,
void *inClientData);
typedef void (*AudioSessionPropertyListener)(void * inClientData,
AudioSessionPropertyID inID,
UInt32 inDataSize,
const void * inData);
调用上述方法,AudioSessionPropertyID参数传kAudioSessionProperty_AudioRouteChange,AudioSessionPropertyListener参数传对应的回调方法。inClientData参数同AudioSessionInitialize方法。
同样作为静态回调方法还是需要统一管理,接到回调时可以把第一个参数inData转换成CFDictionaryRef并从中获取kAudioSession_AudioRouteChangeKey_Reason键值对应的value(应该是一个CFNumberRef),得到这些信息后就可以发送自定义通知给其他模块进行相应操作(例如kAudioSessionRouteChangeReason_OldDeviceUnavailable就可以用来做“拔掉耳机就把歌曲暂停”)。
//AudioSession的AudioRouteChangeReason枚举
enum {
kAudioSessionRouteChangeReason_Unknown = 0,
kAudioSessionRouteChangeReason_NewDeviceAvailable = 1,
kAudioSessionRouteChangeReason_OldDeviceUnavailable = 2,
kAudioSessionRouteChangeReason_CategoryChange = 3,
kAudioSessionRouteChangeReason_Override = 4,
kAudioSessionRouteChangeReason_WakeFromSleep = 6,
kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7,
kAudioSessionRouteChangeReason_RouteConfigurationChange = 8
};
//AVAudioSession的AudioRouteChangeReason枚举
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
AVAudioSessionRouteChangeReasonUnknown = 0,
AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
AVAudioSessionRouteChangeReasonCategoryChange = 3,
AVAudioSessionRouteChangeReasonOverride = 4,
AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8
}
下一步要设置AudioSession的Category,使用AudioSession时调用下面的接口
- (BOOL)setCategory:(AVAudioSessionCategory)category error:(NSError **)outError API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
- (BOOL)setCategory:(AVAudioSessionCategory)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError API_AVAILABLE(ios(6.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
- (BOOL)setCategory:(AVAudioSessionCategory)category mode:(AVAudioSessionMode)mode options:(AVAudioSessionCategoryOptions)options error:(NSError **)outError API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0)) API_UNAVAILABLE(macos);
至于Category的类型在官方文档中都有介绍,我这里也只罗列一下具体就不赘述了,各位在使用时可以依照自己需要的功能设置Category。
//AudioSession的AudioSessionCategory枚举
enum {
kAudioSessionCategory_AmbientSound = 'ambi',
kAudioSessionCategory_SoloAmbientSound = 'solo',
kAudioSessionCategory_MediaPlayback = 'medi',
kAudioSessionCategory_RecordAudio = 'reca',
kAudioSessionCategory_PlayAndRecord = 'plar',
kAudioSessionCategory_AudioProcessing = 'proc'
};
//AudioSession的AudioSessionCategory字符串
/* Use this category for background sounds such as rain, car engine noise, etc.
Mixes with other music. */
AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient;
/* Use this category for background sounds. Other music will stop playing. */
AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient;
/* Use this category for music tracks.*/
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback;
/* Use this category when recording audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryRecord;
/* Use this category when recording and playing back audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord;
/* Use this category when using a hardware codec or signal processor while
not playing or recording audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryAudioProcessing;
启用
有了Category就可以启动AudioSession了,启动方法:
- (BOOL)setActive:(BOOL)active error:(NSError **)outError API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
- (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError API_AVAILABLE(ios(6.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
启动方法调用后必须要判断是否启动成功,启动不成功的情况经常存在,例如一个前台的app正在播放,你的app正在后台想要启动AudioSession那就会返回失败。
一般情况下我们在启动和停止AudioSession调用第一个方法就可以了。但如果你正在做一个即时语音通讯app的话(类似于微信、易信)就需要注意在deactive AudioSession的时候需要使用第二个方法,inFlags参数传入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation(AVAudioSession给options参数传入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)。当你的app deactive自己的AudioSession时系统会通知上一个被打断播放app打断结束(就是上面说到的打断回调),如果你的app在deactive时传入了NotifyOthersOnDeactivation参数,那么其他app在接到打断结束回调时会多得到一个参数kAudioSessionInterruptionType_ShouldResume否则就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume),根据参数的值可以决定是否继续播放。
大概流程是这样的:
- 1.一个音乐软件A正在播放;
- 2.用户打开你的软件播放对话语音,AudioSession active;
- 3.音乐软件A音乐被打断并收到InterruptBegin事件;
- 4.对话语音播放结束,AudioSession deactive并且传入NotifyOthersOnDeactivation参数;
- 5.音乐软件A收到InterruptEnd事件,查看Resume参数,如果是ShouldResume控制音频继续播放,如果是ShouldNotResume就维持打断状态;
示例代码
这里(github.com/Nicholas86/…)有代码示例。