本文转自: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_ptsint64_t video_current_pts_time,时钟的更新会在 video_refresh_timer() 函数中进行:

  1. void video_refresh_timer(void *userdata) {
  2. // ... code ...
  3. if (is->video_st) {
  4. if (is->pictq_size == 0) {
  5. schedule_refresh(is, 1);
  6. } else {
  7. vp = &is->pictq[is->pictq_rindex];
  8. is->video_current_pts = vp->pts;
  9. is->video_current_pts_time = av_gettime();
  10. // ... code ...
  11. }

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

  1. is->video_current_pts_time = av_gettime();

我们接着就实现 get_video_clock()

  1. double get_video_clock(VideoState *is) {
  2. double delta;
  3. delta = (av_gettime() - is->video_current_pts_time) / 1000000.0;
  4. return is->video_current_pts + delta;
  5. }

抽象和封装时钟获取函数

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

  1. enum {
  2. AV_SYNC_AUDIO_MASTER,
  3. AV_SYNC_VIDEO_MASTER,
  4. AV_SYNC_EXTERNAL_MASTER,
  5. };
  6. #define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER
  7. double get_master_clock(VideoState *is) {
  8. if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) {
  9. return get_video_clock(is);
  10. } else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {
  11. return get_audio_clock(is);
  12. } else {
  13. return get_external_clock(is);
  14. }
  15. }
  16. int main(int argc, char *argv[]) {
  17. // ... code ...
  18. is->av_sync_type = DEFAULT_AV_SYNC_TYPE;
  19. // ... code ...
  20. }

音频同步实现

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

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

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

  1. // Add or subtract samples to get a better sync, return new audio buffer size.
  2. int synchronize_audio(VideoState *is, short *samples, int samples_size, double pts) {
  3. int n;
  4. double ref_clock;
  5. n = 2 * is->audio_st->codec->channels;
  6. if (is->av_sync_type != AV_SYNC_AUDIO_MASTER) {
  7. double diff, avg_diff;
  8. int wanted_size, min_size, max_size; //, nb_samples
  9. ref_clock = get_master_clock(is);
  10. diff = get_audio_clock(is) - ref_clock;
  11. if (diff < AV_NOSYNC_THRESHOLD) {
  12. // Accumulate the diffs.
  13. is->audio_diff_cum = diff + is->audio_diff_avg_coef
  14. * is->audio_diff_cum;
  15. if (is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
  16. is->audio_diff_avg_count++;
  17. } else {
  18. avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
  19. if (fabs(avg_diff) >= is->audio_diff_threshold) {
  20. wanted_size = samples_size + ((int) (diff * is->audio_st->codec->sample_rate) * n);
  21. min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100);
  22. max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100);
  23. if (wanted_size < min_size) {
  24. wanted_size = min_size;
  25. } else if (wanted_size > max_size) {
  26. wanted_size = max_size;
  27. }
  28. if (wanted_size < samples_size) {
  29. // Remove samples.
  30. samples_size = wanted_size;
  31. } else if (wanted_size > samples_size) {
  32. uint8_t *samples_end, *q;
  33. int nb;
  34. // Add samples by copying final sample.
  35. nb = (samples_size - wanted_size);
  36. samples_end = (uint8_t *)samples + samples_size - n;
  37. q = samples_end + n;
  38. while (nb > 0) {
  39. memcpy(q, samples_end, n);
  40. q += n;
  41. nb -= n;
  42. }
  43. samples_size = wanted_size;
  44. }
  45. }
  46. }
  47. } else {
  48. // Difference is too big, reset diff stuff.
  49. is->audio_diff_avg_count = 0;
  50. is->audio_diff_cum = 0;
  51. }
  52. }
  53. return samples_size;
  54. }

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

  1. if (fabs(avg_diff) >= is->audio_diff_threshold) {
  2. wanted_size = samples_size + ((int) (diff * is->audio_st->codec->sample_rate) * n);
  3. min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100);
  4. max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100);
  5. if (wanted_size < min_size) {
  6. wanted_size = min_size;
  7. } else if (wanted_size > max_size) {
  8. wanted_size = max_size;
  9. }
  10. // ... code ...

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

