直播 短视频webrtc声音问题iOS

AudioSession 基础

2020-07-05  本文已影响0人  Seacen_Liu

Overwrite

Apple 系统通过 Audio Session 在应用程序中、应用程序之间和应用程序与硬件之间的管理。通过使用 Audio Session 可以在不了解相关硬件操作下监督管理应用程序中的音频使用。

ASPG_intro_2x.png

AVAudioSession 用途:

0. 音频会话管理音频行为的解释

一个Audio Session是APP与操作系统之间配置音频行为的媒介。在应用启动后,APP会自动提供一个Audio Session的单例,我们通过配置这个单例的行为并激活它,以此来告诉系统该如何使用音频。

1. 配置音频会话

1.1 音频会话类别(Category)

音频会话类别(Category)是应用程序一组音频行为,通过设置类别我们可以向系统指明我们使用音频的意图,主要有以下几个功能:

类别(Category) “铃声/静音”按钮和锁屏控制静音 打断不可混音的APP音频 允许音频输入(录音)和输出(播放)
AVAudioSessionCategoryAmbient Yes No 只有输出
AVAudioSessionCategorySoloAmbient (默认) Yes Yes 只有输出
AVAudioSessionCategoryPlayback No 默认为Yes;重写开关为NO 只有输出
AVAudioSessionCategoryRecord No (锁屏后会继续录制) Yes 只有输入
AVAudioSessionCategoryPlayAndRecord No 默认为Yes;重写开关为NO 输入和输出
AVAudioSessionCategoryMultiRoute No Yes 输入和输出

注意:若想在锁屏或静音模式下继续播放音乐,还需要在App的Info.plist中添加UIBackgroundModes音频键。

一般大部分App只会在应用启动的时候都会根据自身所需设置相应的音频类别,但是我们也可以根据我们的需要在应用修改类别:先停用音频会话再修改音频类别,然后重新激活应用修改。

1.2 音频会话模式(mode)

音频会话模式(mode)用于指明音频配置的功能,是对音频会话类别的关联,告诉系统音频具体的使用用途。随着系统的升级,音频会话类别可能会进行完善,通过模式来指定功能,可以兼容不同的系统实现。当前的音频模式如下图所示:

模式标识 关联类别
AVAudioSessionModeDefault All
AVAudioSessionModeMoviePlayback AVAudioSessionCategoryPlayback
AVAudioSessionModeVideoRecording AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord
AVAudioSessionModeVoiceChat AVAudioSessionCategoryPlayAndRecord
AVAudioSessionModeGameChat AVAudioSessionCategoryPlayAndRecord
AVAudioSessionModeVideoChat AVAudioSessionCategoryPlayAndRecord
AVAudioSessionModeSpokenAudio AVAudioSessionCategoryPlayback
AVAudioSessionModeMeasurement AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayback

在使用音频会话中,我们可以先配置类别作为一个音频基本使用方式,再通过配置模式来指定特定行为。

1.3 音频会话类别选项(Options)

音频会话类别选项(Options),用于配置其他的音频用途。在不同的音频类别中,有这些选项的默认值,但我们可以通过设置其值完成相应功能。其值如下所示:

1.4 配置音频会话

配置音频会话最重要是配置类别,模式和选项都是可选次要的。

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

更多详细配置参考官方文档

2. 音频会话的激活

我们可以通过类别(category)、模式(mode)和选项(options)配置一个Audio Session。为了使我们的配置生效,我们需要激活这个Audio Session。激活音频会话顾名思义就是我们告诉系统,我们需要使用音频设备资源。反过来,系统也可以激活和停用音频会话来告诉我们是否可以使用设备资源。

2.1 系统对音频需求冲突的解决

当你的APP启动时,已经构建好的APP(例如Message、Music、Safari)可能会在后台运行。这些软件都有可能生产音频,例如消息推送的提醒、设置的延时播放等等。如果你将设备想象成一个”飞机场“,那么APP就是里面”正在滑行的飞机“,系统服务就是”控制塔”。我们的应用程序可以发送音频请求并声明自己的优先级,但是最终掌控“停机坪“的最终还是系统。我们在使用 Audio Session 与”控制塔“进行交流难免会遇到冲突,下图展示的是MyApp与Music App的冲突解决的过程:

