T-CREATOR

FFmpeg 音ズレを根治:VFR→CFR 変換と PTS 補正の実践ガイド

FFmpeg 音ズレを根治:VFR→CFR 変換と PTS 補正の実践ガイド

動画編集や配信で最も厄介な問題の一つが音ズレです。特に VFR(可変フレームレート)で録画された動画は、エンコード後に音声と映像がずれてしまい、視聴体験を大きく損ないます。

本記事では、FFmpeg を使用して VFR から CFR(固定フレームレート)への変換と PTS 補正により、音ズレを根本的に解決する実践的な手法をご紹介します。技術的な原理から具体的なコマンドまで、段階的に解説していきますので、ぜひ最後までお読みください。

VFR 動画で発生する音ズレの原理

まず、なぜ VFR 動画で音ズレが発生するのかを理解しましょう。この仕組みを把握することで、適切な解決策を選択できるようになります。

VFR と CFR の基本的な違い

VFR(可変フレームレート)は、動画の内容に応じてフレームレートを動的に変更する技術です。静止画面では低いフレームレートで、動きの激しい場面では高いフレームレートで録画することで、ファイルサイズを抑制できます。

一方、CFR(固定フレームレート)は、動画全体を通して一定のフレームレートを維持します。30fps なら必ず 1 秒間に 30 フレームが記録され、タイムスタンプの計算が単純になります。

VFR と CFR の動作原理を図で確認してみましょう。

mermaidflowchart TD
    A[動画録画開始] --> B{シーンの複雑さ}
    B -->|静止画面| C[低フレームレート<br/>15fps]
    B -->|動きのある画面| D[高フレームレート<br/>60fps]
    C --> E[VFR動画<br/>可変タイムスタンプ]
    D --> E

    F[CFR変換処理] --> G[固定フレームレート<br/>30fps]
    G --> H[CFR動画<br/>均等タイムスタンプ]

    E --> F

VFR では各フレームのタイムスタンプが不規則になり、音声トラックとの同期計算が複雑になります。

音ズレが発生するメカニズム

音ズレの根本原因は、映像と音声のタイムスタンプの不整合にあります。VFR 動画では以下のような問題が発生します。

問題の種類発生原因症状
タイムスタンプ不整合VFR フレームの不規則な間隔徐々に音ズレが拡大
PTS/DTS 不整合エンコード時の計算誤差特定の箇所で急激なズレ
サンプルレート不一致音声処理での丸め誤差微細だが継続的なズレ

特に問題となるのは、エンコーダーが映像の可変タイムスタンプに対して音声を固定サンプルレートで処理する際の不整合です。

フレーム間隔の不規則性による影響

VFR 動画では、フレーム間の時間間隔が一定ではありません。例えば、以下のようなタイムスタンプになることがあります。

javascript// VFRのフレームタイムスタンプ例
const vfrTimestamps = [
  0.0, // 1フレーム目
  0.033, // 2フレーム目 (33ms後)
  0.1, // 3フレーム目 (67ms後)
  0.133, // 4フレーム目 (33ms後)
  0.2, // 5フレーム目 (67ms後)
];

// CFRのフレームタイムスタンプ例(30fps)
const cfrTimestamps = [
  0.0, // 1フレーム目
  0.033, // 2フレーム目
  0.067, // 3フレーム目
  0.1, // 4フレーム目
  0.133, // 5フレーム目
];

音声は常に一定のサンプルレート(48kHz など)で記録されるため、VFR の不規則なタイムスタンプと音声の規則的なタイムスタンプの間でずれが累積していきます。

この時間軸の不整合こそが、VFR 動画で音ズレが発生する根本的な原因なのです。

音ズレ診断:問題の特定方法

音ズレを効果的に解決するには、まず正確な診断が必要です。問題の種類と程度を特定することで、最適な解決策を選択できます。

FFprobe による動画情報の解析

