(Linux) VapourSynth で動画エンコード [2]

はじめに
VapourSynth を使った基本的な動画エンコードの手順は前回説明したので、ここでは、実際の音声も合わせた動画エンコード全体の手順を説明していきます。

以下では、例として、変換元の動画を MKV or MP4 (H.264 + AAC) とし、
シーンカットと映像のリサイズを行って、MP4 (H.264 + AAC) に再エンコードするものとします。

ノイズ除去や逆テレシネなどの処理は行っていませんが、必要な場合はスクリプトに記述してください。
エンコード用スクリプトを用意
エンコード用のスクリプトを用意します。
以下では、シーンカットとリサイズを行っています。

<例: enc.vpy>
#!/usr/bin/python3

import vapoursynth as vs

core = vs.core

core.std.LoadPlugin('libdamb.so')

clip = core.ffms2.Source(source='source.mkv')
clip = core.damb.Read(clip, 'src.wav')

clip = clip[0:100] + clip[200:300] + clip[500:]

clip = core.resize.Spline36(clip,width=1280,height=720,format=vs.YUV420P8)

clip = core.damb.Write(clip, 'cut.wav')
clip.set_output()
カット位置の確認
シーンカットをしたい場合は、VapourSynth ビューアなどで、カットするフレームの位置を確認します。

プレビュー用のスクリプトを用意
まずは、映像を読み込むだけの、プレビュー用のスクリプトファイルを用意します。
動画ファイル名の部分は書き換えてください。

<prev.vpy>
#!/usr/bin/python3

import vapoursynth as vs
core = vs.core
c = core.ffms2.Source(source='source.mkv')
c.set_output()

フレーム位置を確認
フレーム位置は、ビューア上でも VapourSynth スクリプト側でも、「0 〜 (最大フレーム数 - 1)」の数値で指定します。

ビューア上で映像を確認しながら、カットするフレーム位置を見つけてください。

なお、スクリプトに書く際には、カットする部分の範囲ではなく、映像として残す部分の範囲が必要なので、「残す部分の先頭位置」と「残す部分の終端位置+1」のフレーム位置を確認します。

位置が確認できたら、エンコード用のスクリプトファイルに、カット処理を記述します。
例えば、フレーム位置 「0〜99、200〜299」 をカットして、「100〜199、300〜終端」 の範囲を残したい場合、以下のようになります。

clip = clip[100:200] + clip[300:]

また、再エンコードする必要が出た場合などは、フレーム位置をもう一度確認するのは面倒なので、カット部分のコードを別のテキストに保存しておくと、再エンコード時に楽になります。
元動画から音声を抽出
音声は、映像とは別にエンコードする必要があるので、元動画から音声のみを抽出して、処理します。

映像も同時に抽出することはできますが、MP4Box コマンドで mp4 から映像を抽出した場合、元動画をそのまま読み込んだ場合と、抽出した映像を読み込んだ場合とで、フレーム位置が 1 ずれる場合があったので、映像は、元動画から直接読み込むことにします。

※動画内に音声が複数格納されている場合は、動画の情報を確認した上で、必要なトラックだけ抽出してください。
※以下は、音声が AAC の場合として扱っています。
ffmpeg を使う
ffmpeg を使えば、大抵の動画は処理できます。

## 動画情報表示

$ ffprobe source.mkv

## 音声のみコピーして出力

$ ffmpeg -i source.mkv -vn -acodec copy src.aac

## 1番目の音声のみコピーして出力
## -map <input_file_no>:a:<audio_no>

$ ffmpeg -i source.mp4 -map 0:a:0 -acodec copy src.aac
MKV 動画の場合
MKV のツール類 (mkv* コマンド) を使います。
Arch Linux: mkvtoolnix-cli パッケージ。
Ubuntu: mkvtoolnix パッケージ。

## 各トラック抽出
## mkvextract <mkv_file> tracks <TrackID>:<output_name>
## (ID:0 が映像、ID:1 が音声の場合)

$ mkvextract source.mkv tracks 1:src.aac
MP4 動画の場合
MP4Box コマンドで抽出します。
Arch Linux/Ubuntu: gpac パッケージ。

## MP4Box -raw <TrackNo>[:output=<filename>] <mp4file>
## ':output=...' を省略すると、適当な名前で出力されます

$ MP4Box -raw 2:output=src.aac source.mp4
音声を wav 変換
シーンカットをする場合は、Damb プラグインで WAV ファイルを読み込む必要があるので、ソースの音声が WAV ではない場合は、WAV に変換します。
ただし、シーンカットや音声編集を行わない場合は、WAV 変換と音声エンコードのコマンドをパイプでつないで、直接エンコードしても構いません。
エンコーダディレイについて
MP3 や AAC でエンコードされたファイルには、音声データの先頭に、「エンコーダディレイ」 と呼ばれるデータが付加されます。

