ffmpeg文档6:同步音频

指导6:同步音频

同步音频

现在我们已经有了一个比较像样的播放器。所以让我们看一下还有哪些零碎的东西没处理。上次,我们掩饰了一点同步问题,也就是同步音频到视频而不是其它的同步方式。我们将采用和视频一样的方式:做一个内部视频时钟来记录视频线程播放了多久,然后同步音频到上面去。后面我们也来看一下如何推而广之把音频和视频都同步到外部时钟。

生成一个视频时钟

现在我们要生成一个类似于上次我们的声音时钟的视频时钟:一个给出当前视频播放时间的内部值。开始,你可能会想这和使用上一帧的时间戳来更新定时器一样简单。但是,不要忘了视频帧之间的时间间隔是很长的,以毫秒为计量的。解决办法是跟踪另外一个值:我们在设置上一帧时间戳的时候的时间值。于是当前视频时间值就是PTS_of_last_frame + (current_time – time_elapsed_since_PTS_value_was_set)。这种解决方式与我们在函数get_audio_clock中的方式很类似。

所在在我们的大结构体中,我们将放上一个双精度浮点变量video_current_pts和一个64位宽整型变量video_current_pts_time。时钟更新将被放在video_refresh_timer函数中。

void video_refresh_timer(void *userdata) {

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();

不要忘记在stream_component_open函数中初始化它:

is->video_current_pts_time = av_gettime();

现在我们需要一种得到信息的方式:

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或者其它的想使用的获得时钟的函数。我们甚至可以使用电脑时钟,这个函数我们叫做 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);

}

}

main() {

is->av_sync_type = DEFAULT_AV_SYNC_TYPE;

}

同步音频

现在是最难的部分:同步音频到视频时钟。我们的策略是测量声音的位置,把它与视频时间比较然后算出我们需要修正多少的样本数,也就是说:我们是否需要通过丢弃样本的方式来加速播放还是需要通过插值样本的方式来放慢播放?

我们将在每次处理声音样本的时候运行一个synchronize_audio的函数来正确的收缩或者扩展声音样本。然而,我们不想在每次发现有偏差的时候都进行同步,因为这样会使同步音频多于视频包。所以我们为函数synchronize_audio设置一个最小连续值来限定需要同步的时刻,这样我们就不会总是在调整了。当然,就像上次那样,”失去同步”意味着声音时钟和视频时钟的差异大于我们的阈值。

所以我们将使用一个分数系数,叫c,所以现在可以说我们得到了N个失去同步的声音样本。失去同步的数量可能会有很多变化,所以我们要计算一下失去同步的长度的均值。例如,第一次调用的时候,显示出来我们失去同步的长度为40ms,下次变为50ms等等。但是我们不会使用一个简单的均值,因为距离现在最近的值比靠前的值要重要的多。所以我们将使用一个分数系统,叫c,然后用这样的公式来计算差异:diff_sum = new_diff + diff_sum*c。当我们准备好去找平均差异的时候,我们用简单的计算方式:avg_diff = diff_sum * (1-c)。

注意:为什么会在这里?这个公式看来很神奇!嗯,它基本上是一个使用等比级数的加权平均值。我不知道这是否有名字(我甚至查过维基百科!),但是如果想要更多的信息,这里是一个解释http://www.dranger.com/ffmpeg/weightedmean.html或者在http://www.dranger.com/ffmpeg/weightedmean.txt里。

下面是我们的函数:

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);

}

} else {

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;

}

记住audio_length * (sample_rate * # of channels * 2)就是audio_length秒时间的声音的样本数。所以,我们想要的样本数就是我们根据声音偏移添加或者减少后的声音样本数。我们也可以设置一个范围来限定我们一次进行修正的长度,因为如果我们改变的太多,用户会听到刺耳的声音。

修正样本数

现在我们要真正的修正一下声音。你可能会注意到我们的同步函数synchronize_audio返回了一个样本数,这可以告诉我们有多少个字节被送到流中。所以我们只要调整样本数为wanted_size就可以了。这会让样本更小一些。但是如果我们想让它变大,我们不能只是让样本大小变大,因为在缓冲区中没有多余的数据!所以我们必需添加上去。但是我们怎样来添加呢?最笨的办法就是试着来推算声音,所以让我们用已有的数据在缓冲的末尾添加上最后的样本。

if(wanted_size < samples_size) {

samples_size = wanted_size;

} else if(wanted_size > samples_size) {

uint8_t *samples_end, *q;

int nb;

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) {

audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);

if(audio_size < 0) {

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;

我们要做的是把函数synchronize_audio插入进去。(同时,保证在初始化上面变量的时候检查一下代码,这些我没有赘述)。

结束之前的最后一件事情:我们需要添加一个if语句来保证我们不会在视频为主时钟的时候也来同步视频。

if(is->av_sync_type != AV_SYNC_VIDEO_MASTER) {

ref_clock = get_master_clock(is);

diff = vp->pts – ref_clock;

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;

}

}

}

添加后就可以了。要保证整个程序中我没有赘述的变量都被初始化过了。然后编译它:

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

然后你就可以运行它了。

下次我们要做的是让你可以让电影快退和快进。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/268207.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Jzoj4778 数列编辑器

我们发现前四个操作可以用双向链表维护&#xff0c;而最后一个操作有一个限制就是1<k<n&#xff0c;所以可以用一个数组存在光标前面的数&#xff0c;每次维护一下前缀和就好 #include<stdio.h> #include<string.h> #include<algorithm> #define N 100…

Error response from daemon: conflict: unable to delete acdcfe83bcc5 (must be forced) - image is bein

