Overview
Apple通過audio sessions管理app, app與其他app, app與外部音訊硬體間的行為.使用audio session可以向系統傳達你將如何使用音訊.audio session充當著app與系統間的中介.這樣我們無需瞭解硬體相關卻可以操控硬體行為.
- 配置audio session類別與模式去告訴系統在app中你想怎麼使用音訊
- 啟用audio session使配置的類別與模式可以工作
- 新增通知,響應重要的audio session通知,例如音訊中斷與硬體線路改變
- 配置音訊取樣率,聲道數等資訊
1.配置Audio Session
1.1. Audio Session管理Audio
audio session是應用程式與系統間的中介,用於配置音訊行為,APP啟動時,會自動獲得一個audio session的單例物件,配置並且啟用它以讓音訊按照期望開始工作.
1.2. Categories代表Audio作用
audio session category代表音訊的主要行為.通過設定類別, 可以指明app是否使用的當前的輸入或輸出音訊裝置,以及當別的app中正在播放音訊進入我們app時他們的音訊是強制停止還是與我們的音訊一起播放等等.
AVFoundation中定義了很多audio session categories, 你可以根據需要自定義音訊行為,很多類別支援播放,錄製,錄製與播放同時進行,當系統瞭解了你定義的音訊規則,它將提供給你合適的路徑去訪問硬體資源.系統也將確保別的app中的音訊以適合你應用的方式執行.
一些categories可以根據Mode進一步定製,該模式用於專門指定類別的行為,例如當使用視訊錄製模式時,系統可能會選擇一個不同於預設內建麥克風的麥克風,系統還可以針對錄製調整麥克風的訊號強度.
1.3. 中斷處理
如果audio意外中斷,系統會將aduio session置為停用狀態,音訊也會因此立即停止.當一個別的app的audio session被啟用並且它的類別未設定與系統類別或你應用程式類別混合時,中斷就會發生.你的應用程式在收到中斷通知後應該儲存當時的狀態,以及更新使用者介面等相關操作.通過註冊AVAudioSessionInterruptionNotification可以觀察中斷的開始與結束點.
1.4. 音訊線路改變
當使用者做出連線,斷開音訊輸入,輸出裝置時,(如:插拔耳機)音訊線路發生變化,通過註冊AVAudioSessionRouteChangeNotification
可以在音訊線路發生變化時做出相應處理.
1.5. Audio Sessions控制裝置配置
App不能直接控制裝置的硬體,但是audio session提供了一些介面去獲取或設定一些高階的音訊設定,如取樣率,聲道數等等.
1.6. Audio Sessions保護使用者隱私
App如果想使用音訊錄製功能必須請求使用者授權,否則無法使用.
2. 啟用Audio Session
在設定了audio session的category, options, mode後,我們可以啟用它以啟動音訊.
2.1. 系統如何解決音訊競爭
隨著app的啟動,內建的一些服務(簡訊,音樂,瀏覽器,電話等)也將在後臺執行.前面的這些內建服務都可能產生音訊,如有電話打來,有簡訊提示等等...
2.2. 啟用,停用Audio Session
雖然AVFoundation中播放與錄製可以自動啟用你的audio session, 但你可以手動啟用並且測試是否啟用成功.
系統會停用你的audio session當有電話打進來,鬧鐘響了,或是日曆提醒等訊息介入.當處理完這些介入的訊息後,系統允許我們手動重新啟用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等),系統負責在中斷結束時重新啟用audio session.然而,如果你註冊了通知去重新啟用audio session,你可以驗證是否啟用成功並且更新使用者介面.
- 確保在後臺執行的VoIP應用程式的音訊會話僅在應用程式處理呼叫時才處於啟用狀態。在後臺,若未收到呼叫,VoIP應用程式的音訊會話不應該是啟用的。
- 確保使用錄製類別的應用程式的音訊會話僅在錄製時處於啟用狀態。在錄製開始和停止之前,請確保您的會話處於未啟用狀態,以允許播放其他聲音,例如系統聲音。
- 如果應用程式支援後臺音訊播放或錄製,但在應用程式未主動使用音訊(或準備使用音訊)時,在進入後臺時停用其音訊會話。這樣做允許系統釋放音訊資源,以便其他程式可以使用它們。
2.3. 檢查別的Audio是否正在播放
當你的app被啟用前,當前裝置可能正在播放別的聲音,如果你的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進行混音時,中斷髮生.註冊通知後我們可以在得知音訊中斷後做出相應處理.
App會因為中斷被暫停,當使用者接到電話時,鬧鐘,或其他系統事件被觸發時,當中斷結束後,App會繼續執行,但是需要我們手動重新啟用audio session.
3.1. 中斷的生命週期
下圖簡單展示了當收到facetime後app的audio session與系統的audio session間啟用與未啟用狀態變化.
3.2. 中斷處理方法
通過註冊監聽中斷的通知可以在中斷來的時候進行處理.處理中斷取決於你當前正在執行的操作:播放,錄製,音訊格式轉換,讀取音訊資料包等等.一般而言,我們應儘量避免中斷並且做到中斷後儘快恢復.
中斷前
- 儲存狀態與上下文
- 更新使用者介面
中斷後
- 恢復狀態與上下文
- 更新使用者介面
- 重新啟用audio session.
Audio technology | How interruptions work |
---|---|
AVFoundation framework | 系統在中斷時會自動暫停錄製與播放,當中斷結束後重新啟用audio session,恢復錄製與播放 |
Audio Queue Services, I/O audio unit | 系統會發出中斷通知,開發者可以儲存播放與錄製狀態並且在中斷結束後重新啟用audio session |
System Sound Services | 使用系統聲音服務在中斷來臨時保持靜音,如果中斷結束,聲音自動播放. |
3.3. 處理Siri
當處理Siri時,與其他中斷不同,我們在中斷期間需要對Siri進行監聽,如在中斷期間,使用者要求Siri去暫停開發者app中的音訊播放,當app收到中斷結束的通知時,不應該自動恢復播放.同時,使用者介面需要跟Siri要求的保持一致.
3.4. 監聽中斷
註冊AVAudioSessionInterruptionNotification
通知可以監聽中斷.
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
}
}
}
複製程式碼
注意: 無法確保在開始中斷後一定有一個結束中斷,所以,如果沒有結束中斷,我們在app重新播放音訊時需要總是檢查aduio session是否被啟用.
3.5. 響應媒體伺服器重置操作
media server通過一個共享伺服器程式提供了音訊和其他多媒體功能.儘管很少見,但是如果在你的app正在執行時收到一條重置命令,可以通過註冊AVAudioSessionMediaServicesWereResetNotification
通知監聽media server是否重置.收到通知後需要做如下操作.
- 銷燬音訊物件並且建立新的音訊物件(如:players,recorders,converters,audio queues)
- 重置所有audio狀態,包括AVAudioSession全部屬性
- 在合適時機重新啟用AVAudioSession物件.
註冊AVAudioSessionMediaServicesWereLostNotification
可以在media server不可用時收到通知.
如果開發者的應用程式中需要重置功能,如設定中有重置選項,可以使用這個方法輕鬆重置.
4. 線路改變
audio hardware route指定的裝置音訊硬體線路發生改變.當使用者插拔耳機,系統會自動改變硬體的線路.開發者可以註冊AVAudioSessionRouteChangeNotification
通知線上路變化時作出相應調整.
如上圖,系統在app啟動時會確定一套音訊線路,而後程式執行期間會繼續監聽當前活躍的音訊線路,在錄製期間,使用者可能插拔耳機,系統會傳送一份改變線路的通知告訴開發者同時音訊停止,開發者可以通過程式碼決定是否重新啟用.
播放與錄製稍有不同,播放時如果使用者拔掉耳機,預設暫停音訊,如果插上耳機,預設繼續播放.
4.1. 監聽Audio線路變化
原因
- 插拔耳機
- 連線,斷開藍芽耳機
- 插拔USB音訊裝置
func setupNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleRouteChange),
name: .AVAudioSessionRouteChange,
object: AVAudioSession.sharedInstance())
}
func handleRouteChange(_ notification: Notification) {
}
複製程式碼
userInfo
中提供了關於線路改變的詳細資訊.可以查詢改變原因通過字典中的AVAudioSessionRouteChangeReason
,如當新的裝置接入時,原因為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. 配置裝置硬體
使用audio session屬性,可以在執行時優化硬體音訊行為.這樣可以讓程式碼適配執行裝置的特性.這樣做同樣適用於使用者對音訊硬體作出的更改.
5.1. 配置初始音訊引數
使用audio session指定音訊裝置的設定,如取樣率, I/O緩衝區時間.
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)為大多數應用提供足夠的相應時間,如44.1kHz音訊大概為20ms響應一次,你可以設定更低的延遲但相應資料量每次過來的也會降低,根據自己的需求進行選擇.
5.2. 設定
在啟用audio session前必須完成設定內容.如果你正在執行audio session, 先停用它,然後改變設定重新啟用.
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. 選擇,配置麥克風
一個裝置可能有多個麥克風(內建,外接),iOS會根據當前使用的audio session mode自動選擇一個.mode指定了輸入數字訊號處理(DSP)和可能的線路.輸入線路針對每種模式的用例進行了優化,設定mode還可能影響正在使用的音訊線路.
開發者可以手動選擇麥克風,甚至可以設定polar pattern如果硬體支援.
在使用任何音訊裝置之前,請為您的應用設定音訊會話類別和模式,然後啟用音訊會話。
- 設定Preferred Input
為了找到當前裝置連線的音訊輸入裝置,可以使用audio session的availableInputs
屬性,該屬性返回一個AVAudioSessionPortDescription
物件的陣列,描述當前可用輸入裝置埠,埠用portType
進行標識.可以使用setPreferredInput:error:
設定可用的音訊輸入裝置.
- 設定Preferred Data Source
部分埠如內建麥克風,USB等支援資料來源(data source),應用程式可以通過查詢埠的dataSources
屬性發現可用的資料來源.對於內建麥克風,返回的資料來源描述物件代表每個單獨的麥克風。不同的裝置為內建麥克風返回不同的值。例如,iPhone 4和iPhone 4S有兩個麥克風:底部和頂部。 iPhone 5有三個麥克風:底部,前部和後部。
可以通過資料來源描述的location
屬性(上,下)和orientation
屬性(前,後等)的組合來識別各個內建麥克風。應用程式可以使用AVAudioSessionPortDescription物件的setPreferredDataSource:error:方法設定首選資料來源。
- 設定 Preferred Polar Pattern
某些iOS裝置支援為某些內建麥克風配置麥克風極性模式。麥克風的極性模式定義了其對聲音相對於聲源方向的靈敏度。使用supportedPolarPatterns
屬性返回資料來源是否支援此模式,此屬性返回資料來源支援的極座標模式陣列(如心形或全向),或者在沒有可選模式時返回nil。如果資料來源具有許多支援的極座標模式,則可以使用資料來源描述的setPreferredPolarPattern:error:方法設定首選極座標模式。
- 選擇特定麥克風並且設定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. 模擬器執行
可以在模擬器或裝置上執行您的應用。但是,Simulator不會模擬不同程式或音訊線路更改中的音訊會話之間的大多數互動。在Simulator中執行應用程式時,您不能:
- 呼叫中斷
- 模擬插入或拔出耳機
- 更改靜音開關的設定
- 模擬螢幕鎖定
- 測試音訊混合行為 - 即播放音訊以及來自其他應用(例如音樂應用)的音訊
#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
複製程式碼
保護使用者隱私
為了保護使用者隱私,應用必須在錄製音訊之前詢問並獲得使用者的許可。如果使用者未授予許可,則僅記錄靜音。當您使用支援錄製的類別並且應用程式嘗試使用輸入線路時,系統會自動提示使用者獲得許可權。
您可以使用requestRecordPermission:
方法手動請求許可權,而不是等待系統提示使用者提供記錄許可權。使用此方法可以讓您的應用獲得許可權,而不會中斷應用的自然流動,從而獲得更好的使用者體驗。
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開始,所有訪問任何裝置麥克風的應用都必須靜態宣告其意圖。為此,應用程式現在必須在其Info.plist檔案中包含NSMicrophoneUsageDescription鍵,併為此金鑰提供目的字串。當系統提示使用者允許訪問時,此字串將顯示為警報的一部分。如果應用程式嘗試訪問任何裝置的麥克風而沒有此鍵和值,則應用程式將終止。