まず、FFprobe を使用して動画の基本情報を確認しましょう。VFR か CFR かの判定と、フレームレート情報の取得が重要です。

bash# 基本的な動画情報の取得
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4

VFR 動画の特徴的な出力例:

json{
  "streams": [
    {
      "codec_type": "video",
      "r_frame_rate": "30000/1001",
      "avg_frame_rate": "24000/1001",
      "time_base": "1/30000"
    }
  ]
}

r_frame_rate(ベースフレームレート)とavg_frame_rate(平均フレームレート)が異なる場合、VFR 動画である可能性が高くなります。

フレームタイムスタンプの詳細分析

より詳細な分析には、各フレームのタイムスタンプを確認します。

bash# 全フレームのタイムスタンプを出力
ffprobe -v quiet -select_streams v:0 -show_entries frame=pkt_pts_time -of csv=p=0 input.mp4 | head -20

出力例(VFR 動画の場合):

0.000000
0.033367
0.100033
0.133400
0.200067
0.233434

フレーム間隔が不規則(33ms、67ms、33ms...)になっているのが VFR の特徴です。CFR 動画では一定間隔(例:33.33ms)になります。

音声同期のずれ測定

音ズレの程度を定量的に測定するには、音声と映像の同期点を比較します。

bash# 音声と映像の開始時間を比較
ffprobe -v quiet -show_entries stream=start_time -select_streams a:0 input.mp4
ffprobe -v quiet -show_entries stream=start_time -select_streams v:0 input.mp4

開始時間に差がある場合、初期オフセットによる音ズレが発生している可能性があります。

実際の音ズレ検証方法

理論的な分析だけでなく、実際の再生での音ズレも確認しましょう。

簡単なテスト用動画を作成して検証する方法:

bash# テスト用のビープ音付き動画を生成
ffmpeg -f lavfi -i testsrc2=duration=10:size=1280x720:rate=30 \
       -f lavfi -i sine=frequency=1000:duration=10 \
       -c:v libx264 -c:a aac test_sync.mp4

このテスト動画を既存の動画と同じ設定でエンコードし、音ズレの発生を確認できます。

主な診断指標をまとめると以下のようになります:

診断項目確認方法判定基準
VFR/CFR 判定r_frame_rate vs avg_frame_rate値が異なれば VFR
フレーム間隔pkt_pts_time 差分不規則なら VFR
初期オフセットstream start_time音声・映像の開始時間差
累積ずれ長時間再生での主観評価時間経過とともに拡大

これらの診断結果をもとに、次の章で説明する適切な解決策を選択していきます。

VFR→CFR 変換による根本解決

VFR 動画の音ズレを根本的に解決する最も効果的な方法は、CFR への変換です。フレームレートを統一することで、タイムスタンプの計算が単純化され、音声との同期が安定します。

フレームレート統一の重要性

CFR 変換の核心は、すべてのフレームを等間隔に配置し直すことです。これにより以下の効果が得られます。

タイムスタンプの正規化効果

VFR の不規則なタイムスタンプを CFR の規則的なタイムスタンプに変換することで、音声サンプルとの対応関係が明確になります。

VFR から CFR への変換プロセスを図で確認してみましょう。

mermaidflowchart LR
    A[VFR入力<br/>不規則間隔] --> B[フレーム解析]
    B --> C[タイムスタンプ計算]
    C --> D[フレーム補間/削除]
    D --> E[CFR出力<br/>等間隔配置]

    subgraph VFR["VFR(入力)"]
        F["0ms"] --> G["33ms"] --> H["100ms"] --> I["133ms"]
    end

    subgraph CFR["CFR(出力)"]
        J["0ms"] --> K["33ms"] --> L["67ms"] --> M["100ms"]
    end

この変換により、音声の固定サンプリングレートと映像のフレームレートが完全に同期します。

エンコーダー互換性の向上

