同步音頻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` |
而後你就能夠運行它了。
下次咱們要作的是讓你可讓電影快退和快進。