AudioSession 基礎(chǔ)

Overwrite

  • Audio Session:音頻會(huì)話(huà)

Apple 系統(tǒng)通過(guò) Audio Session 在應(yīng)用程序中、應(yīng)用程序之間和應(yīng)用程序與硬件之間的管理。通過(guò)使用 Audio Session 可以在不了解相關(guān)硬件操作下監(jiān)督管理應(yīng)用程序中的音頻使用。

ASPG_intro_2x.png

AVAudioSession 用途:

  • 配置Audio Session類(lèi)別和模式去告訴系統(tǒng)在A(yíng)PP中音頻的使用方式

  • 激活A(yù)PP中的 Audio Session 配置的類(lèi)別與模式

  • 訂閱和響應(yīng) Audio Session 重要的通知,比如音頻打斷和線(xiàn)路切換

  • 進(jìn)行相關(guān)高級(jí)音頻設(shè)備配置,比如采樣率、I/O緩沖時(shí)長(zhǎng)和聲道數(shù)

0. 音頻會(huì)話(huà)管理音頻行為的解釋

一個(gè)Audio Session是APP與操作系統(tǒng)之間配置音頻行為的媒介。在應(yīng)用啟動(dòng)后,APP會(huì)自動(dòng)提供一個(gè)Audio Session的單例,我們通過(guò)配置這個(gè)單例的行為并激活它,以此來(lái)告訴系統(tǒng)該如何使用音頻。

1. 配置音頻會(huì)話(huà)

1.1 音頻會(huì)話(huà)類(lèi)別(Category)

音頻會(huì)話(huà)類(lèi)別(Category)是應(yīng)用程序一組音頻行為,通過(guò)設(shè)置類(lèi)別我們可以向系統(tǒng)指明我們使用音頻的意圖,主要有以下幾個(gè)功能:

  • “鈴聲/靜音”按鈕和鎖屏控制靜音:音頻會(huì)在用戶(hù)打開(kāi)靜音按鈕或鎖屏?xí)r靜音。

  • 打斷不可混音的APP音頻:當(dāng)我們激活A(yù)pp的音頻會(huì)話(huà)時(shí),會(huì)打斷其他不可混音App的音頻會(huì)話(huà)。

  • 音頻輸入支持:允許使用麥克風(fēng)等錄音設(shè)備

  • 音頻輸出支持:允許使用揚(yáng)聲器等播放設(shè)備

類(lèi)別(Category) “鈴聲/靜音”按鈕和鎖屏控制靜音 打斷不可混音的APP音頻 允許音頻輸入(錄音)和輸出(播放)
AVAudioSessionCategoryAmbient Yes No 只有輸出
AVAudioSessionCategorySoloAmbient (默認(rèn)) Yes Yes 只有輸出
AVAudioSessionCategoryPlayback No 默認(rèn)為Yes;重寫(xiě)開(kāi)關(guān)為NO 只有輸出
AVAudioSessionCategoryRecord No (鎖屏后會(huì)繼續(xù)錄制) Yes 只有輸入
AVAudioSessionCategoryPlayAndRecord No 默認(rèn)為Yes;重寫(xiě)開(kāi)關(guān)為NO 輸入和輸出
AVAudioSessionCategoryMultiRoute No Yes 輸入和輸出

注意:若想在鎖屏或靜音模式下繼續(xù)播放音樂(lè),還需要在A(yíng)pp的Info.plist中添加UIBackgroundModes音頻鍵。

一般大部分App只會(huì)在應(yīng)用啟動(dòng)的時(shí)候都會(huì)根據(jù)自身所需設(shè)置相應(yīng)的音頻類(lèi)別,但是我們也可以根據(jù)我們的需要在應(yīng)用修改類(lèi)別:先停用音頻會(huì)話(huà)再修改音頻類(lèi)別,然后重新激活應(yīng)用修改。

1.2 音頻會(huì)話(huà)模式(mode)

音頻會(huì)話(huà)模式(mode)用于指明音頻配置的功能,是對(duì)音頻會(huì)話(huà)類(lèi)別的關(guān)聯(lián),告訴系統(tǒng)音頻具體的使用用途。隨著系統(tǒng)的升級(jí),音頻會(huì)話(huà)類(lèi)別可能會(huì)進(jìn)行完善,通過(guò)模式來(lái)指定功能,可以兼容不同的系統(tǒng)實(shí)現(xiàn)。當(dāng)前的音頻模式如下圖所示:

