本文转自:FFmpeg 入门(6):音频同步 | www.samirchen.com

音频同步

上一节我们做了将视频同步到音频时钟,这一节我们反过来,将音频同步到视频。首先,我们要实现一个视频时钟来跟踪视频线程播放了多久,并将音频同步过来。后面我们会看看如何将音频和视频都同步到外部时钟。

实现视频时钟

与音频时钟类似,我们现在要实现一个视频时钟:即一个内部的值来记录视频已经播放的时间。首先,你可能会认为就是简单地根据被显示的最后一帧的 PTS 值来更新一下时间就可以了。但是,不要忘了当我们以毫秒作为衡量单位时视频帧之间的间隔可能会很大的。所以解决方案是跟踪另一个值:我们将视频时钟设置为最后一帧的 PTS 时的时间。这样当前的视频时钟的值就应该是 PTS_of_last_frame + (current_time - time_elapsed_since_PTS_value_was_set)。这个方案和我们前面实现的 get_audio_clock() 类似。

所以,在 VideoState 结构体中我们要添加成员 double video_current_pts 和 int64_t video_current_pts_time,时钟的更新会在 video_refresh_timer() 函数中进行:

void video_refresh_timer(void *userdata) {

	// ... code ...

	if (is->video_st) {
if (is->pictq_size == 0) {
schedule_refresh(is, 1);
} else {
vp = &is->pictq[is->pictq_rindex]; is->video_current_pts = vp->pts;
is->video_current_pts_time = av_gettime(); // ... code ... }

不要忘了在 stream_component_open() 中初始化它:

is->video_current_pts_time = av_gettime();

我们接着就实现 get_video_clock():

double get_video_clock(VideoState *is) {
double delta; delta = (av_gettime() - is->video_current_pts_time) / 1000000.0;
return is->video_current_pts + delta;
}

抽象和封装时钟获取函数

有一点需要我们考虑的是我们不应该把代码写的太耦合,否则当我们需要修改音视频同步逻辑为同步外部时钟时,我们就得修改代码。那在像 FFPlay 那样可以通过命令行选项控制的场景下,就乱套了。所以这里我们要做一些抽象和封装的工作:实现一个包装函数 get_master_clock() 通过检查 av_sync_type 选项的值来决定该选择哪一个时钟作为同步的基准,从而决定去调用 get_audio_clock 或 get_video_clock 还是其他 clock。我们甚至可以使用系统时钟,这里我们叫做 get_external_clock。

enum {
AV_SYNC_AUDIO_MASTER,
AV_SYNC_VIDEO_MASTER,
AV_SYNC_EXTERNAL_MASTER,
}; #define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER double get_master_clock(VideoState *is) {
if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) {
return get_video_clock(is);
} else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {
return get_audio_clock(is);
} else {
return get_external_clock(is);
}
} int main(int argc, char *argv[]) {
// ... code ... is->av_sync_type = DEFAULT_AV_SYNC_TYPE; // ... code ...
}

音频同步实现

现在来到了最难的部分:同步音频到视频时钟。我们的策略是计算音频播放的时间点,然后跟视频时钟做比较,然后计算我们要调整多少个音频采样,也就是:我们需要丢掉多少采样来加速让音频追赶上视频时钟或者我们要添加多少采样来降速来等待视频时钟。

我们要实现一个 synchronize_audio() 函数,在每次处理一组音频采样时去调用它来丢弃音频采样或者拉伸音频采样。但是,我们也不希望一不同步就处理,因为毕竟音频处理的频率比视频要多很多,所以我们会设置一个值来约束连续调用 synchronize_audio() 的次数。当然和前面一样,这里的不同步是指音频时钟和视频时钟的差值超过了我们的阈值。

现在让我们看看当 N 组音频采样已经不同步的情况。而这些音频采样不同步的程度也有很大的不同,所以我们要取平均值来衡量每个采样的不同步情况。比如,第一次调用时显示我们不同步了 40ms,下一次是 50ms,等等。但是我们不会采取简单的平均计算,因为最近的值比之前的值更重要也更有意义,这时候我们会使用一个小数系数 c,并对不同步的延时求和:diff_sum = new_diff + diff_sum * c。当我们找到平均差异值时,我们就简单的计算 avg_diff = diff_sum * (1 - c)。我们代码如下:

// Add or subtract samples to get a better sync, return new audio buffer size.
int synchronize_audio(VideoState *is, short *samples, int samples_size, double pts) {
int n;
double ref_clock; n = 2 * is->audio_st->codec->channels; if (is->av_sync_type != AV_SYNC_AUDIO_MASTER) {
double diff, avg_diff;
int wanted_size, min_size, max_size; //, nb_samples ref_clock = get_master_clock(is);
diff = get_audio_clock(is) - ref_clock; if (diff < AV_NOSYNC_THRESHOLD) {
// Accumulate the diffs.
is->audio_diff_cum = diff + is->audio_diff_avg_coef
* is->audio_diff_cum;
if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
is->audio_diff_avg_count++;
} else {
avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
if (fabs(avg_diff) >= is->audio_diff_threshold) {
wanted_size = samples_size + ((int) (diff * is->audio_st->codec->sample_rate) * n);
min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100);
max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100);
if (wanted_size < min_size) {
wanted_size = min_size;
} else if (wanted_size > max_size) {
wanted_size = max_size;
}
if (wanted_size < samples_size) {
// Remove samples.
samples_size = wanted_size;
} else if (wanted_size > samples_size) {
uint8_t *samples_end, *q;
int nb; // Add samples by copying final sample.
nb = (samples_size - wanted_size);
samples_end = (uint8_t *)samples + samples_size - n;
q = samples_end + n;
while (nb > 0) {
memcpy(q, samples_end, n);
q += n;
nb -= n;
}
samples_size = wanted_size;
}
}
}
} else {
// Difference is too big, reset diff stuff.
is->audio_diff_avg_count = 0;
is->audio_diff_cum = 0;
}
}
return samples_size;
}

