FFmpeg 入门(6):音频同步

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

音频同步

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

实现视频时钟

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

因此,在 VideoState 结构体中咱们要添加成员 double video_current_ptsint64_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() 中初始化它:ui

is->video_current_pts_time = av_gettime();

咱们接着就实现 get_video_clock()this

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_clockget_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() 的次数。固然和前面同样,这里的不一样步是指音频时钟和视频时钟的差值超过了咱们的阈值。code

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

// 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
相关文章
相关标签/搜索