この記事は後半です。前半は Qiita にあります: 64回変換で音声が遅れる理由を追う——WebCodecs AAC と 2112 サンプルの罠

画像: www.remotion.dev より
前半で見たのは、5秒の素材が64回の変換で9秒近くまで伸び、音声も2秒以上遅れたことだった。原因は単純な劣化ではなく、AACエンコード時に先頭へ足される遅延が、WebCodecs側で無視されたまま積み上がっていた点にある。
Apple の技術ノートでは、AAC の代表的なエンコード遅延として 2112 サンプルが挙げられている。AAC は 1024 サンプル単位で扱うので、これは 1024 + 1024 + 64 という内訳になる。つまり、最初の音が見える前に、実装はすでに 2112 サンプルぶんの準備をしている。
ここで大事なのは、無音が足されること自体は間違いではない、という点だ。MDCT ベースの圧縮では、先頭のブロックだけでは完全な波形を作れない。だから前準備が要る。問題は、その準備分を後段が見落として、映像との時刻合わせまで 0 秒前提で扱ってしまうことだ。
2112 は長く使われてきた値だが、万能ではない。Apple の資料でも、エンコード遅延は実装依存とされている。Mediabunny の検証では、AAC でも環境によっては 1024 サンプルになり、Safari ではさらに別の事情が重なって 3136 サンプル相当になるケースまであった。
この差は、仕様書を読んだだけでは埋まらない。W3C の WebCodecs issue #626 でも、AAC の priming samples や padding をエンコーダが返さないため、コンテナに正しく載せるのが難しいと問題提起されている。音楽用途では、ループの継ぎ目がサンプル単位でずれるだけでも困る。映像の口パクより、音の境界のほうが厳しい場面はいくらでもある。
だから、2112 をそのまま引くやり方は楽だが、実装差のある世界では危ない。固定値で当たることはある。でも外れたとき、ずれは静かに蓄積する。64 回まわしたときに露出したのは、その蓄積だった。
Mediabunny が採った方法は、理屈の推測ではなく計測だった。インパルス信号を 4096 サンプル目に置いてエンコーダを往復させ、戻ってきた波形のピーク位置を見て遅延量を測る。戻り先が 6208 サンプル目なら、差分の 2112 がその環境の遅延だと分かる。
このやり方の良さは、ブラウザや OS が何を隠していても、実際にどれだけ遅れるかを直接拾えることだ。AAC に限らず、ブログでは Opus でも同様に測れるとされている。WebCodecs が内部で黙っている以上、外から見えるタイムスタンプだけを信じるより、初回に一度測ったほうが確実という判断になる。
ただし、これは万能薬ではない。測定はあくまでその実装、その環境の値を得るためのものだ。エンコーダが変われば再計測が要るし、測った値をどうパケット時刻へ反映するかも別問題になる。遅延を知ることと、同期を保つことは同義ではない。
実運用で効くのは、変換のたびに後ろへずれていく構成だ。動画編集、音声の再エンコード、ブラウザ内でのトランスコード、素材のループ再生。こうした場面では、先頭の無音を捨てるか、タイムスタンプを前にずらすか、どちらかをきちんとやらないと破綻する。
W3C の議論でも、EncodedAudioChunk に遅延や padding の情報を載せる案が出ている。もしそれが整えば、後処理はもっと単純になる。だが現時点では、WebCodecs はその情報を十分に返してくれない。だから、入力をそのまま通しただけでは同期が崩れる前提で組むしかない。
自分なら、AAC を扱う箇所ではまず「固定 2112」を前提にしない。初回起動時に軽い計測を走らせ、測れたならその値を使う。測れなければ、少なくとも出力時に先頭の可聴サンプルが時刻 0 に来るよう、タイムスタンプを補正する。変換回数が増えるほど、この差は効いてくる。
前半の 64 回変換は、単なる極端なテストではない。ずれを放置したとき、どれだけ静かに壊れるかを見せるための数字だった。2112 サンプルは小さく見えるが、積み上がると 2 秒になる。そこを見逃さない設計だけが、最後まで音を映像に縫い止める。