competing_audio_demands_2x.png
  1. MyApp 请求激活 Audio Session,比如APP启动、用户点击UI进行录音与回放
  2. 系统理解激活请求,比如音频会话的类别
  3. 系统告知Music APP的Audio Session无效,停止他的音频播放
  4. Music App 告知系统他的音频会话已失效
  5. 系统激活 MyApp 的音频会话并允许其可以开始播放

2.2 激活和停用你的音频会话

所有AVFoundation可以在播放和录音的时候自动激活音频会话,但是手动激活音频会话可以给我们一个机会去提前激活音频会话并且能测试是否能激活成功。系统会停用你的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) // pass false to deactivate your audio session
} catch let error as NSError {
    print("Unable to activate audio session:  \(error.localizedDescription)")
}

当我们使用AVFoundation对象(AVPlayer, AVAudioRecorder等),系统会负责在中断结束时重新激活音频会话。然而,如果你注册了通知去重新激活音频会话,你可以验证是否激活成功并且更新用户界面。

很多APP没有必要去明确地停用它们的音频会话,除了VoIP App、导航App和播放录制App。因此一般在使用音频会话都要遵守以下规矩:

总的来说就是用的时候能用,不用的时候必须出来!!!

2.3 检查是否有其他音频在播放?

当我们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. 响应音频打断

我们除了需要配置当前音频会话,还需要应对在音频会话使用中,其他优先级更高的应用程序抢夺系统音频资源的情况。在这种情况下,系统会发出一个“音频打断开始通知”并停用我们的音频会话, 在其他应用程序用完音频资源后,系统会发出一个“音频打断结束通知”并重新激活我们的音频会话。这两个通知的开始到结束就是我们音频会话的打断与恢复,我们需要在代码层面上对这种打断过程做恰当的处理,比如打断开始时暂停播放音频,打断结束后继续播放音频。

3.1 音频打断生命周期

下图描述的是在同一个时间线上,两个音频会话之间的使用音频设备资源的过程,“Your audio session”表示我们配置的音频会话,“Device audio session”表示FaceTime App的音频会话:

audio_session_interrupted_2x.png
  1. 激活我们配置好的音频会话,开始播放我们的音频
  2. 突然有一个 FaceTime 的电话打来,系统激活了 FaceTime App 的音频会话
  3. 这时系统停用了我们的音频会话,在此时系统不负责我们的音频播放了
  4. 系统发送一个“打断开始通知”给我们,并告知我们的音频会话已经停用了
  5. 我们接收到“打断开始通知”,并在通知收到后进行界面刷新并暂停播放逻辑
  6. 用户此时忽略了FaceTime电话,这时系统会停用FaceTime的音频会话,然后重新激活我们的音频会话,并发送一个“打断结束通知”给我们
  7. 我们接收到“打断结束通知”,并在通知收到后进行界面刷新并恢复播放逻辑
  8. (上图没有)如果用户接听了电话,那么我们的音频会话将会被一直挂起,直至用户结束通话。

3.2 音频打断处理方法

我们可以通过注册AVAudioSession发出的打断通知,该通知的名称为AVAudioSessionInterruptionNotification,并且根据里面的AVAudioSessionInterruptionTypeKey值获取打断的状态做出恰当的逻辑处理,保证音频打断结束后能恢复正常。

对于我们使用不同框架,Apple推荐我们以下打断处理方式:

通知监听的代码如下:

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 打断处理

当处理Siri时,应该与其他打断不同。我们在打断期间需要对Siri进行监听,如果用户要求Siri去暂停我们的音频播放,在app收到打断结束的通知时,就不应该自动恢复播放。同时,用户界面需要跟Siri要求的保持一致。

3.4 媒体服务重置处理

媒体服务器在共用服务过程中提供音频和其他多媒体方法,在APP运行中,媒体服务器可能会被重置(不常见)。我们可以通过注册AVAudioSessionMediaServicesWereResetNotification通知去管理媒体服务器重置,接收到通知后,我们需要做以下事情:

注册AVAudioSessionMediaServicesWereLostNotification通知可以在媒体服务器不可用时收到通知。

4. 响应线路切换

