ong>?
同步音頻
現(xiàn)在我們已經(jīng)有了一個(gè)比較像樣的播放器。所以讓我們看一下還有哪些零碎的東西沒(méi)處理。上次,我們掩飾了一點(diǎn)同步問(wèn)題,也就是同步音頻到視頻而不是其它的同步方式。我們將采用和視頻一樣的方式:做一個(gè)內(nèi)部視頻時(shí)鐘來(lái)記錄視頻線程播放了多久,然后同步音頻到上面去。后面我們也來(lái)看一下如何推而廣之把音頻和視頻都同步到外部時(shí)鐘。
?
生成一個(gè)視頻時(shí)鐘
現(xiàn)在我們要生成一個(gè)類(lèi)似于上次我們的聲音時(shí)鐘的視頻時(shí)鐘:一個(gè)給出當(dāng)前視頻播放時(shí)間的內(nèi)部值。開(kāi)始,你可能會(huì)想這和使用上一幀的時(shí)間戳來(lái)更新定時(shí)器一樣簡(jiǎn)單。但是,不要忘了視頻幀之間的時(shí)間間隔是很長(zhǎng)的,以毫秒為計(jì)量的。解決辦法是跟蹤另外一個(gè)值:我們?cè)谠O(shè)置上一幀時(shí)間戳的時(shí)候的時(shí)間值。于是當(dāng)前視頻時(shí)間值就是PTS_of_last_frame + (current_time - time_elapsed_since_PTS_value_was_set)。這種解決方式與我們?cè)诤瘮?shù)get_audio_clock中的方式很類(lèi)似。
所以在我們的大結(jié)構(gòu)體中,我們將放上一個(gè)雙精度浮點(diǎn)變量video_current_pts和一個(gè)64位寬整型變量video_current_pts_time。時(shí)鐘更新將被放在video_refresh_timer函數(shù)中。
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函數(shù)中初始化它:
is->video_current_pts_time = av_gettime();
現(xiàn)在我們需要一種得到信息的方式:
double get_video_clock(VideoState *is) {
double delta;
delta = (av_gettime() - is->video_current_pts_time) / 1000000.0;
return is->video_current_pts + delta;
}
?
提取時(shí)鐘
但是為什么要強(qiáng)制使用視頻時(shí)鐘呢?我們更改視頻同步代碼以致于音頻和視頻不會(huì)試著去相互同步。想像一下我們讓它像ffplay一樣有一個(gè)命令行參數(shù)。所以讓我們抽象一樣這件事情:我們將做一個(gè)新的封裝函數(shù)get_master_clock,用來(lái)檢測(cè)av_sync_type變量然后決定調(diào)用 get_audio_clock還是get_video_clock或者其它的想使用的獲得時(shí)鐘的函數(shù)。我們甚至可以使用電腦時(shí)鐘,這個(gè)函數(shù)我們叫做 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;
...
}
?
同步音頻
現(xiàn)在是最難的部分:同步音頻到視頻時(shí)鐘。我們的策略是測(cè)量聲音的位置,把它與視頻時(shí)間比較然后算出我們需要修正多少的樣本數(shù),也就是說(shuō):我們是否需要通過(guò)丟棄樣本的方式來(lái)加速播放還是需要通過(guò)插值樣本的方式來(lái)放慢播放?
我們將在每次處理聲音樣本的時(shí)候運(yùn)行一個(gè)synchronize_audio的函數(shù)來(lái)正確的收縮或者擴(kuò)展聲音樣本。然而,我們不想在每次發(fā)現(xiàn)有偏差的時(shí)候都進(jìn)行同步,因?yàn)檫@樣會(huì)使同步音頻多于視頻包。所以我們?yōu)楹瘮?shù)synchronize_audio設(shè)置一個(gè)最小連續(xù)值來(lái)限定需要同步的時(shí)刻,這樣我們就不會(huì)總是在調(diào)整了。當(dāng)然,就像上次那樣,“失去同步”意味著聲音時(shí)鐘和視頻時(shí)鐘的差異大于我們的閾值。
?
所以我們將使用一個(gè)分?jǐn)?shù)系數(shù),叫c,所以現(xiàn)在可以說(shuō)我們得到了N個(gè)失去同步的聲音樣本。失去同步的數(shù)量可能會(huì)有很多變化,所以我們要計(jì)算一下失去同步的長(zhǎng)度的均值。例如,第一次調(diào)用的時(shí)候,顯示出來(lái)我們失去同步的長(zhǎng)度為40ms,下次變?yōu)?0ms等等。但是我們不會(huì)使用一個(gè)簡(jiǎn)單的均值,因?yàn)榫嚯x現(xiàn)在最近的值比靠前的值要重要的多。所以我們將使用一個(gè)分?jǐn)?shù)系統(tǒng),叫c,然后用這樣的公式來(lái)計(jì)算差異:diff_sum = new_diff + diff_sum*c。當(dāng)我們準(zhǔn)備好去找平均差異的時(shí)候,我們用簡(jiǎn)單的計(jì)算方式:avg_diff = diff_sum * (1-c)。
注意:為什么會(huì)在這里?這個(gè)公式看來(lái)很神奇!嗯,它基本上是一個(gè)使用等比級(jí)數(shù)的加權(quán)平均值。我不知道這是否有名字(我甚至查過(guò)維基百科!),但是如果想要更多的信息,這里是一個(gè)解釋http://www.dranger.com/ffmpeg/weightedmean.html 或者在http://www.dranger.com/ffmpeg/weightedmean.txt 里。
下面是我們的函數(shù):
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;
}
現(xiàn)在我們已經(jīng)做得很好;我們已經(jīng)近似的知道如何用視頻或者其它的時(shí)鐘來(lái)調(diào)整音頻了。所以讓我們來(lái)計(jì)算一下要在添加和砍掉多少樣本,并且如何在“Shrinking/expanding buffer code”部分來(lái)寫(xiě)上代碼:
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秒時(shí)間的聲音的樣本數(shù)。所以,我們想要的樣本數(shù)就是我們根據(jù)聲音偏移添加或者減少后的聲音樣本數(shù)。我們也可以設(shè)置一個(gè)范圍來(lái)限定我們一次進(jìn)行修正的長(zhǎng)度,因?yàn)槿绻覀兏淖兊奶啵脩魰?huì)聽(tīng)到刺耳的聲音。
?
修正樣本數(shù)
現(xiàn)在我們要真正的修正一下聲音。你可能會(huì)注意到我們的同步函數(shù)synchronize_audio返回了一個(gè)樣本數(shù),這可以告訴我們有多少個(gè)字節(jié)被送到流中。所以我們只要調(diào)整樣本數(shù)為wanted_size就可以了。這會(huì)讓樣本更小一些。但是如果我們想讓它變大,我們不能只是讓樣本大小變大,因?yàn)樵诰彌_區(qū)中沒(méi)有多余的數(shù)據(jù)!所以我們必需添加上去。但是我們?cè)鯓觼?lái)添加呢?最笨的辦法就是試著來(lái)推算聲音,所以讓我們用已有的數(shù)據(jù)在緩沖的末尾添加上最后的樣本。
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;
}
現(xiàn)在我們通過(guò)這個(gè)函數(shù)返回的是樣本數(shù)。我們現(xiàn)在要做的是使用它:
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;
我們要做的是把函數(shù)synchronize_audio插入進(jìn)去。(同時(shí),保證在初始化上面變量的時(shí)候檢查一下代碼,這些我沒(méi)有贅述)。
結(jié)束之前的最后一件事情:我們需要添加一個(gè)if語(yǔ)句來(lái)保證我們不會(huì)在視頻為主時(shí)鐘的時(shí)候也來(lái)同步視頻。
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;
}
}
}
添加后就可以了。要保證整個(gè)程序中我沒(méi)有贅述的變量都被初始化過(guò)了。然后編譯它:
gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs`
然后你就可以運(yùn)行它了。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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