前言
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類首先需要調用初始化方法:
extern osstatus audiosessioninitialize(cfrunloopref inrunloop,
cfstringref inrunloopmode,
audiosessioninterruptionlistener ininterruptionlistener,
void *inclientdata);
前兩個參數一般填null表示audiosession運作在主線程上(但并不代表音頻的相關處理運作在主線程上,隻是audiosession),第三個參數需要傳入一個一個audiosessioninterruptionlistener類型的方法,作為audiosession被打斷時的回調,第四個參數則是代表打斷回調時需要附帶的對象(即回到方法中的inclientdata,如下所示,可以了解為uiview animation中的context)。
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事件:
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就可以用來做“拔掉耳機就把歌曲暫停”)。
//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
};
//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、判斷是否插了耳機:
+ (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)
if (route)
cfrelease(route);
return hasheadset;
+ (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時調用下面的接口
extern osstatus audiosessionsetproperty(audiosessionpropertyid inid,
uint32 indatasize,
const void *indata);
如果我需要的功能是播放,執行如下代碼
uint32 sessioncategory = kaudiosessioncategory_mediaplayback;
audiosessionsetproperty (kaudiosessionproperty_audiocategory,
sizeof(sessioncategory),
&sessioncategory);
使用avaudiosession時調用下面的接口
/* 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。
//audiosession的audiosessioncategory枚舉
kaudiosessioncategory_ambientsound = 'ambi',
kaudiosessioncategory_soloambientsound = 'solo',
kaudiosessioncategory_mediaplayback = 'medi',
kaudiosessioncategory_recordaudio = 'reca',
kaudiosessioncategory_playandrecord = 'plar',
kaudiosessioncategory_audioprocessing = 'proc'
//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了,啟動方法:
//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),根據參數的值可以決定是否繼續播放。
大概流程是這樣的:
1. 一個音樂軟體a正在播放;
2. 使用者打開你的軟體播放對話語音,audiosession active;
3. 音樂軟體a音樂被打斷并收到interruptbegin事件;
4. 對話語音播放結束,audiosession deactive并且傳入notifyothersondeactivation參數;
5. 音樂軟體a收到interruptend事件,檢視resume參數,如果是shouldresume控制音頻繼續播放,如果是shouldnotresume就維持打斷狀态;
然而現在某些語音通訊軟體和某些音樂軟體卻無視了notifyothersondeactivation和shouldresume的正确用法,導緻我們經常接到這樣的使用者回報:“你們的app在使用xx語音軟體聽了一段話後就不會繼續播放了,但xx音樂軟體可以繼續播放啊。”
好吧,上面隻是吐槽一下。請無視我吧。
補充:
發現即使之前已經調用過audiosessioninitialize方法,在某些情況下被打斷之後可能出現audiosession失效的情況,需要再次調用audiosessioninitialize方法來重新生成audiosession。否則調用audiosessionsetactive會傳回560557673(其他audiosession方法也雷同,所有方法調用前必須首先初始化audiosession),轉換成string後為”!ini”即kaudiosessionnotinitialized,這個情況在ios
5.1.x上尤其頻繁,ios 7.x也偶有發生具體的原因還不知曉。
是以每次在調用audiosessionsetactive時應該判斷一下錯誤碼,如果是上述的錯誤碼需要重新初始化一下audiosession。
附上osstatus轉成string的方法:
#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,然後發送一個自定義的通知并帶上對應的參數。
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參數):
- (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的值。