iOS音频播放(二)AudioSession

2,647 阅读6分钟

本篇为《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/…)有代码示例。