模式標(biāo)識(shí) 關(guān)聯(lián)類(lèi)別
AVAudioSessionModeDefault All
AVAudioSessionModeMoviePlayback AVAudioSessionCategoryPlayback
AVAudioSessionModeVideoRecording AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord
AVAudioSessionModeVoiceChat AVAudioSessionCategoryPlayAndRecord
AVAudioSessionModeGameChat AVAudioSessionCategoryPlayAndRecord
AVAudioSessionModeVideoChat AVAudioSessionCategoryPlayAndRecord
AVAudioSessionModeSpokenAudio AVAudioSessionCategoryPlayback
AVAudioSessionModeMeasurement AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayback

在使用音頻會(huì)話(huà)中,我們可以先配置類(lèi)別作為一個(gè)音頻基本使用方式,再通過(guò)配置模式來(lái)指定特定行為。

1.3 音頻會(huì)話(huà)類(lèi)別選項(xiàng)(Options)

音頻會(huì)話(huà)類(lèi)別選項(xiàng)(Options),用于配置其他的音頻用途。在不同的音頻類(lèi)別中,有這些選項(xiàng)的默認(rèn)值,但我們可以通過(guò)設(shè)置其值完成相應(yīng)功能。其值如下所示:

  • AVAudioSessionCategoryOptionMixWithOthers:應(yīng)用程序在音頻會(huì)話(huà)即將激活時(shí),設(shè)置中斷其他會(huì)話(huà)或與其他會(huì)話(huà)混合。

    • PlayAndRecord、MultiRoute、Playback:默認(rèn)為關(guān)閉,但可手動(dòng)開(kāi)啟,使其他應(yīng)用程序的會(huì)話(huà)依然在后臺(tái)運(yùn)行。(AVAudioSessionCategoryPlayback 不受“鈴聲”開(kāi)關(guān)控制)

    • Others:默認(rèn)關(guān)閉且不可改變。

  • AVAudioSessionCategoryOptionDuckOthers:應(yīng)用程序在音頻會(huì)話(huà)即將激活時(shí),減小其他音頻會(huì)話(huà)的音量

    • Playback、PlayAndRecord、MultiRoute:默認(rèn)是不可混音和不可減弱的。

    • Others:設(shè)置這個(gè)屬性就相當(dāng)于設(shè)置了AVAudioSessionCategoryOptionMixWithOthers,因?yàn)橹挥谢煲袅瞬判枰獪p弱其他音頻會(huì)話(huà)。

  • AVAudioSessionCategoryOptionAllowBluetooth:是否支持當(dāng)前音頻會(huì)話(huà)使用藍(lán)牙(HFP)設(shè)備作為音頻線(xiàn)路。

    • PlayAndRecord、Record:默認(rèn)關(guān)閉,但可手動(dòng)開(kāi)啟。

    • Others:默認(rèn)關(guān)閉且不可改變。

  • AVAudioSessionCategoryOptionDefaultToSpeaker:是否默認(rèn)使用揚(yáng)聲器。

    • PlayAndRecord:默認(rèn)關(guān)閉且不可改變。當(dāng)沒(méi)有其他音頻線(xiàn)路連接時(shí),他只會(huì)連接到揚(yáng)聲器。

    • Others:默認(rèn)關(guān)閉,但可手動(dòng)開(kāi)啟。

  • AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers:應(yīng)用程序在音頻會(huì)話(huà)即將激活時(shí),允許打斷其他設(shè)置模式為AVAudioSessionModeSpokenAudio的會(huì)話(huà),也可以與其他會(huì)話(huà)進(jìn)行混合。

  • AVAudioSessionCategoryOptionAllowBluetoothA2DP:是否支持使用藍(lán)牙A2DP耳機(jī)

    • PlayAndRecord:默認(rèn)關(guān)閉,但可手動(dòng)開(kāi)啟。

    • MultiRoute、Record:默認(rèn)關(guān)閉且不可改變。

    • Others:默認(rèn)開(kāi)啟且不可改變。

  • AVAudioSessionCategoryOptionAllowAirPlay:是否支持使用AirPlay