エンコーダディレイの部分は、デコードの際に無音として変換されるため、そのデータの長さ分、先頭に数十 ms 程度の余分な無音が追加されてしまいます。

例えば、動画の音声を AAC → WAV → AAC というように再エンコードする場合、最初の AAC → WAV の部分で、エンコーダディレイの無音部分が追加されると、音声が少し遅れてしまうので、音ズレの原因となります。

正確に音ズレを回避するためには、変換後の WAV ファイルのエンコーダディレイ部分を、手動でカットする必要があります。
ただし、エンコーダディレイの長さは、AAC エンコーダやエンコード形式によって異なります。
AAC を WAV 変換
faad コマンドか、ffmpeg などを使います。
Arch Linux: faad2 パッケージ。
Ubuntu: faad パッケージ。

## faad

$ faad -o out.wav src.aac

## ffmpeg

$ ffmpeg -i src.aac out.wav

## ffmpeg (2048 サンプル分先頭カット)

$ ffmpeg -i src.aac -filter_complex atrim=start_sample=2048,asetpts=PTS-STARTPTS out.wav

※詳しくは後述しますが、faad コマンドの場合、エンコーダディレイを常に 1024 サンプルとして、その分を自動で削除して出力されます(エンコーダディレイを完全に取り除くわけではない)。
ffmpeg の場合、エンコーダディレイはすべて無音として含まれます。


先頭のエンコーダディレイがどれくらいかわからない、または確かめるのが面倒だという場合は、とりあえず faad でデコードしておけば良いでしょう。

エンコーダディレイが 1024 サンプルではなかった場合、先頭の 1024 サンプルを削除しても、まだ無音部分が残る場合がありますが、ffmpeg よりは短くなるので、再生してもそこまで気になることはありません。

ffmpeg で、エンコーダディレイ部分を直接カットして出力したい場合は、atrim フィルタを使います。
「start_sample=」 の所に、エンコーダディレイの先頭サンプル数を指定してください。
シーンカットと音声の編集を行う場合
シーンカットに加えて、音量変更などの編集も行う場合は、シーンカットして出力した音声データに対して行う方が、後々面倒がなくて良いです。

その場合、以下のような流れになります。
「カット前の音声 → 映像エンコードと同時に、カット後の音声出力 → (音声編集) → カット後の音声をエンコード」

この時、カット前の音声に対して、先に音声編集をしてしまうと、映像のエンコード後に音声の編集をやり直したいとなった場合、もう一度映像のエンコードからやり直さなければならなくなります(カット後の音声は、すでに音声編集された状態のため)。

無編集の状態でカットして出力しておけば、音声編集をやり直したい場合は、カット後の音声を再編集・再エンコードするだけで済みます。
映像のエンコードと音声の出力
今回は H.264 でエンコードします。
Arch Linux/Ubuntu: x264 パッケージ。

シーンカットをする場合は、映像のエンコードと同時に、カット後の音声ファイル (VapourSynth スクリプトに記述したファイル名) が出力されます。

$ vspipe -c y4m enc.vpy - | x264 --demuxer y4m \
--profile high --fps 24000/1001 -I 240 --sar 1:1 \
--crf 23 --bframes 3 --ref 5 --b-adapt 2 --direct auto --rc-lookahead 40 \
--me umh --subme 8 --trellis 2 \
--aq-mode 2 --aq-strength 0.8 --psy-rd 0.0:0.0 - -o out.264

上記は、アニメ用 (23.976 fps) で、中〜高画質、高負荷、高圧縮で、どちらかというと圧縮重視の設定です。
ある程度時間はかかってもいいので、できるだけ容量を抑えつつ、画質をそれなりに保つようにしました。
これよりもう少し容量を小さくしたい場合は、--qcomp 0.5 を追加したり、--crf を +0.5 するなどすると良いです。
画質重視にする場合は、また別の設定となります。

出力形式について
なお、L-SMASH ツール (muxer コマンド) で MP4 結合を行った場合、ここで出力ファイルを .mp4 にすると、fps が 0 になるなどして、うまく結合できなかったので、.264 で出力しています。

ただし、.264 ファイルの場合は、再生時にシークができないので、映像を確認する時に不便かもしれません。
MP4Box などで結合する場合は、.mp4 で構いません。
音声の編集とエンコード
シーンカットした場合は、映像エンコード後に、カットされた音声ファイルが出力されるので、そのファイルに対して、音声の編集とエンコードを行います。
音声の編集
GUI で行うなら audacity、CUI で行うなら sox コマンドを使います。
Arch Linux の場合、パッケージ名は、上記のコマンド名と同じです。

