持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第24天,点击查看活动详情
本文是讲解当视频时钟设置为主时钟的时候,音视频同步的逻辑。可以通过以下命令设置 视频时钟为主时钟:
ffplay -sync video -i juren-30s.mp4
当视频时钟设置为主时钟的时候,is->av_sync_type 等于 AV_SYNC_VIDEO_MASTER,之前在《FFplay视频同步分析》提到的3处视频同步的代码,全部都会失效,如下:
因此当视频是主时钟的时候,视频流永远不会丢帧,即使视频播放线程卡顿,也不会丢帧。不过如果视频播放线程卡顿,可能会导致某些帧的播放时长缩短。因为一般情况是 0.01s 检查一次,如果卡顿,导致视频播放比预定的时间慢了0.12s,通常一帧的播放时长是0.04s。所以慢了 3 帧,因此这3帧会在0.03s内播放完毕。
当视频时钟设置为主时钟的时候,音频同步的逻辑就会生效,音频同步的逻辑是在 audio_decode_frame() 函数里面的,如下:
synchronize_audio() 函数就是用来实现音频同步逻辑的。af->frame->nb_samples 代表 AVFrame 原来的样本数,而 wanted_nb_samples 代表 AVFrame 调整之后的样本数,通常会增加样本数或者减少样本数。
synchronize_audio() 函数的重点代码如下:
synchronize_audio() 函数的音频同步算法也是类似的套路,把不同步的程度控制在阈值内。
介绍一下 synchronize_audio() 函数里面的一些变量的作用。
1, AV_NOSYNC_THRESHOLD,这个宏是 10,单位是秒,所以如果差异太大,超过10s,直接不调整样本数,跟之前《FFplay视频同步分析》里面的视频同步逻辑一样,都是超过 10s 不进行同步。
2, AUDIO_DIFF_AVG_NB,这是一个数量值,值为 20,也就是至少要用 21 次的不同步差异,才能预估计算音视频的平均时间差(avg_diff)。
3, is->audio_diff_avg_coef,翻译一下,是指 音频 差异 平均 系数,avg 是平均,coef 是系数,这是一个系数变量,值为 0.79432,计算方式如下:
is->audio_diff_avg_coef = exp(log(0.01) / AUDIO_DIFF_AVG_NB);
这个公式其实是求 什么样的一个数,连续乘以自身 20 次,就等于 0.01,计算出来是 0.79432。
0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 * 0.79432 = 0.01
audio_diff_avg_coef 系数在后面是用来进行操作权重的。
4, is->audio_diff_cum,差异累加,会不断累加音视频的差异,计算方式如下:
is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
可以看到,audio_diff_cum 每次累加 diff 之前,之前的累计值都会乘以 audio_diff_avg_coef,这是什么意思呢?
其实这是一种降权的操作,主要作用是让前面的差异权重越来越小,后面的差异权重越来越大。我把 diff 分为 3 次差异讲解。
a_diff 为第一次差异,b_diff 为第二次差异,c_diff 为第三次差异。
is->audio_diff_cum = c_diff * 1 + ((b_diff + (a_diff * 0.79432) ) * 0.79432)
可以看到,最开始的差异 a_diff 乘了两次 0.79 ,所以 a_diff 会变得越来越小,b_diff 乘了一次 0.79432,c_diff 还是原来的 c_diff ,c_diff 的权重最大,是 1。
注意,如果连续 20 次之后,权重就会变成 0.01,
因此,整个累计的逻辑是这样的,假设最新的差异是第 31 次,那第 31 次的差异的权重是 1,而第 21 ~ 30 次差异的权重就是 0.01 ~ 0.79432。而第 0 ~ 20次的权重会比 0.01 还小的。
5, avg_diff,音视频的平均时间差,是用平均时间差来调整样本数的,计算方式如下:
avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
1.0 - is->audio_diff_avg_coef 等于 0.20678,我也不知道为什么要乘以 0.20678,后面补充。
6, is->audio_diff_threshold,音频同步阈值,音视频不同步的程度超过这个值,就会进行干预。计算方式如下:
/* since we do not have a precise anough audio FIFO fullness,
we correct audio sync only if larger than this threshold */
is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;
可以看到,is->audio_hw_buf_size 等于一次回调要取的数据量,所以 audio_diff_threshold 等于音频 sdl_audio_callbacl() 的回调间隔。
注意,音视频同步阈值是一次回调的时间间隔,而不是一帧音频的播放时长。
通常情况,audio_diff_threshold 等于 0.04s。
计算出来 avg_diff 跟 audio_diff_threshold 之后,就可以进行同步操作了。
当音视频的平均时间差大于 音频阈值,就会进行样本数的调整,调整逻辑如下:
if (fabs(avg_diff) >= is->audio_diff_threshold) {
wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
min_nb_samples = ((nb_samples * (100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100));
max_nb_samples = ((nb_samples * (100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100));
wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
}
可以看到,调整样本数的时候,用的是当前的音视频时间差(diff),而不是平均时间差(avg_diff) 。
而 SAMPLE_CORRECTION_PERCENT_MAX 宏的值为 10,所以替换之后如下:
if (fabs(avg_diff) >= is->audio_diff_threshold) {
wanted_nb_samples = nb_samples + (int)(diff * is->audio_src.freq);
min_nb_samples = ((nb_samples * 0.9);
max_nb_samples = ((nb_samples * 1.1);
wanted_nb_samples = av_clip(wanted_nb_samples, min_nb_samples, max_nb_samples);
}
这是为了每次调整样本数,不能把 AVFrame 的样本数减少或者增加超过 10% ,因为音频的连续性很强,调整幅度太大,耳朵容易察觉到。
做下小总结,音频同步的逻辑就是,取 20 次以上的音视频差异 来求权重平均差异(avg_diff),如果权重平均差异 大于 音频阈值(audio_diff_threshold),就调整样本数量。
计算出 wanted_nb_samples 之后,会通过 swr_set_compensation() 函数进行重采样,如下:
if (wanted_nb_samples != af->frame->nb_samples) {
if (swr_set_compensation(is->swr_ctx,
(wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate,
wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate) < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_set_compensation() failed\n");
return -1;
}
}
上面的 is->audio_tgt.freq / af->frame->sample_rate 操作是为了进行采样率单位转换,audio_tgt.freq 是打开的喇叭的音频采样率,可能跟 AVFrame 的不一样。
重采样相关的函数请阅读《音频重采样函数详解》
之前在《sdl_audio_callback音频播放线程分析》说过,音频其实有 3 块内存等待播放,而音视频同步跳转的只是后面两块内存,如下:
由于红色的 audio_hw_buf_size 是 SDL 内部的内存,没有接口操作,所以 audio_decode_frame() 进行音频同步的时候,操作的是 绿色 的 len + audio_write_buf_size。
无论是拉长或缩短绿色部分内存,还是红色部分内存,达到的效果是差不多的。但是我个人觉得,如果操作红色的内存,音频同步会更加实时。
但是由于音频连续性很强,操作绿色的内存也可以,这种情况,我称为 样本补偿右移,本来应该操作左边红色的内存,但是SDL没有接口可以操作,只能操作右边绿色的内存。
至此,音频同步逻辑分析完毕。