本篇為《iOS音頻播放》系列的第二篇。
在實施前一篇中所述的7個步驟之前還必須面對一個麻煩的問題,AudioSession。
AudioSession這個玩意的主要功能包括以下幾點(圖片來自官方文檔):
AudioSession
AudioSession相關的類有兩個:
AudioToolBox
中的AudioSession
AVFoundation
中的AVAudioSession
其中AudioSession在SDK 7中已經被標注為depracated,而AVAudioSession這個類雖然iOS 3開始就已經存在了,但其中很多方法和變量都是在iOS 6以後甚至是iOS 7才有的。所以各位可以依照以下標准選擇:
AudioSession
,也可以使用AVAudioSession
;AVAudioSession
下面以AudioSession
類為例來講述AudioSession相關功能的使用(很不幸我需要支持iOS 5。。T-T,使用AVAudioSession
的同學可以在其頭文件中尋找對應的方法使用即可,需要注意的點我會加以說明).
注意:在使用AVAudioPlayer/AVPlayer時可以不用關心AudioSession的相關問題,Apple已經把AudioSession的處理過程封裝了,但音樂打斷後的響應還是要做的(比如打斷後音樂暫停了UI狀態也要變化,這個應該通過KVO就可以搞定了吧。。我沒試過瞎猜的>_<)。
使用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事件:
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, ¤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
時調用下面的接口
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_NotifyOthersOnDeactivation
(AVAudioSession
給options參數傳入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
)。當你的app deactive自己的AudioSession時系統會通知上一個被打斷播放app打斷結束(就是上面說到的打斷回調),如果你的app在deactive時傳入了NotifyOthersOnDeactivation參數,那麼其他app在接到打斷結束回調時會多得到一個參數kAudioSessionInterruptionType_ShouldResume
否則就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume
),根據參數的值可以決定是否繼續播放。
大概流程是這樣的:
官方文檔中有一張很形象的圖來闡述這個現象:
然而現在某些語音通訊軟件和某些音樂軟件卻無視了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的方法:
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的話題到此結束(碼字果然很累。。)。小結一下:
AudioSession
也可以考慮使用AVAudioSession
,需要有一個類統一管理AudioSession的所有回調,在接到回調後發送對應的自定義通知;AVAudioSession
,不用統一管理,接AVAudioSession的通知即可;Category
;NotifyOthersOnDeactivation
參數;ShouldResume
的值。
這裡有我自己寫的AudioSession
的封裝,如果各位需要支持iOS 5的話可以使用一下。
下一篇將講述如何使用AudioFileStreamer
分離音頻幀,以及如何使用AudioQueue
進行播放。
下一篇將講述如何使用AudioFileStreamer
提取音頻文件格式信息和分離音頻幀。
AudioSession