sox コマンドで音量正規化
$ sox cut.wav cut_out.wav gain -n -1

-n で、変更後の音量の最大値が、指定した dB になるようにします。
ここでは、-1 dB を指定して、MAX (0 dB) より少し音量を下げて、音割れを防いでいます。
AAC エンコード
音声をエンコードします。
ここでは、fdkaac コマンドで、AAC にエンコードします。
Arch Linux/Ubuntu: fdkacc パッケージ。

音声編集しなかった場合は、カット後の音声ファイルを、音声編集した場合は、編集後のファイルを元にエンコードします。

## HE-AAC 64 kbps
$ fdkaac -p 5 -b 64 -o out.m4a cut.wav

-o <FILE>出力ファイル名
-p <N>プロファイル。
2 : LC-AAC
5 : HE-AAC
29 : HE-AAC v2
-b <N>CBR ビットレート (kbps)

音質重視なら、LC-AAC : 128 kbps〜。
圧縮重視なら、HE-AAC : 48〜80 kbps。
超低ビットレートなら、HE-AAC v2 : 32 kbps 以下。
映像と音声の結合 (MP4)
映像と音声を MP4 に結合するには、「L-SMASH ツールの muxer コマンド」 か 「MP4Box」 を使います。
ffmpeg でも出来ますが、一応 MP4 に特化したツールを使った方が良いでしょう。

音ズレ調整に関しては、L-SMASH の方が使いやすいです。
ただし、うまく結合しない場合もあるので、気に入った方を使ってください。

Arch Linux の場合、L-SMASH は l-smash パッケージ、MP4Box は gpac パッケージ。

L-SMASH: https://github.com/l-smash/l-smash
結合
x264 でエンコードした映像を out.264、AAC エンコードした音声を out.m4a として、この2つを MP4 コンテナに格納します。

※音ズレ調整の数値は、音声ごとに適切な値があるので、後述します。
以下は、fdkaac でエンコード、HE-AAC、44100 Hz の場合の数値です。

## L-SMASH の場合

$ muxer -i out.264 -i out.m4a?encoder-delay=2048 --language jpn -o res.mp4

## MP4Box の場合

$ MP4Box -add out.mp4 -add out.m4a:delay=-46:lang=jpn -new res.mp4

--language jpn は、すべてのトラックの言語を日本語にします。
なくても構いません。

encoder-delay または delay オプションは、音ズレ調整のための設定です。
エンコーダディレイの分、音声を先に読み込んで、映像と合わせます。

出力された動画ファイルを再生してみて、問題なければ完成です。
MP4 タグ
MP4 にタイトルなどのタグを付けたい場合は、mp4tags コマンドを使います。
Arch Linux: libmp4v2 パッケージ。
Ubuntu: mp4v2-utils パッケージ。

## タイトルを付加

$ mp4tags -s "タイトル" res.mp4

## WEB 上でダウンロードしながら再生できるようにする

$ mp4file --optimize res.mp4
音ズレについて
元動画やエンコード後の音声が MP3 や AAC の場合、WAV 変換時や映像・音声結合時に注意しておかないと、再生時に音ズレしてしまいます。
エンコーダディレイ
MP3 や AAC の場合、音声データの先頭と終端に、「エンコーダディレイ」と呼ばれる部分が追加されています。
その部分は、デコード時に無音として扱われるため、再生時間もその分少しだけ長くなります。

通常、デコーダは、エンコーダディレイをそのまま音声データとして変換するので、動画の音声として使う場合は、エンコーダディレイを正しく処理しないと、音ズレの原因となります。

元の音声を WAV に変換する際は、変換後の音声データからエンコーダディレイを除去し、また、音声をエンコードして動画と結合する時は、エンコーダディレイ分の音声をスキップして、映像と合わせるようにする必要があります。
AAC → WAV にデコード
動画から抽出した音声をデコードして WAV に変換する場合、エンコーダディレイは自動で除去できません。
なぜなら、エンコーダディレイがどれだけ含まれているかという情報は、生の音声ファイルには記述されていないからです。
AAC について
MP4 コンテナ (*.m4a) で出力された AAC の場合は、エンコーダが、MP4 コンテナ内にエンコーダディレイの情報を書き込むので、デコーダはそれを元にして、エンコーダディレイを除去できます。

