ffplay 音频向视频同步策略分析
ffplay音视频同步分析——音频同步视频
在“视频同步音频”的策略中,我们是通过丢帧或重复显示的方法来达到追赶或等待音频时钟的目的,但在“音频同步视频”时,却不能这样简单处理。
在音频输出时,最小单位是“样本”。音频一般以数字采样值保存,一般常用的采样频率有44.1K,48K等,也就是每秒钟有44100或48000个样本。
视频输出中与“样本”概念最为接近的画面帧,如一个 24fps(frame per second)的视频,一秒钟有24个画面输出,这里的一个画面和音频中的一个样本是等效的。
可以想见,如果对音频使用一样的丢帧(丢样本)和重复显示方案,是不科学的。(音频的连续性远高于视频,通过重复几百个样本或者丢弃几百个样本来达到同步,会在听觉有很明显的不连贯)
主流程
在分析具体的补偿方法的之前,先回顾下音频输出的流程。
在https://zhuanlan.zhihu.com/p/44139512
一文中我们分析过,音频输出的主要模型是:
在 audio_buf 缓冲不足时,audio_decode_frame 会从FrameQueue中取出数据放入audio_buf.
audio_decode_frame 函数有音视频同步相关的控制代码:
//为了方便阅读,以下代码经过简化,只保留音视频同步相关代码
static int audio_decode_frame(VideoState *is)
{
//1. 根据与vidoe clock的差值,计算应该输出的样本数
wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);
//2. 判断是否需要重采样:如果要输出的样本数与frame的样本数不相等,也就是需要适当缩减或添加样本了
if (wanted_nb_samples != af->frame->nb_samples && !is->swr_ctx){
//创建重采样ctx
is->swr_ctx = swr_alloc_set_opts(NULL,
is->audio_tgt.channel_layout,
is->audio_tgt.fmt, is->audio_tgt.freq,
dec_channel_layout,
af->frame->format, af->frame->sample_rate,
0, NULL);
if (!is->swr_ctx || swr_init(is->swr_ctx) < 0) {
return -1;
}
}
//3. 重采样,利用重采样库进行样本的插入或剔除
if (is->swr_ctx) {
uint8_t **out = &is->audio_buf1;
int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate + 256;
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) {
return -1;
}
}
len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
is->audio_buf = is->audio_buf1;
resampled_data_size = len2 * is->audio_tgt.channels * av_get_bytes_per_sample(is->audio_tgt.fmt);
}
else {//如果重采样ctx没有初始化过,说明无需做同步(这里不考虑音频源格式设备不支持的情况)
is->audio_buf = af->frame->data[0];
resampled_data_size = data_size;
}
return resampled_data_size;
}
主要分3个步骤:
根据与vidoe clock的差值,计算应该输出的样本数。由函数synchronize_audio完成。
判断是否需要重采样:如果要输出的样本数与frame的样本数不相等,也就是需要适当缩减或添加样本了。
重采样——利用重采样库进行样本的插入或剔除
可以看到,与视频的处理略有不同,视频的同步控制主要体现在上一帧显示时长的控制,即对frame_timer的控制;而音频是直接体现在输出样本上的控制。
前面提到如果单纯判断某个时刻应该重复样本或丢弃样本,然后对输出音频进行修改,人耳会很容易感知到这一不连贯,体验不好。
这里的处理方式是利用重采样库进行平滑地样本剔除或添加。即在获知要调整的目标样本数wanted_nb_samples后,通过swr_set_compensation和swr_convert的函数组合完成”重采样“。
需要注意的是,因为增加或删除了样本,样本总数发生了变化,而采样率不变,那么假设原先 1s 的声音将被以大于 1s 或小于 1s 的时长进行播放,这会导致声音整体频率被拉低或拉高。直观感受,就是声音变粗或变尖了。
ffplay也考虑到了这点影响,其做法是设定一个最大、最小调整范围,避免大幅度的音调变化。
synchronize_audio
在了解了整体流程后, 就来看下关键函数:synchronize_audio
synchronize_audio 负责根据与video clock的差值计算出合适的目标样本数,通过样本数控制音频输出速度。
static int synchronize_audio(VideoState *is, int nb_samples)
{
int wanted_nb_samples = nb_samples;
/* if not master, then we try to remove or add samples to correct the clock */
if (get_master_sync_type(is) != AV_SYNC_AUDIO_MASTER) {
double diff, avg_diff;
int min_nb_samples, max_nb_samples;
diff = get_clock(&is->audclk) - get_master_clock(is);
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
/* not enough measures to have a correct estimate */
is->audio_diff_avg_count++;
} else {
/* estimate the A-V difference */
avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
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);
}
av_log(NULL, AV_LOG_TRACE, "diff=%f adiff=%f sample_diff=%d apts=%0.3f %f\n",
diff, avg_diff, wanted_nb_samples - nb_samples,
is->audio_clock, is->audio_diff_threshold);
}
} else {
/* too big difference : may be initial PTS errors, so
reset A-V filter */
is->audio_diff_avg_count = 0;
is->audio_diff_cum = 0;
}
}
return wanted_nb_samples;
}
和compute_target_delay一样,这个函数的源码注释也是ffplay里算多的。
这里首先得先理解一个”神奇的算法“。这里有一组变量
audio_diff_avg_coef、audio_diff_avg_count、audio_diff_cum、avg_diff.
我们会发现在开始播放的 AUDIO_DIFF_AVG_NB(20)个帧内,都是在通过公式
is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;
计算累加值audio_diff_cum。按注释的意思是为了得到一个准确的估计值。接着在后面计算与主时钟的差值时,并不是直接求当前时刻的差值,而是根据累加值计算一个平均值:
avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
然后通过这个均值进行校正。
翻阅了一些资料,这个公式的目的应该是为了让越靠近当前时刻的diff值在平均值中的权重越大,不过还没找到对应的数学公式及含义,希望有知道的读者不吝相告。
继续看在计算得到avg_diff后,如何确定要输出的样本数:
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);
时间差值乘以采样率可以得到用于补偿的样本数,加之原样本数,即应输出样本数。另外考虑到上一节提到的音频音调变化问题,这里限制了调节范围在正负10%以内。
所以如果音视频不同步的差值较大,并不会立即完全同步,最多只调节当前帧样本数的10%,剩余会在下次调节时继续校正。
最后,是与视频同步音频时类似地,有一个准同步的区间,在这个区间内不去做同步校正,其大小是 audio_diff_threshold:
is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;
即音频输出设备内缓冲的音频时长。
以上,就是音频去同步视频时的主要逻辑。简单总结如下:
音频追赶、等待视频采样的方法是直接调整输出样本数量
调整输出样本时为避免听觉上不连贯的体验,使用了重采样库进行音频的剔除和添加
计算校正后输出的样本数量,使用了一个”神奇的公式“,其意义和含义还有待进一步确认
来源:https://zhuanlan.zhihu.com/p/44680734
作者:瞎猫
推荐:
Android FFmpeg 实现带滤镜的微信小视频录制功能
全网最全的 Android 音视频和 OpenGL ES 干货,都在这了
FFmpeg + Android AudioRecorder 音频录制编码