zoukankan      html  css  js  c++  java
  • iOS音频播放(二):AudioSession

    (本文转自码农人生

    前言

    在实施前一篇中所述的7个步骤步之前还必须面对一个麻烦的问题,AudioSession。
     
    AudioSession简介
    AudioSession这个玩意的主要功能包括以下几点(图片来自官方文档):
     
    1. 确定你的app如何使用音频(是播放?还是录音?)
    2. 为你的app选择合适的输入输出设备(比如输入用的麦克风,输出是耳机、手机功放或者airplay)
    3. 协调你的app的音频播放和系统以及其他app行为(例如有电话时需要打断,电话结束时需要恢复,按下静音按钮时是否歌曲也要静音等)
    AudioSession
     
    AudioSession相关的类有两个:
    1. AudioToolBox中的AudioSession
    2. AVFoundation中的AVAudioSession
     
    其中AudioSession在SDK 7中已经被标注为depracated,而AVAudioSession这个类虽然iOS 3开始就已经存在了,但其中很多方法和变量都是在iOS 6以后甚至是iOS 7才有的。所以各位可以依照以下标准选择:
     
    * 如果最低版本支持iOS 5,可以使用AudioSession,也可以使用AVAudioSession;
    * 如果最低版本支持iOS 6及以上,请使用AVAudioSession
     
    下面以AudioSession类为例来讲述AudioSession相关功能的使用(很不幸我需要支持iOS 5。。T-T,使用AVAudioSession的同学可以在其头文件中寻找对应的方法使用即可,需要注意的点我会加以说明)。
     
    注意:在使用AVAudioPlayer/AVPlayer时可以不用关心AudioSession的相关 问题,Apple已经把AudioSession的处理过程封装了,但音乐打断后的响应还是要做的(比如打断后音乐暂停了UI状态也要变化,这个应该通过 KVO就可以搞定了吧。。我没试过瞎猜的>_<)。
     
    初始化AudioSession
    使用AudioSession类首先需要调用初始化方法:
    1. extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop, 
    2.                                        CFStringRef inRunLoopMode, 
    3.                                        AudioSessionInterruptionListener inInterruptionListener, 
    4.                                        void *inClientData); 
    前两个参数一般填NULL表示AudioSession运行在主线程上(但并不代表音频的相关处理运行在主线程上,只是 AudioSession),第三个参数需要传入一个一个AudioSessionInterruptionListener类型的方法,作为 AudioSession被打断时的回调,第四个参数则是代表打断回调时需要附带的对象(即回到方法中的inClientData,如下所示,可以理解为 UIView animation中的context)。
    1. typedef void (*AudioSessionInterruptionListener)(void * inClientData, UInt32 inInterruptionState); 
    这才刚开始,坑就来了。这里会有两个问题:
     
    第一,AudioSessionInitialize可以被多次执行,但AudioSessionInterruptionListener只 能被设置一次,这就意味着这个打断回调方法是一个静态方法,一旦初始化成功以后所有的打断都会回调到这个方法,即便下一次再次调用 AudioSessionInitialize并且把另一个静态方法作为参数传入,当打断到来时还是会回调到第一次设置的方法上。
     
    这种场景并不少见,例如你的app既需要播放歌曲又需要录音,当然你不可能知道用户会先调用哪个功能,所以你必须在播放和录音的模块中都调用 AudioSessionInitialize注册打断方法,但最终打断回调只会作用在先注册的那个模块中,很蛋疼吧。。。所以对于 AudioSession的使用最好的方法是生成一个类单独进行管理,统一接收打断回调并发送自定义的打断通知,在需要用到AudioSession的模 块中接收通知并做相应的操作。
     
    Apple也察觉到了这一点,所以在AVAudioSession中首先取消了Initialize方法,改为了单例方法 sharedInstance。在iOS 5上所有的打断都需要通过设置id<AVAudioSessionDelegate> delegate并实现回调方法来实现,这同样会有上述的问题,所以在iOS 5使用AVAudioSession下仍然需要一个单独管理AudioSession的类存在。在iOS 6以后Apple终于把打断改成了通知的形式。。这下科学了。
     
    第二,AudioSessionInitialize方法的第四个参数inClientData,也就是回调方法的第一个参数。上面已经说了打 断回调是一个静态方法,而这个参数的目的是为了能让回调时拿到context(上下文信息),所以这个inClientData需要是一个有足够长生命周 期的对象(当然前提是你确实需要用到这个参数),如果这个对象被dealloc了,那么回调时拿到的inClientData会是一个野指针。就这一点来 说构造一个单独管理AudioSession的类也是有必要的,因为这个类的生命周期和AudioSession一样长,我们可以把context保存在 这个类中。
     
    监听RouteChange事件
    如果想要实现类似于“拔掉耳机就把歌曲暂停”的功能就需要监听RouteChange事件:
    1. extern OSStatus AudioSessionAddPropertyListener(AudioSessionPropertyID inID, 
    2.                                                 AudioSessionPropertyListener inProc, 
    3.                                                 void *inClientData); 
    4.                                                
    5. typedef void (*AudioSessionPropertyListener)(void * inClientData, 
    6.                                              AudioSessionPropertyID inID, 
    7.                                              UInt32 inDataSize, 
    8.                                              const void * inData); 
    调用上述方法,AudioSessionPropertyID参数传 kAudioSessionProperty_AudioRouteChange,AudioSessionPropertyListener参数传对应 的回调方法。inClientData参数同AudioSessionInitialize方法。
     
    同样作为静态回调方法还是需要统一管理,接到回调时可以把第一个参数inData转换成CFDictionaryRef并从中获取 kAudioSession_AudioRouteChangeKey_Reason键值对应的value(应该是一个CFNumberRef),得到这 些信息后就可以发送自定义通知给其他模块进行相应操作(例如 kAudioSessionRouteChangeReason_OldDeviceUnavailable就可以用来做“拔掉耳机就把歌曲暂停”)。
    1. //AudioSession的AudioRouteChangeReason枚举 
    2. enum { 
    3.       kAudioSessionRouteChangeReason_Unknown = 0, 
    4.       kAudioSessionRouteChangeReason_NewDeviceAvailable = 1, 
    5.       kAudioSessionRouteChangeReason_OldDeviceUnavailable = 2, 
    6.       kAudioSessionRouteChangeReason_CategoryChange = 3, 
    7.       kAudioSessionRouteChangeReason_Override = 4, 
    8.       kAudioSessionRouteChangeReason_WakeFromSleep = 6, 
    9.       kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7, 
    10.       kAudioSessionRouteChangeReason_RouteConfigurationChange = 8 
    11.   }; 
    1. //AVAudioSession的AudioRouteChangeReason枚举 
    2. typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason) 
    3.   AVAudioSessionRouteChangeReasonUnknown = 0, 
    4.   AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1, 
    5.   AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2, 
    6.   AVAudioSessionRouteChangeReasonCategoryChange = 3, 
    7.   AVAudioSessionRouteChangeReasonOverride = 4, 
    8.   AVAudioSessionRouteChangeReasonWakeFromSleep = 6, 
    9.   AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7, 
    10.   AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8 
    注意:iOS 5下如果使用了AVAudioSession由于AVAudioSessionDelegate中并没有定义相关的方法,还是需要用这个方法来实现监听。iOS 6下直接监听AVAudioSession的通知就可以了。
     
    这里附带两个方法的实现,都是基于AudioSession类的(使用AVAudioSession的同学帮不到你们啦)。
     
    1、判断是否插了耳机:
    1. + (BOOL)usingHeadset 
    2. #if TARGET_IPHONE_SIMULATOR 
    3.     return NO; 
    4. #endif 
    5.  
    6.     CFStringRef route; 
    7.     UInt32 propertySize = sizeof(CFStringRef); 
    8.     AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route); 
    9.  
    10.     BOOL hasHeadset = NO; 
    11.     if((route == NULL) || (CFStringGetLength(route) == 0)) 
    12.     { 
    13.         // Silent Mode 
    14.     } 
    15.     else 
    16.     { 
    17.         /* Known values of route: 
    18.          * "Headset" 
    19.          * "Headphone" 
    20.          * "Speaker" 
    21.          * "SpeakerAndMicrophone" 
    22.          * "HeadphonesAndMicrophone" 
    23.          * "HeadsetInOut" 
    24.          * "ReceiverAndMicrophone" 
    25.          * "Lineout" 
    26.          */ 
    27.         NSString* routeStr = (__bridge NSString*)route; 
    28.         NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"]; 
    29.         NSRange headsetRange = [routeStr rangeOfString : @"Headset"]; 
    30.  
    31.         if (headphoneRange.location != NSNotFound) 
    32.         { 
    33.             hasHeadset = YES; 
    34.         } 
    35.         else if(headsetRange.location != NSNotFound) 
    36.         { 
    37.             hasHeadset = YES; 
    38.         } 
    39.     } 
    40.  
    41.     if (route) 
    42.     { 
    43.         CFRelease(route); 
    44.     } 
    45.  
    46.     return hasHeadset; 
     
    2、判断是否开了Airplay(来自StackOverflow):
    1. + (BOOL)isAirplayActived 
    2.     CFDictionaryRef currentRouteDescriptionDictionary = nil; 
    3.     UInt32 dataSize = sizeof(currentRouteDescriptionDictionary); 
    4.     AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription, &dataSize, &currentRouteDescriptionDictionary); 
    5.  
    6.     BOOL airplayActived = NO; 
    7.     if (currentRouteDescriptionDictionary) 
    8.     { 
    9.         CFArrayRef outputs = CFDictionaryGetValue(currentRouteDescriptionDictionary, kAudioSession_AudioRouteKey_Outputs); 
    10.         if(outputs != NULL && CFArrayGetCount(outputs) > 0) 
    11.         { 
    12.             CFDictionaryRef currentOutput = CFArrayGetValueAtIndex(outputs, 0); 
    13.             //Get the output type (will show airplay / hdmi etc 
    14.             CFStringRef outputType = CFDictionaryGetValue(currentOutput, kAudioSession_AudioRouteKey_Type); 
    15.  
    16.             airplayActived = (CFStringCompare(outputType, kAudioSessionOutputRoute_AirPlay, 0) == kCFCompareEqualTo); 
    17.         } 
    18.         CFRelease(currentRouteDescriptionDictionary); 
    19.     } 
    20.     return airplayActived; 
     
    设置类别
    下一步要设置AudioSession的Category,使用AudioSession时调用下面的接口
    1. extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID, 
    2.                                         UInt32 inDataSize, 
    3.                                         const void *inData); 
    如果我需要的功能是播放,执行如下代码
    1. UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback; 
    2. AudioSessionSetProperty (kAudioSessionProperty_AudioCategory, 
    3.                          sizeof(sessionCategory), 
    4.                          &sessionCategory); 
    使用AVAudioSession时调用下面的接口
    1. /* set session category */ 
    2. - (BOOL)setCategory:(NSString *)category error:(NSError **)outError; 
    3. /* set session category with options */ 
    4. - (BOOL)setCategory:(NSString *)category withOptions: (AVAudioSessionCategoryOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0); 
    至于Category的类型在官方文档中都有介绍,我这里也只罗列一下具体就不赘述了,各位在使用时可以依照自己需要的功能设置Category。
    1. //AudioSession的AudioSessionCategory枚举 
    2. enum { 
    3.       kAudioSessionCategory_AmbientSound               = 'ambi', 
    4.       kAudioSessionCategory_SoloAmbientSound           = 'solo', 
    5.       kAudioSessionCategory_MediaPlayback              = 'medi', 
    6.       kAudioSessionCategory_RecordAudio                = 'reca', 
    7.       kAudioSessionCategory_PlayAndRecord              = 'plar', 
    8.       kAudioSessionCategory_AudioProcessing            = 'proc' 
    9.   }; 
    1. //AudioSession的AudioSessionCategory字符串 
    2. /*  Use this category for background sounds such as rain, car engine noise, etc.   
    3.  Mixes with other music. */ 
    4. AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient; 
    5.    
    6. /*  Use this category for background sounds.  Other music will stop playing. */ 
    7. AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient; 
    8.  
    9. /* Use this category for music tracks.*/ 
    10. AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback; 
    11.  
    12. /*  Use this category when recording audio. */ 
    13. AVF_EXPORT NSString *const AVAudioSessionCategoryRecord; 
    14.  
    15. /*  Use this category when recording and playing back audio. */ 
    16. AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord; 
    17.  
    18. /*  Use this category when using a hardware codec or signal processor while 
    19.  not playing or recording audio. */ 
    20. AVF_EXPORT NSString *const AVAudioSessionCategoryAudioProcessing; 
     
    启用
    有了Category就可以启动AudioSession了,启动方法:
    1. //AudioSession的启动方法 
    2. extern OSStatus AudioSessionSetActive(Boolean active); 
    3. extern OSStatus AudioSessionSetActiveWithFlags(Boolean active, UInt32 inFlags); 
    4.  
    5. //AVAudioSession的启动方法 
    6. - (BOOL)setActive:(BOOL)active error:(NSError **)outError; 
    7. - (BOOL)setActive:(BOOL)active withFlags:(NSInteger)flags error:(NSError **)outError NS_DEPRECATED_IOS(4_0, 6_0); 
    8. - (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0); 
    启动方法调用后必须要判断是否启动成功,启动不成功的情况经常存在,例如一个前台的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就维持打断状态;
     
    官方文档中有一张很形象的图来阐述这个现象:
     
    然而现在某些语音通讯软件和某些音乐软件却无视了NotifyOthersOnDeactivation和ShouldResume的正确用 法,导致我们经常接到这样的用户反馈:“你们的app在使用xx语音软件听了一段话后就不会继续播放了,但xx音乐软件可以继续播放啊。”
     
    好吧,上面只是吐槽一下。请无视我吧。
     
    补充:
     
    发现即使之前已经调用过AudioSessionInitialize方法,在某些情况下被打断之后可能出现AudioSession失效的情 况,需要再次调用AudioSessionInitialize方法来重新生成AudioSession。否则调用 AudioSessionSetActive会返回560557673(其他AudioSession方法也雷同,所有方法调用前必须首先初始化 AudioSession),转换成string后为”!ini”即kAudioSessionNotInitialized,这个情况在iOS 5.1.x上尤其频繁,iOS 7.x也偶有发生具体的原因还不知晓。
     
    所以每次在调用AudioSessionSetActive时应该判断一下错误码,如果是上述的错误码需要重新初始化一下AudioSession。
     
    附上OSStatus转成string的方法:
    1. #import <Endian.h> 
    2.  
    3. NSString * OSStatusToString(OSStatus status) 
    4.     size_t len = sizeof(UInt32); 
    5.     long addr = (unsigned long)&status; 
    6.     char cstring[5]; 
    7.  
    8.     len = (status >> 24) == 0 ? len - 1 : len; 
    9.     len = (status >> 16) == 0 ? len - 1 : len; 
    10.     len = (status >>  8) == 0 ? len - 1 : len; 
    11.     len = (status >>  0) == 0 ? len - 1 : len; 
    12.  
    13.     addr += (4 - len); 
    14.  
    15.     status = EndianU32_NtoB(status);        // strings are big endian 
    16.  
    17.     strncpy(cstring, (char *)addr, len); 
    18.     cstring[len] = 0; 
    19.  
    20.     return [NSString stringWithCString:(char *)cstring encoding:NSMacOSRomanStringEncoding]; 
     
    打断处理
    正常启动AudioSession之后就可以播放音频了,下面要讲的是对于打断的处理。之前我们说到打断的回调在iOS 5下需要统一管理,在收到打断开始和结束时需要发送自定义的通知。
     
    使用AudioSession时打断回调应该首先获取kAudioSessionProperty_InterruptionType,然后发送一个自定义的通知并带上对应的参数。
    1. static void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState) 
    2.     AudioSessionInterruptionType interruptionType = kAudioSessionInterruptionType_ShouldNotResume; 
    3.     UInt32 interruptionTypeSize = sizeof(interruptionType); 
    4.     AudioSessionGetProperty(kAudioSessionProperty_InterruptionType, 
    5.                             &interruptionTypeSize, 
    6.                             &interruptionType); 
    7.  
    8.     NSDictionary *userInfo = @{MyAudioInterruptionStateKey:@(inInterruptionState), 
    9.                                MyAudioInterruptionTypeKey:@(interruptionType)}; 
    10.  
    11.     [[NSNotificationCenter defaultCenter] postNotificationName:MyAudioInterruptionNotification object:nil userInfo:userInfo]; 
    收到通知后的处理方法如下(注意ShouldResume参数):
    1. - (void)interruptionNotificationReceived:(NSNotification *)notification 
    2.     UInt32 interruptionState = [notification.userInfo[MyAudioInterruptionStateKey] unsignedIntValue]; 
    3.     AudioSessionInterruptionType interruptionType = [notification.userInfo[MyAudioInterruptionTypeKey] unsignedIntValue]; 
    4.     [self handleAudioSessionInterruptionWithState:interruptionState type:interruptionType]; 
    5.  
    6. - (void)handleAudioSessionInterruptionWithState:(UInt32)interruptionState type:(AudioSessionInterruptionType)interruptionType 
    7.     if (interruptionState == kAudioSessionBeginInterruption) 
    8.     { 
    9.         //控制UI,暂停播放 
    10.     } 
    11.     else if (interruptionState == kAudioSessionEndInterruption) 
    12.     { 
    13.         if (interruptionType == kAudioSessionInterruptionType_ShouldResume) 
    14.         { 
    15.             OSStatus status = AudioSessionSetActive(true); 
    16.             if (status == noErr) 
    17.             { 
    18.                 //控制UI,继续播放 
    19.             } 
    20.         } 
    21.     } 
     
    小结
    关于AudioSession的话题到此结束(码字果然很累。。)。小结一下:
     
    * 如果最低版本支持iOS 5,可以使用AudioSession也可以考虑使用AVAudioSession,需要有一个类统一管理AudioSession的所有回调,在接到回调后发送对应的自定义通知;
    * 如果最低版本支持iOS 6及以上,请使用AVAudioSession,不用统一管理,接AVAudioSession的通知即可;
    * 根据app的应用场景合理选择Category;
    * 在deactive时需要注意app的应用场景来合理的选择是否使用NotifyOthersOnDeactivation参数;
    * 在处理InterruptEnd事件时需要注意ShouldResume的值。
     
    示例代码
    这里有我自己写的AudioSession的封装,如果各位需要支持iOS 5的话可以使用一下。
     
    下一篇将讲述如何使用AudioFileStreamer提取音频文件格式信息和分离音频帧。
     
    参考资料
     
  • 相关阅读:
    javascript 之异常处理try catch finally--05
    javascript 之基本包装类型--04
    javascript 之基本数据类型、引用数据类型区别--02
    javascript 之数据类型--01
    CSS3 object-fit 图像裁剪
    jQuery.extend 使用函数
    ios 不支持iframe 解决方案
    python常用代码积累
    mysql日志类型
    python中列表 元组 字符串如何互相转换
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/5047387.html
Copyright © 2011-2022 走看看