生活随笔
收集整理的這篇文章主要介紹了
ffplay.c学习-7-以音频同步为基准
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
ffplay.c學習-7-以音頻同步為基準
目錄
以音頻為基準 ?頻主流程 視頻主流程 delay的計算 以視頻為基準 視頻主流程 ?頻主流程 synchronize_audio swr_set_compensation 以外部時鐘為基準 回顧 分析
1. 以音頻為基準
1. ?頻主流程
ffplay默認也是采?的這種同步策略。 此時?頻的時鐘設置在sdl_audio_callback:
audio_callback_time
= av_gettime_relative ( ) ; ... if ( ! isnan ( is
- > audio_clock
) ) { set_clock_at ( & is
- > audclk
, is
- > audio_clock
- ( double
) ( 2 * is
- > audio_hw_buf_size
+ is
- > audio_write_buf_size
) / is
- > audio_tgt
. bytes_per_sec
, is
- > audio_clock_serial
, audio_callback_time
/ 1000000.0 ) ; sync_clock_to_slave ( & is
- > extclk
, & is
- > audclk
) ; }
?頻時鐘的維護 我們先來is->audio_clock是在audio_decode_frame賦值:is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;從這?可以看出來,這?的時間戳是audio_buf結束位置的時間戳,?不是audio_buf起始位置的時間戳,所以當audio_buf有剩余時(剩余的?度記錄在audio_write_buf_size),那實際數據的pts就變成is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec,即是 再考慮到,實質上audio_hw_buf_size*2這些數據實際都沒有播放出去,所以就有is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec。 再加上我們在SDL回調進?填充,實際上 是有開始被播放,所以我們這?采?的相對時間是,剛回調產?的,就是內部 在播放的時候,那相對時間實際也在?。
static void
sdl_audio_callback ( void
* opaque
, Uint8
* stream
, int len )
{ VideoState
* is
= opaque
; int audio_size
, len1
; audio_callback_time
= av_gettime_relative ( ) ;
最終
set_clock_at ( & is
- > audclk
, is
- > audio_clock
- ( double
) ( 2 * is
- > audio_hw_buf_size
+ is
-
> audio_write_buf_size
) / is
- > audio_tgt
. bytes_per_sec
, is
- > audio_clock_serial
, audio_callback_time
/ 1000000.0 ) ;
2. 視頻主流程
ffplay中將視頻同步到?頻的主要?案是,如果視頻播放過快,則重復播放上?幀,以等待?頻;如果視頻播放過慢,則丟幀追趕?頻。 這?部分的邏輯實現在視頻輸出函數 video_refresh 中,分析代碼前,我們先來回顧下這個函數的流程圖: 在這個流程中,“計算上?幀顯示時?”這?步驟?關重要。先來看下代碼:
static void
video_refresh ( void
* opaque
, double
* remaining_time
)
{ ... . last_duration
= vp_duration ( is
, lastvp
, vp
) ; delay
= compute_target_delay ( last_duration
, is
) ; time
= av_gettime_relative ( ) / 1000000.0 ; if ( time
< is
- > frame_timer
+ delay
) { * remaining_time
= FFMIN ( is
- > frame_timer
+ delay
- time
, * remaining_time
) ; goto display
; } is
- > frame_timer
+= delay
; if ( delay
> 0 && time
- is
- > frame_timer
> AV_SYNC_THRESHOLD_MAX
) { is
- > frame_timer
= time
; } SDL_LockMutex ( is
- > pictq
. mutex
) ; if ( ! isnan ( vp
- > pts
) ) update_video_pts ( is
, vp
- > pts
, vp
- > pos
, vp
- > serial
) ; SDL_UnlockMutex ( is
- > pictq
. mutex
) ; if ( frame_queue_nb_remaining ( & is
- > pictq
) > 1 ) { Frame
* nextvp
= frame_queue_peek_next ( & is
- > pictq
) ; duration
= vp_duration ( is
, vp
, nextvp
) ; if ( ! is
- > step
&& ( framedrop
> 0 || ( framedrop
&& get_master_sync_type ( is
) != AV_SYNC_VIDEO_MASTER
) ) && time
> is
- > frame_timer
+ duration
) { printf ( "%s(%d) dif:%lfs, drop frame\n" , __FUNCTION__
, __LINE__
, ( is
- > frame_timer
+ duration
) - time
) ; is
- > frame_drops_late
++ ; frame_queue_next ( & is
- > pictq
) ; goto retry
; } } ... }
這段代碼的邏輯在上述流程圖中有包含。主要思路就是?開始提到的: 如果視頻播放過快,則重復播放上?幀,以等待?頻; 如果視頻播放過慢,則丟幀追趕?頻。實現的?式是,參考audio clock,計算上?幀(在屏幕上的那個畫?)還應顯示多久(含幀本身時?),然后與系統時刻對?,是否該顯示下?幀了。 這?與系統時刻的對?,引?了另?個概念——frame_timer。可以理解為幀顯示時刻,如更新前,是上?幀lastvp的顯示時刻;對于更新后( is->frame_timer += delay ),則為當前幀vp顯示時刻。上?幀顯示時刻加上delay(還應顯示多久(含幀本身時?))即為上?幀應結束顯示的時刻。具體原理看如下示意圖: 這?給出了3種情況的示意圖: time1:系統時刻?于lastvp結束顯示的時刻(frame_timer+dealy),即虛線圓圈位置。此時應該繼續顯示lastvp time2:系統時刻?于lastvp的結束顯示時刻,但?于vp的結束顯示時刻(vp的顯示時間開始于虛線圓圈,結束于??圓圈)。此時既不重復顯示lastvp,也不丟棄vp,即應顯示vp time3:系統時刻?于vp結束顯示時刻(??圓圈位置,也是nextvp預計的開始顯示時刻)。此時應該丟棄vp。
3. delay的計算
那么接下來就要看最關鍵的lastvp的顯示時?delay(不是很好理解,要反復體會)是如何計算的。這在函數compute_target_delay中實現:
static double
compute_target_delay ( double delay
, VideoState
* is
)
{ double sync_threshold
, diff
= 0 ; if ( get_master_sync_type ( is
) != AV_SYNC_VIDEO_MASTER
) { diff
= get_clock ( & is
- > vidclk
) - get_master_clock ( is
) ; sync_threshold
= FFMAX ( AV_SYNC_THRESHOLD_MIN
, FFMIN ( AV_SYNC_THRESHOLD_MAX
, delay
) ) ; if ( ! isnan ( diff
) && fabs ( diff
) < is
- > max_frame_duration
) { if ( diff
<= - sync_threshold
) { delay
= FFMAX ( 0 , delay
+ diff
) ; } else if ( diff
>= sync_threshold
&& delay
> AV_SYNC_FRAMEDUP_THRESHOLD
) { delay
= delay
+ diff
; } else if ( diff
>= sync_threshold
) { delay
= 2 * delay
; } else { } } } else { } av_log ( NULL
, AV_LOG_TRACE
, "video: delay=%0.3f A-V=%f\n" , delay
, - diff
) ; return delay
;
}
這段代碼中最難理解的是sync_threshold,sync_threshold值范圍:FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay)),其中delay為傳?的上?幀播放需要持續的時間(本質是幀持續時間 frame duration),即是分以下3種情況: delay >AV_SYNC_THRESHOLD_MAX=0.1秒,則sync_threshold = 0.1秒 delay <AV_SYNC_THRESHOLD_MIN=0.04秒,則sync_threshold = 0.04秒 AV_SYNC_THRESHOLD_MIN = 0.0.4秒 <= delay <= AV_SYNC_THRESHOLD_MAX=0.1秒,則sync_threshold為delay本身。 從這?分析也可以看出來,sync_threshold 最?值為0.1秒,最?值為0.04秒。這?說明?個說明問題呢? 同步精度最好的范圍是:-0.0.4秒~+0.04秒; 同步精度最差的范圍是:-0.1秒~+0.1秒; 和具體視頻的幀率有關系,delay幀間隔(frame duration)落在0.04~0.1秒時,則同步精度為正負1幀。 畫個圖幫助理解: 圖中: 坐標軸是diff值??,diff為0表示video clock與audio clock完全相同,完美同步。 坐標軸下??塊,表示要返回的值,?塊值的delay指傳?參數,結合上?節代碼,即lastvp的顯示時?(frame duration)。 從圖上可以看出來sync_threshold是建??塊區域,在這塊區域內?需調整lastvp的顯示時?,直接返回delay即可。也就是在這塊區域內認為是準同步的(sync_threshold也是最?允許同步誤差)。
1. 同步判斷結果:
diff <= -sync_threshold:如果?于-sync_threshold,那就是視頻播放較慢,需要適當丟幀。具體是返回?個最?為0的值。根據前?frame_timer的圖,?少應更新畫?為vp。 diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD:如果不僅?于sync_threshold,?且超過了AV_SYNC_FRAMEDUP_THRESHOLD,那么返回delay+diff,由具體diff決定還要顯示多久(這?不是很明?代碼意圖,按我理解,統?處理為返回2*delay,或者delay+diff即可,沒有區分的必要) 此邏輯幀間隔delay > AV_SYNC_FRAMEDUP_THRESHOLD =0.1秒,此時sync_threshold =0.1秒,那delay + diff > 0.1 + diff >= 0.1 + 0.1 = 0.2秒。 diff >= sync_threshold:如果?于sync_threshold,那么視頻播放太快,需要適當重復顯示lastvp。具體是返回2delay,也就是2倍的lastvp顯示時?,也就是讓lastvp再顯示?幀。此邏輯?定是 delay <= 0.1時秒,2delay <= 0.2秒 -sync_threshold <diff < +sync_threshold:允許誤差內,按frame duration去顯示視頻,即返回delay ?此,基本上分析完了視頻同步?頻的過程,簡單總結下: 基本策略是:如果視頻播放過快,則重復播放上?幀,以等待?頻;如果視頻播放過慢,則丟幀追趕?頻。 這?策略的實現?式是:引?frame_timer概念,標記幀的顯示時刻和應結束顯示的時刻,再與系統時刻對?,決定重復還是丟幀。 lastvp的應結束顯示的時刻,除了考慮這?幀本身的顯示時?,還應考慮了video clock與audio clock的差值。 并不是每時每刻都在同步,?是有?個“準同步”的差值區域。
2. 以視頻為基準
媒體流??只有視頻成分,這個時候才會?以視頻為基準。 在“視頻同步?頻”的策略中,我們是通過丟幀或重復顯示的?法來達到追趕或等待?頻時鐘的?的,但在“?頻同步視頻”時,卻不能這樣簡單處理。 在?頻輸出時,最?單位是“樣本”。?頻?般以數字采樣值保存,?般常?的采樣頻率有44.1K,48K等,也就是每秒鐘有44100或48000個樣本。視頻輸出中與“樣本”概念最為接近的畫?幀,如?個24fps(frame per second)的視頻,?秒鐘有24個畫?輸出,這?的?個畫?和?頻中的?個樣本是等效的。可以想?,如果對?頻使??樣的丟幀(丟樣本)和重復顯示?案,是不科學的。(?頻的連續性遠?于視頻,通過重復?百個樣本或者丟棄?百個樣本來達到同步,會在聽覺有很明顯的不連貫) ?頻本質上來講:就是做重采樣補償,?頻慢了,重采樣后的樣本就?正常的減少,以趕緊播放下?幀;?頻快了,重采樣后的樣本就?正常的增加,從?播放慢?些。
1. 視頻主流程
video_refresh()-> update_video_pts() 按照著視頻幀間隔去播放,并實時地重新矯正video時鐘。重點主要在audio的播放。
2. ?頻主流程
在分析具體的補償?法的之前,先回顧下?頻輸出的流程。 ?頻輸出的主要模型是: 在 audio_buf 緩沖不?時, audio_decode_frame 會從FrameQueue中取出數據放? audio_buf .audio_decode_frame 函數有?視頻同步相關的控制代碼:
static
int audio_decode_frame ( VideoState
* is
)
{ ... wanted_nb_samples
= synchronize_audio ( is
, af
- > frame
- > nb_samples
) ; if ( af
- > frame
- > format
!= is
- > audio_src
. fmt
|| dec_channel_layout
!= is
- > audio_src
. channel_layout
|| af
- > frame
- > sample_rate
!= is
- > audio_src
. freq
|| ( wanted_nb_samples
!= af
- > frame
- > nb_samples
&& ! is
- > swr_ctx
) ) { swr_free ( & is
- > swr_ctx
) ; is
- > swr_ctx
= swr_alloc_set_opts ( NULL
, is
- > audio_tgt
. channel_layout
, is
- > audio_tgt
. fmt
, is
- > audio_tgt
. freq
, dec_channel_layout
, af
- > frame
- > format
, af
- > frame
- > sample_rate
, 0 , NULL
) ; if ( ! is
- > swr_ctx
|| swr_init ( is
- > swr_ctx
) < 0 ) { av_log ( NULL
, AV_LOG_ERROR
, "Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n" , af
- > frame
- > sample_rate
, av_get_sample_fmt_name ( af
- > frame
- > format
) , af
- > frame
- > channels
, is
- > audio_tgt
. freq
, av_get_sample_fmt_name ( is
- > audio_tgt
. fmt
) , is
- > audio_tgt
. channels
) ; swr_free ( & is
- > swr_ctx
) ; return - 1 ; } is
- > audio_src
. channel_layout
= dec_channel_layout
; is
- > audio_src
. channels
= af
- > frame
- > channels
; is
- > audio_src
. freq
= af
- > frame
- > sample_rate
; is
- > audio_src
. fmt
= af
- > frame
- > format
; } if ( is
- > swr_ctx
) { const uint8_t
* * in
= ( const uint8_t
* * ) af
- > frame
- > extended_data
; uint8_t
* * out
= & is
- > audio_buf1
; int out_count
= ( int64_t
) wanted_nb_samples
* is
- > audio_tgt
. freq
/ af
- > frame
- > sample_rate
+ 256 ; int out_size
= av_samples_get_buffer_size ( NULL
, is
- > audio_tgt
. channels
, out_count
, is
- > audio_tgt
. fmt
, 0 ) ; int len2
; if ( out_size
< 0 ) { av_log ( NULL
, AV_LOG_ERROR
, "av_samples_get_buffer_size() failed\n" ) ; return - 1 ; } if ( wanted_nb_samples
!= af
- > frame
- > nb_samples
) { int sample_delta
= ( wanted_nb_samples
- af
- > frame
- > nb_samples
) * is
- > audio_tgt
. freq
/ af
- > frame
- > sample_rate
; int compensation_distance
= wanted_nb_samples
* is
- > audio_tgt
. freq
/ af
- > frame
- > sample_rate
; if ( swr_set_compensation ( is
- > swr_ctx
, sample_delta
, compensation_distance
) < 0 ) { av_log ( NULL
, AV_LOG_ERROR
, "swr_set_compensation() failed\n" ) ; return - 1 ; } } av_fast_malloc ( & is
- > audio_buf1
, & is
- > audio_buf1_size
, out_size
) ; if ( ! is
- > audio_buf1
) return AVERROR ( ENOMEM
) ; len2
= swr_convert ( is
- > swr_ctx
, out
, out_count
, in
, af
- > frame
- > nb_samples
) ; if ( len2
< 0 ) { av_log ( NULL
, AV_LOG_ERROR
, "swr_convert() failed\n" ) ; return - 1 ; } if ( len2
== out_count
) { av_log ( NULL
, AV_LOG_WARNING
, "audio buffer is probably too small\n" ) ; if ( swr_init ( is
- > swr_ctx
) < 0 ) swr_free ( & is
- > swr_ctx
) ; } is
- > audio_buf
= is
- > audio_buf1
; resampled_data_size
= len2
* is
- > audio_tgt
. channels
* av_get_bytes_per_sample ( is
- > audio_tgt
. fmt
) ; } else { is
- > audio_buf
= af
- > frame
- > data
[ 0 ] ; resampled_data_size
= data_size
; } audio_clock0
= is
- > audio_clock
; if ( ! isnan ( af
- > pts
) ) is
- > audio_clock
= af
- > pts
+ ( double
) af
- > frame
- > nb_samples
/ af
- > frame
- > sample_rate
; else is
- > audio_clock
= NAN
; is
- > audio_clock_serial
= af
- > serial
;
#ifdef DEBUG
{ static double last_clock
; printf ( "audio: delay=%0.3f clock=%0.3f clock0=%0.3f\n" , is
- > audio_clock
- last_clock
, is
- > audio_clock
, audio_clock0
) ; last_clock
= is
- > audio_clock
; }
#endif
return resampled_data_size
;
}
主要分3個步驟: 根據與vidoe clock的差值,計算應該輸出的樣本數。由函數 synchronize_audio 完成: ?頻慢了則樣本數減少 ?頻快了則樣本數增加 判斷是否需要重采樣:如果要輸出的樣本數與frame的樣本數不相等,也就是需要適當減少或增加樣本。 重采樣——利?重采樣庫進?樣本的插?或剔除 可以看到,與視頻的處理略有不同,視頻的同步控制主要體現在上?幀顯示時?的控制,即對frame_timer的控制;??頻是直接體現在輸出樣本上的控制。 前?提到如果單純判斷某個時刻應該重復樣本或丟棄樣本,然后對輸出?頻進?修改,??會很容易感知到這?不連貫,體驗不好。 這?的處理?式是利?重采樣庫進?平滑地樣本剔除或添加。即在獲知要調整的?標樣本數wanted_nb_samples 后,通過 swr_set_compensation 和 swr_convert 的函數組合完成”重采樣“。 需要注意的是,因為增加或刪除了樣本,樣本總數發?了變化,?采樣率不變,那么假設原先1s的聲?將被以?于1s或?于1s的時?進?播放,這會導致聲?整體頻率被拉低或拉?。直觀感受,就是聲?變粗或變尖了。ffplay也考慮到了這點影響,其做法是設定?個最?、最?調整范圍,避免?幅度的?調變化。
3. synchronize_audio
在了解了整體流程后, 就來看下關鍵函數: synchronize_audio synchronize_audio 負責根據與video clock的差值計算出合適的?標樣本數,通過樣本數控制?頻輸出速度。 現在讓我們看看當 N 組?頻采樣已經不同步的情況。?這些?頻采樣不同步的程度也有很?的不同,所以我們要取平均值來衡量每個采樣的不同步情況。?如,第?次調?時顯示我們不同步了 40ms,下?次是50ms,等等。但是我們不會采取簡單的平均計算,因為最近的值?之前的值更重要也更有意義,這時候我們會使??個?數系數 audio_diff_cum,并對不同步的延時求和:is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum;。當我們找到平均差異值時,我們就簡單的計算 avg_diff= is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);。我們代碼如下:
static
int synchronize_audio ( VideoState
* is
, int nb_samples
)
{ int wanted_nb_samples
= nb_samples
; if ( get_master_sync_type ( is
) != AV_SYNC_AUDIO_MASTER
) { double diff
, avg_diff
; int min_nb_samples
, max_nb_samples
; diff
= get_clock ( & is
- > audclk
) - get_master_clock ( is
) ; if ( ! isnan ( diff
) && fabs ( diff
) < AV_NOSYNC_THRESHOLD
) { 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_nb_samples
= nb_samples
+ ( int ) ( diff
* is
- > audio_src
. freq
) ; min_nb_samples
= ( ( nb_samples
* ( 100 - SAMPLE_CORRECTION_PERCENT_MAX
) / 100 ) ) ; max_nb_samples
= ( ( nb_samples
* ( 100 + SAMPLE_CORRECTION_PERCENT_MAX
) / 100 ) ) ; wanted_nb_samples
= av_clip ( wanted_nb_samples
, min_nb_samples
, max_nb_samples
) ; } av_log ( NULL
, AV_LOG_INFO
, "diff=%f adiff=%f sample_diff=%d apts=%0.3f %f\n" , diff
, avg_diff
, wanted_nb_samples
- nb_samples
, is
- > audio_clock
, is
- > audio_diff_threshold
) ; } } else { is
- > audio_diff_avg_count
= 0 ; is
- > audio_diff_cum
= 0 ; } } return wanted_nb_samples
;
}
和 compute_target_delay ?樣,這個函數的源碼注釋也是ffplay?算多的。這??先得先理解?個”神奇的算法“。 這?有?組變量 audio_diff_avg_coef 、audio_diff_avg_count 、 audio_diff_cum 、 avg_diff .我們會發現在開始播放的AUDIO_DIFF_AVG_NB(20)個幀內,都是在通過公式 is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum; 計算累加值 audio_diff_cum 。按注釋的意思是為了得到?個準確的估計值。接著在后?計算與主時鐘的差值時,并不是直接求當前時刻的差值,?是根據累加值計算?個平均值: avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef); ,然后通過這個均值進?校正。 這個公式的?的應該是為了讓越靠近當前時刻的diff值在平均值中的權重越? 繼續看在計算得到 avg_diff 后,如何確定要輸出的樣本數:
wanted_nb_samples
= nb_samples
+ ( int ) ( diff
* is
- > audio_src
. freq
) ; min_nb_samples
= ( ( nb_samples
* ( 100 - SAMPLE_CORRECTION_PERCENT_MAX
) / 100 ) ) ; max_nb_samples
= ( ( nb_samples
* ( 100 + SAMPLE_CORRECTION_PERCENT_MAX
) / 100 ) ) ; wanted_nb_samples
= av_clip ( wanted_nb_samples
, min_nb_samples
, max_nb_samples
) ;
時間差值乘以采樣率可以得到?于補償的樣本數,加之原樣本數,即應輸出樣本數。另外考慮到上?節提到的?頻?調變化問題,這?限制了調節范圍在正負10%以內。 所以如果?視頻不同步的差值較?,并不會?即完全同步,最多只調節當前幀樣本數的10%,剩余會在下次調節時繼續校正。 最后,是與視頻同步?頻時類似地,有?個準同步的區間,在這個區間內不去做同步校正,其??是audio_diff_threshold:
is
- > audio_diff_threshold
= ( double
) ( is
- > audio_hw_buf_size
) / is
- > audio_tgt
. bytes_per_sec
;
即?頻輸出設備內緩沖的?頻時?。 以上,就是?頻去同步視頻時的主要邏輯。簡單總結如下: ?頻追趕、等待視頻采樣的?法是直接調整輸出樣本數量 調整輸出樣本時為避免聽覺上不連貫的體驗,使?了重采樣庫進??頻的剔除和添加 計算校正后輸出的樣本數量,使?了?個”神奇的公式“
4. swr_set_compensation
int swr_set_compensation ( struct SwrContext
* s
, int sample_delta
, int compensation_distance
) ;
激活重采樣補償(“軟”補償)。 在swr_next_pts()中需要時,內部調?此函數。 參數:s:分配Swr上下?。 如果未初始化,或未設置SWR_FLAG_RESAMPLE,則會使?標志集調?swr_init()。 sample_delta:每個樣本PTS的delta compensation_distance:要補償的樣品數量 返回:> = 0成功,AVERROR錯誤代碼如果: s為null compensation_distance?于0, compensation_distance是0,但是sample_delta不是, 補償不?持重采樣器,或 調?時,swr_init()失敗。
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生
總結
以上是生活随笔 為你收集整理的ffplay.c学习-7-以音频同步为基准 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。