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)用程序中的音頻使用。

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ò)程:

- MyApp 請(qǐng)求激活 Audio Session,比如APP啟動(dòng)、用戶(hù)點(diǎn)擊UI進(jìn)行錄音與回放
- 系統(tǒng)理解激活請(qǐng)求,比如音頻會(huì)話(huà)的類(lèi)別
- 系統(tǒng)告知Music APP的Audio Session無(wú)效,停止他的音頻播放
- Music App 告知系統(tǒng)他的音頻會(huì)話(huà)已失效
- 系統(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à):

- 激活我們配置好的音頻會(huì)話(huà),開(kāi)始播放我們的音頻
- 突然有一個(gè) FaceTime 的電話(huà)打來(lái),系統(tǒng)激活了 FaceTime App 的音頻會(huì)話(huà)
- 這時(shí)系統(tǒng)停用了我們的音頻會(huì)話(huà),在此時(shí)系統(tǒng)不負(fù)責(zé)我們的音頻播放了
- 系統(tǒng)發(fā)送一個(gè)“打斷開(kāi)始通知”給我們,并告知我們的音頻會(huì)話(huà)已經(jīng)停用了
- 我們接收到“打斷開(kāi)始通知”,并在通知收到后進(jìn)行界面刷新并暫停播放邏輯
- 用戶(hù)此時(shí)忽略了FaceTime電話(huà),這時(shí)系統(tǒng)會(huì)停用FaceTime的音頻會(huì)話(huà),然后重新激活我們的音頻會(huì)話(huà),并發(fā)送一個(gè)“打斷結(jié)束通知”給我們
- 我們接收到“打斷結(jié)束通知”,并在通知收到后進(jìn)行界面刷新并恢復(fù)播放邏輯
- (上圖沒(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é)果:

系統(tǒng)在A(yíng)pp啟動(dòng)后才開(kāi)始決定音頻線(xiàn)路,并開(kāi)始管理激活的線(xiàn)路。
- 用戶(hù)通過(guò)選擇“開(kāi)始錄音”使當(dāng)前情況進(jìn)入左分支
- 在錄音過(guò)程中,用戶(hù)拔出耳機(jī)應(yīng)該停止錄音
- 在錄音過(guò)程中,用戶(hù)插入耳機(jī)應(yīng)該停止錄音
- 用戶(hù)通過(guò)選擇“開(kāi)始回放”使當(dāng)前情況進(jìn)入左分支
- 在回放過(guò)程中,用戶(hù)拔出耳機(jī)后,應(yīng)該暫停播放;
- 在回放過(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ì)象
- 通過(guò)音頻會(huì)話(huà)的
-
配置偏好數(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退。