1.4 配置音頻會(huì)話(huà)

配置音頻會(huì)話(huà)最重要是配置類(lèi)別,模式和選項(xiàng)都是可選次要的。

// Access the shared, singleton audio session instance
let session = AVAudioSession.sharedInstance()
do {
    // Configure the audio session for movie playback
    try session.setCategory(AVAudioSessionCategoryPlayback,
                            mode: AVAudioSessionModeMoviePlayback,
                            options: [])
} catch let error as NSError {
    print("Failed to set the audio session category and mode: \(error.localizedDescription)")
}

更多詳細(xì)配置參考官方文檔

2. 音頻會(huì)話(huà)的激活

我們可以通過(guò)類(lèi)別(category)、模式(mode)和選項(xiàng)(options)配置一個(gè)Audio Session。為了使我們的配置生效,我們需要激活這個(gè)Audio Session。激活音頻會(huì)話(huà)顧名思義就是我們告訴系統(tǒng),我們需要使用音頻設(shè)備資源。反過(guò)來(lái),系統(tǒng)也可以激活和停用音頻會(huì)話(huà)來(lái)告訴我們是否可以使用設(shè)備資源。

2.1 系統(tǒng)對(duì)音頻需求沖突的解決

當(dāng)你的APP啟動(dòng)時(shí),已經(jīng)構(gòu)建好的APP(例如Message、Music、Safari)可能會(huì)在后臺(tái)運(yùn)行。這些軟件都有可能生產(chǎn)音頻,例如消息推送的提醒、設(shè)置的延時(shí)播放等等。如果你將設(shè)備想象成一個(gè)”飛機(jī)場(chǎng)“,那么APP就是里面”正在滑行的飛機(jī)“,系統(tǒng)服務(wù)就是”控制塔”。我們的應(yīng)用程序可以發(fā)送音頻請(qǐng)求并聲明自己的優(yōu)先級(jí),但是最終掌控“停機(jī)坪“的最終還是系統(tǒng)。我們?cè)谑褂?Audio Session 與”控制塔“進(jìn)行交流難免會(huì)遇到?jīng)_突,下圖展示的是MyApp與Music App的沖突解決的過(guò)程:

competing_audio_demands_2x.png
  1. MyApp 請(qǐng)求激活 Audio Session,比如APP啟動(dòng)、用戶(hù)點(diǎn)擊UI進(jìn)行錄音與回放
  2. 系統(tǒng)理解激活請(qǐng)求,比如音頻會(huì)話(huà)的類(lèi)別
  3. 系統(tǒng)告知Music APP的Audio Session無(wú)效,停止他的音頻播放
  4. Music App 告知系統(tǒng)他的音頻會(huì)話(huà)已失效
  5. 系統(tǒng)激活 MyApp 的音頻會(huì)話(huà)并允許其可以開(kāi)始播放

2.2 激活和停用你的音頻會(huì)話(huà)

所有AVFoundation可以在播放和錄音的時(shí)候自動(dòng)激活音頻會(huì)話(huà),但是手動(dòng)激活音頻會(huì)話(huà)可以給我們一個(gè)機(jī)會(huì)去提前激活音頻會(huì)話(huà)并且能測(cè)試是否能激活成功。系統(tǒng)會(huì)停用你的Audio Session,例如有電話(huà)打進(jìn)來(lái)、鬧鐘響了、或是日歷提醒等消息介入。當(dāng)處理完這些介入的消息后,系統(tǒng)允許我們手動(dòng)重新激活A(yù)udio 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) // pass false to deactivate your audio session
} catch let error as NSError {
    print("Unable to activate audio session:  \(error.localizedDescription)")
}

當(dāng)我們使用AVFoundation對(duì)象(AVPlayer, AVAudioRecorder等),系統(tǒng)會(huì)負(fù)責(zé)在中斷結(jié)束時(shí)重新激活音頻會(huì)話(huà)。然而,如果你注冊(cè)了通知去重新激活音頻會(huì)話(huà),你可以驗(yàn)證是否激活成功并且更新用戶(hù)界面。

