如果最低版本支援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, ¤tRouteDescriptionDictionary); 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_NotifyOthersOnDeactivation(AVAudioSession給options參數傳入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)。當你的app deactive自己的AudioSession時系統會通知上一個被打斷播放app打斷結束(就是上面說到的打斷回調),如果你的app在deactive時傳入了NotifyOthersOnDeactivation參數,那麼其他app在接到打斷結束回調時會多得到一個參數kAudioSessionInterruptionType_ShouldResume否則就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume),根據參數的值可以決定是否繼續播放。
大概流程是這樣的:
- 一個音樂軟體A現正播放;
- 使用者開啟你的軟體播放對話語音,AudioSession active;
- 音樂軟體A音樂被打斷並收到InterruptBegin事件;
- 對話語音播放結束,AudioSession deactive並且傳入NotifyOthersOnDeactivation參數;
- 音樂軟體A收到InterruptEnd事件,查看Resume參數,如果是ShouldResume控制音頻繼續播放,如果是ShouldNotResume就維持打斷狀態;
官方文檔中有一張很形象的圖來闡述這個現象:
然而現在某些語音通訊軟體和某些音樂軟體卻無視了NotifyOthersOnDeactivation和ShouldResume的正確用法,導致我們經常接到這樣的使用者反饋:
你們的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