在 iOS 中同时保留后台音频播放和语音处理能力,最常见的隐患并不是后台任务权限,而是音频会话和语音处理单元各自按照自己的逻辑调整信号,最后导致人声忽高忽低、背景音乐被不必要地压低。尤其是打开 AVAudioSessionCategoryOptionMixWithOthers 之后,系统的噪声估计会把混入的其他音频流一并统计,自动增益控制和噪声抑制会围绕错误的信噪比做决策。要解决这类问题,需要先把会话配置、模式选择、音频引擎连接方式和 VoiceProcessingIO 的控制路径理清。

混音选项与语音突显模式为什么互相牵制
AVAudioSessionCategoryOptionMixWithOthers 的作用是让当前应用的音频与其他应用的音频同时播放,在播放类和录音播放类会话中都会被使用。它本身不会修改音频数据,但会改变音频调度链路:当系统允许混合输出时,其他应用的声音会进入同一个输出总线。对于只做播放的应用,这通常没有副作用;但对于带 VoiceProcessingIO 的语音链路,情况完全不同。语音处理单元的噪声估计模块会持续分析输入信号中的稳态噪声,混入的外部音乐如果能量集中在低频且节奏明显,很容易被误判为环境噪声。
这种误判会直接干扰联合优化。假设系统认为当前环境的噪声功率较高,噪声抑制模块会提高子带衰减比例,自动增益控制也会为了保证语音可懂度而提高增益。真正的人声在噪声抑制之后已经被削掉一部分能量,AGC 再一放大,残留噪声和齿音更容易变得刺耳。与此同时,后台播放的音乐如果被识别为回声,回声消除模块还会把部分音乐成分从上行语音中移除,造成声道空洞或音量抖动。
所以,语音突显模式通常在 .voiceChat 或 .voicePrompt 对应的系统链路中工作得更好,因为它们内部会优先保留语音频段,并启用专门的回声消除和噪声抑制策略。但问题在于,这两种模式对后台音频播放的兼容策略并不相同。voiceChat 偏重双向通话和低延迟,voicePrompt 更偏向短时语音提示和打断逻辑。使用 playAndRecord 类别并叠加 mixWithOthers 时,需要通过实际设备测试确认系统是否仍然保留语音处理路径,而不是自动降级为普通 I/O。
联合优化 AGC 与 NS 的算法思路
自动增益控制与噪声抑制并不是两个完全独立的模块。一个稳定的语音前端通常先做噪声功率谱估计,再根据信噪比决定每个子带的增益上限,最后把增益分配交给 AGC 做时域平滑。简单说,NS 负责压低噪声底,AGC 负责动态提升语音能量,但 AGC 的提升上限必须受到 NS 噪声估计结果的约束。如果 AGC 的上限过低,人声会显得发闷;如果过高,NS 已经削掉的噪声成分会被重新放大,形成更明显的底噪波动。
在 iOS 上,VoiceProcessingIO 会根据当前 AVAudioSession 的类别和模式自动设置一组 AGC 与 NS 参数。开发者通常更关注三件事:一是不要在混音选项打开时盲目附加额外增益;二是尽量使用系统语音处理链路而不是自己串联多个降噪插件;三是把业务侧的增益补偿放到语音处理之后。例如导航播报场景,可以在语音提示结束时延迟 300 毫秒再恢复背景音乐音量,避免 AGC 在语音结束后把残留音乐冲动。
对于能够访问 VoiceProcessingIO 音频单元的代码路径,可以尝试查询或写入 kAUVoiceIOProperty_VoiceProcessingEnableAGC 和 kAUVoiceIOProperty_VoiceProcessingEnableNS 这两个属性。它们分别对应 AGC 和 NS 的开关。在部分平台上它们是可写的,但在 iOS 某些系统版本上可能返回 kAudioUnitErr_InvalidProperty。遇到这种情况时,不要强行走私有属性,而是把联合优化放到更上层的动态增益补偿中:用 AVAudioEngine 的 mixer 控制输出音量,同时用输入 tap 估计短时能量,形成一个轻量的软限幅器。
可落地的会话与音频引擎配置
配置后台音频播放同时进行语音采集时,先设置 AVAudioSession。推荐使用 playAndRecord 类别、voiceChat 或 voicePrompt 模式,并加入 mixWithOthers,同时根据硬件需求搭配蓝牙选项。具体代码如下:
import AVFoundation
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.mixWithOthers, .allowBluetooth, .allowBluetoothHFP])
try audioSession.setActive(true)
} catch {
print("session error: \(error)")
}
上述代码中,playAndRecord 保证可以同时使用扬声器和麦克风,mixWithOthers 让背景音乐不会被本应用独占输出,voiceChat 则会尽可能走系统的语音处理链路。对于纯语音提示的应用,可以把模式改为 voicePrompt,但要注意 voicePrompt 在后台持续播放时可能会受系统打断策略影响,因此需要结合应用实际场景做选择。
接着配置 AVAudioEngine。通常只要把输入节点按原始格式连接到主混音器即可,不要额外添加降噪插件。iOS 的 VoiceProcessingIO 已经在系统层做了 AGC 和 NS,如果再叠加一个第三方降噪或增益插件,反而会让噪声估计失真。基础连接如下:
let engine = AVAudioEngine()
let inputNode = engine.inputNode
let inputFormat = inputNode.inputFormat(forBus: 0)
engine.connect(inputNode, to: engine.mainMixerNode, format: inputFormat)
engine.prepare()
do {
try engine.start()
} catch {
print("engine error: \(error)")
}
如果业务上需要动态控制上行音量,可以额外挂一个 AVAudioMixerNode。这个 mixer 放在输入之后、主混音器之前,作为补偿增益使用。设置 outputVolume 时建议采用慢启动和慢释放曲线,避免改变系统的 AGC 决策。例如人类语音帧之间通常有 10 到 30 毫秒的自然停顿,业务侧如果每帧直接跳变音量,会破坏 VoiceProcessingIO 内置的噪声跟踪。可以用一个简单的指数平滑:当目标增益小于当前增益时每帧下压 0.2 dB,当目标增益大于当前增益时每帧上抬 0.1 dB。
联调时的关键检查点
后台播放和语音突显联合调试时,单看代码很难发现系统内部是否真的启用了语音处理。可以先从输入输出通道数判断:VoiceProcessingIO 的输入通道数通常固定为 1,部分设备会显示为 1 或 2,但它会比普通 RemoteIO 多一层回声参考。在 AVAudioEngine 中读取 inputNode.inputFormat(forBus: 0) 的 channel count 如果和硬件麦克风不一致,可能说明系统已经切换到语音处理单元。
还需要检查 AVAudioSession 的路由变化通知。后台播放音乐时,如果插入或断开蓝牙耳机,系统会改变当前路由并重新建立音频单元连接。mixWithOthers 选项在路由切换后可能被恢复为默认值,因此在 routeChange 通知中重新设置一次 category 和 mode 是稳妥做法。不要把 AVAudioSession 激活放在 viewDidLoad 这类一次性位置,而应放在应用启动音频服务之前统一配置。
最后,噪声抑制和自动增益控制的最终效果受硬件和系统策略影响很大。如果发现语音发闷,优先降低业务侧的额外增益,而不是关闭 NS。如果发现背景音乐被压得太低,可以检查 session 的 secondaryAudioShouldBeSilencedHint,或者将背景音乐走独立的 AVAudioPlayerNode 并设置较低的输出音量,让系统语音处理链路获得更平稳的参考信号。联合优化的本质是让混音、语音处理、增益补偿三者在同一套信号约束下工作,而不是把每个参数单独调到最满。

