Audio Session:系統與應用程式的中介

小東邪發表於2019-05-02

Overview

Apple通過audio sessions管理app, app與其他app, app與外部音訊硬體間的行為.使用audio session可以向系統傳達你將如何使用音訊.audio session充當著app與系統間的中介.這樣我們無需瞭解硬體相關卻可以操控硬體行為.

1.Audio session

  • 配置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間啟用與未啟用狀態變化.

2.interruput_lifecycle

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通知線上路變化時作出相應調整.

3.audio_route_change

如上圖,系統在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鍵,併為此金鑰提供目的字串。當系統提示使用者允許訪問時,此字串將顯示為警報的一部分。如果應用程式嘗試訪問任何裝置的麥克風而沒有此鍵和值,則應用程式將終止。


Apple 官方文件

相關文章