修正音频采样数

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

  1. if (wanted_size < samples_size) {
  2. // Remove samples.
  3. samples_size = wanted_size;
  4. } else if (wanted_size > samples_size) {
  5. uint8_t *samples_end, *q;
  6. int nb;
  7. // Add samples by copying final sample.
  8. nb = (samples_size - wanted_size);
  9. samples_end = (uint8_t *) samples + samples_size - n;
  10. q = samples_end + n;
  11. while (nb > 0) {
  12. memcpy(q, samples_end, n);
  13. q += n;
  14. nb -= n;
  15. }
  16. samples_size = wanted_size;
  17. }

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

  1. void audio_callback(void *userdata, Uint8 *stream, int len) {
  2. VideoState *is = (VideoState *)userdata;
  3. int len1, audio_size;
  4. double pts;
  5. while (len > 0) {
  6. if (is->audio_buf_index >= is->audio_buf_size) {
  7. // We have already sent all our data; get more.
  8. audio_size = audio_decode_frame(is, &pts);
  9. if (audio_size < 0) {
  10. // If error, output silence.
  11. is->audio_buf_size = 1024;
  12. memset(is->audio_buf, 0, is->audio_buf_size);
  13. } else {
  14. audio_size = synchronize_audio(is, (int16_t *)is->audio_buf, audio_size, pts);
  15. is->audio_buf_size = audio_size;
  16. // ... code ...

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

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

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

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

编译执行

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

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

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

  1. $ 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. 检测你的php代码执行效率

    在写程序的时候,经常会为是改用empty()还是isset好,或是用单引号还是双引号来显示连接字符串而发出疑问,现在好了.我们其实可以通过程序很科学的得出精确的答案.知道我们的程序到底怎样写效率会更好 ...

  2. c++11——type_traits 类型萃取

    一. c++ traits traits是c++模板编程中使用的一种技术,主要功能:     把功能相同而参数不同的函数抽象出来,通过traits将不同的参数的相同属性提取出来,在函数中利用这些用tr ...

  3. java基础---->多线程之ThreadLocal(七)

    这里学习一下java多线程中的关于ThreadLocal的用法.人时已尽,人世还长,我在中间,应该休息. ThreadLocal的简单实例 一.ThreadLocal的简单使用 package com ...

  4. Windows域的相关操作

    一.windows域账户组操作: net group /domain #查看所有组 net group GROUP-NAME /domain #查看某一个组 net group GROUP-NAME ...

  5. 超级小的web手势库AlloyFinger

    针对多点触控设备编程的Web手势组件,快速帮助你的web程序增加手势支持,也不用再担心click 300ms的延迟了.拥有两个版本,无依赖的独立版和react版本.除了Dom对象,也可监听Canvas ...

  6. 【BZOJ2087】[Poi2010]Sheep 几何+DP

    [BZOJ2087][Poi2010]Sheep Description Lyx的QQ牧场养了很多偶数个的羊,他是Vip,所以牧场是凸多边形(畸形).现在因为他开挂,受到了惩罚,系统要求他把牧场全部分 ...

  7. 【Enterprise Architect 】

    [Enterprise Architect ]Enterprise Architect 8 key {67SC0O95-SZPS-LIG2-YQ8Q-8D2N-KWTD-0W6R-TWDD-KT6RB ...

  8. DFS判断正环

    hdu1217 Arbitrage Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others ...

  9. postgresql----UNION&&INTERSECT&&EXCEPT

    多个SELECT语句可以使用UNION,INTERSECT和EXCEPT进行集合处理,其中UNION用于求并集,INTERSECT用于求交集,EXCEPT用于求差集.用法如下 query1 UNION ...

  10. 170622、springboot编程之JPA操作数据库

    JPA操作数据库 什么事JAP?JPA全称Java Persistence API.JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中. 1.在pom ...