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);
注意,AudioRecord 的 getTimestamp() 多了一个 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 计算端到端延时
端到端延时,就是从「麦克风采集到声音」到「扬声器发出声音」之间的时间差。这是实时音频应用的核心指标。
计算思路是这样的:
- 录制端拿到一个
AudioTimestamp,记录recordFramePosition和recordNanoTime - 播放端拿到一个
AudioTimestamp,记录playFramePosition和playNanoTime - 对齐到同一个时间基准,算出延时
具体公式:
// 假设采样率 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 录制时钟校准
方法完全一样,只是换成 AudioRecord 的 getTimestamp()。
注意:校准不是一次性的。时钟漂移会随着温度、电压变化。我建议每隔几秒重新校准一次,或者用滑动窗口取平均值。
4.6 知识体系图
下面这张图总结了本章的核心逻辑,从时间戳获取到延时计算再到时钟校准,一条线串下来:
4.7 避坑指南
最后,我总结几个实战中容易踩的坑:
- 不要依赖单次时间戳:单次测量误差很大,建议取多次平均。我一般取 10 次,去掉最大最小值再平均。
- 注意时间戳的时效性:
getTimestamp()返回的是过去某个时刻的快照,不是当前时刻。如果你需要当前帧位置,得自己根据时间差推算。 - 低延时模式下时间戳更准:如果你用
setPerformanceMode(AUDIO_PERFORMANCE_MODE_LOW_LATENCY),时间戳的更新频率会更高,精度也更好。 - 有些设备不支持:我遇到过一些低端平板,
getTimestamp()永远返回失败。这时候你得用其他方法估算延时,比如环回测试。
一个小技巧:如果你在做实时耳返,可以用 AudioTimestamp 来动态调整缓冲区大小。当延时波动大时,适当增大缓冲区来防止卡顿;当延时稳定时,减小缓冲区来降低延迟。这个策略我在多个项目里都用过,效果不错。
好了,这一章就到这里。时间戳这个东西,说难不难,说简单也不简单。关键是要理解它背后的硬件行为,而不是死记 API。希望你能在实际项目中用起来。
公众号:蓝海资料掘金营,微信deep3321