音声のエンコーダディレイ (2) - デコード

先頭の余分な部分を除外する
Opus の場合は、先頭の余分なサンプルは自動でスキップされるので問題ありませんが、生の AAC や MP3 の場合は、エンコードされたデータの中にエンコーダディレイのサンプル数の情報がないので、デコード時に、余分な部分がオーディオデータとして出力されてしまいます。

ここで問題になるのが、「ソースの動画に含まれている AAC をデコードして、音声を再エンコードする場合」と、「エンコードされた AAC を動画内に格納する場合」です。
音声を再エンコードする場合の問題点
ソースの動画を元に、映像のシーンカットを行って、編集した音声を再エンコードしたい場合、デコード後のオーディオデータに存在する、先頭のエンコーダディレイ部分のサンプルを除外しないと、音声がほんの少し遅れる形になってしまいます。

そのため、このページでは、動画内に格納されている生の AAC をデコードして、先頭の余分なサンプルを除外するための方法を紹介します。
ただし、エンコーダが明確に特定出来ない限り、正確な長さで除外することはできません。
動画ファイルから音声を抽出
動画ファイル内の音声を元に再エンコードしたい場合、まずは動画から音声ファイルだけを抽出します。

※「ffmpeg -i src.mp4 src.wav」というような形で、動画ファイルから直接音声を WAV に変換することはしないでください。

ffmpeg で動画から直接音声を変換する場合、ソースの動画にエンコーダディレイ分の遅延情報がセットされていると (音ズレ対策がしてあると)、その分はスキップされてデコードされてしまいます。

もちろん、変換時にエンコーダディレイ分をスキップしてくれるのは良いことなのですが、そもそも、その動画に正しいエンコーダディレイ分の遅延がセットされているかどうかもわかりませんし、HE-AAC/HE-AAC v2 の場合は、正しい遅延が設定されていたとしても、先頭に無音部分が残ってしまいます。

そのため、ここでは、生の AAC データを抽出して、それを WAV に変換した後に、エンコーダディレイのサンプル数を除外する形で行っていきます。
テスト動画を作成して比較
とりあえず、上記のことを確認するため、ffmpeg でテスト動画を作成してみます。

テスト動画の作成
## 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 に結合 (delay 指定。package: gpac)
$ MP4Box -add out.mp4 -add enc.m4a:delay=-2048/44100 -new test.mp4

MP4Box で delay を指定する場合、整数のミリセカンド単位か、分数の秒単位で指定できます。

今回の場合、エンコーダディレイの先頭サンプル数が 2048 なので、秒数を計算する場合は、「サンプル数 ÷ サンプルレート(Hz)」となるため、秒数を分数で表すと "2048/44100" になります。
また、音声をエンコーダディレイ分前にずらすため、負の値にしてあります。

WAV変換時の比較
この test.mp4 を元に、動画から直接 AAC→WAV 変換したものと、一度音声を抽出してから WAV 変換したものとで、時間を比較してみます。

## 動画から直接変換 (conv1.wav)
$ ffmpeg -i test.mp4 conv1.wav

## 一度抽出して変換 (conv2.wav)
$ ffmpeg -i test.mp4 -c:a copy extract.aac
$ ffmpeg -i extract.aac conv2.wav

## 時間を表示
$ ffprobe -show_entries format=duration conv1.wav

duration=2.020136

$ ffprobe -show_entries format=duration conv2.wav

duration=2.066576

動画から直接変換した方は 2.020136 秒、一度抽出してから変換した方は 2.066576 秒 となっており、抽出してから変換した方が時間が長くなっています。

波形を確認してみると、動画から直接変換した方は先頭に無音部分がないので、動画にセットされた遅延分がデコード時に除外されているのがわかります。

