本篇為《iOS音頻播放》系列的第二篇悠汽。
在實(shí)施前一篇中所述的7個(gè)步驟之前還必須面對(duì)一個(gè)麻煩的問(wèn)題葵擎,AudioSession探橱。
本篇主要介紹關(guān)于AudioSession使用资柔、期間需要注意的地方以及可能面臨的坑甘桑。
AudioSession簡(jiǎn)介
AudioSession這個(gè)玩意的主要功能包括以下幾點(diǎn)(圖片來(lái)自官方文檔):
確定你的app如何使用音頻(是播放拍皮?還是錄音歹叮?)
為你的app選擇合適的輸入輸出設(shè)備(比如輸入用的麥克風(fēng),輸出是耳機(jī)铆帽、手機(jī)功放或者airplay)
協(xié)調(diào)你的app的音頻播放和系統(tǒng)以及其他app行為(例如有電話時(shí)需要打斷咆耿,電話結(jié)束時(shí)需要恢復(fù),按下靜音按鈕時(shí)是否歌曲也要靜音等)
AudioSession
AudioSession相關(guān)的類有兩個(gè):
AudioToolBox中的AudioSession
AVFoundation中的AVAudioSession
其中AudioSession在SDK 7中已經(jīng)被標(biāo)注為depracated爹橱,而AVAudioSession這個(gè)類雖然iOS 3開始就已經(jīng)存在了萨螺,但其中很多方法和變量都是在iOS 6以后甚至是iOS 7才有的。所以各位可以依照以下標(biāo)準(zhǔn)選擇:
如果最低版本支持iOS 5愧驱,可以使用AudioSession慰技,也可以使用AVAudioSession;
如果最低版本支持iOS 6及以上组砚,請(qǐng)使用AVAudioSession
下面以AudioSession類為例來(lái)講述AudioSession相關(guān)功能的使用(很不幸我需要支持iOS 5惹盼。。T-T惫确,使用AVAudioSession的同學(xué)可以在其頭文件中尋找對(duì)應(yīng)的方法使用即可手报,需要注意的點(diǎn)我會(huì)加以說(shuō)明).
注意:在使用AVAudioPlayer/AVPlayer時(shí)可以不用關(guān)心AudioSession的相關(guān)問(wèn)題,Apple已經(jīng)把AudioSession的處理過(guò)程封裝了改化,但音樂(lè)打斷后的響應(yīng)還是要做的(比如打斷后音樂(lè)暫停了UI狀態(tài)也要變化掩蛤,這個(gè)應(yīng)該通過(guò)KVO就可以搞定了吧。陈肛。我沒(méi)試過(guò)瞎猜的>_<)揍鸟。
注意:在使用MPMusicPlayerController時(shí)不必關(guān)心AudioSession的問(wèn)題。
初始化AudioSession
使用AudioSession類首先需要調(diào)用初始化方法:
1234externOSStatusAudioSessionInitialize(CFRunLoopRefinRunLoop,CFStringRefinRunLoopMode,AudioSessionInterruptionListenerinInterruptionListener,void*inClientData);
前兩個(gè)參數(shù)一般填NULL表示AudioSession運(yùn)行在主線程上(但并不代表音頻的相關(guān)處理運(yùn)行在主線程上句旱,只是AudioSession)阳藻,第三個(gè)參數(shù)需要傳入一個(gè)AudioSessionInterruptionListener類型的方法,作為AudioSession被打斷時(shí)的回調(diào)谈撒,第四個(gè)參數(shù)則是代表打斷回調(diào)時(shí)需要附帶的對(duì)象(即回到方法中的inClientData腥泥,如下所示,可以理解為UIView animation中的context)啃匿。
1typedefvoid(*AudioSessionInterruptionListener)(void*inClientData,UInt32inInterruptionState);
這才剛開始蛔外,坑就來(lái)了。這里會(huì)有兩個(gè)問(wèn)題:
第一溯乒,AudioSessionInitialize可以被多次執(zhí)行夹厌,但AudioSessionInterruptionListener只能被設(shè)置一次,這就意味著這個(gè)打斷回調(diào)方法是一個(gè)靜態(tài)方法裆悄,一旦初始化成功以后所有的打斷都會(huì)回調(diào)到這個(gè)方法矛纹,即便下一次再次調(diào)用AudioSessionInitialize并且把另一個(gè)靜態(tài)方法作為參數(shù)傳入,當(dāng)打斷到來(lái)時(shí)還是會(huì)回調(diào)到第一次設(shè)置的方法上光稼。
這種場(chǎng)景并不少見或南,例如你的app既需要播放歌曲又需要錄音逻住,當(dāng)然你不可能知道用戶會(huì)先調(diào)用哪個(gè)功能,所以你必須在播放和錄音的模塊中都調(diào)用AudioSessionInitialize注冊(cè)打斷方法迎献,但最終打斷回調(diào)只會(huì)作用在先注冊(cè)的那個(gè)模塊中瞎访,很蛋疼吧。吁恍。扒秸。所以對(duì)于AudioSession的使用最好的方法是生成一個(gè)類單獨(dú)進(jìn)行管理,統(tǒng)一接收打斷回調(diào)并發(fā)送自定義的打斷通知冀瓦,在需要用到AudioSession的模塊中接收通知并做相應(yīng)的操作伴奥。
Apple也察覺(jué)到了這一點(diǎn),所以在AVAudioSession中首先取消了Initialize方法翼闽,改為了單例方法sharedInstance拾徙。在iOS 5上所有的打斷都需要通過(guò)設(shè)置id delegate并實(shí)現(xiàn)回調(diào)方法來(lái)實(shí)現(xiàn),這同樣會(huì)有上述的問(wèn)題感局,所以在iOS 5使用AVAudioSession下仍然需要一個(gè)單獨(dú)管理AudioSession的類存在尼啡。在iOS 6以后Apple終于把打斷改成了通知的形式。询微。這下科學(xué)了崖瞭。
第二,AudioSessionInitialize方法的第四個(gè)參數(shù)inClientData撑毛,也就是回調(diào)方法的第一個(gè)參數(shù)书聚。上面已經(jīng)說(shuō)了打斷回調(diào)是一個(gè)靜態(tài)方法,而這個(gè)參數(shù)的目的是為了能讓回調(diào)時(shí)拿到context(上下文信息)藻雌,所以這個(gè)inClientData需要是一個(gè)有足夠長(zhǎng)生命周期的對(duì)象(當(dāng)然前提是你確實(shí)需要用到這個(gè)參數(shù))雌续,如果這個(gè)對(duì)象被dealloc了,那么回調(diào)時(shí)拿到的inClientData會(huì)是一個(gè)野指針胯杭。就這一點(diǎn)來(lái)說(shuō)構(gòu)造一個(gè)單獨(dú)管理AudioSession的類也是有必要的驯杜,因?yàn)檫@個(gè)類的生命周期和AudioSession一樣長(zhǎng),我們可以把context保存在這個(gè)類中歉摧。
監(jiān)聽RouteChange事件
如果想要實(shí)現(xiàn)類似于“拔掉耳機(jī)就把歌曲暫屯щ龋”的功能就需要監(jiān)聽RouteChange事件:
12345678externOSStatusAudioSessionAddPropertyListener(AudioSessionPropertyIDinID,AudioSessionPropertyListenerinProc,void*inClientData);typedefvoid(*AudioSessionPropertyListener)(void*inClientData,AudioSessionPropertyIDinID,UInt32inDataSize,constvoid*inData);
調(diào)用上述方法,AudioSessionPropertyID參數(shù)傳kAudioSessionProperty_AudioRouteChange叁温,AudioSessionPropertyListener參數(shù)傳對(duì)應(yīng)的回調(diào)方法。inClientData參數(shù)同AudioSessionInitialize方法核畴。
同樣作為靜態(tài)回調(diào)方法還是需要統(tǒng)一管理膝但,接到回調(diào)時(shí)可以把第一個(gè)參數(shù)inData轉(zhuǎn)換成CFDictionaryRef并從中獲取kAudioSession_AudioRouteChangeKey_Reason鍵值對(duì)應(yīng)的value(應(yīng)該是一個(gè)CFNumberRef),得到這些信息后就可以發(fā)送自定義通知給其他模塊進(jìn)行相應(yīng)操作(例如kAudioSessionRouteChangeReason_OldDeviceUnavailable就可以用來(lái)做“拔掉耳機(jī)就把歌曲暫桶荩”)跟束。
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枚舉typedefNS_ENUM(NSUInteger,AVAudioSessionRouteChangeReason){AVAudioSessionRouteChangeReasonUnknown=0,AVAudioSessionRouteChangeReasonNewDeviceAvailable=1,AVAudioSessionRouteChangeReasonOldDeviceUnavailable=2,AVAudioSessionRouteChangeReasonCategoryChange=3,AVAudioSessionRouteChangeReasonOverride=4,AVAudioSessionRouteChangeReasonWakeFromSleep=6,AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory=7,AVAudioSessionRouteChangeReasonRouteConfigurationChangeNS_ENUM_AVAILABLE_IOS(7_0)=8}
注意:iOS 5下如果使用了AVAudioSession由于AVAudioSessionDelegate中并沒(méi)有定義相關(guān)的方法莺奸,還是需要用這個(gè)方法來(lái)實(shí)現(xiàn)監(jiān)聽。iOS 6下直接監(jiān)聽AVAudioSession的通知就可以了冀宴。
這里附帶兩個(gè)方法的實(shí)現(xiàn)灭贷,都是基于AudioSession類的(使用AVAudioSession的同學(xué)幫不到你們啦)。
1略贮、判斷是否插了耳機(jī):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748+(BOOL)usingHeadset{#if TARGET_IPHONE_SIMULATORreturnNO;#endifCFStringRefroute;UInt32propertySize=sizeof(CFStringRef);AudioSessionGetProperty(kAudioSessionProperty_AudioRoute,&propertySize,&route);BOOLhasHeadset=NO;if((route==NULL)||(CFStringGetLength(route)==0)){// Silent Mode}else{/* Known values of route:* "Headset"* "Headphone"* "Speaker"* "SpeakerAndMicrophone"* "HeadphonesAndMicrophone"* "HeadsetInOut"* "ReceiverAndMicrophone"* "Lineout"*/NSString*routeStr=(__bridgeNSString*)route;NSRangeheadphoneRange=[routeStrrangeOfString:@"Headphone"];NSRangeheadsetRange=[routeStrrangeOfString:@"Headset"];if(headphoneRange.location!=NSNotFound){hasHeadset=YES;}elseif(headsetRange.location!=NSNotFound){hasHeadset=YES;}}if(route){CFRelease(route);}returnhasHeadset;}
2甚疟、判斷是否開了Airplay(來(lái)自StackOverflow):
12345678910111213141516171819202122+(BOOL)isAirplayActived{CFDictionaryRefcurrentRouteDescriptionDictionary=nil;UInt32dataSize=sizeof(currentRouteDescriptionDictionary);AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription,&dataSize,¤tRouteDescriptionDictionary);BOOLairplayActived=NO;if(currentRouteDescriptionDictionary){CFArrayRefoutputs=CFDictionaryGetValue(currentRouteDescriptionDictionary,kAudioSession_AudioRouteKey_Outputs);if(outputs!=NULL&&CFArrayGetCount(outputs)>0){CFDictionaryRefcurrentOutput=CFArrayGetValueAtIndex(outputs,0);//Get the output type (will show airplay / hdmi etcCFStringRefoutputType=CFDictionaryGetValue(currentOutput,kAudioSession_AudioRouteKey_Type);airplayActived=(CFStringCompare(outputType,kAudioSessionOutputRoute_AirPlay,0)==kCFCompareEqualTo);}CFRelease(currentRouteDescriptionDictionary);}returnairplayActived;}
設(shè)置類別
下一步要設(shè)置AudioSession的Category,使用AudioSession時(shí)調(diào)用下面的接口
123externOSStatusAudioSessionSetProperty(AudioSessionPropertyIDinID,UInt32inDataSize,constvoid*inData);
如果我需要的功能是播放逃延,執(zhí)行如下代碼
1234UInt32sessionCategory=kAudioSessionCategory_MediaPlayback;AudioSessionSetProperty(kAudioSessionProperty_AudioCategory,sizeof(sessionCategory),&sessionCategory);
使用AVAudioSession時(shí)調(diào)用下面的接口
1234/* set session category */-(BOOL)setCategory:(NSString*)categoryerror:(NSError**)outError;/* set session category with options */-(BOOL)setCategory:(NSString*)categorywithOptions:(AVAudioSessionCategoryOptions)optionserror:(NSError**)outErrorNS_AVAILABLE_IOS(6_0);
至于Category的類型在官方文檔中都有介紹览妖,我這里也只羅列一下具體就不贅述了,各位在使用時(shí)可以依照自己需要的功能設(shè)置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_EXPORTNSString*constAVAudioSessionCategoryAmbient;/*? Use this category for background sounds.? Other music will stop playing. */AVF_EXPORTNSString*constAVAudioSessionCategorySoloAmbient;/* Use this category for music tracks.*/AVF_EXPORTNSString*constAVAudioSessionCategoryPlayback;/*? Use this category when recording audio. */AVF_EXPORTNSString*constAVAudioSessionCategoryRecord;/*? Use this category when recording and playing back audio. */AVF_EXPORTNSString*constAVAudioSessionCategoryPlayAndRecord;/*? Use this category when using a hardware codec or signal processor whilenot playing or recording audio. */AVF_EXPORTNSString*constAVAudioSessionCategoryAudioProcessing;
啟用
有了Category就可以啟動(dòng)AudioSession了讽膏,啟動(dòng)方法:
12345678//AudioSession的啟動(dòng)方法externOSStatusAudioSessionSetActive(Booleanactive);externOSStatusAudioSessionSetActiveWithFlags(Booleanactive,UInt32inFlags);//AVAudioSession的啟動(dòng)方法-(BOOL)setActive:(BOOL)activeerror:(NSError**)outError;-(BOOL)setActive:(BOOL)activewithFlags:(NSInteger)flagserror:(NSError**)outErrorNS_DEPRECATED_IOS(4_0,6_0);-(BOOL)setActive:(BOOL)activewithOptions:(AVAudioSessionSetActiveOptions)optionserror:(NSError**)outErrorNS_AVAILABLE_IOS(6_0);
啟動(dòng)方法調(diào)用后必須要判斷是否啟動(dòng)成功,啟動(dòng)不成功的情況經(jīng)常存在拄丰,例如一個(gè)前臺(tái)的app正在播放府树,你的app正在后臺(tái)想要啟動(dòng)AudioSession那就會(huì)返回失敗。
一般情況下我們?cè)趩?dòng)和停止AudioSession調(diào)用第一個(gè)方法就可以了料按。但如果你正在做一個(gè)即時(shí)語(yǔ)音通訊app的話(類似于微信挺尾、易信)就需要注意在deactive AudioSession的時(shí)候需要使用第二個(gè)方法,inFlags參數(shù)傳入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation(AVAudioSession給options參數(shù)傳入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)站绪。當(dāng)你的app deactive自己的AudioSession時(shí)系統(tǒng)會(huì)通知上一個(gè)被打斷播放app打斷結(jié)束(就是上面說(shuō)到的打斷回調(diào))遭铺,如果你的app在deactive時(shí)傳入了NotifyOthersOnDeactivation參數(shù),那么其他app在接到打斷結(jié)束回調(diào)時(shí)會(huì)多得到一個(gè)參數(shù)kAudioSessionInterruptionType_ShouldResume否則就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume)恢准,根據(jù)參數(shù)的值可以決定是否繼續(xù)播放魂挂。
大概流程是這樣的:
一個(gè)音樂(lè)軟件A正在播放;
用戶打開你的軟件播放對(duì)話語(yǔ)音馁筐,AudioSession active涂召;
音樂(lè)軟件A音樂(lè)被打斷并收到InterruptBegin事件;
對(duì)話語(yǔ)音播放結(jié)束敏沉,AudioSession deactive并且傳入NotifyOthersOnDeactivation參數(shù)果正;
音樂(lè)軟件A收到InterruptEnd事件,查看Resume參數(shù)盟迟,如果是ShouldResume控制音頻繼續(xù)播放秋泳,如果是ShouldNotResume就維持打斷狀態(tài);
官方文檔中有一張很形象的圖來(lái)闡述這個(gè)現(xiàn)象:
然而現(xiàn)在某些語(yǔ)音通訊軟件和某些音樂(lè)軟件卻無(wú)視了NotifyOthersOnDeactivation和ShouldResume的正確用法攒菠,導(dǎo)致我們經(jīng)常接到這樣的用戶反饋:
你們的app在使用xx語(yǔ)音軟件聽了一段話后就不會(huì)繼續(xù)播放了迫皱,但xx音樂(lè)軟件可以繼續(xù)播放啊。
好吧辖众,上面只是吐槽一下卓起。請(qǐng)無(wú)視我吧和敬。
2014.7.14補(bǔ)充,7.19更新:
發(fā)現(xiàn)即使之前已經(jīng)調(diào)用過(guò)AudioSessionInitialize方法戏阅,在某些情況下被打斷之后可能出現(xiàn)AudioSession失效的情況昼弟,需要再次調(diào)用AudioSessionInitialize方法來(lái)重新生成AudioSession。否則調(diào)用AudioSessionSetActive會(huì)返回560557673(其他AudioSession方法也雷同奕筐,所有方法調(diào)用前必須首先初始化AudioSession)舱痘,轉(zhuǎn)換成string后為”!ini”即kAudioSessionNotInitialized,這個(gè)情況在iOS 5.1.x上比較容易發(fā)生救欧,iOS 6.x 和 7.x也偶有發(fā)生(具體的原因還不知曉好像和打斷時(shí)直接調(diào)用AudioOutputUnitStop有關(guān)衰粹,又是個(gè)坑啊)笆怠。
所以每次在調(diào)用AudioSessionSetActive時(shí)應(yīng)該判斷一下錯(cuò)誤碼铝耻,如果是上述的錯(cuò)誤碼需要重新初始化一下AudioSession。
附上OSStatus轉(zhuǎn)成string的方法:
12345678910111213141516171819202122#import NSString*OSStatusToString(OSStatusstatus){size_tlen=sizeof(UInt32);longaddr=(unsignedlong)&status;charcstring[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 endianstrncpy(cstring,(char*)addr,len);cstring[len]=0;return[NSStringstringWithCString:(char*)cstringencoding:NSMacOSRomanStringEncoding];}
打斷處理
正常啟動(dòng)AudioSession之后就可以播放音頻了蹬刷,下面要講的是對(duì)于打斷的處理瓢捉。之前我們說(shuō)到打斷的回調(diào)在iOS 5下需要統(tǒng)一管理,在收到打斷開始和結(jié)束時(shí)需要發(fā)送自定義的通知办成。
使用AudioSession時(shí)打斷回調(diào)應(yīng)該首先獲取kAudioSessionProperty_InterruptionType泡态,然后發(fā)送一個(gè)自定義的通知并帶上對(duì)應(yīng)的參數(shù)。
12345678910111213staticvoidMyAudioSessionInterruptionListener(void*inClientData,UInt32inInterruptionState){AudioSessionInterruptionTypeinterruptionType=kAudioSessionInterruptionType_ShouldNotResume;UInt32interruptionTypeSize=sizeof(interruptionType);AudioSessionGetProperty(kAudioSessionProperty_InterruptionType,&interruptionTypeSize,&interruptionType);NSDictionary*userInfo=@{MyAudioInterruptionStateKey:@(inInterruptionState),MyAudioInterruptionTypeKey:@(interruptionType)};[[NSNotificationCenterdefaultCenter]postNotificationName:MyAudioInterruptionNotificationobject:niluserInfo:userInfo];}
收到通知后的處理方法如下(注意ShouldResume參數(shù)):
12345678910111213141516171819202122232425-(void)interruptionNotificationReceived:(NSNotification*)notification{UInt32interruptionState=[notification.userInfo[MyAudioInterruptionStateKey]unsignedIntValue];AudioSessionInterruptionTypeinterruptionType=[notification.userInfo[MyAudioInterruptionTypeKey]unsignedIntValue];[selfhandleAudioSessionInterruptionWithState:interruptionStatetype:interruptionType];}-(void)handleAudioSessionInterruptionWithState:(UInt32)interruptionStatetype:(AudioSessionInterruptionType)interruptionType{if(interruptionState==kAudioSessionBeginInterruption){//控制UI迂卢,暫停播放}elseif(interruptionState==kAudioSessionEndInterruption){if(interruptionType==kAudioSessionInterruptionType_ShouldResume){OSStatusstatus=AudioSessionSetActive(true);if(status==noErr){//控制UI某弦,繼續(xù)播放}}}}
小結(jié)
關(guān)于AudioSession的話題到此結(jié)束(碼字果然很累。而克。)靶壮。小結(jié)一下:
如果最低版本支持iOS 5,可以使用AudioSession也可以考慮使用AVAudioSession员萍,需要有一個(gè)類統(tǒng)一管理AudioSession的所有回調(diào)腾降,在接到回調(diào)后發(fā)送對(duì)應(yīng)的自定義通知;
如果最低版本支持iOS 6及以上碎绎,請(qǐng)使用AVAudioSession螃壤,不用統(tǒng)一管理,接AVAudioSession的通知即可筋帖;
根據(jù)app的應(yīng)用場(chǎng)景合理選擇Category奸晴;
在deactive時(shí)需要注意app的應(yīng)用場(chǎng)景來(lái)合理的選擇是否使用NotifyOthersOnDeactivation參數(shù);
在處理InterruptEnd事件時(shí)需要注意ShouldResume的值幕随。