CFR 動画は、ほとんどの動画エンコーダーや再生プレイヤーで安定して処理されます。VFR に対応していない古いプレイヤーでも正常に再生できるようになります。

FFmpeg での CFR 変換コマンド

FFmpeg を使用した CFR 変換の基本的なコマンドから、高度な設定まで段階的に説明します。

基本的な CFR 変換

最もシンプルな CFR 変換コマンドです:

bash# 基本的なVFR→CFR変換
ffmpeg -i input_vfr.mp4 -r 30 -c:v libx264 -c:a copy output_cfr.mp4

-r 30パラメータで出力フレームレートを 30fps に固定します。音声は-c:a copyでそのままコピーし、不要な再エンコードを避けます。

フレーム補間を含む高品質変換

より滑らかな変換には、フレーム補間フィルターを使用します:

bash# minterpolateフィルターを使用した高品質変換
ffmpeg -i input_vfr.mp4 \
       -vf "minterpolate=fps=30:mi_mode=mci:mc_mode=aobmc" \
       -c:v libx264 -preset medium -crf 18 \
       -c:a copy output_cfr.mp4

主要パラメータの説明:

パラメータ機能推奨値
fps=30出力フレームレート30 または 60
mi_mode=mci補間モードmci(動き補償)
mc_mode=aobmc動き補償アルゴリズムaobmc(高品質)
crf=18品質設定18-23(高品質)

フレームレート自動検出変換

入力動画の平均フレームレートを自動的に使用する方法:

bash# 平均フレームレートを使用したCFR変換
ffmpeg -i input_vfr.mp4 \
       -vf "fps=fps=source_fps" \
       -c:v libx264 -preset fast \
       -c:a copy output_cfr.mp4

この方法では、FFmpeg が自動的に入力動画の平均フレームレートを計算し、それを CFR の基準として使用します。

高フレームレート対応変換

60fps やそれ以上の高フレームレート動画の場合:

bash# 高フレームレート動画のCFR変換
ffmpeg -i input_vfr_60fps.mp4 \
       -r 60 \
       -vf "scale=1920:1080:flags=lanczos" \
       -c:v libx264 -preset medium -profile:v high \
       -level:v 4.2 -rc-lookahead 60 \
       -c:a copy output_cfr_60fps.mp4

高フレームレートでは-rc-lookaheadパラメータを調整して、エンコード効率を向上させます。

音声同期の確実な保持

CFR 変換時に音声同期を確実に保持するための追加設定:

bash# 音声同期を重視したCFR変換
ffmpeg -i input_vfr.mp4 \
       -vsync cfr -r 30 \
       -af "aresample=async=1000" \
       -c:v libx264 -preset medium \
       -c:a aac -b:a 128k output_cfr.mp4

-vsync cfrで強制的に CFR モードを指定し、aresample=async=1000で音声の非同期問題を自動補正します。

これらのコマンドを使用することで、VFR 動画を CFR に変換し、音ズレの根本原因を解決できます。次の章では、さらに精密な調整のための PTS 補正について説明します。

PTS 補正による精密調整

CFR 変換だけでは解決できない微細な音ズレや、特定の箇所での同期ずれには、PTS(Presentation Time Stamp)補正による精密調整が効果的です。

PTS と DTS の基礎知識

まず、動画ファイルで使用される時間情報の仕組みを理解しましょう。

PTS と DTS の役割

PTS(Presentation Time Stamp)と DTS(Decode Time Stamp)は、動画・音声データの時間管理を行う重要な情報です。

タイムスタンプ役割用途
PTS表示タイミング実際に画面に表示される時間
DTSデコードタイミングデータをデコードするべき時間
PCR参照時間システム全体の時間基準

通常は PTS = DTS ですが、B フレームを含む動画ではデコード順序と表示順序が異なるため、PTS と DTS に差が生じます。

タイムベースの概念

FFmpeg では、タイムベース(time_base)という単位でタイムスタンプを管理します。

