"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 latency 和 speaker 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_CALL 或 MODE_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 callback | Oboe onAudioReady | 不能阻塞、不能分配、不能 IO |
| postMessage 18ms 延迟 | AAudio HAL buffer | 通信延迟由底层决定,不是 API 决定 |
| SharedArrayBuffer + Atomics | MMAP 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 能不能跑,而是这条音频路径到底被系统送去了哪里。 真正的低延迟不是设置出来的,是量出来的。
📖 参考证据:
- Oboe audio library 官方主页 — Android 官方文档
- Android 低延迟音频 checklist — 官方优化指南
- Issue #2075: VoIP use case and Low Latency — 23 台设备测试,20 台未命中低延迟输入
- Issue #2123: Galaxy A15 VoiceCommunication 空白音频 — Android 14 设备 InputPreset 异常
- Issue #1814: Android 13 音量控制异常 — Usage::VoiceComm + Speech 音量控制器错误
- Android 音频输入共享官方文档 — 系统通话录音权限限制
🔗 相关文章:
- Web性能无人区:我为什么抛弃postMessage — 不要相信 API 承诺,要测量真实物理延迟
- 逃离 V8 的引力:用 WebAssembly 重写 AudioWorklet 核心引擎 — 确定性延迟 vs JIT 幻觉
- V8 冻结 700ms,AudioWorklet 心跳 2.67ms — 线程隔离的绝对统治力
🔗 完整工程分析:diffserv.xyz/blog/oboe-v…