Overview
Apple通過audio sessions管理app, app與其他app, app與外部音頻硬件間的行為.使用audio session可以向系統(tǒng)傳達你將如何使用音頻.audio session充當著app與系統(tǒng)間的中介.這樣我們無需了解硬件相關(guān)卻可以操控硬件行為.
- 配置audio session類別與模式去告訴系統(tǒng)在app中你想怎么使用音頻
- 激活audio session使配置的類別與模式可以工作
- 添加通知,響應重要的audio session通知,例如音頻中斷與硬件線路改變
- 配置音頻采樣率,聲道數(shù)等信息
1.配置Audio Session
1.1. Audio Session管理Audio
audio session是應用程序與系統(tǒng)間的中介,用于配置音頻行為,APP啟動時,會自動獲得一個audio session的單例對象,配置并且激活它以讓音頻按照期望開始工作.
1.2. Categories代表Audio作用
audio session category代表音頻的主要行為.通過設(shè)置類別, 可以指明app是否使用的當前的輸入或輸出音頻設(shè)備,以及當別的app中正在播放音頻進入我們app時他們的音頻是強制停止還是與我們的音頻一起播放等等.
AVFoundation中定義了很多audio session categories, 你可以根據(jù)需要自定義音頻行為,很多類別支持播放,錄制,錄制與播放同時進行,當系統(tǒng)了解了你定義的音頻規(guī)則,它將提供給你合適的路徑去訪問硬件資源.系統(tǒng)也將確保別的app中的音頻以適合你應用的方式運行.
一些categories可以根據(jù)Mode進一步定制,該模式用于專門指定類別的行為,例如當使用視頻錄制模式時,系統(tǒng)可能會選擇一個不同于默認內(nèi)置麥克風的麥克風,系統(tǒng)還可以針對錄制調(diào)整麥克風的信號強度.
1.3. 中斷處理
如果audio意外中斷,系統(tǒng)會將aduio session置為停用狀態(tài),音頻也會因此立即停止.當一個別的app的audio session被激活并且它的類別未設(shè)置與系統(tǒng)類別或你應用程序類別混合時,中斷就會發(fā)生.你的應用程序在收到中斷通知后應該保存當時的狀態(tài),以及更新用戶界面等相關(guān)操作.通過注冊AVAudioSessionInterruptionNotification可以觀察中斷的開始與結(jié)束點.
1.4. 音頻線路改變
當用戶做出連接,斷開音頻輸入,輸出設(shè)備時,(如:插拔耳機)音頻線路發(fā)生變化,通過注冊AVAudioSessionRouteChangeNotification
可以在音頻線路發(fā)生變化時做出相應處理.
1.5. Audio Sessions控制設(shè)備配置
App不能直接控制設(shè)備的硬件,但是audio session提供了一些接口去獲取或設(shè)置一些高級的音頻設(shè)置,如采樣率,聲道數(shù)等等.
1.6. Audio Sessions保護用戶隱私
App如果想使用音頻錄制功能必須請求用戶授權(quán),否則無法使用.
2. 激活Audio Session
在設(shè)置了audio session的category, options, mode后,我們可以激活它以啟動音頻.
2.1. 系統(tǒng)如何解決音頻競爭
隨著app的啟動,內(nèi)置的一些服務(短信,音樂,瀏覽器,電話等)也將在后臺運行.前面的這些內(nèi)置服務都可能產(chǎn)生音頻,如有電話打來,有短信提示等等...
2.2. 激活,停用Audio Session
雖然AVFoundation中播放與錄制可以自動激活你的audio session, 但你可以手動激活并且測試是否激活成功.
系統(tǒng)會停用你的audio session當有電話打進來,鬧鐘響了,或是日歷提醒等消息介入.當處理完這些介入的消息后,系統(tǒng)允許我們手動重新激活audio sesseion.
let session = AVAudioSession.sharedInstance()
do {
// 1) Configure your audio session category, options, and mode
// 2) Activate your audio session to enable your custom configuration
try session.setActive(true)
} catch let error as NSError {
print("Unable to activate audio session: \(error.localizedDescription)")
}
如果我們使用AVFoundation對象(AVPlayer, AVAudioRecorder等),系統(tǒng)負責在中斷結(jié)束時重新激活audio session.然而,如果你注冊了通知去重新激活audio session,你可以驗證是否激活成功并且更新用戶界面.
- 確保在后臺運行的VoIP應用程序的音頻會話僅在應用程序處理呼叫時才處于激活狀態(tài)匀借。在后臺颜阐,若未收到呼叫,VoIP應用程序的音頻會話不應該是激活的。
- 確保使用錄制類別的應用程序的音頻會話僅在錄制時處于激活狀態(tài)吓肋。在錄制開始和停止之前凳怨,請確保您的會話處于未激活狀態(tài),以允許播放其他聲音,例如系統(tǒng)聲音肤舞。
- 如果應用程序支持后臺音頻播放或錄制紫新,但在應用程序未主動使用音頻(或準備使用音頻)時,在進入后臺時停用其音頻會話李剖。這樣做允許系統(tǒng)釋放音頻資源芒率,以便其他進程可以使用它們。
2.3. 檢查別的Audio是否正在播放
當你的app被激活前,當前設(shè)備可能正在播放別的聲音,如果你的app是一個游戲的app,知道別的聲音來源顯得十分重要,因為許多游戲允許同時播放別的音樂以增強用戶體驗.
在app進入前臺前,我們可以通過applicationDidBecomeActive:
代理方法在其中使用secondaryAudioShouldBeSilencedHint
屬性來確定音頻是否正在播放.當別的app正在播放的audio session為不可混音配置時,該值為true. app可以使用此屬性消除次要音頻.
func setupNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleSecondaryAudio),
name: .AVAudioSessionSilenceSecondaryAudioHint,
object: AVAudioSession.sharedInstance())
}
func handleSecondaryAudio(notification: Notification) {
// Determine hint type
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt,
let type = AVAudioSessionSilenceSecondaryAudioHintType(rawValue: typeValue) else {
return
}
if type == .begin {
// Other app audio started playing - mute secondary audio
} else {
// Other app audio stopped playing - restart secondary audio
}
}
3. 響應中斷
在app中斷后可以通過代碼做出響應.音頻中斷將會導致audio session停用,同時應用程序中音頻立即終止.當一個來自其他app的競爭的audio session被激活且這個audio session類別不支持與你的app進行混音時,中斷發(fā)生.注冊通知后我們可以在得知音頻中斷后做出相應處理.
App會因為中斷被暫停,當用戶接到電話時,鬧鐘,或其他系統(tǒng)事件被觸發(fā)時,當中斷結(jié)束后,App會繼續(xù)運行,但是需要我們手動重新激活audio session.
3.1. 中斷的生命周期
下圖簡單展示了當收到facetime后app的audio session與系統(tǒng)的audio session間激活與未激活狀態(tài)變化.
3.2. 中斷處理方法
通過注冊監(jiān)聽中斷的通知可以在中斷來的時候進行處理.處理中斷取決于你當前正在執(zhí)行的操作:播放,錄制,音頻格式轉(zhuǎn)換,讀取音頻數(shù)據(jù)包等等.一般而言,我們應盡量避免中斷并且做到中斷后盡快恢復.
中斷前
- 保存狀態(tài)與上下文
- 更新用戶界面
中斷后
- 恢復狀態(tài)與上下文
- 更新用戶界面
- 重新激活audio session.
Audio technology | How interruptions work |
---|---|
AVFoundation framework | 系統(tǒng)在中斷時會自動暫停錄制與播放,當中斷結(jié)束后重新激活audio session,恢復錄制與播放 |
Audio Queue Services, I/O audio unit | 系統(tǒng)會發(fā)出中斷通知,開發(fā)者可以保存播放與錄制狀態(tài)并且在中斷結(jié)束后重新激活audio session |
System Sound Services | 使用系統(tǒng)聲音服務在中斷來臨時保持靜音,如果中斷結(jié)束,聲音自動播放. |
3.3. 處理Siri
當處理Siri時,與其他中斷不同,我們在中斷期間需要對Siri進行監(jiān)聽,如在中斷期間,用戶要求Siri去暫停開發(fā)者app中的音頻播放,當app收到中斷結(jié)束的通知時,不應該自動恢復播放.同時,用戶界面需要跟Siri要求的保持一致.
3.4. 監(jiān)聽中斷
注冊AVAudioSessionInterruptionNotification
通知可以監(jiān)聽中斷.
func registerForNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
object: AVAudioSession.sharedInstance())
}
func handleInterruption(_ notification: Notification) {
// Handle interruption
}
func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions (save state, update user interface)
}
else if type == .ended {
guard let optionsValue =
userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
return
}
let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
}
}
}
注意: 無法確保在開始中斷后一定有一個結(jié)束中斷,所以,如果沒有結(jié)束中斷,我們在app重新播放音頻時需要總是檢查aduio session是否被激活.
3.5. 響應媒體服務器重置操作
media server通過一個共享服務器進程提供了音頻和其他多媒體功能.盡管很少見,但是如果在你的app正在運行時收到一條重置命令,可以通過注冊AVAudioSessionMediaServicesWereResetNotification
通知監(jiān)聽media server是否重置.收到通知后需要做如下操作.
- 銷毀音頻對象并且創(chuàng)建新的音頻對象(如:players,recorders,converters,audio queues)
- 重置所有audio狀態(tài),包括AVAudioSession全部屬性
- 在合適時機重新激活AVAudioSession對象.
注冊AVAudioSessionMediaServicesWereLostNotification
可以在media server不可用時收到通知.
如果開發(fā)者的應用程序中需要重置功能,如設(shè)置中有重置選項,可以使用這個方法輕松重置.
4. 線路改變
audio hardware route指定的設(shè)備音頻硬件線路發(fā)生改變.當用戶插拔耳機,系統(tǒng)會自動改變硬件的線路.開發(fā)者可以注冊AVAudioSessionRouteChangeNotification
通知在線路變化時作出相應調(diào)整.
如上圖,系統(tǒng)在app啟動時會確定一套音頻線路,而后程序運行期間會繼續(xù)監(jiān)聽當前活躍的音頻線路,在錄制期間,用戶可能插拔耳機,系統(tǒng)會發(fā)送一份改變線路的通知告訴開發(fā)者同時音頻停止,開發(fā)者可以通過代碼決定是否重新激活.
播放與錄制稍有不同,播放時如果用戶拔掉耳機,默認暫停音頻,如果插上耳機,默認繼續(xù)播放.
4.1. 監(jiān)聽Audio線路變化
原因
- 插拔耳機
- 連接,斷開藍牙耳機
- 插拔USB音頻設(shè)備
func setupNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleRouteChange),
name: .AVAudioSessionRouteChange,
object: AVAudioSession.sharedInstance())
}
func handleRouteChange(_ notification: Notification) {
}
userInfo
中提供了關(guān)于線路改變的詳細信息.可以查詢改變原因通過字典中的AVAudioSessionRouteChangeReason
,如當新的設(shè)備接入時,原因為AVAudioSessionRouteChangeReason
,移除時為AVAudioSessionRouteChangeReasonOldDeviceUnavailable
func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
// Handle new device available.
case .oldDeviceUnavailable:
// Handle old device removed.
default: ()
}
}
當有音頻硬件插入時,你可以查詢audio session的currentRoute
屬性去確定當前音頻輸出的位置.它將返回一個AVAudioSessionRouteDescription
對象包含audio session全部的輸入輸出信息.當一個音頻硬件被移除時,我們也可以從該對象中查詢上一個線路.在以上兩種情況中,我們都可以查詢outputs
屬性,通過返回的AVAudioSessionPortDescription
對象提供了音頻輸出的全部信息.
func handleRouteChange(notification: NSNotification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
let session = AVAudioSession.sharedInstance()
for output in session.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
headphonesConnected = true
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
headphonesConnected = false
}
}
default: ()
}
}
5. 配置設(shè)備硬件
使用audio session屬性,可以在運行時優(yōu)化硬件音頻行為.這樣可以讓代碼適配運行設(shè)備的特性.這樣做同樣適用于用戶對音頻硬件作出的更改.
5.1. 配置初始音頻參數(shù)
使用audio session指定音頻設(shè)備的設(shè)置,如采樣率, I/O緩沖區(qū)時間.
Setting | Preferred sample rate | Preferred I/O buffer duration |
---|---|---|
High value | Example: 48 kHz, + High audio quality, – Large file or buffer size | Example: 500 mS, + Less-frequent file access, – Longer latency |
Low value | Example: 8 kHz, + Small file or buffer size, – Low audio quality | Example: 5 mS,+ Low latency, – Frequent file access |
Note: 默認音頻輸入輸出緩沖時間(I/O buffer duration)為大多數(shù)應用提供足夠的相應時間,如44.1kHz音頻大概為20ms響應一次,你可以設(shè)置更低的延遲但相應數(shù)據(jù)量每次過來的也會降低,根據(jù)自己的需求進行選擇.
5.2. 設(shè)置
在激活audio session前必須完成設(shè)置內(nèi)容.如果你正在運行audio session, 先停用它,然后改變設(shè)置重新激活.
let session = AVAudioSession.sharedInstance()
// Configure category and mode
do {
try session.setCategory(AVAudioSessionCategoryRecord, mode: AVAudioSessionModeDefault)
} catch let error as NSError {
print("Unable to set category: \(error.localizedDescription)")
}
// Set preferred sample rate
do {
try session.setPreferredSampleRate(44_100)
} catch let error as NSError {
print("Unable to set preferred sample rate: \(error.localizedDescription)")
}
// Set preferred I/O buffer duration
do {
try session.setPreferredIOBufferDuration(0.005)
} catch let error as NSError {
print("Unable to set preferred I/O buffer duration: \(error.localizedDescription)")
}
// Activate the audio session
do {
try session.setActive(true)
} catch let error as NSError {
print("Unable to activate session. \(error.localizedDescription)")
}
// Query the audio session's ioBufferDuration and sampleRate properties
// to determine if the preferred values were set
print("Audio Session ioBufferDuration: \(session.ioBufferDuration), sampleRate: \(session.sampleRate)")
5.3. 選擇,配置麥克風
一個設(shè)備可能有多個麥克風(內(nèi)置,外接),iOS會根據(jù)當前使用的audio session mode自動選擇一個.mode指定了輸入數(shù)字信號處理(DSP)和可能的線路.輸入線路針對每種模式的用例進行了優(yōu)化,設(shè)置mode還可能影響正在使用的音頻線路.
開發(fā)者可以手動選擇麥克風,甚至可以設(shè)置polar pattern如果硬件支持.
在使用任何音頻設(shè)備之前杖爽,請為您的應用設(shè)置音頻會話類別和模式敲董,然后激活音頻會話。
- 設(shè)置Preferred Input
為了找到當前設(shè)備連接的音頻輸入設(shè)備,可以使用audio session的availableInputs
屬性,該屬性返回一個AVAudioSessionPortDescription
對象的數(shù)組,描述當前可用輸入設(shè)備端口,端口用portType
進行標識.可以使用setPreferredInput:error:
設(shè)置可用的音頻輸入設(shè)備.
- 設(shè)置Preferred Data Source
部分端口如內(nèi)置麥克風,USB等支持數(shù)據(jù)源(data source),應用程序可以通過查詢端口的dataSources
屬性發(fā)現(xiàn)可用的數(shù)據(jù)源.對于內(nèi)置麥克風慰安,返回的數(shù)據(jù)源描述對象代表每個單獨的麥克風腋寨。不同的設(shè)備為內(nèi)置麥克風返回不同的值。例如化焕,iPhone 4和iPhone 4S有兩個麥克風:底部和頂部萄窜。 iPhone 5有三個麥克風:底部,前部和后部撒桨。
可以通過數(shù)據(jù)源描述的location
屬性(上查刻,下)和orientation
屬性(前,后等)的組合來識別各個內(nèi)置麥克風凤类。應用程序可以使用AVAudioSessionPortDescription對象的setPreferredDataSource:error:方法設(shè)置首選數(shù)據(jù)源穗泵。
- 設(shè)置 Preferred Polar Pattern
某些iOS設(shè)備支持為某些內(nèi)置麥克風配置麥克風極性模式。麥克風的極性模式定義了其對聲音相對于聲源方向的靈敏度谜疤。使用supportedPolarPatterns
屬性返回數(shù)據(jù)源是否支持此模式,此屬性返回數(shù)據(jù)源支持的極坐標模式數(shù)組(如心形或全向)佃延,或者在沒有可選模式時返回nil。如果數(shù)據(jù)源具有許多支持的極坐標模式夷磕,則可以使用數(shù)據(jù)源描述的setPreferredPolarPattern:error:方法設(shè)置首選極坐標模式履肃。
- 選擇特定麥克風并且設(shè)置polar pattern.
// Preferred Mic = Front, Preferred Polar Pattern = Cardioid
let preferredMicOrientation = AVAudioSessionOrientationFront
let preferredPolarPattern = AVAudioSessionPolarPatternCardioid
// Retrieve your configured and activated audio session
let session = AVAudioSession.sharedInstance()
// Get available inputs
guard let inputs = session.availableInputs else { return }
// Find built-in mic
guard let builtInMic = inputs.first(where: {
$0.portType == AVAudioSessionPortBuiltInMic
}) else { return }
// Find the data source at the specified orientation
guard let dataSource = builtInMic.dataSources?.first (where: {
$0.orientation == preferredMicOrientation
}) else { return }
// Set data source's polar pattern
do {
try dataSource.setPreferredPolarPattern(preferredPolarPattern)
} catch let error as NSError {
print("Unable to preferred polar pattern: \(error.localizedDescription)")
}
// Set the data source as the input's preferred data source
do {
try builtInMic.setPreferredDataSource(dataSource)
} catch let error as NSError {
print("Unable to preferred dataSource: \(error.localizedDescription)")
}
// Set the built-in mic as the preferred input
// This call will be a no-op if already selected
do {
try session.setPreferredInput(builtInMic)
} catch let error as NSError {
print("Unable to preferred input: \(error.localizedDescription)")
}
// Print Active Configuration
session.currentRoute.inputs.forEach { portDesc in
print("Port: \(portDesc.portType)")
if let ds = portDesc.selectedDataSource {
print("Name: \(ds.dataSourceName)")
print("Polar Pattern: \(ds.selectedPolarPattern ?? "[none]")")
}
}
Running this code on an iPhone 6s produces the following console output:
Port: MicrophoneBuiltIn
Name: Front
Polar Pattern: Cardioid
5.4. 模擬器運行
可以在模擬器或設(shè)備上運行您的應用。但是坐桩,Simulator不會模擬不同進程或音頻線路更改中的音頻會話之間的大多數(shù)交互尺棋。在Simulator中運行應用程序時,您不能:
- 調(diào)用中斷
- 模擬插入或拔出耳機
- 更改靜音開關(guān)的設(shè)置
- 模擬屏幕鎖定
- 測試音頻混合行為 - 即播放音頻以及來自其他應用(例如音樂應用)的音頻
#if arch(i386) || arch(x86_64)
// Execute subset of code that works in the Simulator
#else
// Execute device-only code as well as the other code
#endif
保護用戶隱私
為了保護用戶隱私绵跷,應用必須在錄制音頻之前詢問并獲得用戶的許可膘螟。如果用戶未授予許可,則僅記錄靜音。當您使用支持錄制的類別并且應用程序嘗試使用輸入線路時,系統(tǒng)會自動提示用戶獲得權(quán)限肮柜。
您可以使用requestRecordPermission:
方法手動請求權(quán)限,而不是等待系統(tǒng)提示用戶提供記錄權(quán)限脊阴。使用此方法可以讓您的應用獲得權(quán)限,而不會中斷應用的自然流動,從而獲得更好的用戶體驗嘿期。
AVAudioSession.sharedInstance().requestRecordPermission { granted in
if granted {
// User granted access. Present recording interface.
} else {
// Present message to user indicating that recording
// can't be performed until they change their preference
// under Settings -> Privacy -> Microphone
}
}
從iOS 10開始品擎,所有訪問任何設(shè)備麥克風的應用都必須靜態(tài)聲明其意圖。為此备徐,應用程序現(xiàn)在必須在其Info.plist文件中包含NSMicrophoneUsageDescription鍵萄传,并為此密鑰提供目的字符串。當系統(tǒng)提示用戶允許訪問時蜜猾,此字符串將顯示為警報的一部分秀菱。如果應用程序嘗試訪問任何設(shè)備的麥克風而沒有此鍵和值,則應用程序?qū)⒔K止蹭睡。