音声のエンコーダディレイ (3) - 遅延のセット

AAC を動画の音声として使う場合
AAC を動画の音声として使う場合、動画ファイルには生の AAC データとして格納されるため、エンコーダディレイのサンプル数の情報はどこにも含まれない状態となります。

何も設定をせずに、そのまま AAC を格納してしまうと、再生時にエンコーダディレイの部分がデコードされてしまうため、音声が映像よりも少し遅れてしまうことになります。
(実際に視聴する分には、そこまで気になるほどの遅延ではありませんが)

AAC などのように、音声の先頭にエンコーダディレイがある場合、動画ファイルを作成する時に、音声に対して、エンコーダディレイ分の遅延 (delay) を設定する必要があります。

「遅延」と言うと、遅くなるという響きに聞こえますが、負の値を設定した場合は、逆に開始が早まります。

※Opus の場合は、結合時に自動でサンプル数が取得されて、遅延が設定されるので、エンコーダディレイを気にする必要はありません。
MP4 動画の作成
MP4 コンテナで動画を作成する場合、MP4Box コマンド (gpac パッケージ) で映像と音声を結合できます。

## 通常の結合
$ MP4Box -add video.mp4 -add audio.m4a -new out.mp4

-add <FILE> で、一つのトラックを追加します。
-new は、常に新規状態の動画を作成します。
遅延の設定
音声にエンコーダディレイがあり、その部分をスキップしたい場合は、入力ファイル名の後にオプションを指定します。

## 遅延を設定
$ MP4Box -add video.mp4 -add audio.m4a:delay=-2048/44100 -new out.mp4

delay で、このトラックの遅延をセットします。
正の値なら指定時間分遅れて、負の値なら早まります。

整数で指定した場合はミリセカンド単位、分数 (NUM/DEN) で指定した場合は秒単位となります。

サンプル数を秒数に変換する場合、「サンプル数÷サンプルレート(Hz)」となるので、"<サンプル数>/<サンプルレート>" という形の分数で表すと、秒単位になります。

上記の場合は、44100Hz の音声で、エンコーダディレイの先頭が 2048 サンプルの時の指定になります。

これで、音声はエンコーダディレイ分がスキップされた位置から再生が開始されるので、映像と音声が正しく同期します。
確認
エンコーダディレイが正しくスキップされていることを確認したい場合は、ffmpeg で showwavespic フィルタを使って、音声波形の画像を作成します。

※ffmpeg で動画から直接音声を WAV 変換する方法もありますが、音声が HE/HEv2 の場合は、先頭部分が正しくスキップされないので、使用しません。
テスト動画で確認してみる
## H.264, 680x480 24fps 2秒 白の画面
$ ffmpeg -f lavfi -i color=c=white:s=640x480:r=24:d=2 -c:v libx264 out.mp4

## 44100Hz 2秒のノコギリ波 wav を作成
$ ffmpeg -f lavfi -i "aevalsrc=t*440-floor(t*440)|t*440-floor(t*440):s=44100:d=2" src.wav

## AAC エンコード (LC-AAC)
$ fdkaac -b 128 -o enc.m4a src.wav

## MP4 に結合
$ MP4Box -add out.mp4 -add enc.m4a -new test1.mp4
$ MP4Box -add out.mp4 -add enc.m4a:delay=-2048/44100 -new test2.mp4

## 音声波形画像を作成 (0.0〜0.1 秒まで)
$ ffmpeg -i test1.mp4 -filter_complex atrim=0:0.1,showwavespic=s=800x240:filter=peak test1.png
$ ffmpeg -i test2.mp4 -filter_complex atrim=0:0.1,showwavespic=s=800x240:filter=peak test2.png

delay を設定していない test1.mp4 と、delay を設定した test2.mp4 で、画像を比較してみてください。

test1.png には無音部分があり、test2.png には無音部分がありません。
遅延が正しく設定され、エンコーダディレイ部分がスキップされているのがわかります。
HE-AAC で確認してみる
今度は、音声を HE-AAC でエンコードして確認してみます。
上記の out.mp4, src.wav ファイルをそのまま使います。

## HE-AAC エンコード
$ fdkaac -p 5 -b 64 -o enc.m4a src.wav

## MP4 に結合 (delay セット)
$ MP4Box -add out.mp4 -add enc.m4a:delay=-5058/44100 -new test.mp4

## 音声波形画像を作成
$ ffmpeg -i test.mp4 -filter_complex atrim=0:0.1,showwavespic=s=800x240:filter=peak test.png