报错如下&#xff0c;使用docker rmi 镜像名 报错了 Error response from daemon: conflict: unable to delete acdcfe83bcc5 (must be forced) - image is being used by stopped container 79e028dc2cbf 解决方法&#xff1a; 加上-f参数&#xff0c;表示强制删除 使用doc…

.NET 并行(多核)编程系列之六 Task基础部分完结篇

.NET 并行(多核)编程系列之六 Task基础部分完结篇 前言&#xff1a;之前的文章介绍了了并行编程的一些基本的&#xff0c;也注重的讲述了Task的一些使用方法&#xff0c;本篇很短&#xff0c;将会结束Task的基础知识的介绍。 本篇的主要议题如下: 1. 获取Task的状态 2. …

Sring AOP(简记)

什么是AOP AOP&#xff08;Aspect-OrientedProgramming&#xff0c;面向方面编程&#xff09;&#xff0c;可以说是OOP&#xff08;Object-Oriented Programing&#xff0c;面向对象编程&#xff09;的补充和完善。OOP允许你定义从上到下的关系&#xff0c;但并不适合定义从左到…

docker 什么是虚悬镜像

什么是虚悬镜像 仓库名、标签都是 none的镜像就是虚悬镜像(dangling image) 这种镜像需要删除&#xff0c;根据镜像ID删除该虚悬镜像 docker rmi -f [IMAGE ID]

2010年06月12日

为什么80%的码农都做不了架构师&#xff1f;>>> 今天装了个rhel 5.5,想制定光盘做yum源&#xff0c;网上找了个资料&#xff1a; rhel 5.x 将YUM指定为光盘--yum配置格式示例 后经过自己测试发现&#xff0c;只需要&#xff1a; 1. mount -o loop rhel-5-server-dv…

2017.10.25

日期计算 时间限制&#xff1a;3000 ms | 内存限制&#xff1a;65535 KB难度&#xff1a;1描述如题&#xff0c;输入一个日期&#xff0c;格式如&#xff1a;2010 10 24 &#xff0c;判断这一天是这一年中的第几天。输入第一行输入一个数N&#xff08;0<N<100&#xff…

如何现实CITRIX XenAPP内容重定向

在使用CITRIX XenAPP为用户交付各大应用时&#xff0c;我们会让到一个问题&#xff0c;比如说&#xff0c;我采用XENAPP交付给用户了MS-OFFICE WORD应用&#xff0c;而此用户在他本机并未安装任何的OFFICE 版本&#xff0c;那么&#xff0c;当用户在打开*.doc/*.docx文件时&…

JAVA-初步认识-第八章-继承-单继承和多重继承

一. 说完了继承的基本概述之后&#xff0c;了解一下在java中它的体现方式。 大家都知道java来自于c&#xff0c;c有多继承&#xff0c;java对其中的多继承进行了改良。为什么不直接支持多继承呢&#xff1f;要牢记&#xff0c;这个原因在后面可以解决很多问题。 二. 观看下面的…

docker重启容器

docker restart 容器id

让你的Silverlight程序部署在任意服务器上

今天在CSDN上逛&#xff0c;看到一篇不错的教程贴&#xff0c;“让你的SilverLight程序部署在任意服务器上”&#xff0c;转到园子里&#xff0c;希望更多朋友受益。 即使是免费的只支持HTML的空间&#xff0c;同样可以部署SilverLight应用。众所周知&#xff0c;SilverLight的…

VS2010小Bug:找不到System.Web.Extensions.dll引用

用上VS 2010的日子&#xff0c;快乐并痛着... 今天本来是想写这篇随笔的&#xff0c;却在处理朝阳无限提交的新模板的CSS时&#xff0c;一打开CSS文件&#xff0c;VS2010就崩溃&#xff0c;于是发现了VS2010的另一个Bug。 这个VS2010的Bug是在将博客园博问的代码从VS2008升级至…

TCP 之 RST 原因分析

5. 往一个对端已经关闭的套接字上写入数据会收到一个RST信号 1.发送端的 发送缓冲区还有数据&#xff0c;但接收端tcp的接收通道已关闭 2. SYN到达某端口但此端口上没有正在监听的服务器。对于UDP,当一个数据报到达目的端口时,该端口没在使用,它将产生一个ICMP端口不可达的信息…

《Windows核心编程》---剪贴板

剪贴板是由系统定义的&#xff0c;并不属于任何一个特定的进程。系统中所有进程都可以访问和设置剪贴板。剪贴板最大的特点就是数据传输没有明确的目标&#xff0c;数据是被动访问的&#xff1b;剪贴板的内容可以被多次访问&#xff0c;直到新的数据写入。剪贴板是一种可供选择…

docker后台守护式启动

docker后台启动 docker run -d 镜像名

浏览器打不开网页,但是还可以聊qq?

电脑网络明明已经连接&#xff0c;但是就是打不开网页&#xff1f;下面介绍下解决方法。 原因&#xff1a;DNS解析导致网页打不开。路由器没有获取到DNS 我们的系统会缓存我们平时用到的一些DNS地址&#xff0c;这个功能主要是加速我们对网络的访问。但是有时候这些缓存的DNS地…

项目重构方案设计

最近接手到一个已经成型的项目&#xff0c;然后我们的任务就是对它进行重构&#xff0c;这个项目是一个功能很齐全的WPF视频播放器&#xff08;附带很多其他功能&#xff09;&#xff0c;在仔细 研究了项目的背景和架构以后&#xff0c;初步做出了一下的重构方案&#xff1a; 目…

docker top查看容器中运行的进程信息

docker top :查看容器中运行的进程信息&#xff0c;支持 ps 命令参数。 语法 docker top [OPTIONS] CONTAINER [ps OPTIONS] 容器运行时不一定有/bin/bash终端来交互执行top命令&#xff0c;而且容器还不一定有top命令&#xff0c;可以使用docker top来实现查看container中正…