这样一来,我们就知道音频和视频不同步时间的近似值了,我们也知道我们的时钟使用的是什么值来计算。所以接下来我们要计算要丢弃或增加多少个音频采样。「Shrinking/expanding buffer code」 部分即:

if (fabs(avg_diff) >= is->audio_diff_threshold) {
wanted_size = samples_size + ((int) (diff * is->audio_st->codec->sample_rate) * n);
min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100);
max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100);
if (wanted_size < min_size) {
wanted_size = min_size;
} else if (wanted_size > max_size) {
wanted_size = max_size;
} // ... code ...

注意 audio_length * (sample_rate * # of channels * 2) 是时长为 audio_length 的音频中采样的数量。因此,我们想要的采样数将是已有的采样数量加上或减去对应于音频偏移的时长的采样数量。我们还会对我们的修正值做一个上限和下限,否则当我们的修正值太大,对用户来说就太刺激了。

修正音频采样数

现在我们要着手校正音频了。你可能已经注意到,我们的 synchronize_audio 函数返回一个采样的大小,这个是告诉我们要发送到流的字节数。因此,我们只需要将采样大小调整为 wanted_size,这样就可以减少采样数。但是,如果我们想要增大采样数,我们不能只是使这个 size 变大,因为这时并没有更多的对应数据在缓冲区!所以我们必须添加采样。但我们应该添加什么采样呢?尝试推算音频是不靠谱的,所以使用已经有的音频来填充即可。这里我们用最后一个音频采样的值填充缓冲区。

if (wanted_size < samples_size) {
// Remove samples.
samples_size = wanted_size;
} else if (wanted_size > samples_size) {
uint8_t *samples_end, *q;
int nb; // Add samples by copying final sample.
nb = (samples_size - wanted_size);
samples_end = (uint8_t *) samples + samples_size - n;
q = samples_end + n;
while (nb > 0) {
memcpy(q, samples_end, n);
q += n;
nb -= n;
}
samples_size = wanted_size;
}

在上面的函数里我们返回了采样的尺寸,现在我们要做的就是用好它:

void audio_callback(void *userdata, Uint8 *stream, int len) {
VideoState *is = (VideoState *)userdata;
int len1, audio_size;
double pts; while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {
// We have already sent all our data; get more.
audio_size = audio_decode_frame(is, &pts);
if (audio_size < 0) {
// If error, output silence.
is->audio_buf_size = 1024;
memset(is->audio_buf, 0, is->audio_buf_size);
} else {
audio_size = synchronize_audio(is, (int16_t *)is->audio_buf, audio_size, pts);
is->audio_buf_size = audio_size; // ... code ...

我们在这里做的就是插入对 synchronize_audio() 的调用,当然也要检查一下这里用到的变量的初始化相关的代码。

最后,我们需要确保当视频时钟作为参考时钟时,我们不去做视频同步操作:

// Update delay to sync to audio if not master source.
if (is->av_sync_type != AV_SYNC_VIDEO_MASTER) {
ref_clock = get_master_clock(is);
diff = vp->pts - ref_clock; // Skip or repeat the frame. Take delay into account FFPlay still doesn't "know if this is the best guess.".
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold) {
delay = 0;
} else if (diff >= sync_threshold) {
delay = 2 * delay;
}
}
}

以上便是我们这节教程的全部内容,其中的完整代码你可以从这里获得:https://github.com/samirchen/TestFFmpeg

编译执行

你可以使用下面的命令编译它:

$ gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`

找一个视频文件,你可以这样执行一下试试:

$ tutorial06 myvideofile.mp4

FFmpeg 入门(6):音频同步的更多相关文章

  1. FFmpeg 入门(5):视频同步

    本文转自:FFmpeg 入门(5):视频同步 | www.samirchen.com 视频如何同步 在之前的教程中,我们已经可以开始播放视频了,也已经可以开始播放音频了,但是视频和音频的播放还未同步, ...

  2. FFmpeg学习6:视音频同步

    在上一篇文章中,视频和音频是各自独立播放的,并不同步.本文主要描述了如何以音频的播放时长为基准,将视频同步到音频上以实现视音频的同步播放的.主要有以下几个方面的内容 视音频同步的简单介绍 DTS 和 ...

  3. FFmpeg 入门(3):播放音频

    本文转自:FFmpeg 入门(3):播放音频 | www.samirchen.com 音频 SDL 提供了播放音频的方法.SDL_OpenAudio 函数用来让设备播放音频,它需要我们传入一个包含了所 ...

  4. [原]如何在Android用FFmpeg+SDL2.0之同步音频

    同步音频的原理可以参考:http://dranger.com/ffmpeg/tutorial05.html  本文是在 [原]如何在Android用FFmpeg+SDL2.0之同步视频 的基础上面继续 ...

  5. FFmpeg 入门(4):线程分治

    本文转自:FFmpeg 入门(4):线程分治 | www.samirchen.com 概览 上一节教程中,我们使用 SDL 的音频相关的函数来支持音频播放.SDL 起了一个线程来在需要音频数据的时候去 ...

  6. FFmpeg 入门(7):Seeking

    本文转自:FFmpeg 入门(7):Seeking | www.samirchen.com 处理 seek 命令 我们将为播放器添加 seek 的能力.这个过程中,我们会看到 av_seek_fram ...

  7. FFmpeg 入门(1):截取视频帧

    本文转自:FFmpeg 入门(1):截取视频帧 | www.samirchen.com 背景 在 Mac OS 上如果要运行教程中的相关代码需要先安装 FFmpeg,建议使用 brew 来安装: // ...

  8. C# 使用ffmpeg.exe进行音频转换完整demo-asp.net转换代码

    C# 使用ffmpeg.exe进行音频转换完整demo-asp.net转换代码 上一篇说了在winform下进行调用cmd.exe执行ffmpeg.exe进行音频转换完整demo.后来我又需要移植这个 ...

  9. FFmpeg入门,简单播放器

    一个偶然的机缘,好像要做直播相关的项目 为了筹备,前期做一些只是储备,于是开始学习ffmpeg 这是学习的第一课 做一个简单的播放器,播放视频画面帧 思路是,将视频文件解码,得到帧,然后使用定时器,1 ...

随机推荐

  1. CSS3 Transform变形(2D转换)

    Transform:对元素进行变形:Transition:对元素某个属性或多个属性的变化,进行控制(时间等),类似flash的补间动画.但只有两个关键贞.开始,结束.Animation:对元素某个属性 ...

  2. MQTT-SN协议乱翻之简要介绍

    前言 这一段时间在翻看MQTT-SN的协议,对针对不依赖于TCP传输的MQTT协议十分感兴趣,总是再想着这货到底是怎么定义的.一系列文章皆有MQTT-SN 1.2协议所拼装组成,原文档地址: MQTT ...

  3. 图论之最短路径(2)——Bellman-Ford算法

    继续最短路径!说说Bellman—Ford算法 思路:假设起点为s,图中有n个顶点和m个边,那么它到任一点(比如i)的最短路径 最多可以有n-1条(没有回路就是n-1条):因为最短路径中不可能包含回路 ...

  4. linux动态查看某组进程状态的办法

    这里记录一下我监控某组进程的解决办法. 1.首先要获取要监控的进程的进程id,如果你要勇ps grep 那你就out了,强大的linux系统有一个pidof命令,用来查找相关进程的进程id,其实还有一 ...

  5. Mysql explain执行计划

    EXPLAIN(小写explain)显示了mysql如何使用索引来处理select语句以及连接表.可以帮助选择更好的索引和写出更优化的查询语句. EXPLAIN + sql语句可以查看mysql的执行 ...

  6. git add -A和git add . 的区别

    git add -A和 git add . git add -u在功能上看似很相近,但还是有所差别. git add . :他会监控工作区的状态树,使用它会把工作时的所有变化提交到暂存区,包括文件内容 ...

  7. angular -- 无刷新做分页

    无刷新做分页参考地址: http://www.jq22.com/demo/angular201707111100/ 示例代码: <!DOCTYPE html> <html lang= ...

  8. 软件IT

    软件IT 周期: 攻防:攻击性板块 范畴:科技板块 业务:软件,互联网,人工智能,区块链... 行情主要因素: 主要问题:新领域,成长不确定性高 投资策略: 个股:科大讯飞,中国软件,用友网络,浪潮信 ...

  9. 【转】SpringMVC+Spring3+Hibernate4开发环境搭建

    原文地址: SpringMVC+Spring3+Hibernate4开发环境搭建

  10. docx4j基本操作

    最近需要用docx4j来对docx进行一些操作,用到的技术是docx4j,这个技术在国内其实用的不是很多,看了一些博主的文章,有些感悟,做了一些总结,如果有疑问或错误之处欢迎交流. 创建包: Word ...