很多APP沒(méi)有必要去明確地停用它們的音頻會(huì)話(huà),除了VoIP App、導(dǎo)航App和播放錄制App。因此一般在使用音頻會(huì)話(huà)都要遵守以下規(guī)矩:

  • 確保在后臺(tái)運(yùn)行的VoIP App的音頻會(huì)話(huà)僅在應(yīng)用程序處理呼叫時(shí)才處于激活狀態(tài)。在后臺(tái),若未收到呼叫,VoIP App 的音頻會(huì)話(huà)不應(yīng)該是激活的。

  • 確保使用錄制類(lèi)別的應(yīng)用程序的音頻會(huì)話(huà)僅在錄制時(shí)處于激活狀態(tài)。在錄制開(kāi)始和停止之前,請(qǐng)確保您的會(huì)話(huà)處于未激活狀態(tài),以允許播放其他聲音,例如消息推送。

  • 如果應(yīng)用程序支持后臺(tái)音頻播放或錄制,但在應(yīng)用程序未主動(dòng)使用音頻(或準(zhǔn)備使用音頻)時(shí),在進(jìn)入后臺(tái)時(shí)應(yīng)該停用其音頻會(huì)話(huà)。這樣做是允許系統(tǒng)釋放音頻資源,以便其他進(jìn)程可以使用它們。

總的來(lái)說(shuō)就是用的時(shí)候能用,不用的時(shí)候必須出來(lái)?。?!

2.3 檢查是否有其他音頻在播放?

當(dāng)我們App的音頻會(huì)話(huà)被激活前,當(dāng)前設(shè)備可能正在播放別的聲音,我們可以通過(guò)檢查是否有其他音頻在播放來(lái)做相應(yīng)的處理。有兩個(gè)獲取方式:

  • AppDelegate中的applicationDidBecomeActive:代理方法中讀取音頻會(huì)話(huà)的secondaryAudioShouldBeSilencedHint屬性(Bool值),當(dāng)別的App正在播放的音頻會(huì)話(huà)為不可混音配置時(shí),該值為true。App 可以使用此屬性關(guān)閉次要與應(yīng)用程序的音頻。
  • 訂閱AVAudioSessionSilenceSecondaryAudioHintNotification的通知獲取AVAudioSessionSilenceSecondaryAudioHintType屬性(枚舉值),該操作如下:
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. 響應(yīng)音頻打斷

我們除了需要配置當(dāng)前音頻會(huì)話(huà),還需要應(yīng)對(duì)在音頻會(huì)話(huà)使用中,其他優(yōu)先級(jí)更高的應(yīng)用程序搶奪系統(tǒng)音頻資源的情況。在這種情況下,系統(tǒng)會(huì)發(fā)出一個(gè)“音頻打斷開(kāi)始通知”并停用我們的音頻會(huì)話(huà), 在其他應(yīng)用程序用完音頻資源后,系統(tǒng)會(huì)發(fā)出一個(gè)“音頻打斷結(jié)束通知”并重新激活我們的音頻會(huì)話(huà)。這兩個(gè)通知的開(kāi)始到結(jié)束就是我們音頻會(huì)話(huà)的打斷與恢復(fù),我們需要在代碼層面上對(duì)這種打斷過(guò)程做恰當(dāng)?shù)奶幚?,比如打斷開(kāi)始時(shí)暫停播放音頻,打斷結(jié)束后繼續(xù)播放音頻。

3.1 音頻打斷生命周期

下圖描述的是在同一個(gè)時(shí)間線(xiàn)上,兩個(gè)音頻會(huì)話(huà)之間的使用音頻設(shè)備資源的過(guò)程,“Your audio session”表示我們配置的音頻會(huì)話(huà),“Device audio session”表示FaceTime App的音頻會(huì)話(huà):

