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 是系统绝对时间。你需要把它们转换成统一的“播放时间轴”。

我常用的校准方法是:

  1. 记录第一次获取时间戳时的 framePosition0nanoTime0
  2. 后续每次获取,计算相对帧偏移:deltaFrames = framePosition - framePosition0
  3. 将帧偏移转换成时间:deltaTimeMs = deltaFrames / sampleRate * 1000
  4. 最终播放时间 = nanoTime0 + deltaTimeMs

这样,你就得到了一个以第一次采样为基准的“相对播放时间”。

小技巧: 采样率建议用 AudioTrack 实际配置的采样率,而不是硬编码 44100。有些设备会重采样,导致帧数计算偏差。

我曾经在一个项目里,发现时间戳越走越偏。排查了半天,原来是采样率写死了 48000,但设备实际跑的是 44100。嗯,这种低级错误,犯过一次就记住了。

3.3 音频延迟补偿:让音画真正同步

时间戳校准完了,音画还是不同步?别急,还有最后一关——延迟补偿。

音频从写入到真正从扬声器发出,中间有多个缓冲环节:

  • 应用层缓冲区(你写入 AudioTrack 的数据)
  • AudioFlinger 的混音缓冲区
  • ALSA 驱动缓冲区
  • 硬件 DAC 和扬声器的物理延迟

这些加起来,少则几十毫秒,多则上百毫秒。如果不补偿,视频画面会比声音早到,看起来就像“口型对不上”。

补偿的思路其实很简单:

  1. 估算总延迟 totalLatencyMs
  2. 在视频渲染时,把视频时间戳往后推 totalLatencyMs
  3. 这样视频会“等”一下音频,实现同步

那延迟怎么估算?我一般用两种方法:

方法 原理 精度 适用场景
固定值法 根据设备型号预设一个延迟值 快速验证、低要求场景
动态测量法 播放已知信号,用麦克风回采测量 专业音视频应用

动态测量法虽然准,但实现复杂。我早期做的一个 K 歌项目,就用了这个方法:播放一个短脉冲,同时用麦克风录音,计算发出和收到的时间差。嗯,效果不错,但调试过程挺折腾的。

我的建议: 如果项目对同步要求不是特别苛刻(比如视频播放器),用固定值法就够了。Android 设备常见的音频延迟在 30ms~80ms 之间,取个中间值 50ms 通常能接受。

3.4 一张图看懂时间戳与延迟补偿

说了这么多,咱们用一张图把整个流程串起来:

音频时间戳获取与延迟补偿流程 getTimestamp() AudioTimestamp framePosition + nanoTime 时间戳校准:相对时间转换 deltaFrames / sampleRate 延迟估算:固定值/动态测量 总延迟 totalLatencyMs 视频时间戳 + 补偿 音画同步:音频时间轴 = 视频时间轴 写入端时间戳 校准后相对时间 补偿后同步时间

从这张图你可以看到,整个流程分三步:先拿到原始时间戳,然后做相对时间校准,最后加上延迟补偿。每一步都有坑,但每一步也都有解法。

3.5 避坑指南:我踩过的那些雷

最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。

坑一:getTimestamp() 返回 false

我曾经在一台低端平板上,发现 getTimestamp() 始终返回 false。查了源码才知道,有些设备的 HAL 层没有实现时间戳上报。解决方案是:准备一个 fallback 方案,比如用系统时钟 + 帧计数自己推算时间。

坑二:时间戳跳跃

有一次,时间戳突然跳了 200ms,导致音画瞬间不同步。后来发现是 AudioTrack 在 buffer 耗尽后重新填充时,framePosition 会有一个“跳跃”。解决办法是:检测到跳跃时,重新校准基准点。

坑三:延迟补偿过度

补偿不是越多越好。我见过有人补偿了 100ms,结果声音比画面晚了很多,看起来像“配音”。记住:补偿的目标是让音频和视频的时间轴对齐,不是让音频“拖后腿”。

嗯,这一章的内容就到这里。时间戳这个东西,说难不难,说简单也不简单。关键是要理解它的本质——它只是一个“写入端”的参考,不是“播放端”的真相。理解了这一点,后面的校准和补偿就顺理成章了。