bash# タイムベース情報の確認
ffprobe -v quiet -show_entries stream=time_base,start_time -select_streams v:0 input.mp4

出力例:

initime_base=1/30000
start_time=0.033367

タイムベース1​/​30000は、1 秒を 30000 分割した精度でタイムスタンプを管理することを意味します。

音声同期の補正手法

PTS 補正による音声同期の調整には、複数のアプローチがあります。

固定オフセット補正

音声全体に一定の遅延またはアドバンスを適用する方法:

bash# 音声を0.5秒遅延させる
ffmpeg -i input.mp4 \
       -itsoffset 0.5 -i input.mp4 \
       -map 0:v -map 1:a \
       -c:v copy -c:a copy output.mp4

-itsoffsetパラメータで音声トラック全体のタイミングを調整します。正の値で遅延、負の値でアドバンス(先行)させます。

動的 PTS 調整

再生時間に応じて徐々に変化する音ズレには、動的な PTS 調整が有効です:

bash# asetpts フィルターによる動的PTS調整
ffmpeg -i input.mp4 \
       -af "asetpts=PTS*0.999" \
       -c:v copy -c:a aac output.mp4

PTS*0.999により、音声のタイムスタンプを 0.1%短縮し、徐々に拡大する音ズレを補正します。

フレーム単位での PTS 補正

より精密な制御には、フレーム単位での PTS 操作を行います:

bash# setpts フィルターによる映像PTS調整
ffmpeg -i input.mp4 \
       -vf "setpts=PTS*1.001" \
       -af "asetpts=PTS*1.000" \
       -c:v libx264 -c:a aac output.mp4

映像の PTS を 0.1%延長することで、音声に対する相対的な調整を行います。

高度な PTS 操作テクニック

複雑な音ズレパターンには、より高度な PTS 操作が必要になります。

セグメント別 PTS 補正

動画の特定の部分でのみ音ズレが発生する場合:

bash# 条件付きPTS調整(例:60秒以降のみ補正)
ffmpeg -i input.mp4 \
       -af "asetpts=if(gte(T,60),PTS*0.998,PTS)" \
       -c:v copy -c:a aac output.mp4

if(gte(T,60),PTS*0.998,PTS)により、60 秒以降のみ PTS を調整します。

音声チャンネル別 PTS 補正

ステレオ音声で左右チャンネルの同期ずれがある場合:

bash# チャンネル別の遅延調整
ffmpeg -i input.mp4 \
       -af "pan=stereo|c0=0.95*c0+0.05*c1|c1=0.05*c0+0.95*c1,adelay=0|10" \
       -c:v copy -c:a aac output.mp4

adelay=0|10により、右チャンネル(c1)に 10ms の遅延を追加します。

PTS 補正のパラメータ一覧:

フィルター機能用途
asetpts=PTS*N音声 PTS 倍率調整累積ずれの補正
setpts=PTS*N映像 PTS 倍率調整フレームレート微調整
itsoffset=N固定時間オフセット初期ずれの補正
adelay=N音声遅延チャンネル別調整

これらの PTS 補正技術を組み合わせることで、CFR 変換だけでは解決できない微細な音ズレも精密に調整できます。

実践的な変換ワークフロー

ここまで学んだ知識を統合して、実際の作業で使える効率的なワークフローを構築しましょう。診断から変換、検証まで一連の流れを自動化できます。

動画解析から変換まで

効率的な音ズレ解決には、体系的なアプローチが重要です。以下のステップで確実に問題を解決できます。

ステップ 1: 包括的な動画分析

まず、入力動画の詳細な情報を収集します:

bash#!/bin/bash
# 動画分析スクリプト (analyze_video.sh)

VIDEO_FILE="$1"
OUTPUT_LOG="analysis_$(basename "$VIDEO_FILE" .mp4).txt"

echo "=== 動画分析開始: $VIDEO_FILE ===" > "$OUTPUT_LOG"