しかし、*.aac の AAC ファイルの場合は、ファイル内に AAC の生の音声データしか含まれていないため、エンコーダディレイの情報はありません。
そのため、デコーダは、エンコーダディレイも含めてすべて音声データとして処理します。

動画の結合時には、生の音声データのみが書き込まれるため、エンコード後のファイルにエンコーダディレイの情報が含まれていても、それらは動画内には格納されません。

AAC デコーダ
AAC をデコードする場合、faad や ffmpeg などが使えます。

faad の場合は特殊で、エンコーダディレイの除去は、中途半端な形で対応しています。

faac でエンコードされた AAC ファイルの場合、先頭のエンコーダディレイは 1024 サンプルなので、常に先頭の 1024 サンプル分を削除して出力します。
実際はエンコーダや形式ごとにサンプル数が違うので、これだけでは完全にエンコーダディレイを取り除けない場合がありますが、先頭のエンコーダディレイが 1024 サンプルを下回ることはないので、切り取りすぎる心配はないし、全く除去しないよりはマシという形になります。

ffmpeg など、通常のデコーダの場合、エンコーダディレイはすべて音声データとしてデコードするので、エンコーダディレイ部分は無音となります。

正確にエンコーダディレイを取り除きたい場合は、ffmpeg を使ってデコードし、先頭や終端のエンコーダディレイ部分を手動でカットしてください。

デコードの状態を確認してみる
fdkaac で LC-AAC 128 kbps にエンコードした *.aac ファイル (fdkaac -b 128 -f 2) を、ffmpeg と faad でデコードしたものを、元の WAV ファイルと比較してみました。
※ fdkaac はデフォルトで m4a 形式で出力されるので、"-f 2" オプションで ADTS の生 AAC データにします。

>> 比較画像

※ fdkaac LC-AAC の場合、先頭のエンコーダディレイのサンプル数は 2048 です。
※ 下2つの先頭部分に、ソースにない波形が出ていますが、その部分はエンコーダディレイなので、無視します。

ffmpeg でデコードした場合は、2048 サンプル (44100Hz で 46ms) 分、先頭に余分な部分があります。
faad でデコードした場合は、2048 - 1024 = 1024 サンプル分、先頭に余分な部分があります。

結論
元動画が MKV の場合は、mkvinfo コマンドで、音声の 「デフォルトのデュレーション」 を見れば、それが先頭のエンコーダディレイの長さになっていると思います。
「小数点以下の時間 × samplerate(Hz)」で、サンプル数を逆算できます。

例えば、時間が「00:00:00.021333333」となっていて、音声が 48000Hz の場合、"0.021333 * 48000 = 1023.984" で、1024 サンプルとなります。
(0.2133... は、1秒を 1.0 とした時の、1秒未満の時間を表しています)

元動画が MP4 の場合は、エンコーダディレイ対策として、音声遅延が設定されていれば、その秒数から取得できます。
ただ、エンコーダディレイ対策ではなく、単純に音ズレ調整のために使われている場合もあるので、注意してください。

元動画にエンコーダディレイの情報がない場合、使われた AAC エンコーダがわかっている場合は、以下の表にある値でサンプル数を特定できます。

動画内に情報がなく、エンコーダもわからない場合は、映像の長さと音声の長さを比べたり、WAV 変換して波形を確認したりして、なんとなくこのくらいかなという当たりをつけるしかありません。

ただ、元の音声が LC-AAC で、高サンプリングレートの場合、エンコーダディレイはそれほど長くないので、最低でも 1024 サンプル削っておけば、それほど気にならないかもしれません。
各 AAC エンコーダのエンコーダディレイの長さ
※ HE-AAC v2 は載せていません。
終端のサンプル数は、エンコーダのバージョンによって変わることが多かったり、正確な値がわからない場合があるので、割愛しています。

形式先頭サンプル数
fdkaac (ver 1.0.2)
LC2048
HE2048
NeroAACEnc (ver 1.5.4)
LC2624
HE4672
qaac (ver 2.64)
LC2112
HE2112
ffmpeg (ver 3.4.1, 内蔵エンコーダ)
LC1024
エンコーダディレイの長さを調べる
エンコーダディレイは、使ったエンコーダや出力形式などによって、長さが変わります。

HE-AAC/HE-AAC v2 の場合や、サンプリングレートが低い場合、エンコーダディレイは長くなるので、何も対策をしていない場合は、音ズレが気になるかもしれません。

qaacfdkaac を使って、*.m4a (MP4 コンテナ) で出力した AAC の場合は、ファイル内に iTunSMPB というデータがあるので、その情報からエンコーダディレイの長さ (サンプル数) を取得できます。
※ *.aac で出力した場合、iTunSMPB は書き込まれません。