用户在听歌的过程中,难免会接上或拔出耳机,在这个过程中,音频的线路就在扬声器和耳机中进行切换,这就是线路切换。一个好的应用程序应该对线路切换就行处理,比如在用户拔下耳机后,进行静音处理避免尴尬。除此之外导致音频线路切换有多种情况,包括用户插拔蓝牙耳机、插拔USB设备等等。

4.1 多种音频硬件线路切换

音频硬件线路是一个有线的音频电子信号线路,下图描述的是在录音和回放过程中,进行耳机的插入和拔出引发一系列的线路切换事件,总共有四种可能的处理结果:

audio_route_change_2x.png

系统在App启动后才开始决定音频线路,并开始管理激活的线路。

  1. 用户通过选择“开始录音”使当前情况进入左分支
  2. 在录音过程中,用户拔出耳机应该停止录音
  3. 在录音过程中,用户插入耳机应该停止录音
  4. 用户通过选择“开始回放”使当前情况进入左分支
  5. 在回放过程中,用户拔出耳机后,应该暂停播放;
  6. 在回放过程中,用户插入耳机后,应该重新播放

4.2 监听音频线路切换

开发者可以通过注册AVAudioSessionRouteChangeNotification通知来获取线路切换事件:

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

通知中的userInfo中提供了线路切换的相关信息,可以通过查询AVAudioSessionRouteChangeReason键来获取切换原因(以下枚举值省略AVAudioSessionRouteChangeReason前缀):

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

开发者还可以通过音频会话中的currentRoute属性获取当前的音频线路,通过userInfo中的AVAudioSessionRouteChangePreviousRouteKey获取上一个音频线路:

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

具体的音频线路(以下名称省略AVAudioSessionPort前缀):

TODO: - 无线耳机的麦克风?

注意:音频线路切换可能会导致音频会话的采集率、I/O缓冲时间、声道数量和其他硬件属性的变化。如果这些对于应用的影响较大,需要进行兼容操作。

5. 配置物理设备

在音频硬件运行过程中,开发者可以通过使用音频会话的属性进行运行优化。比如:

5.1 推荐音频硬件值

如果需要获取高质量音频就需要设置一个高采样率,除非出现文件过大或缓冲过大问题

配置 推荐采样率 推荐I/O缓冲时长
高质量 Example: 48 kHz,+ High audio quality,– Large file or buffer size Example: 500 mS,+ Less-frequent file access, – Longer latency
低质量 Example: 8 kHz,+ Small file or buffer size,– Low audio quality Example: 5 mS, + Low latency,– Frequent file access

注意:默认音频输入输出缓冲时间为大多数应用提供足够的相应时间,如44.1kHz音频大概为20ms响应一次。设置更低的延迟,相应数据量每次过来的也会降低,因此需要根据自己的需要设置。

5.2 配置音频硬件值

开发者需要在音频会话激活前配置相关音频硬件值,如果在音频会话激活后调整硬件值则需要先停用,再设置,最后重新激活。

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)")

注意:当两个冲突的音频会话同时配置硬件值,系统会优先配置不允许混合的会话。因此Ambient类别或MixWithOthers类别选项的音频会话不会被优先设置。

5.3 选择并配置麦克风

一个iOS设备中会有两个或多个麦克风,系统会根据配置的音频会话模式自动选择麦克风。开发者也可以手动选择麦克风,甚至选择麦克风的极性模式(polar pattern)。

对象范围:AVAudioSessionPortDescription > AVAudioSessionDataSourceDescription

以下代码为手动配置麦克风全过程:

// 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 模拟器运行

我们可以在模拟器或真机上运行添加了音频会话的应用程序,但是模拟器不会模拟音频会话不同进程或音频线切换的大多数交互,比如:

附:模拟器判断的代码:

#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. 保护用户隐私

为了保护用户隐私,在录制音频前我们必须向用户询问并获取使用麦克风的权限。如果用户不给权限,我们只能录制静音。向用户咨询麦克风权限有两种方式,一种是在用户首次使用麦克风功能时会自动提醒用户,另一种是通过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键,并为其提供目的用途字符串。当系统提示用户允许访问时,此字符串将显示为警报提示的一部分。如果应用程序尝试访问任何设备的麦克风而没有此键值,则应用程序将直接闪退。

参考文献

Audio Session Programming Guide

上一篇 下一篇

猜你喜欢

热点阅读