# 基本情報の取得
echo "1. 基本情報:" >> "$OUTPUT_LOG"
ffprobe -v quiet -print_format json -show_format -show_streams "$VIDEO_FILE" >> "$OUTPUT_LOG"

# フレームレート分析
echo -e "\n2. フレームレート情報:" >> "$OUTPUT_LOG"
ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate,avg_frame_rate "$VIDEO_FILE" >> "$OUTPUT_LOG"

# 最初の100フレームのタイムスタンプ
echo -e "\n3. フレームタイムスタンプ(先頭100フレーム):" >> "$OUTPUT_LOG"
ffprobe -v quiet -select_streams v:0 -show_entries frame=pkt_pts_time -of csv=p=0 "$VIDEO_FILE" | head -100 >> "$OUTPUT_LOG"

echo "分析完了: $OUTPUT_LOG"

このスクリプトにより、VFR/CFR 判定と具体的な問題箇所の特定ができます。

ステップ 2: VFR/CFR 自動判定

分析結果から最適な変換戦略を決定します:

bash#!/bin/bash
# VFR判定と変換戦略決定スクリプト

determine_conversion_strategy() {
    local video_file="$1"

    # フレームレート情報の取得
    local r_frame_rate=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$video_file")
    local avg_frame_rate=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=avg_frame_rate -of csv=p=0 "$video_file")

    # 分数を小数に変換
    local r_fps=$(echo "$r_frame_rate" | awk -F'/' '{print $1/$2}')
    local avg_fps=$(echo "$avg_frame_rate" | awk -F'/' '{print $1/$2}')

    # フレームレート差の計算
    local fps_diff=$(echo "$r_fps - $avg_fps" | bc -l | sed 's/-//')

    if (( $(echo "$fps_diff > 0.1" | bc -l) )); then
        echo "VFR_DETECTED"
        echo "推奨変換: CFR変換 + PTS補正"
        echo "ベースFPS: $avg_fps"
    else
        echo "CFR_LIKELY"
        echo "推奨変換: PTS補正のみ"
        echo "フレームレート: $r_fps"
    fi
}

ステップ 3: 自動変換処理

判定結果に基づいて適切な変換を実行します:

bash#!/bin/bash
# 自動変換スクリプト (auto_convert.sh)

INPUT_FILE="$1"
OUTPUT_FILE="$2"
STRATEGY=$(determine_conversion_strategy "$INPUT_FILE" | head -1)

case "$STRATEGY" in
    "VFR_DETECTED")
        echo "VFR動画を検出。CFR変換を実行します..."

        # 高品質CFR変換
        ffmpeg -i "$INPUT_FILE" \
               -vf "minterpolate=fps=30:mi_mode=mci:mc_mode=aobmc" \
               -af "aresample=async=1000" \
               -c:v libx264 -preset medium -crf 20 \
               -c:a aac -b:a 128k \
               -movflags +faststart \
               "$OUTPUT_FILE"
        ;;

    "CFR_LIKELY")
        echo "CFR動画と判定。PTS補正を実行します..."

        # PTS補正のみ
        ffmpeg -i "$INPUT_FILE" \
               -af "asetpts=PTS*1.000" \
               -c:v copy -c:a aac \
               -movflags +faststart \
               "$OUTPUT_FILE"
        ;;
esac

品質確認とトラブルシューティング

変換後の品質確認は、音ズレ解決の成功を保証する重要な工程です。

自動品質チェック

変換結果を自動的に検証するスクリプト:

bash#!/bin/bash
# 品質チェックスクリプト (quality_check.sh)

ORIGINAL="$1"
CONVERTED="$2"

echo "=== 品質チェック開始 ==="

# ファイルサイズ比較
orig_size=$(stat -f%z "$ORIGINAL" 2>/dev/null || stat -c%s "$ORIGINAL")
conv_size=$(stat -f%z "$CONVERTED" 2>/dev/null || stat -c%s "$CONVERTED")
size_ratio=$(echo "scale=2; $conv_size * 100 / $orig_size" | bc)