iTunSMPB が書き込まれないエンコーダの場合は、サイン波などを生成してエンコードし、その波形から確認する方法があります。

iTunSMPB 確認方法
エンコードした *.m4a ファイルをバイナリエディタで開き、先頭か終端部分で "iTunSMPB" という文字列がある部分を探します。

バイナリエディタがない場合、fdkaac は終端にデータがあるので、以下のコマンドで確認できます。
※ qaac の場合は先頭から少し進んだ所にあります。

$ od -A n -t x1z -w140 -j $(($(wc -c out.m4a  | cut -d ' ' -f1) - 140)) out.m4a
※ 2箇所の "out.m4a" のファイル名部分は置き換えてください。

iTunSMPB のデータは 140 byte なので、ファイルサイズから 140 byte を引いた位置から、バイナリデータとその文字を表示しています。
データがファイルの終端に無い場合は、このままでは見えないかもしれません。
先頭の数値部分は無視して、">iTunSMPB....data" 以降のデータを見てください。

以下は、fdkaac / HE-AAC でエンコードした場合のデータです。
データは、16進数の数値を文字列にして空白で区切ったものとなっています。

iTunSMPB....data........
00000000 00000800 000002AC 0000000000035D54 (以下略)

00000000-
00000800先頭のエンコーダディレイのサンプル数 (=2048)
000002AC終端のエンコーダディレイのサンプル数 (=684)
0000000000035D54エンコード前の音声の全サンプル数 (=220500)

16進数の文字列を10進数に変換する場合は、以下のようにします。
"16#" の後に変換したい値を入れてください。

$ echo $((16#00000800))
2048

fdkaac / HE-AAC の場合、エンコーダディレイの先頭のサンプル数は 2048 であることが分かりました。
サンプル数を秒数に変換
ところで、「サンプル数」とは何だろうと思うかもしれません。

音声データでよく目にする、44100 Hz や 22050 Hz などのサンプリングレートの値は、「音声データの一秒間のサンプル数」 を表しています。
48000 Hz なら、一秒間に 48000 のサンプルデータがあります。

ということは、音声のサンプリングレート値と、エンコーダディレイのサンプル数を使えば、エンコーダディレイの音声としての時間が計算できることになります。

ms = delay_samples * 1000 / samplerate(Hz)

samplerate は、44100 などの音声のサンプリングレート値。
delay_samples は、エンコーダディレイのサンプル数。
結果は、ミリセカンド (1/1000 秒) です。

44100 Hz で 2048 サンプルなら、46.4399..
22050 Hz で 2048 サンプルなら、92.8798..

MP4 の結合に MP4Box などを使う場合、音声遅延は ms 単位で指定しなければならないので、その場合は、サンプル数を秒数に変換した値を使います。
動画の結合時にエンコーダディレイ対策をする
動画の再生側では、音声のエンコーダディレイ部分はそのまま音声として無音に変換されるので、何も対策をせずに結合すると、エンコーダディレイ分、音声が遅れることになります。

音声に AAC などを使う場合、結合時にエンコーダディレイの対策をする必要があります。

MKV で結合する場合は、おそらく自動で音声ファイルからエンコーダディレイの長さが取得されて、動画内に情報を書き込んでくれるので、基本的には何もする必要はありません(音声ファイルにエンコーダディレイの情報がある場合)。

MP4 で結合する場合は、エンコーダディレイを自動で処理してくれないので、動画に音声遅延の情報を設定して、エンコーダディレイの秒数をずらして再生させる必要があります。
MP4 結合時に音声遅延の設定を行う
MP4 結合時に音声遅延の設定を行う方法を説明します。
結合ツールによって、指定方法が異なります。

L-SMASH ツール
muxer コマンドで、音声のソース指定時に、「?encoder-delay=<サンプル数>」 を追加します。
ツール側で、サンプル数と音声のサンプリングレート値を元に秒数に変換してくれるので、便利です。

$ muxer -i out.mp4 -i out.m4a?encoder-delay=2048 -o res.mp4

MP4Box
L-SMASH 以外のツールでは、基本的に秒数で指定するので、自分でエンコーダディレイの秒数を計算する必要があります。

MP4Box コマンドを使う場合は、音声のソース指定時に 「:delay=-<ms 秒数>」 を追加します。

※この時、数値はマイナスを付けて負の値にしてください。
音声の開始を早めて、先頭の無音部分をスキップする必要があるので、開始位置はマイナスにしなければなりません。
正の値にすると、逆に音が遅れてしまいます。

$ MP4Box -add out.mp4 -add out.m4a:delay=-46 -new res.mp4