iOS 音頻流播(二)

前言

第一篇中介紹了音頻基礎(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í)是否歌曲也要靜音等)
aspg_intro_2x.png

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勾選


E9C88AEE-16EF-46A8-B815-6CB8356BB42D.png

設(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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末忠烛,一起剝皮案震驚了整個(gè)濱河市属提,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌况木,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旬迹,死亡現(xiàn)場(chǎng)離奇詭異火惊,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)奔垦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)屹耐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人椿猎,你說(shuō)我怎么就攤上這事惶岭。” “怎么了犯眠?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵按灶,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我筐咧,道長(zhǎng)鸯旁,這世上最難降的妖魔是什么噪矛? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮铺罢,結(jié)果婚禮上艇挨,老公的妹妹穿的比我還像新娘。我一直安慰自己韭赘,他們只是感情好缩滨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著泉瞻,像睡著了一般脉漏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓦灶,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天鸠删,我揣著相機(jī)與錄音,去河邊找鬼贼陶。 笑死刃泡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碉怔。 我是一名探鬼主播烘贴,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼撮胧!你這毒婦竟也來(lái)了桨踪?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芹啥,失蹤者是張志新(化名)和其女友劉穎锻离,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體墓怀,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汽纠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了傀履。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虱朵。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖钓账,靈堂內(nèi)的尸體忽然破棺而出碴犬,到底是詐尸還是另有隱情,我是刑警寧澤梆暮,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布服协,位于F島的核電站,受9級(jí)特大地震影響啦粹,放射性物質(zhì)發(fā)生泄漏蚯涮。R本人自食惡果不足惜治专,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望遭顶。 院中可真熱鬧张峰,春花似錦、人聲如沸棒旗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)铣揉。三九已至饶深,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逛拱,已是汗流浹背敌厘。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留朽合,地道東北人俱两。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像曹步,于是被迫代替她去往敵國(guó)和親宪彩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容