echo "ファイルサイズ: ${orig_size}${conv_size} (${size_ratio}%)"

# フレームレート確認
orig_fps=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=avg_frame_rate -of csv=p=0 "$ORIGINAL")
conv_fps=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=avg_frame_rate -of csv=p=0 "$CONVERTED")

echo "フレームレート: ${orig_fps}${conv_fps}"

# 音声同期チェック(最初と最後の5秒間のPTS差分)
echo "音声同期チェック実行中..."
ffprobe -v quiet -select_streams a:0 -show_entries frame=pkt_pts_time -of csv=p=0 "$CONVERTED" | head -5 > temp_start.txt
ffprobe -v quiet -select_streams a:0 -show_entries frame=pkt_pts_time -of csv=p=0 "$CONVERTED" | tail -5 > temp_end.txt

echo "同期チェック完了"
rm -f temp_start.txt temp_end.txt

一般的な問題と解決策

変換時によく発生する問題と対処法:

問題症状解決策
フレーム補間の品質低下映像がぼやけるmi_mode=blendに変更
音声品質の劣化音が籠もる-c:a copyで音声コピー
ファイルサイズの増大元の 2 倍以上-crf 23で CRF 値を上げる
処理時間の長時間化実時間の 10 倍以上-preset fastで高速化

バッチ処理での効率化

複数ファイルの一括処理スクリプト:

bash#!/bin/bash
# バッチ変換スクリプト (batch_convert.sh)

INPUT_DIR="$1"
OUTPUT_DIR="$2"
WORKERS="${3:-4}"  # 並列処理数(デフォルト4)

mkdir -p "$OUTPUT_DIR"

# 処理対象ファイルのリスト作成
find "$INPUT_DIR" -name "*.mp4" -o -name "*.mov" -o -name "*.avi" > file_list.txt

# 並列処理で変換実行
parallel -j "$WORKERS" -a file_list.txt bash auto_convert.sh {} "$OUTPUT_DIR"/converted_{/}

echo "バッチ変換完了: $(wc -l < file_list.txt)ファイル処理"
rm file_list.txt

効率的なワークフローのポイント:

  • 段階的アプローチ: 診断 → 戦略決定 → 変換 → 検証の順序を守る
  • 自動化の活用: 繰り返し作業はスクリプト化して効率化
  • 品質とパフォーマンスのバランス: 用途に応じて設定を最適化
  • ログの保持: トラブルシューティングのため処理ログを記録

このワークフローにより、VFR 動画の音ズレ問題を確実かつ効率的に解決できます。

まとめ

VFR 動画の音ズレ問題は、その発生メカニズムを理解し、適切な技術的アプローチを適用することで確実に解決できます。

本記事では、VFR から CFR への変換による根本的解決から、PTS 補正による精密調整まで、段階的な解決手法をご紹介しました。特に重要なポイントは以下の通りです:

技術的な要点

  • VFR 動画の不規則なタイムスタンプが音ズレの根本原因
  • CFR 変換により、フレームタイミングの正規化が可能
  • PTS 補正で、CFR では解決できない微細なずれも調整可能
  • 診断 → 変換 → 検証の体系的なワークフローが成功の鍵

実装面でのポイント

  • FFmpeg のminterpolateフィルターによる高品質なフレーム補間
  • asetptssetptsによる精密なタイムスタンプ制御
  • 自動判定スクリプトによる効率的な処理戦略選択
  • バッチ処理による大量ファイルの効率的な変換

これらの技術を組み合わせることで、配信やアーカイブで視聴体験を損なう音ズレ問題を根本的に解決し、高品質な動画コンテンツを制作できるようになります。

ぜひ本記事の手法を参考に、皆様の動画制作ワークフローに取り入れてください。

関連リンク