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


    原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/

    前言

    本篇为《iOS音频播放》系列的第二篇。

    在实施前一篇中所述的7个步骤之前还必须面对一个麻烦的问题,AudioSession。


    AudioSession简单介绍

    AudioSession这个玩意的主要功能包含下面几点(图片来自官方文档):

    1. 确定你的app怎样使用音频(是播放?还是录音?)
    2. 为你的app选择合适的输入输出设备(比方输入用的麦克风,输出是耳机、手机功放或者airplay)
    3. 协调你的app的音频播放和系统以及其它app行为(比如有电话时须要打断,电话结束时须要恢复,按下静音button时是否歌曲也要静音等)

    AudioSessionAudioSession

    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
    2
    3
    4
    
    extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop,
                                           CFStringRef inRunLoopMode,
                                           AudioSessionInterruptionListener inInterruptionListener,
                                           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
    2
    3
    4
    5
    6
    7
    8
    
    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就能够用来做“拔掉耳机就把歌曲暂停”)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    //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
      };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    //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
    }
    

    注意:iOS 5下假设使用了AVAudioSession因为AVAudioSessionDelegate中并未定义相关的方法,还是须要用这种方法来实现监听。

    iOS 6下直接监听AVAudioSession的通知就能够了。


    这里附带两个方法的实现。都是基于AudioSession类的(使用AVAudioSession的同学帮不到你们啦)。

    1、推断是否插了耳机:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    
    + (BOOL)usingHeadset
    {
    #if TARGET_IPHONE_SIMULATOR
        return NO;
    #endif
    
        CFStringRef route;
        UInt32 propertySize = sizeof(CFStringRef);
        AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route);
    
        BOOL hasHeadset = NO;
        if((route == NULL) || (CFStringGetLength(route) == 0))
        {
            // Silent Mode
        }
        else
        {
            /* Known values of route:
             * "Headset"
             * "Headphone"
             * "Speaker"
             * "SpeakerAndMicrophone"
             * "HeadphonesAndMicrophone"
             * "HeadsetInOut"
             * "ReceiverAndMicrophone"
             * "Lineout"
             */
            NSString* routeStr = (__bridge NSString*)route;
            NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"];
            NSRange headsetRange = [routeStr rangeOfString : @"Headset"];
    
            if (headphoneRange.location != NSNotFound)
            {
                hasHeadset = YES;
            }
            else if(headsetRange.location != NSNotFound)
            {
                hasHeadset = YES;
            }
        }
    
        if (route)
        {
            CFRelease(route);
        }
    
        return hasHeadset;
    }
    

    2、推断是否开了Airplay(来自StackOverflow):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    + (BOOL)isAirplayActived
    {
        CFDictionaryRef currentRouteDescriptionDictionary = nil;
        UInt32 dataSize = sizeof(currentRouteDescriptionDictionary);
        AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription, &dataSize, &currentRouteDescriptionDictionary);
    
        BOOL airplayActived = NO;
        if (currentRouteDescriptionDictionary)
        {
            CFArrayRef outputs = CFDictionaryGetValue(currentRouteDescriptionDictionary, kAudioSession_AudioRouteKey_Outputs);
            if(outputs != NULL && CFArrayGetCount(outputs) > 0)
            {
                CFDictionaryRef currentOutput = CFArrayGetValueAtIndex(outputs, 0);
                //Get the output type (will show airplay / hdmi etc
                CFStringRef outputType = CFDictionaryGetValue(currentOutput, kAudioSession_AudioRouteKey_Type);
    
                airplayActived = (CFStringCompare(outputType, kAudioSessionOutputRoute_AirPlay, 0) == kCFCompareEqualTo);
            }
            CFRelease(currentRouteDescriptionDictionary);
        }
        return airplayActived;
    }
    

    设置类别

    下一步要设置AudioSession的Category。使用AudioSession时调用以下的接口

    1
    2
    3
    
    extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID,
                                            UInt32 inDataSize,
                                            const void *inData);
    

    假设我须要的功能是播放,运行例如以下代码

    1
    2
    3
    4
    
    UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;
    AudioSessionSetProperty (kAudioSessionProperty_AudioCategory,
                             sizeof(sessionCategory),
                             &sessionCategory);
    

    使用AVAudioSession时调用以下的接口

    1
    2
    3
    4
    
    /* set session category */
    - (BOOL)setCategory:(NSString *)category error:(NSError **)outError;
    /* set session category with options */
    - (BOOL)setCategory:(NSString *)category withOptions: (AVAudioSessionCategoryOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0);
    

    至于Category的类型在官方文档中都有介绍。我这里也仅仅罗列一下详细就不赘述了,各位在使用时能够按照自己须要的功能设置Category。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    //AudioSession的AudioSessionCategory枚举
    enum {
          kAudioSessionCategory_AmbientSound               = 'ambi',
          kAudioSessionCategory_SoloAmbientSound           = 'solo',
          kAudioSessionCategory_MediaPlayback              = 'medi',
          kAudioSessionCategory_RecordAudio                = 'reca',
          kAudioSessionCategory_PlayAndRecord              = 'plar',
          kAudioSessionCategory_AudioProcessing            = 'proc'
      };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    //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了,启动方法:

    1
    2
    3
    4
    5
    6
    7
    8
    
    //AudioSession的启动方法
    extern OSStatus AudioSessionSetActive(Boolean active);
    extern OSStatus AudioSessionSetActiveWithFlags(Boolean active, UInt32 inFlags);
    
    //AVAudioSession的启动方法
    - (BOOL)setActive:(BOOL)active error:(NSError **)outError;
    - (BOOL)setActive:(BOOL)active withFlags:(NSInteger)flags error:(NSError **)outError NS_DEPRECATED_IOS(4_0, 6_0);
    - (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0);
    

    启动方法调用后必需要推断是否启动成功。启动不成功的情况常常存在。比如一个前台的app正在播放,你的app正在后台想要启动AudioSession那就会返回失败。

    普通情况下我们在启动和停止AudioSession调用第一个方法就能够了。但假设你正在做一个即时语音通讯app的话(类似于微信、易信)就须要注意在deactive AudioSession的时候须要使用第二个方法,inFlags參数传入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivationAVAudioSession给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就维持打断状态;

    官方文档中有一张非常形象的图来阐述这个现象:

    然而如今某些语音通讯软件和某些音乐软件却无视了NotifyOthersOnDeactivationShouldResume的正确使用方法,导致我们常常接到这种用户反馈:

    你们的app在使用xx语音软件听了一段话后就不会继续播放了。但xx音乐软件能够继续播放啊。
    

    好吧,上面仅仅是吐槽一下。请无视我吧。

    2014.7.14补充,7.19更新:

    发现即使之前已经调用过AudioSessionInitialize方法,在某些情况下被打断之后可能出现AudioSession失效的情况。须要再次调用AudioSessionInitialize方法来又一次生成AudioSession。

    否则调用AudioSessionSetActive会返回560557673(其它AudioSession方法也雷同,全部方法调用前必须首先初始化AudioSession),转换成string后为”!ini”即kAudioSessionNotInitialized。这个情况在iOS 5.1.x上比較easy发生,iOS 6.x 和 7.x也偶有发生(详细的原因还不知晓好像和打断时直接调用AudioOutputUnitStop有关,又是个坑啊)。

    所以每次在调用AudioSessionSetActive时应该推断一下错误码,假设是上述的错误码须要又一次初始化一下AudioSession。

    附上OSStatus转成string的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    #import <Endian.h>
    
    NSString * OSStatusToString(OSStatus status)
    {
        size_t len = sizeof(UInt32);
        long addr = (unsigned long)&status;
        char cstring[5];
    
        len = (status >> 24) == 0 ? len - 1 : len;
        len = (status >> 16) == 0 ? len - 1 : len;
        len = (status >>  8) == 0 ? len - 1 : len;
        len = (status >>  0) == 0 ?

    len - 1 : len; addr += (4 - len); status = EndianU32_NtoB(status); // strings are big endian strncpy(cstring, (char *)addr, len); cstring[len] = 0; return [NSString stringWithCString:(char *)cstring encoding:NSMacOSRomanStringEncoding]; }


    打断处理

    正常启动AudioSession之后就能够播放音频了,以下要讲的是对于打断的处理。之前我们说到打断的回调在iOS 5下须要统一管理。在收到打断開始和结束时须要发送自己定义的通知。

    使用AudioSession时打断回调应该首先获取kAudioSessionProperty_InterruptionType。然后发送一个自己定义的通知并带上相应的參数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    static void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState)
    {
        AudioSessionInterruptionType interruptionType = kAudioSessionInterruptionType_ShouldNotResume;
        UInt32 interruptionTypeSize = sizeof(interruptionType);
        AudioSessionGetProperty(kAudioSessionProperty_InterruptionType,
                                &interruptionTypeSize,
                                &interruptionType);
    
        NSDictionary *userInfo = @{MyAudioInterruptionStateKey:@(inInterruptionState),
                                   MyAudioInterruptionTypeKey:@(interruptionType)};
    
        [[NSNotificationCenter defaultCenter] postNotificationName:MyAudioInterruptionNotification object:nil userInfo:userInfo];
    }
    

    收到通知后的处理方法例如以下(注意ShouldResume參数):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    - (void)interruptionNotificationReceived:(NSNotification *)notification
    {
        UInt32 interruptionState = [notification.userInfo[MyAudioInterruptionStateKey] unsignedIntValue];
        AudioSessionInterruptionType interruptionType = [notification.userInfo[MyAudioInterruptionTypeKey] unsignedIntValue];
        [self handleAudioSessionInterruptionWithState:interruptionState type:interruptionType];
    }
    
    - (void)handleAudioSessionInterruptionWithState:(UInt32)interruptionState type:(AudioSessionInterruptionType)interruptionType
    {
        if (interruptionState == kAudioSessionBeginInterruption)
        {
            //控制UI。暂停播放
        }
        else if (interruptionState == kAudioSessionEndInterruption)
        {
            if (interruptionType == kAudioSessionInterruptionType_ShouldResume)
            {
                OSStatus status = AudioSessionSetActive(true);
                if (status == noErr)
                {
                    //控制UI,继续播放
                }
            }
        }
    }
    

    小结

    关于AudioSession的话题到此结束(码字果然非常累。

    )。小结一下:

    • 假设最低版本号支持iOS 5,能够使用AudioSession也能够考虑使用AVAudioSession。须要有一个类统一管理AudioSession的全部回调,在接到回调后发送相应的自己定义通知;
    • 假设最低版本号支持iOS 6及以上。请使用AVAudioSession,不用统一管理,接AVAudioSession的通知就可以;
    • 依据app的应用场景合理选择Category
    • 在deactive时须要注意app的应用场景来合理的选择是否使用NotifyOthersOnDeactivation參数。
    • 在处理InterruptEnd事件时须要注意ShouldResume的值。


    演示样例代码

    这里有我自己写的AudioSession的封装,假设各位须要支持iOS 5的话能够使用一下。


    下篇预告

    下一篇将讲述怎样使用AudioFileStreamer分离音频帧。以及怎样使用AudioQueue进行播放。

    下一篇将讲述怎样使用AudioFileStreamer提取音频文件格式信息和分离音频帧。


    參考资料

    AudioSession


  • 相关阅读:
    C#如何从普通C++动态库导入一个类?
    MFC的子类化技术
    [转贴] 不要以为使用了模式就是好设计
    VC编程经验汇总(三)
    钩子技术介绍及函数使用
    关于VC中的时间函数讨论
    my read_girl
    Linux + SVN / CVS / ClearCase
    OS + Linux Edit emacs /vi vim gvim /SciTE /gedit /kedit /UltraEdit /nedit /sedf
    java Regular Expression / regexp / zhengzebiaodashi
  • 原文地址:https://www.cnblogs.com/brucemengbm/p/6852540.html
Copyright © 2011-2022 走看看