Android VoIP 的低延迟幻觉:Oboe、AAudio 与 VoiceCommunication 的真实边界

6 阅读10分钟

"LowLatency 只是请求,不是承诺。用公开 Issue 证据拆解 Oboe VoIP 链路里真正的延迟来源。"

0. 你以为你打开了低延迟,其实你只是发了一个请求

Oboe 是 Android 官方推荐的高性能音频 C++ 库,属于 AGDK,目标是在不同 Android 版本上获得尽可能低的音频延迟。它在 Android 8.1/API 27+ 上走 AAudio,旧版本走 OpenSL ES。

官方低延迟 checklist 会建议你:用 Oboe、请求 LowLatency、请求 Exclusive、避免采样率转换、用 callback、不在 callback 里加锁/IO/分配内存。

但 Oboe 的 setPerformanceMode(LowLatency) 文档里写得很清楚:

PerformanceMode::LowLatency — Request a stream with minimal latency. Note that this is a request, and the stream may still have higher latency than expected.

Request. Not promise. Not contract. Not guarantee.

这句话是整篇文章的灵魂。记住它。

而 VoIP 不是普通低延迟播放。VoIP 必须同时面对 VoiceCommunication、通话音量路由、AEC/NS/AGC、蓝牙、麦克风选择、隐私敏感输入、设备厂商实现差异。这条链路上,每一个环节都可能把你的"低延迟"变成"低延迟幻觉"。

1. Oboe 能做什么,不能做什么

1.1 Oboe 的真正价值

Oboe 的核心价值是统一 API 表面,把 AAudio(Android 8.1+)和 OpenSL ES(旧版本)的差异封装掉。你写一次 C++ 代码,它帮你选后端。

// 这是 Oboe 能给你的:统一的 C++ API
oboe::AudioStreamBuilder builder;
builder.setDirection(oboe::Direction::Output);
builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
builder.setSharingMode(oboe::SharingMode::Exclusive);
builder.setFormat(oboe::AudioFormat::I16);
builder.setSampleRate(48000);
builder.setChannelCount(oboe::ChannelCount::Mono);
builder.setDataCallback(myCallback);

oboe::Result result = builder.openStream(stream);

这段代码能编译,能跑,能在很多设备上打开低延迟流。但它不保证任何东西。

1.2 "请求"与"保证"的鸿沟

你以为的实际发生的
setPerformanceMode(LowLatency) → 获得低延迟流只是请求,设备可以降级
setSharingMode(Exclusive) → 独占音频硬件设备可能给不了,自动降成 Shared
setSampleRate(48000) → 避免采样率转换设备原生采样率不同时仍会转
setDataCallback → callback 跑在低延迟路径路径由系统路由决定,不是你决定

API 名字不是物理事实。真正的低延迟不是设置出来的,是量出来的。

2. VoIP 和游戏低延迟不是一回事

2.1 游戏音频只需要关心输出

游戏场景的延迟链路是:

Game Engine → Audio Callback → Speaker
            ↓
        ~10-30ms total

只有一条路径,方向单一,优化目标明确。

2.2 VoIP 是双向实时链路

VoIP 的延迟链路是:

Mic input latency
+ AEC / NS / AGC processing
+ jitter buffer
+ network packetization
+ decoder
+ speaker output latency
+ acoustic echo path

VoIP 不只是 output fast path,而是双向链路。 任何一段链路出问题,用户都会说"有延迟""有回声""声音怪"。

而 Oboe 只帮你优化了上面链路中的 Mic input latencyspeaker output latency 两段。中间的信号处理、网络、抖动缓冲——Oboe 管不了,也没打算管。

3. 最大坑——VoiceCommunication 不等于低延迟

3.1 你自然会这样写

用 Oboe 做 VoIP,你很自然会写出这样的代码:

// 你以为这是"VoIP 正确写法"
if (direction == oboe::Direction::Input) {
  builder.setInputPreset(oboe::InputPreset::VoiceCommunication);
} else {
  builder.setUsage(oboe::Usage::VoiceCommunication);
  builder.setContentType(oboe::ContentType::Speech);
}

逻辑上完全正确:VoiceCommunication 就是为通话场景设计的 preset/usage。 但真实世界里,这个选择可能把你送进系统通信处理路径——这条路径更适合通话质量、回声控制、路由和音量策略,却不一定给你最低 callback 延迟

3.2 公开证据:23 台设备,20 台拿不到低延迟输入