一度抽出してから変換した方は、AAC エンコードした時の out.m4a の AAC データ部分と全く同じになるので、先頭のエンコーダディレイ部分は残った状態になります。
音声の抽出方法
動画から音声を抽出する場合、ffmpeg のオプション -c:a copy で、オーディオのコーデックを無劣化コピーに指定すると、そのまま取り出すことができます。

AAC の場合拡張子は *.aac で、Opus の場合拡張子は *.opus で、それぞれ元のコーデックに合わせた拡張子で出力してください。

## 最初の音声を抽出
$ ffmpeg -i <source> -c:a copy <output>

## 2番目の音声を抽出
$ ffmpeg -i <source> -map 0:a:1 -c:a copy <output>

3番目以降は、-map 0:a:1 の 1 の部分を 2〜に置き換えます。

## MP4Box で mp4 の音声を抽出
$ MP4Box -raw <trackID(1〜)>:output=<output> <source>

## mkvextract で mkv/webm の音声を抽出 (package: mkvtoolnix-cli)
$ mkvextract <source> tracks <trackID(0〜)>:<output>
抽出した音声を WAV 変換
次に、抽出した音声を、無圧縮 WAV に変換します。

エンコーダディレイのサンプル数を特定する時に、WAV 変換した音声が必要になる場合があるので、一応ここで変換しておきます。

## AAC->WAV 変換
$ ffmpeg -i extract.aac conv.wav

## 5.1ch などを 2ch に変換してデコード
$ ffmpeg -i extract.aac -ac 2 conv.wav

ソースが 5.1ch などの場合、必要であれば 2ch ステレオに変換してください。

※faad コマンドで AAC→WAV 変換すると、常に先頭の 1024 サンプルが削除されてしまうので、注意してください。
※Opus の場合は、この時点ですでにエンコーダディレイ部分は除外されています。
サンプル数を特定する
次に、エンコーダディレイのサンプル数を特定します。
AAC エンコーダを特定
正確なサンプル数を知るためには、ソースの AAC を作成したエンコーダを特定する必要があります。
エンコーダによって、余分な部分のサンプル数が異なるためです。

  • fdkaac の場合、データの先頭などに特定できるような情報は存在しません。
  • ffmpeg の内蔵エンコーダ (-acodec aac) でエンコードされた LC-AAC の場合、データの先頭あたりに、"Lavf60.3.100" というような文字があります。
  • ffmpeg (-acodec libfdk_aac) の場合、fdkaac と同じで、特定できるような情報は存在しません。

AAC データの先頭あたりに、"Lavf60.3.100" というような文字があれば、ffmpeg の内蔵エンコーダで確定で、サンプル数は 1024 です。

他のエンコーダの場合、特定できるような情報はないので、映像と音声の時間差からサンプル数を推測します。
映像と音声の時間差からサンプル数を推測
音声にエンコーダディレイがある場合、映像よりも音声の方が再生時間が長くなっているので、その差を元にサンプル数を推測します。

映像と音声の再生時間を取得
ここでは、先ほど作成した、テスト動画の test.mp4 を使って、映像と音声の再生時間を取得してみます。

## 各映像と音声の情報を表示
$ ffprobe -show_streams test.mp4
[STREAM]
...
codec_type=video
...
duration=2.000000
...
[/STREAM]
[STREAM]
...
codec_type=audio
...
duration=2.020000
[/STREAM]

"[STREAM]...[/STREAM]" ごとに、各ストリームの情報が表示されます。
codec_type の値で、映像か音声かを判別できます。

この場合、映像が 2.0 秒で、音声が 2.02 秒です。
だたし、test.mp4 には、エンコーダディレイ分の遅延がセットされているため、この秒数は、遅延時間分が引かれた状態となっています。

今回は、エンコーダディレイを含む音声の長さが知りたいので、動画の情報から取得した音声の長さは使いません。

音声の正確な長さは、動画から抽出した音声データを WAV 変換してから、取得します。
※エンコードされた音声データの場合、ffprobe などでは正確な長さを取得できない場合があります (特に VBR の場合)。これは、先頭にあるデータだけを基準にして再生時間を推測しているためです。

