(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.get_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 Editor でカット位置の確認
シーンカットをしたい場合は、VapourSynth Editor で、カットするフレームの位置を確認します。

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

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

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

フレーム位置を確認
vsedit コマンドで VapourSynth Editor を起動して、プレビュー用のスクリプトを読み込みます。
「Script」 > 「Preview」、または F5 キーで、プレビュー画面を表示します。

時間のバーやフレーム位置の入力欄を使ってシークし、カットするフレームの位置を確認します。
時間のバーの左側に、現在のフレーム位置 (0〜) が表示されているので、その位置を記録します。

なお、スクリプトに書く際には、カットする部分の範囲ではなく、映像として残す部分の範囲が必要なので、
残す部分の先頭」と「残す部分の終端+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 ファイルにはエンコーダディレイの情報が含まれていないため)、正確に音ズレを回避するためには、変換後の 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 サンプル削除しても先頭に余分な部分が残る場合はありますが、ffmpeg よりは短くなるので、再生してもそこまで気になることはありません。

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

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

音声編集を行いたい場合は、カット後の音声に対して行うべきです。

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

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

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

$ vspipe --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 の場合、音声データの先頭と終端に 「エンコーダディレイ」 と呼ばれる部分が追加されています。
その部分はデコード時に基本的に無音として変換され、再生時間もその分少しだけ長くなります。

通常、デコーダはエンコーダディレイをそのまま音声データとして変換するので、動画の音声として使う場合は、正しく処理しないと、音ズレの原因となります。
AAC → WAV にデコード
元動画から抽出した音声をデコードして WAV 変換する場合、基本的に、エンコーダディレイは自動で除去できません。

AAC について
*.m4a 形式の AAC の場合は、エンコーダが MP4 コンテナ内にエンコーダディレイの情報を書き込むので、デコーダはそれを元にエンコーダディレイを除去できます。
*.aac 形式の AAC の場合は、ファイル内に AAC の生の音声データしか含まれていないため、エンコーダディレイの情報はありません。そのため、デコーダはエンコーダディレイも含めすべて音声データとして処理します。

動画の音声として AAC を使っている場合は、生の AAC データしか抽出できないので、AAC ファイルだけでは、エンコーダディレイの情報は取得できません。

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

faad の場合は特殊で、エンコーダディレイの除去には中途半端に対応しています。
faac でエンコードした場合、先頭のエンコーダディレイが 1024 サンプルなので、その分を先頭から削除して出力されます。
実際はエンコーダごとにサンプル数が違うので、これだけではエンコーダディレイを取り除けない場合が多いですが、先頭のエンコーダディレイが 1024 サンプルを下回ることはないので、何もしないよりはマシという形になります。

ffmpeg など、通常のデコーダの場合は、エンコーダディレイはすべて音声データとしてデコードするので、エンコーダディレイの分、確実に長くなります。
エンコーダディレイのサンプル数がわかっている場合は、こちらを使ってデコードし、先頭・終端を手動でカットして、エンコーダディレイを取り除くことができます。

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

>> aacdelay.png

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

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

結論:どうするべきか
元動画が MKV の場合は、mkvinfo コマンドで音声の 「デフォルトのフレーム持続期間」 を見れば、それが先頭のエンコーダディレイの長さになっていると思います。
ms * samplerate / 1000 でサンプル数を逆算できます。

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

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

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

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

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

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

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

iTunSMPB の情報がない場合は、サイン波などを生成してエンコードし、波形から確認します。

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

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

2箇所の "out.m4a" のファイル名部分は置き換えてください。
$ od -A n -t x1z -w140 -j $(($(wc -c out.m4a  | cut -d ' ' -f1) - 140)) 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 などのサンプリングレートの値は、「音声データの一秒間のサンプル数」 を表しています。
ということは、サンプリングレート値とエンコーダディレイのサンプル数を使えば、エンコーダディレイの秒数が計算できることになります。

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 単位で指定しなければならないので、その場合はサンプル数を秒数に変換した値を使います。
動画の結合時にエンコーダディレイ対策をする
動画の再生側では、音声のエンコーダディレイは処理してくれないので (動画結合時は生の音声データしか格納されないので、M4A のエンコーダディレイ情報などは削除されて、動画内に含まれていないため)、音声に 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

ffprobe で情報を見てみる
ファイルに遅延時間が正しく設定されているか確認したい場合は、ffmpeg 用のツール ffprobe コマンドで、動画の情報を表示してみてください。

$ ffprobe res.mp4
...
Duration: 00:19:45.01, start: -0.046440

Duration が全体の長さで、start が音声遅延の時間 (秒) です。

res.mp4 は、muxer コマンドで、44100 Hz、エンコーダディレイ 2048 サンプルの AAC を結合した MP4 です。
2048 * 1000 / 44100 = 46.4399.. ms、四捨五入すると 46.440 ms なので、秒数にすると 0.046440 sec で、正しく値が記録されているのがわかります。