audio_session_interrupted_2x.png
  1. 激活我們配置好的音頻會(huì)話(huà),開(kāi)始播放我們的音頻
  2. 突然有一個(gè) FaceTime 的電話(huà)打來(lái),系統(tǒng)激活了 FaceTime App 的音頻會(huì)話(huà)
  3. 這時(shí)系統(tǒng)停用了我們的音頻會(huì)話(huà),在此時(shí)系統(tǒng)不負(fù)責(zé)我們的音頻播放了
  4. 系統(tǒng)發(fā)送一個(gè)“打斷開(kāi)始通知”給我們,并告知我們的音頻會(huì)話(huà)已經(jīng)停用了
  5. 我們接收到“打斷開(kāi)始通知”,并在通知收到后進(jìn)行界面刷新并暫停播放邏輯
  6. 用戶(hù)此時(shí)忽略了FaceTime電話(huà),這時(shí)系統(tǒng)會(huì)停用FaceTime的音頻會(huì)話(huà),然后重新激活我們的音頻會(huì)話(huà),并發(fā)送一個(gè)“打斷結(jié)束通知”給我們
  7. 我們接收到“打斷結(jié)束通知”,并在通知收到后進(jìn)行界面刷新并恢復(fù)播放邏輯
  8. (上圖沒(méi)有)如果用戶(hù)接聽(tīng)了電話(huà),那么我們的音頻會(huì)話(huà)將會(huì)被一直掛起,直至用戶(hù)結(jié)束通話(huà)。

3.2 音頻打斷處理方法

我們可以通過(guò)注冊(cè)AVAudioSession發(fā)出的打斷通知,該通知的名稱(chēng)為AVAudioSessionInterruptionNotification,并且根據(jù)里面的AVAudioSessionInterruptionTypeKey值獲取打斷的狀態(tài)做出恰當(dāng)?shù)倪壿嬏幚恚WC音頻打斷結(jié)束后能恢復(fù)正常。

  • 音頻打斷開(kāi)始 - AVAudioSessionInterruptionTypeBegan
    • 保存當(dāng)前狀態(tài)與上下文
    • 更新用戶(hù)界面
  • 音頻打斷結(jié)束- AVAudioSessionInterruptionTypeEnded
    • 重置狀態(tài)和上下文
    • 更新用戶(hù)界面
    • 重新激活音頻會(huì)話(huà)

對(duì)于我們使用不同框架,Apple推薦我們以下打斷處理方式:

  • AVFoundation framework:系統(tǒng)在中斷時(shí)會(huì)自動(dòng)暫停錄制與播放,當(dāng)中斷結(jié)束后重新激活音頻會(huì)話(huà),恢復(fù)錄制與播放
  • Audio Queue Services, I/O audio unit:系統(tǒng)會(huì)發(fā)出中斷通知,我們可以保存播放與錄制狀態(tài)并且在中斷結(jié)束后重新激活音頻會(huì)話(huà)
  • System Sound Services:使用系統(tǒng)聲音服務(wù)在中斷來(lái)臨時(shí)保持靜音,如果中斷結(jié)束,聲音自動(dòng)播放

通知監(jiān)聽(tīng)的代碼如下:

func registerForNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(handleInterruption),
                                           name: .AVAudioSessionInterruption,
                                           object: AVAudioSession.sharedInstance())
}
 
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
        }
    }
}

3.3 Siri 打斷處理

當(dāng)處理Siri時(shí),應(yīng)該與其他打斷不同。我們?cè)诖驍嗥陂g需要對(duì)Siri進(jìn)行監(jiān)聽(tīng),如果用戶(hù)要求Siri去暫停我們的音頻播放,在app收到打斷結(jié)束的通知時(shí),就不應(yīng)該自動(dòng)恢復(fù)播放。同時(shí),用戶(hù)界面需要跟Siri要求的保持一致。

3.4 媒體服務(wù)重置處理

媒體服務(wù)器在共用服務(wù)過(guò)程中提供音頻和其他多媒體方法,在A(yíng)PP運(yùn)行中,媒體服務(wù)器可能會(huì)被重置(不常見(jiàn))。我們可以通過(guò)注冊(cè)AVAudioSessionMediaServicesWereResetNotification通知去管理媒體服務(wù)器重置,接收到通知后,我們需要做以下事情:

  • 銷(xiāo)毀沒(méi)有被引用的音頻對(duì)象(比如players、recorders、 converters,或audio queues)并重新創(chuàng)建一個(gè)
  • 重置內(nèi)部所有被跟蹤的音頻狀態(tài),包括AVAudioSession中的所有屬性
  • 在適當(dāng)?shù)臅r(shí)機(jī)使用setActive:error:方法重新激活音頻會(huì)話(huà)

