1. 程式人生 > >FFmpeg和SDL教程(六):同步音訊

FFmpeg和SDL教程(六):同步音訊

同步音訊

所以現在我們有一個足夠體面的球員來看電影,那麼讓我們看看我們有什麼樣的鬆散結局。最後一次,我們掩蓋了一點同步,即將音訊同步到視訊時鐘,而不是相反。我們將以與視訊相同的方式來做到這一點:製作一個內部視訊時鐘,以跟蹤視訊執行緒的距離,並將音訊同步到該視訊執行緒。稍後我們將介紹如何將音訊和視訊同步到外部時鐘。

實現視訊時鐘

現在我們要實現一個類似於我們上次音訊時鐘的視訊時鐘:一個內部值,給出當前正在播放的視訊的當前時間偏移量。起初,你會認為這將像用最後一幀的當前PTS更新定時器一樣簡單。但是,不要忘記,當我們達到毫秒級時,視訊幀之間的時間可能相當長。解決方案是跟蹤另一個值,即將視訊時鐘設定為最後一幀的PTS的時間。

這樣視訊時鐘的當前值將是PTS_of_last_frame +(current_time - time_elapsed_since_PTS_value_was_set)。這個解決方案和我們用get_audio_clock做的非常相似。

所以,在我們的大結構中,我們將放置一個double video_current_pts和一個int64_t 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函式的最小數量,在我們干擾任何事情之前,必須不同步。當然,就像上次一樣,“不同步”意味著音訊時鐘和視訊時鐘相差超過我們的同步閾值。

注意:這裡發生了什麼?這個等式看起來很神奇!那麼,它基本上是一個使用幾何系列作為權重的加權平均值。我不知道是否有這個名字(我甚至查了維基百科!)但是對於更多的資訊,這裡是一個解釋(或在weightedmean.txt)所以我們要使用一個分數係數,比如c,所以現在假設我們已經得到了N個不同步的音訊樣本集。我們不同步的數量也可能會有很大差異,所以我們將平均估算每個資料的同步程度。例如,第一個電話可能表明我們已經不同步了40毫秒,接下來是50毫秒,依此類推。但是我們不打算取簡單的平均值,因為最近的值比以前的值更重要。所以我們將使用一個分數係數,比如說c,然後像下面這樣求和:diff_sum = new_diff + diff_sum * c。當我們準備找到平均差值時,我們只需計算avg_diff = diff_sum *(1-c)。

以下是我們的功能到目前為止:

/* Add or subtract samples to get a better sync, return new
   audio buffer size */
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);

       /* Shrinking/expanding buffer code.... */

      }
    } else {
      /* difference is TOO big; reset diff stuff */
      is->audio_diff_avg_count = 0;
      is->audio_diff_cum = 0;
    }
  }
  return samples_size;
}
所以我們做得很好, 我們大概知道音訊來自視訊或者我們用於時鐘的時間。 現在我們來計算需要新增或刪除多少個樣本,方法是將此程式碼放在“收縮/展開緩衝區程式碼”部分:
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 *通道數* 2)是audio_length秒音訊中的取樣數。因此,我們想要的樣本數量將是我們已經擁有的樣本數量加上或減去對應於音訊漂移時間量的樣本數量。我們也會對我們的修正大小做一個限制,因為如果我們更改緩衝區太多,對使用者來說就太難了。

糾正樣本數量

現在我們必須實際改正音訊。您可能已經注意到,我們的synchronize_audio函式返回一個樣本大小,然後告訴我們要傳送到流的位元組數。所以我們只需要將樣本大小調整為wanted_size。這可以使樣本尺寸變小。但是如果我們想把它做得更大,我們不能僅僅增加樣本的大小,因為緩衝區中沒有更多的資料!所以我們必須新增它。但是我們應該新增什麼?嘗試和推斷音訊是愚蠢的,所以讓我們通過用最後一個樣本的值填充緩衝區來使用我們已有的音訊。

if(wanted_size < samples_size) {
  /* remove samples */
  samples_size = wanted_size;
} else if(wanted_size > samples_size) {
  uint8_t *samples_end, *q;
  int nb;

  /* add samples by copying final samples */
  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) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
      if(audio_size < 0) {
	/* If error, output silence */
	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;

  /* Skip or repeat the frame. Take delay into account
     FFPlay still doesn't "know if this is the best guess." */
  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 -lswscale -lz -lm \
`sdl-config --cflags --libs`
你會很好走。

下一次,我們將這樣做,所以你可以倒帶和快進你的電影。