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
相關文章
相關標籤/搜索