Ffmpeg和SDL教程(六)同步音頻

本文轉:http://blog.csdn.net/jinhaijian/archive/2010/08/24/5834269.aspx

 

同步音頻

現在我們已經有了一個比較像樣的播放器。所以讓我們看一下還有哪些零碎的東西沒處理。上次,我們掩飾了一點同步問題,也就是同步音頻到視頻而不是其它的同步方式。我們將採用和視頻一樣的方式:做一個內部視頻時鐘來記錄視頻線程播放了多久,然後同步音頻到上面去。後面我們也來看一下如何推而廣之把音頻和視頻都同步到外部時鐘。

 

生成一個視頻時鐘

現在我們要生成一個類似於上次我們的聲音時鐘的視頻時鐘:一個給出當前視頻播放時間的內部值。開始,你可能會想這和使用上一幀的時間戳來更新定時器一樣簡單。但是,不要忘了視頻幀之間的時間間隔是很長的,以毫秒爲計量的。解決辦法是跟蹤另外一個值:我們在設置上一幀時間戳的時候的時間值。於是當前視頻時間值就是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`

然後你就可以運行它了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章