4、AudioTimestamp:时间戳的获取与含义、计算端到端延时、校准播放与录制时钟

各位好,欢迎来到第四章。

这一章我们聊聊 AudioTimestamp。说实话,这个 API 在 Android 音频开发里属于那种「看着简单,用起来全是坑」的东西。我最早接触它是在做一款 K 歌 App 的时候,当时为了对齐伴奏和人声的录制时间,折腾了好几个通宵。嗯,后来才发现,问题就出在我对时间戳的理解太浅了。

4.1 AudioTimestamp 是什么?

简单说,AudioTimestamp 是系统提供的一个快照。它告诉你:在某个时刻,音频硬件已经处理到了哪个位置。

它包含两个核心字段:

  • framePosition:硬件已经消费或产生的帧数
  • nanoTime:对应这个帧数时的系统单调时钟(CLOCK_MONOTONIC)

你想想看,有了这两个值,你就能算出音频数据在管道里走了多久。说白了,这就是一个「时间-位置」对。

关键点AudioTimestamp 不是实时的,它是历史快照。你拿到它的时候,硬件可能已经往前跑了。

4.2 如何获取 AudioTimestamp?

获取方式有两种,我分别说一下。

4.2.1 通过 AudioTrack 获取

AudioTrack track = new AudioTrack(...);
AudioTimestamp timestamp = new AudioTimestamp();
int ret = track.getTimestamp(timestamp);

if (ret == AudioTrack.SUCCESS) {
    // timestamp.framePosition 和 timestamp.nanoTime 可用
}

这里有个坑:getTimestamp() 不一定每次都成功。我在项目中遇到过,某些低端设备上这个方法会频繁返回失败。原因?嗯,驱动层的时间戳更新不及时。

4.2.2 通过 AudioRecord 获取

AudioRecord recorder = new AudioRecord(...);
AudioTimestamp timestamp = new AudioTimestamp();
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);

注意,AudioRecordgetTimestamp() 多了一个 TIMEBASE_MONOTONIC 参数。我个人习惯用这个,因为它和系统的 System.nanoTime() 是同一个时间基准,方便后续计算。

曾经踩过的坑:我曾经在某个项目里直接用 System.currentTimeMillis() 去和 nanoTime 做加减,结果算出来的延时忽大忽小。后来才意识到,currentTimeMillis() 可能因为 NTP 同步而跳变,而 nanoTime 是单调递增的。两者不能混用。

4.3 时间戳的含义:你到底拿到了什么?

很多人以为 framePosition 是「当前播放到的位置」。其实不是。

它表示的是:nanoTime 这个时刻,音频硬件已经处理完成的帧数。对于播放来说,就是已经送到 DAC 的帧数;对于录制来说,就是已经从 ADC 拿到的帧数。

举个例子:

  • 你调用 track.write() 写入了 1000 帧
  • 过了一会,你拿到 framePosition = 500
  • 这意味着硬件已经播放了 500 帧,还有 500 帧在缓冲区里排队

所以,AudioTimestamp 本质上是一个「水位标记」。它告诉你数据流到了哪里。

4.4 计算端到端延时

端到端延时,就是从「麦克风采集到声音」到「扬声器发出声音」之间的时间差。这是实时音频应用的核心指标。

计算思路是这样的:

  1. 录制端拿到一个 AudioTimestamp,记录 recordFramePositionrecordNanoTime
  2. 播放端拿到一个 AudioTimestamp,记录 playFramePositionplayNanoTime
  3. 对齐到同一个时间基准,算出延时

具体公式:

// 假设采样率 48000Hz,双声道,16bit
// 录制端
long recordTimeNs = recordTimestamp.nanoTime;
long recordFrame = recordTimestamp.framePosition;

// 播放端
long playTimeNs = playTimestamp.nanoTime;
long playFrame = playTimestamp.framePosition;

// 计算录制端当前时间对应的播放帧位置(线性插值)
// 注意:这里假设播放时钟是稳定的
long estimatedPlayFrame = playFrame + 
    (long)((recordTimeNs - playTimeNs) * sampleRate / 1_000_000_000L);

// 端到端延时(帧数差)
long latencyFrames = estimatedPlayFrame - recordFrame;
double latencyMs = latencyFrames * 1000.0 / sampleRate;

我的经验:这个计算有个前提——录制和播放的采样率必须一致。如果不一致,你得先做重采样,否则算出来的延时全是错的。我曾经在这个问题上浪费了两天,后来发现是采样率没对齐。

4.5 校准播放与录制时钟

为什么需要校准?因为播放和录制用的是不同的时钟源。即使标称都是 48000Hz,实际频率也会有微小偏差。比如播放是 48001Hz,录制是 47999Hz。时间一长,误差就会累积。

校准的方法,说白了就是「用时间戳来估计真实的采样率」。

4.5.1 播放时钟校准

你连续取两个 AudioTimestamp

AudioTimestamp ts1 = new AudioTimestamp();
AudioTimestamp ts2 = new AudioTimestamp();
track.getTimestamp(ts1);
Thread.sleep(100); // 等一段时间
track.getTimestamp(ts2);

long frameDelta = ts2.framePosition - ts1.framePosition;
long timeDeltaNs = ts2.nanoTime - ts1.nanoTime;

// 实际采样率
double actualSampleRate = frameDelta * 1_000_000_000.0 / timeDeltaNs;

这个 actualSampleRate 就是播放端真实的采样率。你拿它和标称值对比,就能知道时钟偏差。

4.5.2 录制时钟校准

方法完全一样,只是换成 AudioRecordgetTimestamp()

注意:校准不是一次性的。时钟漂移会随着温度、电压变化。我建议每隔几秒重新校准一次,或者用滑动窗口取平均值。

4.6 知识体系图

下面这张图总结了本章的核心逻辑,从时间戳获取到延时计算再到时钟校准,一条线串下来:

AudioTimestamp 知识体系 AudioTrack.getTimestamp() AudioRecord.getTimestamp() framePosition nanoTime framePosition nanoTime 端到端延时计算 latency = playFrame - recordFrame 时钟校准:连续采样 → 实际采样率 播放端 录制端

4.7 避坑指南

最后,我总结几个实战中容易踩的坑:

  • 不要依赖单次时间戳:单次测量误差很大,建议取多次平均。我一般取 10 次,去掉最大最小值再平均。
  • 注意时间戳的时效性getTimestamp() 返回的是过去某个时刻的快照,不是当前时刻。如果你需要当前帧位置,得自己根据时间差推算。
  • 低延时模式下时间戳更准:如果你用 setPerformanceMode(AUDIO_PERFORMANCE_MODE_LOW_LATENCY),时间戳的更新频率会更高,精度也更好。
  • 有些设备不支持:我遇到过一些低端平板,getTimestamp() 永远返回失败。这时候你得用其他方法估算延时,比如环回测试。

一个小技巧:如果你在做实时耳返,可以用 AudioTimestamp 来动态调整缓冲区大小。当延时波动大时,适当增大缓冲区来防止卡顿;当延时稳定时,减小缓冲区来降低延迟。这个策略我在多个项目里都用过,效果不错。

好了,这一章就到这里。时间戳这个东西,说难不难,说简单也不简单。关键是要理解它背后的硬件行为,而不是死记 API。希望你能在实际项目中用起来。


公众号:蓝海资料掘金营,微信deep3321