3、AudioTrack与时间戳:AudioTrack的getTimestamp()方法、音频时间戳的获取与校准、音频延迟补偿
好,咱们进入第三章。这一章我打算跟你好好聊聊 AudioTrack 的时间戳。
说实话,在 Android 多媒体开发里,音频时间戳是个特别容易让人头疼的东西。我早期做直播项目时,就吃过它的亏——音画不同步,用户投诉一堆。后来我才明白,问题就出在时间戳的获取和校准上。
这一章,我会把 AudioTrack 的 getTimestamp() 方法、时间戳的校准逻辑,以及延迟补偿这些核心知识点,掰开揉碎了讲给你听。
3.1 getTimestamp():音频时间戳的“官方”入口
AudioTrack 从 API 19 开始提供了 getTimestamp() 方法。它的作用,是返回一个 AudioTimestamp 对象,里面包含两个关键字段:
- framePosition:当前已经写入 AudioTrack 的帧数(注意,是写入,不是播放)
- nanoTime:对应这个帧位置的系统时钟时间(纳秒)
说白了,它告诉你:“在某个时刻,音频数据写到了第几帧。” 这个信息,是音视频同步的基础。
核心理解: getTimestamp() 返回的是“写入端”的时间戳,不是“播放端”的。这个区别很重要,后面我会细讲。
来看一个典型的调用方式:
AudioTimestamp timestamp = new AudioTimestamp();
if (audioTrack.getTimestamp(timestamp)) {
long framePos = timestamp.framePosition;
long timeNs = timestamp.nanoTime;
// 用 framePos 和 timeNs 做同步
}
嗯,代码很简单。但实际项目中,我遇到过不少坑。比如:
- 某些设备上
getTimestamp()返回 false,表示不支持 - 返回的时间戳可能不是实时更新的,有几十毫秒的延迟
- 在低延迟模式下,时间戳的精度会下降
注意: 不要在主线程频繁调用 getTimestamp()。它内部可能涉及跨进程通信,调用太频繁会影响 UI 流畅度。我个人习惯是每 100ms 采样一次。
3.2 音频时间戳的获取与校准
拿到原始时间戳后,不能直接用。为什么?因为 framePosition 是累积值,而 nanoTime 是系统绝对时间。你需要把它们转换成统一的“播放时间轴”。
我常用的校准方法是:
- 记录第一次获取时间戳时的
framePosition0和nanoTime0 - 后续每次获取,计算相对帧偏移:
deltaFrames = framePosition - framePosition0 - 将帧偏移转换成时间:
deltaTimeMs = deltaFrames / sampleRate * 1000 - 最终播放时间 =
nanoTime0 + deltaTimeMs
这样,你就得到了一个以第一次采样为基准的“相对播放时间”。
小技巧: 采样率建议用 AudioTrack 实际配置的采样率,而不是硬编码 44100。有些设备会重采样,导致帧数计算偏差。
我曾经在一个项目里,发现时间戳越走越偏。排查了半天,原来是采样率写死了 48000,但设备实际跑的是 44100。嗯,这种低级错误,犯过一次就记住了。
3.3 音频延迟补偿:让音画真正同步
时间戳校准完了,音画还是不同步?别急,还有最后一关——延迟补偿。
音频从写入到真正从扬声器发出,中间有多个缓冲环节:
- 应用层缓冲区(你写入 AudioTrack 的数据)
- AudioFlinger 的混音缓冲区
- ALSA 驱动缓冲区
- 硬件 DAC 和扬声器的物理延迟
这些加起来,少则几十毫秒,多则上百毫秒。如果不补偿,视频画面会比声音早到,看起来就像“口型对不上”。
补偿的思路其实很简单:
- 估算总延迟
totalLatencyMs - 在视频渲染时,把视频时间戳往后推
totalLatencyMs - 这样视频会“等”一下音频,实现同步
那延迟怎么估算?我一般用两种方法:
| 方法 | 原理 | 精度 | 适用场景 |
|---|---|---|---|
| 固定值法 | 根据设备型号预设一个延迟值 | 低 | 快速验证、低要求场景 |
| 动态测量法 | 播放已知信号,用麦克风回采测量 | 高 | 专业音视频应用 |
动态测量法虽然准,但实现复杂。我早期做的一个 K 歌项目,就用了这个方法:播放一个短脉冲,同时用麦克风录音,计算发出和收到的时间差。嗯,效果不错,但调试过程挺折腾的。
我的建议: 如果项目对同步要求不是特别苛刻(比如视频播放器),用固定值法就够了。Android 设备常见的音频延迟在 30ms~80ms 之间,取个中间值 50ms 通常能接受。
3.4 一张图看懂时间戳与延迟补偿
说了这么多,咱们用一张图把整个流程串起来:
从这张图你可以看到,整个流程分三步:先拿到原始时间戳,然后做相对时间校准,最后加上延迟补偿。每一步都有坑,但每一步也都有解法。
3.5 避坑指南:我踩过的那些雷
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
坑一:getTimestamp() 返回 false
我曾经在一台低端平板上,发现 getTimestamp() 始终返回 false。查了源码才知道,有些设备的 HAL 层没有实现时间戳上报。解决方案是:准备一个 fallback 方案,比如用系统时钟 + 帧计数自己推算时间。
坑二:时间戳跳跃
有一次,时间戳突然跳了 200ms,导致音画瞬间不同步。后来发现是 AudioTrack 在 buffer 耗尽后重新填充时,framePosition 会有一个“跳跃”。解决办法是:检测到跳跃时,重新校准基准点。
坑三:延迟补偿过度
补偿不是越多越好。我见过有人补偿了 100ms,结果声音比画面晚了很多,看起来像“配音”。记住:补偿的目标是让音频和视频的时间轴对齐,不是让音频“拖后腿”。
嗯,这一章的内容就到这里。时间戳这个东西,说难不难,说简单也不简单。关键是要理解它的本质——它只是一个“写入端”的参考,不是“播放端”的真相。理解了这一点,后面的校准和补偿就顺理成章了。