fdkaac で HE-AAC の場合、実際の先頭サンプル数は 5058 になります。
test.png には無音部分がないので、遅延が正しく設定されているのがわかります。

delay の値を -5000/44100 などにして、遅延を少し下げて試してみると、確かに開始位置から始まっているのがわかります。

ちなみに、"ffmpeg -i test.mp4 conv.wav" などとして、動画ファイルから直接音声を WAV 変換すると、先頭に無音部分が残ってしまいます。
HE/HEv2 の場合は、デコード時に正しくスキップできないようなので、注意してください。
MP4 ファイル内の情報
セットした遅延情報が、MP4 ファイル内のどこに格納されているかを確認してみます。

「MP4Box -dmp4 <file>」で、MP4 ファイルの中身の情報を XML ファイルに出力できます。
<filename>_info.xml のファイル名で出力されます。

$ MP4Box -dmp4 test1.mp4
$ MP4Box -dmp4 test2.mp4

=> test1_info.xml, test2_info.xml

ここでは、先程作ったテスト動画の情報を出力してみます。
test1.mp4 が遅延なし、test2.mp4 が遅延ありです。
xml ファイルを調べる
遅延情報は、edts ボックス内の elst に設定されています。

ただし、映像と音声の両方のトラックに設定されている場合があるので、2つ見つかった場合は、おそらく2番目の方が音声です (トラックの順番による)。

出力された xml ファイルで elst の文字を検索してみると、test1.mp4 には1つしかなく、test2.mp4 には2つあります。

test1.mp4 は音声の遅延が設定されていないので、音声の elst はありません。
一方、test2.mp4 は音声の遅延が設定されているので、音声の elst があります (2番目)。

test2_info.xml の2番目の elst の情報を見てみると、以下のようになっています。

<EditListBox Size="28" Type="elst" Version="0" Flags="0" Specification="p12" Container="edts" EntryCount="1">
<EditListEntry Duration="2020" MediaTime="2048" MediaRate="1"/>

DurationMovieHeaderBox の TimeScale 単位での再生時間。
MovieHeaderBox は先頭の方にあり、TimeScale="1000" となっているので、1秒=1000 として計算します。
上記の場合、2020/1000 = 2.02 秒です。先頭のエンコーダディレイ分が除外されています。
MediaTime各トラックの MediaHeaderBox の TimeScale 単位での開始時間です。
test2.mp4 の場合、elst の少し下に MediaHeaderBox があり、TimeScale="44100" となっています。
44100 はサンプルレートと同じなので、そのままサンプル数と一致します。
MediaRateメディアの再生速度。1 なので、x1 です。

音声は 2048/44100 秒の位置から開始し、再生時間は 2020/1000 秒という情報になります。
HE-AAC の場合
同様に、音声が HE-AAC の場合の情報も確認してみます。

<EditListBox Size="28" Type="elst" Version="0" Flags="0" Specification="p12" Container="edts" EntryCount="1">
<EditListEntry Duration="2022" MediaTime="2529" MediaRate="1"/>

<MediaHeaderBox Size="32" Type="mdhd" ... TimeScale="22050" Duration="47104">

ソースのサンプルレートは 44100 でしたが、SBR によってサンプルレートが半分になっているので、TimeScale が 22050 になっています。
44100 として換算すると、MediaTime は 2529 * 2 = 5058 となるので、エンコーダディレイのサンプル数と一致します。
Opus の場合
MP4 は音声に Opus を使うこともできます。
音声を Opus にした場合の動画でも確認してみます。

## Opus エンコード (package: opus-tools)
$ opusenc --bitrate 128 src.wav out.opus

## MP4 に結合 (delay なし)
$ MP4Box -add out.mp4 -add out.opus -new test_opus.mp4

## xml 出力
$ MP4Box -dmp4 test_opus.mp4

<EditListBox Size="28" Type="elst" Version="0" Flags="0" Specification="p12" Container="edts" EntryCount="1">
<EditListEntry Duration="1209" MediaTime="312" MediaRate="1"/>

<MediaHeaderBox Size="32" Type="mdhd" ... TimeScale="48000" Duration="96960">

Opus の場合は、エンコード後のデータに、エンコーダディレイのサンプル数の情報があるため、自動で開始時間がセットされています。

TimeScale=48000、MediaTime=312 なので、そのままエンコーダディレイのサンプル数になっています。

※Opus は 44100 Hz を扱えないので、エンコード時に 48000 Hz に変換されています。