注冊(cè)AVAudioSessionMediaServicesWereLostNotification通知可以在媒體服務(wù)器不可用時(shí)收到通知。

4. 響應(yīng)線(xiàn)路切換

用戶(hù)在聽(tīng)歌的過(guò)程中,難免會(huì)接上或拔出耳機(jī),在這個(gè)過(guò)程中,音頻的線(xiàn)路就在揚(yáng)聲器和耳機(jī)中進(jìn)行切換,這就是線(xiàn)路切換。一個(gè)好的應(yīng)用程序應(yīng)該對(duì)線(xiàn)路切換就行處理,比如在用戶(hù)拔下耳機(jī)后,進(jìn)行靜音處理避免尷尬。除此之外導(dǎo)致音頻線(xiàn)路切換有多種情況,包括用戶(hù)插拔藍(lán)牙耳機(jī)、插拔USB設(shè)備等等。

4.1 多種音頻硬件線(xiàn)路切換

音頻硬件線(xiàn)路是一個(gè)有線(xiàn)的音頻電子信號(hào)線(xiàn)路,下圖描述的是在錄音和回放過(guò)程中,進(jìn)行耳機(jī)的插入和拔出引發(fā)一系列的線(xiàn)路切換事件,總共有四種可能的處理結(jié)果:

audio_route_change_2x.png

系統(tǒng)在A(yíng)pp啟動(dòng)后才開(kāi)始決定音頻線(xiàn)路,并開(kāi)始管理激活的線(xiàn)路。

  1. 用戶(hù)通過(guò)選擇“開(kāi)始錄音”使當(dāng)前情況進(jìn)入左分支
  2. 在錄音過(guò)程中,用戶(hù)拔出耳機(jī)應(yīng)該停止錄音
  3. 在錄音過(guò)程中,用戶(hù)插入耳機(jī)應(yīng)該停止錄音
  4. 用戶(hù)通過(guò)選擇“開(kāi)始回放”使當(dāng)前情況進(jìn)入左分支
  5. 在回放過(guò)程中,用戶(hù)拔出耳機(jī)后,應(yīng)該暫停播放;
  6. 在回放過(guò)程中,用戶(hù)插入耳機(jī)后,應(yīng)該重新播放

4.2 監(jiān)聽(tīng)音頻線(xiàn)路切換

開(kāi)發(fā)者可以通過(guò)注冊(cè)AVAudioSessionRouteChangeNotification通知來(lái)獲取線(xiàn)路切換事件:

func setupNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(handleRouteChange),
                                           name: .AVAudioSessionRouteChange,
                                           object: AVAudioSession.sharedInstance())
}
 
func handleRouteChange(_ notification: Notification) {
 
}

通知中的userInfo中提供了線(xiàn)路切換的相關(guān)信息,可以通過(guò)查詢(xún)AVAudioSessionRouteChangeReason鍵來(lái)獲取切換原因(以下枚舉值省略AVAudioSessionRouteChangeReason前綴):

  • Unknown:切換原因未知,用于處理未指定情況
  • NewDeviceAvailable:有新的音頻設(shè)備接入
  • OldDeviceUnavailable:之前的音頻輸出路徑不可用,例如斷開(kāi)舊的音頻設(shè)備連接
  • CategoryChange:音頻會(huì)話(huà)的類(lèi)別發(fā)生了變化
  • Override:音頻輸出路線(xiàn)被覆蓋
  • WakeFromSleep:設(shè)備從休眠中喚醒導(dǎo)致線(xiàn)路切換
  • NoSuitableRouteForCategory:由于當(dāng)前音頻類(lèi)別不支持當(dāng)前音頻線(xiàn)路引發(fā)的切換
  • RouteConfigurationChange:I/O端口切換引發(fā)的線(xiàn)路切換
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: ()
    }
}

開(kāi)發(fā)者還可以通過(guò)音頻會(huì)話(huà)中的currentRoute屬性獲取當(dāng)前的音頻線(xiàn)路,通過(guò)userInfo中的AVAudioSessionRouteChangePreviousRouteKey獲取上一個(gè)音頻線(xiàn)路:

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: ()
    }
}

