你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS音頻播放 (二):AudioSession 轉

iOS音頻播放 (二):AudioSession 轉

編輯:IOS開發綜合

 

 

前言

 

本篇為《iOS音頻播放》系列的第二篇。

在實施前一篇中所述的7個步驟之前還必須面對一個麻煩的問題,AudioSession。


AudioSession簡介

AudioSession這個玩意的主要功能包括以下幾點(圖片來自官方文檔):

  1. 確定你的app如何使用音頻(是播放?還是錄音?)
  2. 為你的app選擇合適的輸入輸出設備(比如輸入用的麥克風,輸出是耳機、手機功放或者airplay)
  3. 協調你的app的音頻播放和系統以及其他app行為(例如有電話時需要打斷,電話結束時需要恢復,按下靜音按鈕時是否歌曲也要靜音等)

    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 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上比較容易發生,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 
          
          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

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved