前言
第一篇中介紹了音頻基礎(chǔ)知識(shí)和編碼的技術(shù)棧颂碧,沒(méi)有看過(guò)的同學(xué)可以花幾分鐘瀏覽一下遇汞,把握一下大體方向软驰。接下來(lái)的幾篇文章,會(huì)依次介紹AudioFileStream请唱、AudioFile弥咪、AudioConverter和AudioUnit,最后會(huì)給大家分析DOUAudioStreamer的源碼十绑。不過(guò)在這之前聚至,還有一個(gè)很重要知識(shí)點(diǎn),那就是AudioSession本橙。
AudioSession簡(jiǎn)介
iOS中關(guān)于AudioSession有兩個(gè)類(lèi)可以使用扳躬。對(duì)于第二個(gè),已經(jīng)被標(biāo)注為deprecated甚亭,這里就不多做介紹了贷币。
- AVFoundation中的AVAudioSession
- AudioToolBox中的AudioSession
AVAudioSession的作用
一言以概之,AVAudioSession是iOS中的音頻小管家亏狰。iOS通過(guò)AVAudioSession協(xié)調(diào)應(yīng)用程序役纹、應(yīng)用程序之間甚至是設(shè)備級(jí)別的音頻行為。我們知道暇唾,手機(jī)所處的環(huán)境其實(shí)非常復(fù)雜促脉,比如說(shuō):
- 你正聽(tīng)著歌呢,一個(gè)電話進(jìn)來(lái)了
- 你正聽(tīng)著歌呢策州,不小心按下了靜音鍵
- 你正聽(tīng)著歌呢嘲叔,有人找你,你取下了耳機(jī)
- etc,...
通過(guò)配置AVAudioSession抽活,可以讓你控制你的應(yīng)用的音頻行為硫戈,比如:
- 確定你的app如何使用音頻(是播放?還是錄音下硕?)
- 為你的app選擇合適的輸入輸出設(shè)備(比如輸入用的麥克風(fēng)丁逝,輸出是耳機(jī)或者手機(jī)功放)
- 協(xié)調(diào)你的app的音頻播放和系統(tǒng)以及其他app行為(例如有電話時(shí)需要打斷,電話結(jié)束時(shí)需要恢復(fù)梭姓,按下靜音按鈕時(shí)是否歌曲也要靜音等)
AVAudioSession的使用
AVAudioSession設(shè)計(jì)為單例模式霜幼。
AVAudioSession *session = [AVAudioSession sharedInstance];
監(jiān)聽(tīng)打斷
當(dāng)你正播著音頻,此時(shí)來(lái)了個(gè)電話誉尖,或者啟動(dòng)了其他音樂(lè)播放程序罪既,而它是獨(dú)占式,不與其他應(yīng)用進(jìn)行混音(參見(jiàn)下面的設(shè)置類(lèi)別),此時(shí)你的AudioSession就會(huì)deactive,同時(shí)進(jìn)入打斷。你可以注冊(cè)打斷監(jiān)聽(tīng)颈将,用以在打斷時(shí)暫停播放,打斷結(jié)束后繼續(xù)播放烘挫。
// AVFoundation 定義的打斷通知
AVF_EXPORT NSString *const AVAudioSessionInterruptionNotification;
// 注冊(cè)打斷通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_audioSessionInterruptionListener:)
name:AVAudioSessionInterruptionNotification object:nil];
// 處理打斷
- (void)_audioSessionInterruptionListener:(NSNotification *)notification
{
// 獲取打斷的描述信息
NSDictionary *interruptionDictionary = [notification userInfo];
// 獲取打斷的狀態(tài)
AVAudioSessionInterruptionType type =
[interruptionDictionary [AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
// 打斷開(kāi)始
if (type == AVAudioSessionInterruptionTypeBegan) {
// 更新UI,暫停播放
} else if (type == AVAudioSessionInterruptionTypeEnded){
// 重新激活A(yù)udioSession柬甥,更新UI饮六,繼續(xù)播放
}
}
監(jiān)聽(tīng)route change
我們知道,手機(jī)是可以外接耳機(jī)苛蒲,或者藍(lán)牙音箱等等外部輸出設(shè)備卤橄,當(dāng)你改變輸出設(shè)備時(shí),比如從手機(jī)功放改到耳機(jī)臂外,此時(shí)iOS會(huì)告訴我們音頻輸出方式發(fā)生了變化虽风。
// AVFoundation 定義的route change通知
AVF_EXPORT NSString *const AVAudioSessionRouteChangeNotification;
// 注冊(cè)route change
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_audioSessionRouteChangeListener:)
name:AVAudioSessionRouteChangeNotification object:nil];
// 處理route change
- (void)_audioSessionRouteChangeListener:(NSNotification*)notification
{
// 取出描述信息
NSDictionary *routeChangeDic = notification.userInfo;
// 取出音頻輸出方式改變的原因
NSInteger routeChangeReason = [[routeChangeDic
valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
// 耳機(jī)插入
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
break;
// 耳機(jī)拔出
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
break;
// called at start - also when other audio wants to play
case AVAudioSessionRouteChangeReasonCategoryChange:
break;
}
}
其中AVAudioSessionRouteChangeReasonOldDeviceUnavailable可以用來(lái)實(shí)現(xiàn)用戶拔掉耳機(jī),停止播放這個(gè)功能寄月。
設(shè)置類(lèi)別
通過(guò)設(shè)置類(lèi)別,可以指明你想要使用音頻服務(wù)的意圖无牵。比如是要錄音還是播放漾肮,還是錄音和播放同時(shí)進(jìn)行等等。
NSError *error = nil;
BOOL status = [session setCategory:AVAudioSessionCategoryPlayback error:&error];
if (!status) {
NSLog(@"%@",error);
// 出錯(cuò)處理
}
以下是常見(jiàn)的幾種類(lèi)別:
// 設(shè)置此類(lèi)別茎毁,iOS允許其他后臺(tái)應(yīng)用繼續(xù)播放音頻克懊,按下靜音鍵和鎖屏狀態(tài)會(huì)停止播放
AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient;
// 基本同AVAudioSessionCategoryAmbient,唯一的不同在于此類(lèi)別是獨(dú)占式七蜘,它會(huì)阻斷其他應(yīng)用播放音頻
AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient;
// 允許后臺(tái)播放
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback;
// 僅提供錄音功能谭溉,無(wú)法進(jìn)行播放
AVF_EXPORT NSString *const AVAudioSessionCategoryRecord;
// 可以同時(shí)進(jìn)行播放和錄制
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord;
注意:如果需要支持后臺(tái)播放(包括鎖屏?xí)r繼續(xù)播放音頻),還必須在info.plist-->Required background modes添加App plays audio or streams audio/video using AirPlay或者在Xcode勾選
設(shè)置類(lèi)別還有另外一個(gè)版本
/* set session category with options */
- (BOOL)setCategory:(NSString *)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError;
下面是幾個(gè)常用的AVAudioSessionCategoryOptions枚舉(新增加的AVAudioSessionCategoryOptionAllowBluetoothA2DP和AVAudioSessionCategoryOptionAllowAirPlay是用來(lái)支持藍(lán)牙A2DP和AirPlay)
// 在AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute下有效橡卤,允許和后臺(tái)應(yīng)用混音
AVAudioSessionCategoryOptionMixWithOthers
//在AVAudioSessionCategoryAmbient, AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute 有效扮念,會(huì)降低其他應(yīng)用的聲音
AVAudioSessionCategoryOptionDuckOthers
//在AVAudioSessionCategoryRecord and AVAudioSessionCategoryPlayAndRecord 下有效,提供對(duì)藍(lán)牙耳機(jī)的支持
AVAudioSessionCategoryOptionAllowBluetooth
//在AVAudioSessionCategoryPlayAndRecord 下有效碧库,使用手機(jī)揚(yáng)聲器
AVAudioSessionCategoryOptionDefaultToSpeaker
激活
設(shè)置完類(lèi)別以后柜与,通過(guò)激活A(yù)udioSession就可以使用了。
- (BOOL)setActive:(BOOL)active error:(NSError * *)outError;
- (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError ;
- 參數(shù)active傳入YES表示激活A(yù)udioSession嵌灰,傳入NO表示解除激活狀態(tài)
- 傳入的error若在返回時(shí)有值弄匕,說(shuō)明發(fā)生了錯(cuò)誤
- 返回值同樣表示執(zhí)行狀態(tài)
該方法的第二個(gè)版本,可以傳入一個(gè)AVAudioSessionSetActiveOptions的枚舉值沽瞭。
typedef NS_OPTIONS(NSUInteger, AVAudioSessionSetActiveOptions)
{
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation = 1
}
當(dāng)你的app deactive自己的AudioSession時(shí)系統(tǒng)會(huì)通知上一個(gè)被打斷播放app打斷結(jié)束迁匠,如果你的app在deactive時(shí)傳入了NotifyOthersOnDeactivation參數(shù),那么其他app在接到打斷結(jié)束回調(diào)時(shí)會(huì)多得到一個(gè)參數(shù)。在打斷處理中我們得到了打斷的描述信息interruptionDictionary城丧,通過(guò)key AVAudioSessionInterruptionOptionKey可以取出一個(gè)AVAudioSessionInterruptionOptions類(lèi)型的值延曙,如果是AVAudioSessionInterruptionOptionShouldResume,那么就可以重新激活A(yù)udioSession芙贫,控制UI繼續(xù)播放搂鲫,如是ShouldNotResume,那就繼續(xù)維持打斷狀態(tài)磺平。
// 完整的處理流程
- (void) _audioSessionInterruptionListener:(NSNotification*)notification {
// 獲取打斷的描述信息
NSDictionary *interruptionDictionary = [notification userInfo];
// 獲取打斷的狀態(tài)
AVAudioSessionInterruptionType type =
[interruptionDictionary [AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
// 能否重新激活A(yù)udioSession
AVAudioSessionInterruptionOptions option = [interruptionDictionary [AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
// 打斷開(kāi)始
if (type == AVAudioSessionInterruptionTypeBegan) {
// 更新UI魂仍,暫停播放
} else if (type == AVAudioSessionInterruptionTypeEnded){
// 如果可以恢復(fù)
if (option == AVAudioSessionInterruptionOptionShouldResume){
// 重新激活A(yù)udioSession,更新UI拣挪,繼續(xù)播放
}
} else {
NSLog(@"Something else happened");
}
}
大概流程是這樣的:
- 一個(gè)音樂(lè)軟件A正在播放擦酌;
- 用戶打開(kāi)你的軟件播放對(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)寓调;
注意:?jiǎn)?dòng)方法調(diào)用后必須要判斷是否啟動(dòng)成功,啟動(dòng)不成功的情況經(jīng)常存在锄码,例如一個(gè)前臺(tái)的app正在播放夺英,你的app正在后臺(tái)想要啟動(dòng)AudioSession那就會(huì)返回失敗。
一點(diǎn)關(guān)于后臺(tái)切換上下曲的小tip
按下HOME鍵后滋捶,程序退到后臺(tái)痛悯,但是聲音仍在播放。但是如果要實(shí)現(xiàn)播放列表的依次播放重窟、循環(huán)播放载萌,即放完一首后自動(dòng)切換到下一首,會(huì)出現(xiàn)一個(gè)問(wèn)題巡扇,當(dāng)app在后臺(tái)放完一首后炒考,就會(huì)停下來(lái)。原因是在后臺(tái)運(yùn)行時(shí)霎迫,一旦聲音停下來(lái)斋枢,程序也隨之suspend。因?yàn)樵谇袚Q文件加載的間隙知给,程序就會(huì)被suspend瓤帚。
??對(duì)這個(gè)問(wèn)題描姚,可以通過(guò)申請(qǐng)后臺(tái)taskID達(dá)到后臺(tái)切換播放文件的功能。即聲明后臺(tái)task id戈次,并通過(guò)beginBackgroundTaskWithExpirationHandler將App設(shè)為后臺(tái)Task轩勘,達(dá)到持續(xù)后臺(tái)運(yùn)行的目的。我們知道一般情況下怯邪,按HOME將程序送到后臺(tái)绊寻,可以有5或10秒時(shí)間可以進(jìn)行一些收尾工作,具體時(shí)間[[UIApplication sharedApplication] backgroundTimeRemaining]返回值悬秉。超時(shí)后app會(huì)被suspend澄步,現(xiàn)在要做的就是用[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL]開(kāi)始后臺(tái)任務(wù),可以將后臺(tái)運(yùn)行超時(shí)時(shí)間長(zhǎng)時(shí)間的延長(zhǎng)和泌,具體延長(zhǎng)多少時(shí)間還是見(jiàn)返回值村缸,總之對(duì)于放段時(shí)間音樂(lè)應(yīng)該夠了。另一個(gè)問(wèn)題是每個(gè)開(kāi)始的后臺(tái)任務(wù)武氓,都必須用endBackgroundTask來(lái)結(jié)束梯皿。 因此,在每次開(kāi)始播放后啟動(dòng)新的后臺(tái)任務(wù)县恕,同時(shí)結(jié)束上一個(gè)后臺(tái)任務(wù)东羹。
// 聲明上一個(gè)taskID
@property (nonatomic) UIBackgroundTaskIdentifier oldTaskId;
// 申請(qǐng)一點(diǎn)后臺(tái)執(zhí)行時(shí)間
UIBackgroundTaskIdentifier newTaskId = UIBackgroundTaskInvalid;
// 在這里進(jìn)行播放下一曲操作
newTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
if (newTaskId != UIBackgroundTaskInvalid && oldTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask: oldTaskId];
}
oldTaskId = newTaskId;
下篇將會(huì)介紹AudioFileStream。