
1. 音视频同步这件事为什么在嵌入式录像里格外难搞做过嵌入式录像的人都有一个共同体会视频画面单独跑没问题音频单独录也没问题可一旦要把两路合到一起唇形对不上、声音比画面快半拍、录久了越偏越多这些问题就全冒出来了。我前后在几个基于 ARM-Linux 的录像项目里踩过这个坑从最早用 OV7670 这类不带 FIFO 的传感器直接怼到后来上 V4L2 拿时间戳、自己搭音频采集链路最后真正把 A/V 对齐靠的不是什么高深算法而是把音频链路拆成了三级 FIFO让每一级各管一段节奏。先把话说清楚这篇讲的“音视频同步”指的是在嵌入式设备上同时采集摄像头视频和麦克风/线路音频最终封装成一份播放时唇形一致的录像文件。核心手段是时间戳对齐加三级 FIFO 缓冲。适合谁看正在做嵌入式 Linux 录像、行车记录、工业视觉记录、教学录播这类项目的开发者尤其是被 A/V 漂移折磨过、又不想上重型框架的人。如果你还在用“采集完再统一对齐”的粗暴思路那大概率会和我早期一样录十分钟偏出去好几百毫秒。为什么嵌入式场景特别难因为这里没有 PC 上那么富裕的 CPU 和内存也没有成熟的媒体框架帮你兜底。视频这边V4L2 的 buffer 是有限的摄像头出帧节奏受曝光、分辨率、MIPI/LVDS 带宽影响音频这边ALSA 的 period 和 buffer 大小直接决定你多久拿一次数据采样率还未必和硬件晶振完全一致。两边各自有各自的时钟各自有各自的抖动你硬把它们往一个时间轴上塞不对齐才是正常的。所以真正要解决的不是“怎么对齐”而是“怎么让两路数据在进入封装前各自都有一个稳定、可预期的时间基准”。三级 FIFO 的思路本质上就是把“采集抖动”和“封装节奏”解耦。第一级贴着硬件负责吸收驱动层的突发第二级做时间戳重排和速率微调第三级面向封装器提供一个匀速、稳定的数据流。每一级 FIFO 的深度、触发水位、超时策略都不一样这也是为什么不能简单用一个大队列糊弄过去。下面我按实际项目里的设计顺序把整套方案拆开讲。2. 整体设计为什么是三级 FIFO而不是一个大缓冲2.1 从一次“录久了就偏”的翻车说起最早那版方案特别朴素视频线程往一个队列里塞帧音频线程往另一个队列里塞 PCM封装线程从两个队列各取一份按到达顺序写文件。测试五分钟没问题录到二十分钟声音开始比画面快越到后面越明显。当时第一反应是“队列太小丢数据了”于是把队列从 8 帧加到 64 帧结果漂移没解决延迟反而上去了实时预览卡得没法看。后来用示波器和日志一起抓才发现问题根本不在队列大小。视频帧的到达间隔是 33ms 上下但抖动能有 ±8ms音频 period 是 10ms 一次可 ALSA 偶尔会一次给你两三个 period 的数据。两个线程各自按“我拿到就写”的逻辑走时间轴自然越拉越歪。更关键的是音频的采样时钟来自板载晶振视频的帧率来自传感器和 SoC 的时钟域两者天生就有 ppm 级的频差这个频差在长时间录制里会累积成肉眼可见的偏移。所以问题的本质是你需要一个机制既能吸收短时抖动又能补偿长时频差还不能引入过大延迟。一个 FIFO 做不到这三件事因为它只能吸收抖动没法做速率调整。三级 FIFO 的分工就清楚了第一级吸收抖动第二级做重排和速率观测第三级做匀速输出和频差补偿。2.2 三级 FIFO 各自的分工与选型逻辑我把三级 FIFO 的职责列成一张表方便对照理解。这里的“级”不是指串联的管道而是指数据从硬件到封装器经过的三个缓冲阶段每一级解决一类问题。层级位置主要职责典型深度触发策略一级 FIFO驱动/采集线程内吸收硬件突发防止丢帧丢样视频 4~8 帧音频 4~8 个 period水位过半即唤醒消费者二级 FIFO同步线程内时间戳重排观测两路速率差视频 8~16 帧音频 16~32 period按时间戳排序超时强制出队三级 FIFO封装线程前匀速输出做频差微调视频 2~4 帧音频 4~8 period按目标时间轴节拍取数据一级 FIFO 贴着硬件深度不能大。视频这边V4L2 的 buffer 数量本来就有限你再多缓冲只会增加端到端延迟音频这边ALSA 的 period 太大采集延迟就上去了实时性差。所以一级 FIFO 的原则是“够吸收一次突发就行”一般视频 4 到 8 帧、音频 4 到 8 个 period 足够。二级 FIFO 是整套方案的核心。它不只是缓冲更是一个“观测站”。在这里我给每一帧视频、每一段音频都打上基于同一时钟源的时间戳然后按时间戳排序。排序的目的不是重新排列数据顺序而是让同步线程能清楚看到“当前视频时间轴走到哪、音频时间轴走到哪、两者差多少”。这个差值就是后续做速率微调的依据。二级 FIFO 深度可以稍大因为它允许一定的重排窗口但也不能无限大否则延迟和内存都受不了。三级 FIFO 面向封装器它的任务是“匀速喂数据”。封装器最怕的就是数据忽多忽少所以三级 FIFO 要做的就是把二级送来的、已经对齐好的数据按目标时间轴的节拍稳定输出。如果发现音频比视频快了就在这里做微调——注意微调不是丢数据而是通过重采样或者调整读取节奏来慢慢拉平避免出现“咔”的一声。2.3 时间戳对齐的基准怎么选三级 FIFO 要工作前提是两路数据有可比的时间戳。这里有个关键选择用哪个时钟做基准常见的有三种系统单调时钟、视频帧的 PTS、音频采样的累计样本数换算时间。我最终选的是系统单调时钟作为统一基准理由是它不受任何一路硬件节奏影响是一个独立、稳定的参考。具体做法是视频帧在出队时用 V4L2 的 buffer 时间戳如果驱动支持或者采集时刻的单调时钟打标音频在每个 period 到达时用同一个单调时钟打标同时记录累计样本数。这样每一段音频既有“到达时间”也有“理论播放时间”两者一对比就能算出音频链路的实际速率。注意有些 V4L2 驱动返回的时间戳是单调时钟有些是实时时钟还有些干脆不填。上板第一件事就是确认时间戳来源用v4l2-ctl --stream-mmap --stream-count10 --verbose看每帧的 timestamp 字段别想当然。选单调时钟还有一个好处它不会因为系统对时、时区调整而跳变。录像这种长时间运行的任务最怕基准时钟中途跳一下那前面所有的对齐都白做了。3. 核心细节每一级 FIFO 到底怎么实现3.1 一级 FIFO贴着硬件做突发吸收一级 FIFO 的实现要贴着具体的采集接口。视频这边我用的是 V4L2 的 mmap 方式申请 4 个 bufferVIDIOC_QBUF全部入队然后VIDIOC_DQBUF取帧。这里有个细节DQBUF 返回的 buffer 里带时间戳但这个时间戳是“帧被采集”的时刻不是“帧被取走”的时刻。如果你在 DQBUF 之后才打自己的时间戳那就把驱动层的抖动也算进去了。正确做法是优先用驱动时间戳只有在驱动不提供时才用 DQBUF 时刻兜底。音频这边ALSA 的snd_pcm_readi一次读一个 period。period 大小怎么定我一般取 10ms 对应的样本数。比如 48kHz 采样、双声道、16 位10ms 就是 480 帧即 1920 字节。period 太小中断太频繁CPU 吃不消period 太大采集延迟高同步窗口就粗。10ms 是一个在嵌入式和实时性之间比较平衡的值实测下来大多数 ARM 板子都能扛住。一级 FIFO 的唤醒策略也值得说。视频线程和音频线程各自往一级 FIFO 里塞数据同步线程从里面取。如果每来一帧就唤醒一次同步线程上下文切换太频繁如果等 FIFO 满了再唤醒延迟又太大。我的做法是设一个水位线比如视频 FIFO 到 4 帧、音频 FIFO 到 4 个 period 就唤醒。这样既不会频繁切换又能保证同步线程及时拿到数据。/* 一级 FIFO 的水位唤醒示意伪代码 */ #define VIDEO_FIFO_WATERMARK 4 #define AUDIO_FIFO_WATERMARK 4 void on_video_frame_ready(frame_t *f) { fifo_push(video_fifo_l1, f); if (fifo_count(video_fifo_l1) VIDEO_FIFO_WATERMARK) { pthread_cond_signal(sync_cond); } } void on_audio_period_ready(pcm_t *p) { fifo_push(audio_fifo_l1, p); if (fifo_count(audio_fifo_l1) AUDIO_FIFO_WATERMARK) { pthread_cond_signal(sync_cond); } }这里有个坑水位唤醒要配合超时。如果视频那边因为光线暗、曝光时间长帧率掉下来了音频 FIFO 可能一直等不到视频数据同步线程就饿死了。所以pthread_cond_timedwait必须带超时比如 50ms超时后即使水位没到也要处理已有数据。3.2 二级 FIFO时间戳重排与速率观测二级 FIFO 是同步线程的主战场。同步线程被唤醒后把一级 FIFO 里的数据搬到二级 FIFO同时做两件事一是给每份数据打上统一基准的时间戳二是按时间戳排序。排序这件事听起来简单做起来有讲究。视频帧的时间戳是采集时刻音频 period 的时间戳也是采集时刻但音频一个 period 覆盖 10ms它的时间戳应该代表这段音频的起始时刻还是中间时刻我选的是起始时刻因为这样和视频帧的“曝光开始时刻”语义一致。如果你用中间时刻那音频天然就比视频“晚”了 5ms这个偏差在唇形同步上刚好处于可察觉的边缘。排序窗口不能无限大。我的做法是维护一个按时间戳排序的链表新数据插入时找到合适位置同时检查队首数据的时间戳是否已经比“当前视频时间轴”落后太多。如果落后超过一个阈值比如 200ms说明这帧视频或这段音频已经太旧了再等下去只会增加延迟这时候就强制出队交给三级 FIFO 处理。速率观测是二级 FIFO 的另一个核心任务。具体怎么观测我维护两个累计量视频累计帧数和音频累计样本数以及它们各自对应的基准时间跨度。每隔一段时间比如每秒一次计算视频的实际帧率和音频的实际样本率和标称值对比。如果音频实际样本率比标称值高了 0.1%那就说明音频时钟偏快长时间录下去音频会越来越超前。/* 速率观测示意 */ typedef struct { uint64_t base_time_us; /* 基准时间微秒 */ uint64_t video_frames; /* 累计视频帧数 */ uint64_t audio_samples; /* 累计音频样本数 */ double nominal_fps; /* 标称帧率 */ double nominal_sample_rate; /* 标称采样率 */ } rate_observer_t; void observe_rate(rate_observer_t *obs, uint64_t now_us) { uint64_t elapsed now_us - obs-base_time_us; if (elapsed 1000000) return; /* 每秒统计一次 */ double actual_fps obs-video_frames * 1e6 / elapsed; double actual_sr obs-audio_samples * 1e6 / elapsed; /* 与标称值对比得到偏差比例供三级 FIFO 微调 */ double fps_dev actual_fps / obs-nominal_fps - 1.0; double sr_dev actual_sr / obs-nominal_sample_rate - 1.0; /* 重置统计窗口 */ obs-base_time_us now_us; obs-video_frames 0; obs-audio_samples 0; }这个偏差比例就是三级 FIFO 做微调的依据。注意偏差要平滑处理不能拿单次统计结果直接去调否则会震荡。我一般用滑动平均窗口取 5 到 10 秒这样调出来的节奏比较稳。3.3 三级 FIFO匀速输出与频差补偿三级 FIFO 面向封装器核心目标是“匀速”。封装器比如自己写的 MP4 muxer 或者 FFmpeg 的 API最理想的情况是视频按固定帧率来音频按固定样本率来时间戳严格递增。三级 FIFO 要做的就是制造这种理想情况。具体做法是同步线程根据二级 FIFO 给出的对齐结果按目标时间轴的节拍往三级 FIFO 里放数据。视频这边如果目标帧率是 30fps那就每 33.33ms 放一帧音频这边按目标采样率换算每 10ms 放一个 period。如果实际数据比目标快了就稍微等一下如果慢了就从二级 FIFO 里多取一点补上。频差补偿在这里落地。假设观测到音频时钟偏快 0.1%那目标采样率就微调为标称值乘以 1.001让音频“稍微慢一点消费”从而和视频拉平。这个微调不是改硬件采样率而是在读取节奏上做文章。对于音频更精细的做法是重采样但重采样计算量大嵌入式上要谨慎。我的经验是ppm 级的偏差用读取节奏微调就够了只有偏差大到千分之一以上才考虑重采样。提示三级 FIFO 的深度要小视频 2 到 4 帧、音频 4 到 8 个 period 即可。深度太大微调的响应就慢频差补偿会滞后。三级 FIFO 还要处理“数据不足”的情况。如果二级 FIFO 里暂时没有对齐好的数据三级 FIFO 不能空转否则封装器会以为流结束了。这时候要么插入静音帧音频和重复帧视频要么让封装器等待。我倾向于让封装器等待因为插帧会引入额外的时间戳问题处理不好反而更乱。4. 实操过程从采集到封装的完整链路4.1 环境准备与关键参数确认动手之前先把环境摸清楚。我用的是 ARM-Linux 平台内核里 V4L2 和 ALSA 都是标配。第一步是确认摄像头支持哪种采集方式。OV7670 这类不带 FIFO 的传感器通常通过并口或者 MIPI 接入驱动层可能不支持时间戳这时候就得自己在 DQBUF 时打单调时钟。带 FIFO 的传感器或者 MIPI 摄像头驱动一般会填时间戳优先用驱动的。确认命令很简单# 查看视频设备支持的格式和帧率 v4l2-ctl -d /dev/video0 --list-formats-ext # 抓 10 帧看时间戳 v4l2-ctl -d /dev/video0 --stream-mmap --stream-count10 --verbose # 查看音频设备参数 arecord -D hw:0,0 --dump-hw-params /dev/null音频这边重点看支持的采样率、声道数、period 范围。--dump-hw-params会列出 buffer size 和 period size 的最小值、最大值选一个居中的值别贴着边界选。时间戳基准统一用CLOCK_MONOTONIC。在代码里用clock_gettime(CLOCK_MONOTONIC, ts)取时间单位换算成微秒方便后续计算。4.2 视频采集线程的实现要点视频线程的职责很单一不断 DQBUF拿到帧后打时间戳塞进一级 FIFO。这里有几个实操细节。第一buffer 数量。V4L2 的 buffer 不是越多越好。我一般申请 4 个刚好够驱动流水线运转。申请太多DQBUF 拿到的帧可能是好几帧之前的时间戳虽然对但端到端延迟大了。第二DQBUF 的超时。VIDIOC_DQBUF默认是阻塞的如果摄像头掉线线程就卡死了。用select或者poll加超时超时后检查设备状态必要时重新初始化。第三时间戳的取舍。如果驱动时间戳可用直接用如果不可用用 DQBUF 返回时刻的单调时钟。但要注意DQBUF 时刻比实际采集时刻晚晚多少取决于驱动和 buffer 数量。这个偏差是固定的可以在标定时测出来后续统一补偿。/* 视频采集线程核心循环伪代码 */ while (running) { struct v4l2_buffer buf {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, buf) 0) { if (errno EAGAIN) continue; handle_error(); } frame_t *f malloc(sizeof(frame_t)); f-data buffers[buf.index].start; f-size buf.bytesused; if (buf.timestamp.tv_sec || buf.timestamp.tv_usec) { f-ts_us buf.timestamp.tv_sec * 1000000ULL buf.timestamp.tv_usec; } else { f-ts_us now_monotonic_us(); } f-index buf.index; fifo_push(video_fifo_l1, f); maybe_wake_sync_thread(); }注意buffer 是 mmap 出来的DQBUF 之后数据还在但 QBUF 回去之后驱动可能覆写。所以塞进 FIFO 的应该是 buffer 索引或者拷贝出来的数据。如果 FIFO 深度不大用索引就行同步线程处理完再 QBUF如果 FIFO 深度大就得拷贝但拷贝有开销要权衡。4.3 音频采集线程与 period 策略音频线程用 ALSA 的snd_pcm_readi读 period。参数设置上我用的是采样率48000Hz格式S16_LE声道2period size480 帧10msbuffer size4 个 period40msperiod size 和 buffer size 的关系要搞清楚。buffer size 是环形缓冲的总大小period size 是一次中断触发读取的量。buffer size 至少要是 period size 的两倍否则容易 overrun。我取 4 倍留足余量。/* ALSA 参数设置示意 */ snd_pcm_hw_params_t *params; snd_pcm_hw_params_alloca(params); snd_pcm_hw_params_any(handle, params); snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); snd_pcm_hw_params_set_channels(handle, params, 2); unsigned int rate 48000; snd_pcm_hw_params_set_rate_near(handle, params, rate, 0); snd_pcm_uframes_t period 480; snd_pcm_hw_params_set_period_size_near(handle, params, period, 0); snd_pcm_uframes_t buffer period * 4; snd_pcm_hw_params_set_buffer_size_near(handle, params, buffer); snd_pcm_hw_params(handle, params);读取时用snd_pcm_readi返回实际读到的帧数。如果返回负值用snd_pcm_recover恢复。每次读到数据后记录累计样本数和单调时钟时间戳塞进一级 FIFO。音频这边有个容易忽略的点snd_pcm_readi返回的帧数可能不等于 period size。ALSA 在某些情况下会返回部分数据这时候累计样本数要按实际返回的算不能按 period size 算否则速率观测就偏了。4.4 同步线程三级 FIFO 的调度中枢同步线程是整个方案的大脑。它被一级 FIFO 的水位唤醒把数据搬到二级 FIFO做时间戳排序和速率观测然后按目标节拍往三级 FIFO 放数据。调度逻辑我写成状态机空闲、填充、输出三个状态。空闲时等水位填充时把一级数据搬到二级并排序输出时按节拍从二级取数据放三级。状态切换由水位和时间共同驱动。/* 同步线程主循环伪代码 */ while (running) { pthread_mutex_lock(sync_mutex); pthread_cond_timedwait(sync_cond, sync_mutex, timeout); /* 阶段一一级到二级 */ drain_l1_to_l2(video_fifo_l1, video_fifo_l2); drain_l1_to_l2(audio_fifo_l1, audio_fifo_l2); /* 阶段二时间戳排序与超时出队 */ sort_l2_by_timestamp(video_fifo_l2); sort_l2_by_timestamp(audio_fifo_l2); force_dequeue_if_stale(video_fifo_l2, now_monotonic_us()); force_dequeue_if_stale(audio_fifo_l2, now_monotonic_us()); /* 阶段三速率观测 */ observe_rate(rate_obs, now_monotonic_us()); /* 阶段四按节拍输出到三级 FIFO */ pump_to_l3(video_fifo_l2, video_fifo_l3, rate_obs); pump_to_l3(audio_fifo_l2, audio_fifo_l3, rate_obs); pthread_mutex_unlock(sync_mutex); }pump_to_l3是核心。它根据目标时间轴和当前三级 FIFO 的水位决定这次放多少数据。如果三级 FIFO 快空了就多放一点如果快满了就少放或不放。同时根据速率观测的偏差微调放数据的节拍。4.5 封装线程与时间戳写入封装线程从三级 FIFO 取数据写进 MP4 或者自定义容器。这里的关键是时间戳的写入。视频帧的 PTS 用它的采集时间戳减去起始时间戳音频的 PTS 用累计样本数除以采样率换算。两者都基于同一个起始点这样播放器才能正确对齐。起始时间戳怎么定我取第一帧视频和第一段音频中较早的那个作为时间轴零点。后续所有时间戳都相对这个零点计算。/* 时间戳换算示意 */ uint64_t video_pts_us frame-ts_us - timeline_start_us; uint64_t audio_pts_us (audio_total_samples * 1000000ULL) / sample_rate;封装器写入时视频和音频的 PTS 要严格递增。如果发现音频 PTS 比上一段小了说明速率微调过头了要回退。这个检查在三级 FIFO 输出时做比在封装器里做更及时。5. 常见问题与排查技巧实录5.1 唇形对不上先查时间戳基准是否统一最常见的现象是唇形对不上声音比画面快或者慢。排查第一步确认两路时间戳是不是同一个基准。我遇到过视频用实时时钟、音频用单调时钟的情况两者差了一个纪元对齐结果完全是乱的。用日志把两路时间戳打出来看它们的差值是否稳定。如果差值一直在变说明基准不统一或者有一路时间戳不可靠。第二步检查音频时间戳的语义。前面说过音频 period 的时间戳应该代表起始时刻。如果代码里用的是读取完成时刻那音频天然就晚了。这个偏差是固定的可以通过在时间戳上减去一个 period 的时长来补偿。第三步看漂移是线性的还是突变的。线性漂移说明是频差靠三级 FIFO 的速率微调解决突变说明是丢帧或者丢样要查采集链路。5.2 音频出现“咔”声多半是 FIFO 水位震荡“咔”声是音频数据不连续的表现。在三级 FIFO 方案里最常见的原因是水位震荡三级 FIFO 一会儿空一会儿满输出节奏忽快忽慢。解决办法是给水位加滞回区间比如低水位 2 个 period、高水位 6 个 period低于低水位才补数据高于高水位才停中间区域保持不动。另一个原因是速率微调太激进。如果每次观测到偏差就立刻调整音频读取节奏会抖。用滑动平均平滑偏差调整步长限制在每次 0.01% 以内让节奏慢慢变化。注意调试音频问题时先把速率微调关掉确认基础链路没问题再逐步打开微调。否则你分不清是链路问题还是微调问题。5.3 视频丢帧导致同步崩掉视频丢帧在嵌入式上很常见尤其是分辨率高、带宽紧张的时候。丢帧本身不可怕可怕的是丢帧后时间戳不连续同步线程按时间戳排序时会出现空洞。如果音频还在正常走视频时间轴就落后了三级 FIFO 会一直等视频数据音频越积越多。我的处理方式是视频丢帧后不试图“补帧”而是让视频时间轴接受这个空洞。同步线程在排序时如果发现视频时间戳跳跃超过一个帧间隔的 1.5 倍就认为丢帧了直接推进视频时间轴到下一个有效帧同时通知三级 FIFO 调整节拍。音频这边不受影响继续按自己的节奏走。这样虽然视频少了几帧但整体时间轴还是对齐的。排查丢帧用v4l2-ctl --stream-mmap --stream-count300 --verbose看 DQBUF 的 sequence 字段如果 sequence 不连续就是驱动层丢了。5.4 常见问题速查表现象可能原因排查方法解决思路唇形逐渐偏移两路时钟频差统计每秒实际帧率/样本率三级 FIFO 速率微调唇形固定偏移时间戳语义不一致打印两路时间戳差值统一时间戳基准和语义音频咔声三级 FIFO 水位震荡记录水位变化曲线加滞回区间平滑微调视频卡顿后不同步丢帧导致时间轴空洞检查 DQBUF sequence接受空洞推进时间轴录制启动时不同步起始时间戳未对齐检查 timeline_start取两路较早时间戳为零点长时间录制内存上涨FIFO 只进不出监控各级 FIFO 深度加超时强制出队5.5 几个我踩过的坑第一个坑一级 FIFO 用指针存 buffer 索引但同步线程处理慢buffer 被驱动覆写了。后来改成同步线程处理完立刻 QBUF并且一级 FIFO 深度限制在 buffer 数量以内。第二个坑速率观测窗口太短偏差抖动大三级 FIFO 跟着抖。后来把窗口拉到 10 秒偏差就稳了。第三个坑音频 period 设置成 5ms中断太频繁CPU 占用高反而影响了视频采集。改回 10ms 后整体就稳了。第四个坑时间戳用 32 位微秒存储录到 71 分钟就溢出了。改成 64 位后解决。这个坑很隐蔽因为前一个小时都正常。6. 一些参数调优的经验值三级 FIFO 这套方案参数没有绝对的最优值但有一些经验范围可以参考。视频一级 FIFO 深度 4 到 8 帧二级 8 到 16 帧三级 2 到 4 帧音频一级 4 到 8 个 period二级 16 到 32 个 period三级 4 到 8 个 period。这些值在 1080p30 加 48kHz 立体声的场景下实测比较稳。速率观测窗口 5 到 10 秒微调步长每次不超过 0.01%滞回区间低水位 2 个 period、高水位 6 个 period。超时强制出队阈值 200ms超过这个值的数据基本没有同步价值了留着只会增加延迟。时间戳统一用 64 位微秒基准用CLOCK_MONOTONIC。起始时间戳取两路较早者。音频时间戳语义用 period 起始时刻。这套参数不是拍脑袋来的是在几块不同的 ARM 板子上反复调出来的。换平台或者换分辨率可能需要微调但大方向不变。我个人的体会是三级 FIFO 的价值不在于缓冲本身而在于它把“抖动吸收”“速率观测”“匀速输出”这三件事分开了每一件都能独立调试。如果你现在还在用一个大队列硬扛不妨试试拆成三级很多之前想不通的漂移问题会一下子变得清晰。