具體的音頻線(xiàn)路(以下名稱(chēng)省略AVAudioSessionPort前綴):

  • 輸入端口
    • LineIn:連接在基座接口的有線(xiàn)音頻輸入設(shè)備
    • BuiltInMic:iOS 設(shè)備上的內(nèi)置麥克風(fēng)
    • HeadsetMic:有線(xiàn)耳機(jī)的麥克風(fēng)
  • 輸出端口
    • LineOut:連接在基座接口的有線(xiàn)音頻輸出設(shè)備
    • Headphones:耳機(jī)
    • BluetoothA2DP:藍(lán)牙 A2DP 設(shè)備,比如 AirPods
    • BuiltInReceiver:手機(jī)通話(huà)使用的耳邊聽(tīng)筒
    • BuiltInSpeaker:iOS 設(shè)備上的內(nèi)置揚(yáng)聲器
    • HDMI:通過(guò)HDMI接口連接的設(shè)備
    • AirPlay:通過(guò)AirPlay輸出的設(shè)備
    • BluetoothLE:低能耗藍(lán)牙輸出設(shè)備
  • 輸入輸出端口:
    • BluetoothHFP:使用HFP的藍(lán)牙設(shè)備
    • USBAudio:使用USB接口連接的設(shè)備
    • CarAudio:車(chē)載輸出設(shè)備

TODO: - 無(wú)線(xiàn)耳機(jī)的麥克風(fēng)?

注意:音頻線(xiàn)路切換可能會(huì)導(dǎo)致音頻會(huì)話(huà)的采集率、I/O緩沖時(shí)間、聲道數(shù)量和其他硬件屬性的變化。如果這些對(duì)于應(yīng)用的影響較大,需要進(jìn)行兼容操作。

5. 配置物理設(shè)備

在音頻硬件運(yùn)行過(guò)程中,開(kāi)發(fā)者可以通過(guò)使用音頻會(huì)話(huà)的屬性進(jìn)行運(yùn)行優(yōu)化。比如:

  • 指定適合硬件的采樣率和I/O緩沖時(shí)長(zhǎng)
  • 獲取輸入輸出的延遲、輸入輸出的的聲道數(shù)、硬件采樣率、硬件音量設(shè)置和音頻輸入的可用性

5.1 推薦音頻硬件值

如果需要獲取高質(zhì)量音頻就需要設(shè)置一個(gè)高采樣率,除非出現(xiàn)文件過(guò)大或緩沖過(guò)大問(wèn)題

配置 推薦采樣率 推薦I/O緩沖時(shí)長(zhǎng)
高質(zhì)量 Example: 48 kHz,+ High audio quality,– Large file or buffer size Example: 500 mS,+ Less-frequent file access, – Longer latency
低質(zhì)量 Example: 8 kHz,+ Small file or buffer size,– Low audio quality Example: 5 mS, + Low latency,– Frequent file access

注意:默認(rèn)音頻輸入輸出緩沖時(shí)間為大多數(shù)應(yīng)用提供足夠的相應(yīng)時(shí)間,如44.1kHz音頻大概為20ms響應(yīng)一次。設(shè)置更低的延遲,相應(yīng)數(shù)據(jù)量每次過(guò)來(lái)的也會(huì)降低,因此需要根據(jù)自己的需要設(shè)置。

5.2 配置音頻硬件值

開(kāi)發(fā)者需要在音頻會(huì)話(huà)激活前配置相關(guān)音頻硬件值,如果在音頻會(huì)話(huà)激活后調(diào)整硬件值則需要先停用,再設(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)")

注意:當(dāng)兩個(gè)沖突的音頻會(huì)話(huà)同時(shí)配置硬件值,系統(tǒng)會(huì)優(yōu)先配置不允許混合的會(huì)話(huà)。因此Ambient類(lèi)別或MixWithOthers類(lèi)別選項(xiàng)的音頻會(huì)話(huà)不會(huì)被優(yōu)先設(shè)置。

5.3 選擇并配置麥克風(fēng)

