Project Sync: WebCodecsによるサンプル単位で完璧なオーディオ/ビデオ同期への道のり
Project Sync: The journey to sample-perfect audio/video sync with WebCodecs (mediabunny.dev)
要約
WebCodecsを使用したメディア処理において、オーディオとビデオの同期(AVシンク)がサンプル単位で完璧に保たれるようにするための「Project Sync」について解説しています。オーディオコーデックが追加する不可避な遅延(エンコーダープライミング遅延)を、古代DNA分析のように正確に測定・補正することで、メディアの再エンコードを繰り返しても同期ずれが発生しないようにする手法を開発しました。
全文翻訳
Project Sync: WebCodecsによるサンプル単位で完璧なオーディオ/ビデオ同期への道のり
[画像: Vanilagy氏のGitHubプロフィールへのリンク]
2026年11月3日
[画像: Vanilagy氏のGitHubプロフィールへのリンク]
By Vanilagy
今日、オーディオとビデオの同期(AVシンク)は、ほとんど解決された問題だと考えるかもしれません。そして、それは正しいです!しかし、それは非常にデリケートな問題であり、それを維持するためにはすべてのシステムが正しく機能する必要があります。メディアパイプラインのどこかに間違いや見落としがあれば、それは壊れてしまいます。
Mediabunnyは高品質なライブラリであることを誇りに思っており、私は長い間、AVシンクは完全に解決されていると考えていました。結局のところ、数百万もの新しいメディアファイルを生成する過程で実証され、オーディオのエンコードとデコードにはWebCodecsを使用しています。WebCodecsは、長年、あるいは数十年にわたってメディア関連の仕事をしてきた業界の専門家やブラウザ開発者によって作成されたAPIであり、AVシンクに関するすべてを知っています。
しかし、仮定は単なる仮定にすぎず、完璧なものを作りたいのであれば、それでは不十分です。では、試してみましょう!Big Buck Bunnyの distinctive なサウンドを含むセクションをMediabunnyでトランスコードしてみます。聞いてみてください:
見た目も音も完璧に見えます!リンゴが特徴的なサウンドエフェクトと同時に当たった瞬間に、 poor butterfly が叩かれているのがわかります。
人間の知覚は曖昧
しかし、実際には同期していません!オーディオは実際には遅延しています。私たちはそれを知覚できませんが、それは人間がビデオに対してオーディオが遅延しているのを認識する125ミリ秒の閾値を下回っているからです。そして、これは実際に測定されていることです!興味深いことに、オーディオがビデオに先行している方がはるかに悪く、わずか45ミリ秒で検出可能になります。私の全く根拠のない推測ですが、人間の目は、実際の生活では音速が非常に遅いため、遅延したオーディオを正しく知覚するように進化したのではないでしょうか。つまり、現実世界でも「同期ずれ」が発生する可能性があるということです!
変換を64回連続で行い、各回の出力を次の入力として使用すると、遅延は非常に明白になります:
品質はある程度低下しましたが、AVシンクは今やspectacularly にずれています。オーディオが入ってくるまでに2秒以上かかり、poor butterfly はリンゴが地面に落ちる音を聞く前にとっくに死んでいます。メディアファイルの長さも、元の5秒から約9秒に伸びています。
なぜこれが起こるのか?
では、この遅延は一体どこから来るのでしょうか?それは間違いなくMediabunnyから来るものではありません。なぜなら、Mediabunnyは単にファイルを読み込み、オーディオパケットをオーディオデコーダーに渡し、その出力をオーディオエンコーダーに渡し、その出力を最終メディアファイルにパッケージ化するだけだからです。オーディオデコーダーとエンコーダーの両方がWebCodecsを使用しており、タイムスタンプも変更されていません。したがって、このようなデコード->エンコードのラウンドトリップは、うまく機能すると仮定するでしょう。
エンコーダーが信号に何をするのか、さらに深く掘り下げてみましょう。ここでは、48 kHzでサンプリングされた、1秒間で20 Hzから100 Hzに周波数が変化するサイン波という単純なテスト信号を使用します:
[画像: 周波数が変化するサイン波のグラフ]
これをMac上のWebCodecs AAC AudioEncoderでエンコードすると、最初は問題なく動作するように見えます。パケットメタデータを調べると、すでに何かがおかしいことがわかります。最初のパケットは信号と同じく0.0秒で始まりますが、最後のパケットは1.066666秒で終了します。つまり、エンコーダーは信号に約0.066秒を追加したようです!実際に作成されたものをデコードすると、信号がどのように変化したかがわかります:
[画像: エンコードされた信号のグラフ]
エンコーダーは信号を長くしただけでなく、遅延させました!そして、この信号をラウンドトリップするたびに、さらに遅延します。8回のラウンドトリップ後の様子は以下の通りです:
[画像: 8回のラウンドトリップ後の信号のグラフ]
元の信号とエンコードされた信号をクロス相関させると、正確に44ミリ秒のオフセットで最も相関が高いことがわかります。これは、エンコードされた信号が入力より44ミリ秒遅延していることを意味します。48 kHzでは、これは正確に2112オーディオサンプルの遅延に相当します。しかし、なぜこれが起こるのでしょうか?
オーディオコーデックが遅延を追加する理由
ここで起こっているのは、補償されていないエンコーダープライミング遅延の犠牲になっているということです。オーディオコーデックは、与えられた信号をエンコードするために必要なビット数を削減するために多大な努力を払っています。多くのコーデックには「キーフレーム」という概念はありません(つまり、ファイルのどの部分からでもオーディオのデコードを開始できます)。しかし、デコードを開始してから正しい出力に収束するまでには時間がかかります。これは、前のパケットの情報が現在のパケットのデコード方法に影響を与える可能性があることを意味します。
AACの場合、コーデックが行うことは非常に単純です。信号を1024サンプルのAACフレームに分割し、それぞれを「修正離散コサイン変換」(MDCT)を使用して圧縮・エンコードします。正しい結果を得るためには、2つの隣接するMDCTブロックをオーバーラップさせる必要があります。そのため、後続の1024サンプルを正確に再構築するには、2048サンプル分のパケットが必要です。
FFmpegのAACエンコーダーは、実際には最小限必要な1024サンプルの遅延のみを追加します。一方、上記の実験で使用したAppleのAACエンコーダーは、代わりに2112(1024 + 1024 + 64)サンプルの遅延を追加します。私の知る限り、なぜこの正確な数が選択されたのかは、どこにも明確に文書化されていません。
WebCodecsの間違いを元に戻す
オーディオエンコーダーは信号を維持するために遅延を追加する必要がありますが、ビデオとの同期も保ちたいと考えています。これには2つの明白なアプローチがあります:
- エンコーダープライミング遅延をカバーするために、入力信号の開始部分を無音でパディングし、その後、信号全体を時間的に後ろにシフトする。
- エンコーダープライミング遅延の分だけ、入力信号の先頭部分を破棄する。
私が知っているほとんどすべてのエンコーダーは最初のオプションを選択します。なぜなら、それはデータを追加するだけで、何も削除しないからです。したがって、WebCodecsが先頭に無音を追加するのは正しいことです。もしそうしなければ、信号の先頭部分を失ってしまいます!正しくないのは、この信号がタイムスタンプ0から始まると偽ることです。私はこれがWebCodecsの仕様と実装の両方(そしてそれらのすべてがこれを間違っています)における見落としだと考えています。このトピックに関するGitHub issueがありますが、議論は3年以上続いています。
WebCodecsチームが仕様を修正し、その後ブラウザベンダーが変更を実装してリリースするのを待つことは、Mediabunnyにとって選択肢ではありません。私は、今すぐ正しいファイルを作成したいのです。そこで私は、MediabunnyのAVシンクの不完全さを一度きり修正するための取り組みであるProject Syncを開始しました。
良いニュースは、WebCodecsが間違っていることを修正できることです。エンコードされた信号を適切な量(この場合は-44 ms)だけシフトするとどうなるか見てみましょう:
[画像: シフトされた信号のグラフ]
結果の信号は両端からはみ出していますが、私たちの生の信号はそれに完全に収まっているだけでなく、入力と再び完璧に同期しています!それほど難しくなかったでしょう?
困難なのは、WebCodecsがエンコーダーの遅延を教えてくれる方法がないため、Mediabunnyはエンコーダーの遅延を全く知らないということです。それに加えて、システムごとに大きく異なります。時には1024サンプル、時には2112サンプル、そしてSafariの場合は技術的には2112サンプルですが、さらに最初の1024サンプルを無音として扱います(バグのため)。つまり、追加で1024サンプルのパディングを提供する必要があり、合計3136サンプルになります。
これらをすべてハードコーディングする代わりに、Mediabunnyが実行するすべてのシステムで経験的にそれを把握するのが賢明だと判断しました。仕組みは次のとおりです。エンコーダーにインパルス信号を与えます。インパルスはサンプル4096にあり、つまり1以外のすべてが0である信号です。これは基本的に数学におけるディラックのデルタ関数の離散バージョンです。この信号は非常に明確に定義されたピークを持つため、エンコーダーを通してラウンドトリップすると、処理された信号のピークが遅延したサンプル数を正確に教えてくれるはずです。
そして実際に、AACエンコーダーでこれを行うと、処理された信号には非常に明確に定義されたピークが現れます:
[画像: インパルス信号のピークを示すグラフ]
このピークの周りには明らかにリンギングがありますが、これは予想されることです。なぜなら、完璧なピークを得るには、コーデックがすべての周波数を均等に維持する必要があるからです。そうはしません。可逆ではないオーディオコーデックの性質上、一部を破棄します。しかし、私たちはピークだけを気にします。そして、ここのピークはサンプル番号6208にあります。そこから4096を引くと、正確に2112、つまりこのAACエンコーダーの実際の遅延が得られます。素晴らしい!
多くの異なるブラウザやOSでこれを試しましたが、常に正しい答えが得られました。また、ブラウザがエンコードできる別のコーデックであるOpusでも機能します。
ネガティブタイムスタンプをどこでも機能させる
したがって、Mediabunnyが完璧なAVシンクを実現するために取るべきステップは次のとおりです:
- オーディオエンコーダーが初めて使用されるときは、そのシステムでのエンコーダープライミング遅延を経験的に測定する小さなプログラムを実行する。
- 生のオーディオ信号をエンコードする。
- 測定された遅延に基づいて、すべての出力パケットのタイムスタンプを負の方向にシフトする。
この場合、最初のパケットのタイムスタンプは正確に-0.044秒になります。メディア処理に慣れていない場合、ネガティブメディアタイムスタンプは意味がないように感じるかもしれませんが、実際には至るところで使用されています。FFmpegも全く同じパターンを使用しています。一般的な慣習として、メディアはタイムスタンプ0から「提示」され、それ以前のものはすべて破棄されます。
したがって、Mediabunnyに残された最後のピースは、すべてのミキサー(MP4やWebMのようなコンテナファイルを生成するコード)が実際にネガティブな入力タイムスタンプを処理できることを確認することでした。長らく、これはMediabunnyで検証エラーを引き起こしていました。なぜなら、すべてのフォーマットがそれをわずかに異なる方法で保存しているからです。今やAVシンクにとって必要不可欠になったため、各フォーマットを調べてネガティブタイムスタンプのサポートを追加しました:
-
ISOBMFF(例: .mp4および.mov)は、いわゆる「編集リスト」を通じてネガティブタイムスタンプを示します。実際のメディアデータは常にt=0から書き込まれ、編集リストが先頭をトリミングします。MediabunnyとFFmpegのリーダーはこれをネガティブタイムスタンプシフトとして実装しています。
-
Matroska(.webmおよび.mkv)は、技術的には約-32秒までのネガティブタイムスタンプをサポートしますが、すべてのリーダーがそれをサポートしているわけではありません(VLC 👀)。代わりに、このシフトを明示的にモデル化する専用の
CodecDelay要素があります。そのため、MediabunnyのMatroskaミキサーはネガティブタイムスタンプシフトを元に戻し、そのシフト量をその要素に保存します。 -
MPEG-TSの内部タイムスタンプは、モジュロ233の算術を使用しており、233/90000秒ごとにラップアラウンドします。したがって、-0.5秒のようなネガティブタイムスタンプは、大きな内部タイムスタンプ8589889592で表されます。その後、これを95443.217689秒ではなく-0.5秒として解釈できるように、リーダーが賢くある必要があります(曖昧です)。MediabunnyとFFmpegは、この種のパターンを正確に検出するためのヒューリスティックを持っているため、これを正しく行いますが、例えばVLCはそうではありません。そのため、この戦略は多くのプレイヤーを混乱させるため使用できません。
より良いアプローチは、コンポジション全体をある一定の時間だけ前方にシフトすることです。これにより、AVシンクを維持しながらネガティブタイムスタンプを完全に排除できます。HLSミキサー(今日MPEG-TSが作成される主な理由)を修正し、デフォルトで10秒の内部タイムスタンプシフトを使用するようにしました。
-
MP3やADTSのようなパケット化されたオーディオフォーマットはタイムスタンプを保持しませんが、HLSプレイリストでの正確な位置を示す正しいID3v2 PRIVメタデータを含み、エンコーダー遅延を補償します。HLS以外では、MP3はLAME固有のメタデータを保持でき、正確なエンコーダープライミング遅延を示します。そのため、ネガティブタイムスタンプはそれを正確に信号するために使用されるようになりました。
それを同期させて考えよう
プライミング遅延を補償したオーディオエンコーダーと、コンテナフォーマットからネガティブタイムスタンプを読み書きする機能の組み合わせにより、Mediabunnyはメディアを何度処理しても(可能な限り)サンプル単位で完璧なAVシンクを保証するようになりました。
これをテストするために、同じBig Buck Bunnyクリップを合計256回再変換してみましょう:
品質は依然として驚くほど良好で、最も重要なのは、ビデオとオーディオが完璧に同期したままでした!
Project Syncが完成していくのを見るのは非常に満足のいくものでした。結果はv1.63.0以来Mediabunnyでライブになっていますので、まだアップグレードしていない場合は必ずアップグレードしてください。