GitHub Oboe issue #2075 是一条珍贵的公开记录:有人在做 WebRTC ADM + Oboe 集成时,发现 VoiceCommunication 输入 preset 下,23 台设备里 20 台拿不到低延迟输入

这不是某一个人的偶发 bug。这是设备矩阵的统计结果。

更糟糕的是:换成 VoiceRecognition 能拿到更低延迟,但会带来音量、音控、路由等副作用——因为 VoiceRecognition 走的是录音音量通道,不是通话音量通道,AEC 参考信号可能不对,用户按音量键调节的也可能是"媒体音量"而不是"通话音量"。

3.3 公开证据:Galaxy A15 上 VoiceCommunication 输入拿到空音频

GitHub Oboe issue #2123 报告了一个更极端的案例:Galaxy A15 / Android 14 上,当 InputPreset 设为 VoiceCommunication 时,麦克风回调收到的音频数据是空白的(静音)。换成其他 preset 有数据。

这意味着:同一套参数,不同设备结果完全不同。

你的代码没有错。Oboe 的 API 用法也没有错。错的是你以为"按文档传参就能得到一致行为"这件事本身。

设备 / Android 版本VoiceCommunication 输入VoiceRecognition 输入低延迟命中
23 台设备矩阵(issue #2075)20 台未命中低延迟部分设备更好3/23
Galaxy A15 / Android 14(issue #2123)空白音频有数据失败
Android 6.0.1–14.0 多设备样本(issue #2075)低延迟命中不稳定部分设备更好需要设备矩阵验证

4. 第二个坑——音量控制和路由的暗礁

4.1 Usage::VoiceCommunication + ContentType::Speech 的意外行为

Oboe issue #1814 报告了一个 Android 13 设备上的异常:当设置 Usage::VoiceCommunication + ContentType::Speech 时,音量控制走错了控制器——用户按音量键,调节的不是他们期望的音量通道。

对游戏来说,最低延迟是几乎唯一目标(只要不失真)。但对 VoIP 来说,你还需要:

  • 正确的通话音量——用户按音量键要能调节通话音量
  • 正确的路由——听筒/扬声器/蓝牙耳机切换符合用户预期
  • 正确的 AEC 参考信号——回声消除能拿到正确的参考音
  • 正确的用户预期——"打电话"的感觉 vs "播放音乐"的感觉

很多时候,最"快"的路径不是最"像电话"的路径。

4.2 蓝牙:低延迟的终极墓地

即使你用 Oboe + AAudio + Exclusive + LowLatency 全部命中,一旦用户切换到蓝牙耳机,延迟立刻爆炸。

链路工程判断风险
有线 / 内置扬声器最容易接近低延迟路径仍受 HAL、buffer、采样率影响
Classic Bluetooth / A2DP通常不是 VoIP 低延迟优先路径编码、缓冲、耳机实现不可控
Bluetooth SCO / HFP更像传统通话路径音质、采样率和系统路由受限
BLE Audio / LC3面向新一代低功耗与通信场景需要系统、手机、耳机共同支持

Oboe 对此无能为力。 蓝牙音频的延迟由耳机硬件、编码协商、Android 蓝牙堆栈共同决定。Oboe 的 LowLatency request 在蓝牙面前,就像在飓风中撑伞——姿态很对,效果为零。

5. 不要碰的边界——系统电话录音

Android 对语音通话中的音频输入共享有明确限制:当 MODE_IN_CALLMODE_IN_COMMUNICATION 表示语音通话活跃时,普通 app 想捕获通话音频会受到严格限制。

捕获 voice call uplink/downlink/both 需要特权 app 和相关权限/音频源。普通第三方 app 做不到。

⚠️ 本文讨论的是应用自己发起的 VoIP/RTC 音频链路,不讨论绕过系统限制录制电话通话。

6. 真正的工程方法——测量,而不是假设

6.1 如何测量 Oboe 流的真实延迟

Oboe 的 calculateLatencyMillis() 可以估算系统已知路径上的输入/输出延迟,但它不是端到端物理真值。外部 DAC、蓝牙耳机、声学路径、设备 HAL 的未知缓冲,都可能不在这个估算里。

真正的端到端测量应该做 loopback:输出一个已知脉冲,通过线缆或声学回路采回输入,再用信号对齐计算 round-trip latency。callback 里只记录最小状态,不做阻塞调用。

// callback 内只做:写入预分配 ring buffer + 记录 frame counter
oboe::DataCallbackResult onAudioReady(
    oboe::AudioStream* stream,
    void* audioData,
    int32_t numFrames
) {
  ringBuffer.write(audioData, numFrames);
  frameCounter += numFrames;
  return oboe::DataCallbackResult::Continue;
}

// callback 外:分析输出脉冲与输入采样的对齐位置
// roundTripFrames = inputPulseFrame - outputPulseFrame
// roundTripMs = roundTripFrames * 1000.0 / sampleRate

Android 官方也提供了 Oboe Loopback 示例(oboe/samples/Loopback),用回路测试测量延迟。这是唯一值得信任的方法

6.2 设备矩阵测试不是可选项

从公开 Issue 证据来看,以下设备维度都会影响 Oboe + VoIP 的真实表现:

  • Android 版本:AAudio 在 Android 8.1 才引入,且各版本 bug 修复不同
  • SoC 厂商:Qualcomm / MediaTek / Samsung Exynos / Google Tensor 的音频 HAL 行为不同
  • ROM 定制:MIUI / One UI / ColorOS / OriginOS 都可能修改音频策略
  • 蓝牙栈:不同耳机 + 不同 Android 版本的协商结果不同

真正的低延迟不是设置出来的,是量出来的。

7. 给 WebRTC ADM 开发者的具体建议

7.1 InputPreset 的选择策略

基于公开证据,以下是更稳妥的 preset 选择策略:

场景推荐 InputPreset风险
VoIP 输入(优先低延迟)VoiceRecognition(然后验证音量/路由)音量通道可能不对
VoIP 输入(优先兼容性)VoiceCommunication低延迟命中率低(issue #2075)
录音(非通话)Generic延迟可能较高
热词检测VoiceRecognition最适合

没有唯一正确答案。 你需要针对你的目标设备矩阵做实际测量。

7.2 回调里的绝对禁忌

无论你用哪个 preset,以下行为在音频 callback 里都是自杀

// 绝对禁止在音频回调里做的事:
// ❌ 分配内存(new / malloc / std::vector push_back)
// ❌ 锁(mutex / critical section)
// ❌ 文件 IO
// ❌ 网络 IO
// ❌ 日志(logcat 在某些设备上有锁)
// ❌ 任何可能触发系统调用的操作

// ✅ 只能做:数字信号处理 + 把数据塞进预分配的环形缓冲区
void onAudioReady(/* ... */) {
  // 预分配的 ring buffer,无锁,无分配
  ringBuffer.write(audioData, numFrames);
}

这条规则不是 Oboe 独有的。如果你做过 Web Audio 的 AudioWorklet 开发,你会觉得非常眼熟——实时音频 callback 的禁忌,从浏览器到 Android 是同一套物理法则

8. 与 Web 前端实时音频的连接

这篇文章不只属于 Android 开发者。如果你做过 Web 前端的实时音频开发,你会发现:

Web 前端Android共同的物理法则
AudioWorklet callbackOboe onAudioReady不能阻塞、不能分配、不能 IO
postMessage 18ms 延迟AAudio HAL buffer通信延迟由底层决定,不是 API 决定
SharedArrayBuffer + AtomicsMMAP buffer零拷贝是终极追求
V8 GC STW 冻结设备 HAL 不一致运行时行为不可预测
Chrome/Safari 差异AAudio/OpenSL ES + 厂商差异碎片化是永恒敌人

同一个物理法则,不同的战场。

不要相信 API 名字,要看真实调度路径、buffer、设备行为和生产证据。

结语:Oboe 值得用,但不值得信

Oboe 是一个优秀的库。它统一了 AAudio 和 OpenSL ES 的 API 差异,降低了 Android 音频开发的入门成本,提供了现代 C++ 的友好接口。

但 Oboe 不能消灭 Android 设备的碎片化。 不能强迫硬件厂商实现低延迟路径。不能让蓝牙变成有线。不能让 VoiceCommunication 在所有设备上走同一条音频路径。

真正可靠的 VoIP 音频工程,不是"打开低延迟模式",而是**"测量每一段链路是否真的低延迟"**。

LowLatency 是申请,不是合同。 VoiceCommunication 解决的是"像通话",不保证"最低延迟"。 Oboe 能统一 API,但不能统一 Android 设备宇宙。 VoIP 的真相不是 callback 能不能跑,而是这条音频路径到底被系统送去了哪里。 真正的低延迟不是设置出来的,是量出来的。


📖 参考证据

🔗 相关文章

🔗 完整工程分析diffserv.xyz/blog/oboe-v…