同步音频html
如今咱们已经有了一个比较像样的播放器。因此让咱们看一下还有哪些零碎的东西没处理。上次,咱们掩饰了一点同步问题,也就是同步音频到视频而不是其它的同步方式。咱们将采用和视频同样的方式:作一个内部视频时钟来记录视频线程播放了多久,而后同步音频到上面去。后面咱们也来看一下如何推而广之把音频和视频都同步到外部时钟。ide
生成一个视频时钟函数
如今咱们要生成一个相似于上次咱们的声音时钟的视频时钟:一个给出当前视频播放时间的内部值。开始,你可能会想这和使用上一帧的时间戳来更新定时器同样简单。可是,不要忘了视频帧之间的时间间隔是很长的,以毫秒为计量的。解决办法是跟踪另一个值:咱们在设置上一帧时间戳的时候的时间值。因而当前视频时间值就是PTS_of_last_frame + (current_time - time_elapsed_since_PTS_value_was_set)。这种解决方式与咱们在函数get_audio_clock中的方式很相似。ui
所在在咱们的大结构体中,咱们将放上一个双精度浮点变量video_current_pts和一个64位宽整型变量video_current_pts_time。时钟更新将被放在video_refresh_timer函数中。spa
void video_refresh_timer(void *userdata) {命令行
if(is->video_st) {线程 if(is->pictq_size == 0) {code schedule_refresh(is, 1);component } else {orm 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` |
而后你就能够运行它了。
下次咱们要作的是让你可让电影快退和快进。