iOS音頻播放 (二):AudioSession 轉

來源:互聯網
上載者:User

iOS音頻播放 (二):AudioSession 轉

 

 

前言

 

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

在實施前一篇中所述的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類首先需要調用初始化方法:

        1234
        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事件:

        12345678
        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就可以用來做“拔掉耳機就把歌曲暫停”)。

        1234567891011
        //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  };
        123456789101112
        //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、判斷是否插了耳機:

        123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
        + (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):

        12345678910111213141516171819202122
        + (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時調用下面的介面

        123
        extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID,                                        UInt32 inDataSize,                                        const void *inData);

        如果我需要的功能是播放,執行如下代碼

        1234
        UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;AudioSessionSetProperty (kAudioSessionProperty_AudioCategory,                         sizeof(sessionCategory),                         &sessionCategory);

        使用AVAudioSession時調用下面的介面

        1234
        /* 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。

        123456789
        //AudioSession的AudioSessionCategory枚舉enum {      kAudioSessionCategory_AmbientSound               = 'ambi',      kAudioSessionCategory_SoloAmbientSound           = 'solo',      kAudioSessionCategory_MediaPlayback              = 'medi',      kAudioSessionCategory_RecordAudio                = 'reca',      kAudioSessionCategory_PlayAndRecord              = 'plar',      kAudioSessionCategory_AudioProcessing            = 'proc'  };
        1234567891011121314151617181920
        //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了,啟動方法:

        12345678
        //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的方法:

          12345678910111213141516171819202122
          #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,然後發送一個自訂的通知並帶上對應的參數。

          12345678910111213
          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參數):

          12345678910111213141516171819202122232425
          - (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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.