音頻輸出作為硬件資源,對於iOS系統來說是唯一的,那么要如何協調和各個App之間對這個稀缺的硬件持有關系呢?
iOS給出的解決方案是"AVAudioSession" ,通過它可以實現對App當前上下文音頻資源的控制,比如
插拔耳機、接電話、是否和其他音頻數據混音等。當你遇到:
- 是進行錄音還是播放?
- 當系統靜音鍵按下時該如何表現?
- 是從揚聲器還是從聽筒里面播放聲音?
- 插拔耳機后如何表現?
- 來電話/鬧鍾響了后如何表現?
- 其他音頻App啟動后如何表現?
- ...
這些場景的時候,就可以考慮一下“AVAudioSession”了。
iOS AudioSession APIs處理各個App之間音頻行為,交互(圖來自apple文檔)

通過AudioSession API,可以控制App的audio相關的行為
- 為app選擇輸入輸出的路由 (通過揚聲器還是聽筒播放)
- 協調音頻播放的app之間的關聯,以及系統的聲音處理
- 處理被其他apps打斷
- 創建一個錄音或者播放音樂的
在很久以前(其實也是不是太久--iOS7以前)還有個AudioSession的存在,其功能與AVAudioSession類似,但是在iOS7 以后就已經被標記為
“Not Applicable”,所以如果Google到了說AudioSession的內容而不是用的AVAudioSession,那么就可以直接PASS了,當然如果要兼容iOS6
就另當別論了,不過現在QQ/微信都是要求iOS7的情況下,是否需要兼容iOS6就看老板們的意思吧。
Session默認行為
- 可以進行播放,但是不能進行錄制。
- 當用戶將手機上的靜音撥片撥到“靜音”狀態時,此時如果正在播放音頻,那么播放內容會被靜音。
- 當用戶按了手機的鎖屏鍵或者手機自動鎖屏了,此時如果正在播放音頻,那么播放會靜音並被暫停。
- 如果你的App在開始播放的時候,此時QQ音樂等其他App正在播放,那么其他播放器會被靜音並暫停。
默認的行為相當於設置了Category為“AVAudioSessionCategorySoloAmbient”
可以寫個demo 輸出驗證一下 上面的默認Session行為。
AVAudioSession
AVAudioSession以一個單例實體的形式存在,通過類方法:
+ (AVAudioSession *)sharedInstance;
獲得單例。
雖然系統會在App啟動的時候,激活這個唯一的AVAudioSession,但是最好還是在自己ViewController的viewDidLoad
里面再次進行激活:
- (BOOL)setActive:(BOOL)active
error:(NSError * _Nullable *)outError;
通過設置active
為"YES"激活Session,設置為“�NO”解除Session的激活狀態。BOOL返回值表示是否成功,如果失敗的話可以通過NSError的error.localizedDescription
查看出錯原因。
因為AVAudioSession會影響其他App的表現,當自己App的Session被激活,其他App的就會被解除激活,如何要讓自己的Session解除激活后恢復其他App Session的激活狀態呢?
此時可以使用:
- (BOOL)setActive:(BOOL)active
withOptions:(AVAudioSessionSetActiveOptions)options
error:(NSError * _Nullable *)outError;這里的options傳入
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
即可。當然,也可以通過
otherAudioPlaying
變量來提前判斷當前是否有其他App在播放音頻。
可以通過:
@property(readonly) NSString *category;
屬性,獲取當前的Category,比如上面的播放其,默認是
NSLog(@"Current Category:%@", [AVAudioSession sharedInstance].category);
輸出:
Current Category:AVAudioSessionCategorySoloAmbien
七大Category
AVAudioSession主要能控制App的哪些表現以及如何控制的呢?首先AVAudioSession將使用音頻的場景分成七大類,通過設置Session為不同的類別,可以控制:
會話類型 | 說明 | 是否要求輸入 | 是否要求輸出 | 是否遵從靜音鍵 |
---|---|---|---|---|
AVAudioSessionCategoryAmbient | 混音播放,可以與其他音頻應用同時播放 | 否 | 是 | 是 |
AVAudioSessionCategorySoloAmbient | 獨占播放 | 否 | 是 | 是 |
AVAudioSessionCategoryPlayback | 后台播放,獨占 | 否 | 是 | 否 |
AVAudioSessionCategoryRecord | 錄音模式 | 是 | 否 | 否 |
AVAudioSessionCategoryPlayAndRecord | 播放和錄音,此時可以錄音也可以播放 | 是 | 是 | 否 |
AVAudioSessionCategoryAudioProcessing | 硬件解碼音頻,此時不能播放和錄制 | 否 | 否 | 否 |
AVAudioSessionCategoryMultiRoute | 多種輸入輸出,例如可以耳機、USB設備同時播放 | 是 | 是 | 否 |
可以看到,其實默認的就是“AVAudioSessionCategorySoloAmbient”類別。從表中我們可以總結如下:
- AVAudioSessionCategoryAmbient : 只用於播放音樂時,並且可以和QQ音樂同時播放,比如玩游戲的時候還想聽QQ音樂的歌,那么把游戲播放背景音就設置成這種類別。同時,當用戶鎖屏或者靜音時也會隨着靜音,這種類別基本使用所有App的背景場景。
- AVAudioSessionCategorySoloAmbient: 也是只用於播放,但是和"AVAudioSessionCategoryAmbient"不同的是,用了它就別想聽QQ音樂了,比如不希望QQ音樂干擾的App,類似節奏大師。同樣當用戶鎖屏或者靜音時也會隨着靜音,鎖屏了就玩不了節奏大師了。
- AVAudioSessionCategoryPlayback: 如果鎖屏了還想聽聲音怎么辦?用這個類別,比如App本身就是播放器,同時當App播放時,其他類似QQ音樂就不能播放了。所以這種類別一般用於播放器類App
- AVAudioSessionCategoryRecord: 有了播放器,肯定要錄音機,比如微信語音的錄制,就要用到這個類別,既然要安靜的錄音,肯定不希望有QQ音樂了,所以其他播放聲音會中斷。想想微信語音的場景,就知道什么時候用他了。
- AVAudioSessionCategoryPlayAndRecord: 如果既想播放又想錄制該用什么模式呢?比如VoIP,打電話這種場景,PlayAndRecord就是專門為這樣的場景設計的 。
- AVAudioSessionCategoryMultiRoute: 想象一個DJ用的App,手機連着HDMI到揚聲器播放當前的音樂,然后耳機里面播放下一曲,這種常人不理解的場景,這個類別可以支持多個設備輸入輸出。
- AVAudioSessionCategoryAudioProcessing: 主要用於音頻格式處理,一般可以配合AudioUnit進行使用
了解了這七大類別,我們就可以根據自己的需要進行對應類別的設置了:
- (BOOL)setCategory:(NSString *)category error:(NSError **)outError;
傳入對應的列表枚舉即可。如果返回"NO"可以通過NSError的error.localizedDescription
查看原因。
可以通過:
@property(readonly) NSArray<NSString *> *availableCategories;
屬性,查看當前設備支持哪些類別,然后再進行設置,從而保證傳入參數的合法,減少錯誤的可能。
比如修改上面的Demo例子:
NSLog(@"Current Category:%@", [AVAudioSession sharedInstance].category); NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; if (nil != error) { NSLog(@"set Option error %@", error.localizedDescription); } NSLog(@"Current Category:%@", [AVAudioSession sharedInstance].category);
此時在播放音樂的時候,再去按下靜音鍵,會發現,音樂還在繼續播放,不會被靜音。
類別的選項
上面介紹的這個七大類別,可以認為是設定了七種主場景,而這七類肯定是不能滿足開發者所有的需求的。CoreAudio提供的方法是,首先定下七種的一種基調,然后在進行微調。CoreAudio為每種Category都提供了些許選項來進行微調。
在設置完類別后,可以通過
@property(readonly) AVAudioSessionCategoryOptions categoryOptions;
屬性,查看當前類別設置了哪些選項,注意這里的返回值是AVAudioSessionCategoryOptions,實際是多個options的“|”運算。默認情況下是0。
選項 | 適用類別 | 作用 |
---|---|---|
AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute | 是否可以和其他后台App進行混音 |
AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryAmbient, AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute | 是否壓低其他App聲音 |
AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryRecord and AVAudioSessionCategoryPlayAndRecord | 是否支持藍牙耳機 |
AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryPlayAndRecord | 是否默認用免提聲音 |
目前主要的選項有這幾種,都有對應的使用場景,除此之外,在iOS9還提供了AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
最新的iOS10又新加了兩個AVAudioSessionCategoryOptionAllowBluetoothA2DP
、AVAudioSessionCategoryOptionAllowAirPlay
用來支持藍牙A2DP耳機和AirPlay。
來看每個選項的基本作用:
- AVAudioSessionCategoryOptionMixWithOthers : 如果確實用的AVAudioSessionCategoryPlayback實現的一個背景音,但是呢,又想和QQ音樂並存,那么可以在AVAudioSessionCategoryPlayback類別下在設置這個選項,就可以實現共存了。
- AVAudioSessionCategoryOptionDuckOthers:在實時通話的場景,比如QQ音樂,當進行視頻通話的時候,會發現QQ音樂自動聲音降低了,此時就是通過設置這個選項來對其他音樂App進行了壓制。
- AVAudioSessionCategoryOptionAllowBluetooth:如果要支持藍牙耳機電話,則需要設置這個選項
- AVAudioSessionCategoryOptionDefaultToSpeaker: 如果在VoIP模式下,希望默認打開免提功能,需要設置這個選項
通過接口:
- (BOOL)setCategory:(NSString *)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError
來對當前的類別進行選項的設置。
比如Demo中:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]; if (nil != error) { NSLog(@"set Option error %@", error.localizedDescription); } options = [[AVAudioSession sharedInstance] categoryOptions]; NSLog(@"Category[%@] has %lu options", [AVAudioSession sharedInstance].category, options);
此時,先打開QQ音樂播放器,然后再開始進行播放,會發現,QQ和我們的播放器都在播放,並且進行了自動混音。
不過這個過程,感覺CoreAudio缺少一個setOption
的接口,既然已經是當前處於的Category,干嘛還要再設置選項的時候再指定Category呢??疑惑。。。
七大模式
剛講完七大類別,現在再來七大模式。通過上面的七大類別,我們基本覆蓋了常用的主場景,在每個主場景中可以通過Option進行微調。為此CoreAudio提供了七大比較常見微調后的子場景。叫做各個類別的模式。
模式 | 適用的類別 | 場景 |
---|---|---|
AVAudioSessionModeDefault | 所有類別 | 默認的模式 |
AVAudioSessionModeVoiceChat | AVAudioSessionCategoryPlayAndRecord | VoIP |
AVAudioSessionModeGameChat | AVAudioSessionCategoryPlayAndRecord | 游戲錄制,由GKVoiceChat自動設置,無需手動調用 |
AVAudioSessionModeVideoRecording | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord | 錄制視頻時 |
AVAudioSessionModeMoviePlayback | AVAudioSessionCategoryPlayback | 視頻播放 |
AVAudioSessionModeMeasurement | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayback | 最小系統 |
AVAudioSessionModeVideoChat | AVAudioSessionCategoryPlayAndRecord | 視頻通話 |
每個模式有其適用的類別,所以,並不是有“七七 四十九”種組合。如果當前處於的類別下沒有這個模式,那么是設置不成功的。設置完Category后可以通過:
@property(readonly) NSArray<NSString *> *availableModes;
屬性,查看其支持哪些屬性,做合法性校驗。
來看具體應用:
- AVAudioSessionModeDefault: 每種類別默認的就是這個模式,所有要想還原的話,就設置成這個模式。
- AVAudioSessionModeVoiceChat:主要用於VoIP場景,此時系統會選擇最佳的輸入設備,比如插上耳機就使用耳機上的麥克風進行采集。此時有個副作用,他會設置類別的選項為"AVAudioSessionCategoryOptionAllowBluetooth"從而支持藍牙耳機。
- AVAudioSessionModeVideoChat : 主要用於視頻通話,比如QQ視頻、FaceTime。時系統也會選擇最佳的輸入設備,比如插上耳機就使用耳機上的麥克風進行采集並且會設置類別的選項為"AVAudioSessionCategoryOptionAllowBluetooth" 和 "AVAudioSessionCategoryOptionDefaultToSpeaker"。
- AVAudioSessionModeGameChat : 適用於游戲App的采集和播放,比如“GKVoiceChat”對象,一般不需要手動設置
另外幾種和音頻APP關系不大,一般我們只需要關注VoIP或者視頻通話即可。
通過調用:
- (BOOL)setMode:(NSString *)mode error:(NSError **)outError
可以在設置Category之后再設置模式。
當然,這些模式只是CoreAduio總結的,不一定完全滿足要求,對於具體的模式,在iOS10中還是可以微調的。通過接口:
- (BOOL)setCategory:(NSString *)category mode:(NSString *)mode options:(AVAudioSessionCategoryOptions)options error:(NSError **)outError
但是在iOS9及以下就只能在Category上調了,其實本質是一樣的,可以認為是個API糖,接口封裝。
有關聲音的一些通知
系統中斷響應通知
上面說的這些Category啊、Option啊以及Mode都是對自己作為播放主體時的表現,但是假設,現在正在播放着,突然來電話了、鬧鍾響了或者你在后台放歌但是用戶啟動其他App用上面的方法影響的時候,我們的App該如何表現呢?最常用的場景當然是先暫停,待恢復的時候再繼續。那我們的App要如何感知到這個終端以及何時恢復呢?
AVAudioSession提供了多種Notifications來進行此類狀況的通知。其中將來電話、鬧鈴響等都歸結為一般性的中斷,用
AVAudioSessionInterruptionNotification
來通知。其回調回來的userInfo主要包含兩個鍵:
- AVAudioSessionInterruptionTypeKey: 取值為
AVAudioSessionInterruptionTypeBegan
表示中斷開始,我們應該暫停播放和采集,取值為AVAudioSessionInterruptionTypeEnded
表示中斷結束,我們可以繼續播放和采集。 - AVAudioSessionInterruptionOptionKey: 當前只有一種值
AVAudioSessionInterruptionOptionShouldResume
表示此時也應該恢復繼續播放和采集。
而將其他App占據AudioSession的時候用AVAudioSessionSilenceSecondaryAudioHintNotification
來進行通知。其回調回來的userInfo鍵為:
AVAudioSessionSilenceSecondaryAudioHintTypeKey
可能包含的值:
- AVAudioSessionSilenceSecondaryAudioHintTypeBegin: 表示其他App開始占據Session
- AVAudioSessionSilenceSecondaryAudioHintTypeEnd: 表示其他App開始釋放Session
外設改變通知
除了其他App和系統服務,會對我們的App產生影響以外,用戶的手也會對我們產生影響。默認情況下,AudioSession會在App啟動時選擇一個最優的輸出方案,比如插入耳機的時候,就用耳機。但是這個過程中,用戶可能拔出耳機,我們App要如何感知這樣的情況呢?
同樣AVAudioSession也是通過Notifications來進行此類狀況的通知。
假設有這樣的App:
最開始在錄音時,用戶插入和拔出耳機我們都停止錄音,這里通過Notification來通知有新設備了,或者設備被退出了,然后我們控制停止錄音。或者在播放時,當耳機被拔出出時,Notification給了通知,我們先暫停音樂播放,待耳機插回時,在繼續播放。
在NSNotificationCenter中對AVAudioSessionRouteChangeNotification進行注冊。在其userInfo中有鍵:
- AVAudioSessionRouteChangeReasonKey : 表示改變的原因
枚舉值 | 意義 |
---|---|
AVAudioSessionRouteChangeReasonUnknown | 未知原因 |
AVAudioSessionRouteChangeReasonNewDeviceAvailable | 有新設備可用 |
AVAudioSessionRouteChangeReasonOldDeviceUnavailable | 老設備不可用 |
AVAudioSessionRouteChangeReasonCategoryChange | 類別改變了 |
AVAudioSessionRouteChangeReasonOverride | App重置了輸出設置 |
AVAudioSessionRouteChangeReasonWakeFromSleep | 從睡眠狀態呼醒 |
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory | 當前Category下沒有合適的設備 |
AVAudioSessionRouteChangeReasonRouteConfigurationChange | Rotuer的配置改變了 |
- AVAudioSessionSilenceSecondaryAudioHintTypeKey: 和上面的中斷意義意義。
其他的一些通知Notification
AVAudioSessionMediaServicesWereLostNotification
當媒體服務終止時通知主線程
將該通知作為提示來在重新啟動服務之前做相關處理
此notification不包含userInfo
AVAudioSessionMediaServicesWereResetNotification
媒體服務重新啟動時通知主線程
不包含userInfo
AVAudioSessionSilenceSecondaryAudioHintNotification
當其他app的首要音頻開始播放或者停止時通知主線程
userInfo中AVAudioSessionSilenceSecondaryAudioHintTypeKey的值為AVAudioSessionSilenceSecondaryAudioHintType類型
AVAudioSession 一些屬性介紹 詳細的需要看API 介紹
/* get session category. Examples: AVAudioSessionCategoryRecord, AVAudioSessionCategoryPlayAndRecord, etc. */ @property (readonly) NSString *category; /* get the current set of AVAudioSessionCategoryOptions */ @property (readonly) AVAudioSessionCategoryOptions categoryOptions NS_AVAILABLE_IOS(6_0); //描述當前音頻輸入輸出的route @property(readonly) AVAudioSessionRouteDescription *currentRoute //表明當前設備是否支持音頻輸入 @property(readonly, getter=isInputAvailable) BOOL inputAvailable //其他app是否正在播放音頻 //在iOS 8.0以后要使用secondaryAudioShouldBeSilencedHint來代替這個屬性 @property(readonly, getter=isOtherAudioPlaying) BOOL otherAudioPlaying typedef NS_OPTIONS(NSUInteger, AVAudioSessionSetActiveOptions) { AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation = 1 } NS_AVAILABLE_IOS(6_0); 注釋的意思是:“通知中斷程序中斷已經結束,可以恢復播放 表明當你的Audio Session被取消時,其他被你的Audio Session所打斷的Audio Session可以恢復至其激活狀態 //暫時更改當前的音頻路線 //第一個參數: enum { AVAudioSessionPortOverrideNone = 0, // 不覆寫 AVAudioSessionPortOverrideSpeaker = 'spkr' // 將輸入輸出變換到內置揚聲器和話筒 }; typedef NSUInteger AVAudioSessionPortOverride; //使用AVAudioSessionPortOverrideSpeaker和AVAudioSessionCategoryPlayAndRecord的話會無視其他設置進行話筒錄音和揚聲器播放,不過會在重新調用這個方法並且參數為AVAudioSessionPortOverrideNone時失效 - (BOOL)overrideOutputAudioPort:(AVAudioSessionPortOverride)portOverride error:(NSError **)outError
總結:
AVAudioSession構建了一個音頻使用生命周期的上下文。當前狀態是否可以錄音、對其他App有怎樣的影響、是否響應系統的靜音鍵、如何感知來電話了等都可以通過它來實現。尤為重要的是AVAudioSession不僅可以和AVFoundation中的AVAudioPlyaer/AVAudioRecorder配合,其他錄音/播放工具比如AudioUnit、AudioQueueService也都需要他進行錄音、靜音等上下文配合。
參考文檔:
揚聲器和聽筒的切換
//方法一 切換這個方法會自動切換設備 耳機的插拔會自動切換 if (_isSpeakerMode) {//揚聲器模式 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil]; }else{ //在PlayAndRecord這個category下,聽筒會成為默認的輸出設備 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:0 error:nil]; }
//方法二 切換這個方法會自動切換設備 耳機需要自己判斷 if ([self hasHeadset]) { [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil]; }else{ [[AVAudioSession sharedInstance] overrideOutputAudioPort:_isSpeakerMode?AVAudioSessionPortOverrideSpeaker: AVAudioSessionPortOverrideNone error:nil]; }