一個(gè)iOS設(shè)備中會(huì)有兩個(gè)或多個(gè)麥克風(fēng),系統(tǒng)會(huì)根據(jù)配置的音頻會(huì)話(huà)模式自動(dòng)選擇麥克風(fēng)。開(kāi)發(fā)者也可以手動(dòng)選擇麥克風(fēng),甚至選擇麥克風(fēng)的極性模式(polar pattern)。

  • 配置偏好輸入:一個(gè)設(shè)備中可能有許多可用的麥克風(fēng)種類(lèi)。

    • 通過(guò)音頻會(huì)話(huà)的availableInputs屬性獲取內(nèi)置或鏈接的輸入麥克風(fēng),這個(gè)屬性返回的是一個(gè)AVAudioSessionPortDescription數(shù)組。這個(gè)AVAudioSessionPortDescription對(duì)象類(lèi)似于音頻線(xiàn)路的AVAudioSessionPort。
    • 通過(guò)setPreferredInput:error:方法配置上面數(shù)組中唯一一個(gè)對(duì)象
  • 配置偏好數(shù)據(jù)源:一些端口,例如內(nèi)置麥克風(fēng)和一些USB鏈接設(shè)備,支持?jǐn)?shù)據(jù)源配置,即設(shè)備上有不同的麥克風(fēng)。

    • 在選中的AVAudioSessionPortDescription能通過(guò)dataSources屬性獲取它們可用的數(shù)據(jù)源數(shù)組,比如內(nèi)置的麥克風(fēng)會(huì)有多個(gè)獨(dú)立的麥克風(fēng)組成,這些麥克風(fēng)使用數(shù)據(jù)源描述信息的location屬性(upper, lower)和orientation屬性(top,bottom,front, back,left,right)共同描述。
    • 通過(guò)setPreferredDataSource:error:方法配置對(duì)應(yīng)的數(shù)據(jù)源對(duì)象
  • 配置偏好極性模式:極性模式定義了其對(duì)聲音相對(duì)于聲源方向的靈敏度。

    • 在選中的AVAudioSessionDataSourceDescription能通過(guò)supportedPolarPatterns獲取可用的極性模式,如心形或全向。
    • 通過(guò)setPreferredPolarPattern:error:方法配置對(duì)應(yīng)的極性模式

對(duì)象范圍:AVAudioSessionPortDescription > AVAudioSessionDataSourceDescription

以下代碼為手動(dòng)配置麥克風(fēng)全過(guò)程:

// 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]")")
    }
}

5.4 模擬器運(yùn)行

我們可以在模擬器或真機(jī)上運(yùn)行添加了音頻會(huì)話(huà)的應(yīng)用程序,但是模擬器不會(huì)模擬音頻會(huì)話(huà)不同進(jìn)程或音頻線(xiàn)切換的大多數(shù)交互,比如:

  • 調(diào)用打斷
  • 模擬插入或拔出耳機(jī)
  • 更改靜音開(kāi)關(guān)的設(shè)置
  • 模擬屏幕鎖定
  • 測(cè)試音頻混合行為 - 即播放音頻以及來(lái)自其他應(yīng)用(例如音樂(lè)應(yīng)用)的音頻

附:模擬器判斷的代碼:

#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

6. 保護(hù)用戶(hù)隱私

為了保護(hù)用戶(hù)隱私,在錄制音頻前我們必須向用戶(hù)詢(xún)問(wèn)并獲取使用麥克風(fēng)的權(quán)限。如果用戶(hù)不給權(quán)限,我們只能錄制靜音。向用戶(hù)咨詢(xún)麥克風(fēng)權(quán)限有兩種方式,一種是在用戶(hù)首次使用麥克風(fēng)功能時(shí)會(huì)自動(dòng)提醒用戶(hù),另一種是通過(guò)requestRecordPermission:方法手動(dòng)請(qǐng)求權(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開(kāi)始,所有訪(fǎng)問(wèn)麥克風(fēng)的應(yīng)用都必須靜態(tài)聲明其意圖。為此,應(yīng)用程序必須在其Info.plist文件中添加NSMicrophoneUsageDescription鍵,并為其提供目的用途字符串。當(dāng)系統(tǒng)提示用戶(hù)允許訪(fǎng)問(wèn)時(shí),此字符串將顯示為警報(bào)提示的一部分。如果應(yīng)用程序嘗試訪(fǎng)問(wèn)任何設(shè)備的麥克風(fēng)而沒(méi)有此鍵值,則應(yīng)用程序?qū)⒅苯娱W退。

參考文獻(xiàn)

Audio Session Programming Guide

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容