ong>?
同步音頻
現在我們已經有了一個比較像樣的播放器。所以讓我們看一下還有哪些零碎的東西沒處理。上次,我們掩飾了一點同步問題,也就是同步音頻到視頻而不是其它的同步方式。我們將采用和視頻一樣的方式:做一個內部視頻時鐘來記錄視頻線程播放了多久,然后同步音頻到上面去。后面我們也來看一下如何推而廣之把音頻和視頻都同步到外部時鐘。
?
生成一個視頻時鐘
現在我們要生成一個類似于上次我們的聲音時鐘的視頻時鐘:一個給出當前視頻播放時間的內部值。開始,你可能會想這和使用上一幀的時間戳來更新定時器一樣簡單。但是,不要忘了視頻幀之間的時間間隔是很長的,以毫秒為計量的。解決辦法是跟蹤另外一個值:我們在設置上一幀時間戳的時候的時間值。于是當前視頻時間值就是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`
然后你就可以運行它了。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