MP3 や AAC の場合、実際に全体をデコードしてみないと正確な時間はわかりませんので、WAV 変換した音声データから、長さを確認します。

## AAC->WAV 変換
$ ffmpeg -i extract.aac conv.wav

## extract.aac の長さ
$ ffprobe -show_entries format=duration extract.aac

duration=2.069319

## conv.wav の長さ
$ ffprobe -show_entries format=duration conv.wav

duration=2.066576

extract.aac と conv.wav では、長さが少し違うのがわかります。
ここでは、conv.wav の 2.066576 の値を使います。

これで、映像が 2.0 秒で、音声が 2.066576 秒であることが確認できたので、次はこの時間の差から、エンコーダディレイのサンプル数を推測します。

映像と音声の再生時間の差から計算
test.mp4 の場合、音声の時間から映像の時間を引くと、2.066576 - 2.0 = 0.066576 秒となります。

これをサンプル数に変換する場合、「秒数 × サンプルレート(Hz)」となるので、
今回の場合は 0.066576 * 44100 = 2936.0016 サンプルとなります。
これが、音声全体の余分なサンプル数です。

AAC の場合は、先頭と終端に余分なサンプルがあるので、終端のサンプル数も含めて考える必要があります。

今回の場合、test.mp4 を作る時に作成した enc.m4a で iTunSMPB のサンプル数を確認してみると、先頭は 2048、終端は 888 サンプルなので、2048 + 888 = 2936 と、ちょうど時間差から計算したサンプル数と一致します。
0.0016 の部分は誤差の範囲なので、気にしないでください。

ただし、終端のサンプル数は、オーディオの長さなどによって増減するので、fdkaac でエンコードしたものが常に 888 になるというわけではありません。
そのため、先頭+終端サンプル数の値は、ざっくりとした目安になります。

以下の表を参考にしてエンコーダを特定し、一番近いと思われる先頭サンプル数を決めてください。

fdkaac
LC-AAC2048
HE-AAC2048 (実際は 5058)
HE-AAC v23072 (実際は 7106)
qaac
LC-AAC2112
HE-AAC2112 (実際は 5186)
ffmpeg
(-acodec aac) LC-AAC1024 (内蔵エンコーダ)
(-acodec libfdk_aac)fdkaac と同じ
(-acodec aac_at)macOS のみ。未確認

※HE/HEv2 では、「(上記のサンプル数 + 481) * 2」で実際のサンプル数を計算します。

開始位置ちょうどから音が出ている音声であれば、波形から開始位置の秒数を目算して、先頭サンプル数を計算する方法もありますが、そのように求めることができるケースは少ないでしょう。
先頭のサンプルを除外する
エンコーダディレイの先頭サンプル数が (推測であっても) 特定できたら、ffmpeg や sox コマンドを使って、先頭のサンプルを除外します。

以下は、LC-AAC で、先頭サンプル数が 2048 の場合の使用例です。
conv.wav の先頭 2048 サンプルを除外して、out.wav に出力します。

## sox
$ sox conv.wav out.wav trim 2048s

## ffmpeg
$ ffmpeg -i conv.wav -filter_complex atrim=start_sample=2048 out.wav

なお、ffmpeg を使う場合は、抽出した音声を WAV 変換する時に、先頭のサンプルを除外した上で出力することもできます。

## AAC->WAV 変換 (先頭サンプルを除外)
$ ffmpeg -i extract.aac -filter_complex atrim=start_sample=2048 out.wav

fdkaac でエンコードした HE-AAC の場合は、デコード後の実際のサンプル数は (2048 + 481) * 2 = 5058 となるので、この値を指定します。

これで、エンコーダディレイの先頭サンプルを除外できたので、後はこの WAV ファイルを使って編集した後、再エンコードしたりしてください。