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

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

以下では、例として、変換元の動画ファイル名を video.mkv (H.264 + AAC) とし、
シーンカットと映像のリサイズを行って、MP4 (H.264 + HE-AAC) に再エンコードするものとします。
ノイズ除去や逆テレシネなどの処理は行っていませんが、必要な場合はスクリプトに記述してください。
エンコード用スクリプトを用意
エンコード用のスクリプトを用意します。

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

import vapoursynth as vs

core = vs.get_core()

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

clip = core.ffms2.Source(source='vsrc.mp4')
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 で、カットするフレームの位置を確認します。

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

#!/usr/bin/python3

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

vsedit コマンドで VapourSynth Editor を起動して、プレビュー用のスクリプトを読み込みます。
Script > Preview (F5) でプレビューを表示します。
バーやフレーム位置の入力欄を使ってシークして、カットするフレームの位置を確認します。
シークバーの左側に、現在のフレーム位置 (0〜) が表示されているので、その位置を記録します。

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

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

clip = clip[1000:2000] + clip[3000:]

もし、後で同じファイルを再エンコードする可能性がある場合は、フレーム位置をもう一度確認するのは面倒なので、カット部分のコードを別のテキストに保存しておくと、再エンコードが楽になります。
元動画の映像と音声を分離
音声は映像とは別にエンコードする必要があるので、まずは元動画の映像と音声を分離して、音声を wav 変換します。
映像の分離は必須ではなく、スクリプト内で元動画を直接読み込むこともできますが、一応分離しておきます。
映像と音声の分離
※ 映像や音声が複数格納されている場合は、必要なトラックだけ分離してください。

ffmpeg を使う

以下は、元動画が H.264 + AAC の場合です。
フォーマットが異なる場合は、拡張子を変更してください。

$ ffmpeg -i video.mkv -vcodec copy -an vsrc.mp4
$ ffmpeg -i video.mkv -vn -acodec copy asrc.aac

(mkv の場合) MKV のツールを使う

Arch Linux の場合は mkvtoolnix-cli パッケージ。

## トラック情報表示
$ mkvinfo video.mkv

## トラック抽出
## ID:0 が映像、ID:1 が音声の場合
$ mkvextract tracks 0:vsrc.264 1:asrc.aac
音声を wav 変換
AAC の場合

faad コマンドを使います。
Arch Linux の場合、faad2 パッケージ。
※ ffmpeg で wav 変換すると、先頭に余分な無音部分が追加されて音ズレするので、使わないでください。

$ faad -o src.wav asrc.aac

注意点

※ 音量変更などをする場合は、この段階で行わずに、シーンカット後の音声データに対して編集を行う方が良いです。

映像エンコードの前に音声編集を行った場合は、「音声編集→映像エンコード(カット後の音声データ出力)→音声エンコード」という手順になりますが、もし、音量が気に入らないから音声編集をやり直したいとなった場合、カット後の音声データはすでに音声編集が行われた後の状態なので、また「音声編集→映像エンコード(カット後の音声データ出力)→音声エンコード」の手順をやり直さなければならなくなります。

しかし、カット後の音声データに対して音声編集を行う場合は、「音声編集→音声エンコード」をやり直せば良いだけなので、後から音声編集をやり直したい場合は手順が楽になります。
映像のエンコードと音声の出力
x264 コマンドで、H.264 エンコードを行います。
Arch Linux の場合は、x264 パッケージ。

シーンカットをする場合は、映像の出力と同時に、カット後の音声ファイル (ここでは cut.wav) が出力されます。

$ 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

上記は、アニメ用の設定です。

なお、L-SMASH ツールでの MP4 結合時は、ここでの出力ファイルが .mp4 だと、fps が 0 になるなどしてうまく結合できなかったので、.264 で出力しています。
.264 ファイルの場合は、再生時にシークができないので、確認再生する時に不便かもしれません。
MP4Box などで結合する場合は、.mp4 で構いません。
音声の編集とエンコード
音声の編集
音量などを変更する場合は、VapourSynth で出力された音声ファイル (cut.wav) に対して行います。
GUI で行うなら audacity、CUI で行うなら sox コマンドを使います。
Arch Linux の場合、パッケージ名は、上記のコマンド名と同じです。

sox コマンドで音量正規化

$ sox cut.wav cut2.wav gain -n -1

-n で、最大が指定した dB になるようにする。
ここでは、-1 dB を指定して、MAX (0dB) よりは少し音量を下げて音割れを防いでいます。
AAC エンコード
音声をエンコードします。
ここでは、fdkaac コマンドで HE-AAC 64kpbs にエンコードします。

Arch Linux の場合、fdkacc パッケージ。
自分でビルドする場合は、libfdk_aacfdkaac をビルドします。

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

$ fdkaac -p 5 -b 64 -o out.m4a cut2.wav

-p : プロファイルの指定。
2AAC LC
5HE-AAC
29HE-AAC v2

-b : 固定ビットレート (kbps)
映像と音声の結合
映像と音声の結合には、L-SMASH ツールか MP4Box を使います。
ffmpeg でも出来ますが、一応 MP4 に特化したツールを使った方が良いでしょう。

音ズレ調整に関しては L-SMASH の方が使いやすいのですが、うまく結合しない場合があったので、気に入った方を使ってください。
Arch Linux の場合、L-SMASH は l-smash パッケージ。MP4Box は gpac パッケージにあります。

x264 でエンコードした映像は out.264、AAC エンコードした音声は out.m4a となっているので、この2つを MP4 コンテナに格納します。

## 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 の詳細については、後述します。
値に関しては、それぞれで適切な値を指定する必要があるので、音ズレについての部分を読んでください。
音声が AAC などの場合、先頭に余分な無音部分があるので、音声遅延を指定しないと、数十〜数百 ms ほど音ズレします (音が遅れる)。

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

## タイトルを付加
$ mp4tags -s タイトル res.mp4

## WEB 上でダウンロードしながら再生できるようにする
$ mp4file --optimize res.mp4
AAC の音ズレについて
音声が AAC の場合、wav 変換時や MP4 結合時などは注意しておかないと、再生時に音ズレしてしまいます。
Encoder Delay
AAC ファイルの場合、先頭と終端に、Encoder Delay と呼ばれる余分なデータ部分が含まれていて、その部分は wav 変換時には無音となり、再生時間もその分だけ長くなります。
AAC デコーダはこの部分を取り除いて変換するのが正しいのですが、デコーダによっては、対応しているものと対応していないものがあります。

ffmpeg は非対応なので、AAC->WAV 変換すると、前後に無音部分が追加されます。
faad は対応しているので、AAC->WAV 変換すると、前後の無音部分は出力されません。

元動画の音声を wav 変換する時に ffmpeg を使わなかったのは、これが原因です。
AAC を wav 変換して再エンコードする場合は、faad コマンドを使いましょう。

気になる場合は、AAC ファイルをそれぞれのコマンドで wav 変換したものを、audacity などで波形表示して比較してみてください。

>> aacdelay.png
上の画像は、LC-AAC のファイルを faad と ffmpeg でそれぞれ wav にデコードして比較した時のものです。
ffmpeg の方が 23ms ほど遅くなっていることから、先頭に無音部分が追加されているのがわかります。
Encoder Delay の長さを調べる
Encoder Delay の長さは、使ったエンコーダや LC/HE/HEv2 の形式によって変わるので、共通して一定というわけではありません。
LC-AAC や高サンプリングレートの場合はそれほど長くはならないので、音ズレ対策をしないまま再生してもあまり差がないかもしれませんが、HE-AAC/HE-AAC v2 の場合やサンプリングレートが低い場合は長くなってくるので、そのままだと音ズレが気になるかもしれません。

qaacfdkaac を使って M4A で出力した場合は、ファイルの終端に iTunSMPB というデータがあるので、その情報から Encoder Delay のサンプル数を取得できます。
(*.aac で出力した場合は iTunSMPB は出力されません)

エンコードした AAC ファイルをバイナリエディタで開き、終端部分に "iTunSMPB" という文字列がある所を探します。
バイナリエディタがない&インストールしたくないという場合は、以下のコマンドで確認できます。
out.m4a の部分は、エンコードしたファイル名に置き換えてください。

$ od -A n -t x1z -w140 -j $(($(wc -c out.m4a  | cut -d ' ' -f1) - 140)) out.m4a

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

以下は、fdkaac / HE-AAC でエンコードした場合のデータです。

iTunSMPB....data........
00000000 00000800 00000200 0000000001E60A00 (以下略)

データは、16進数の数値を文字列にして空白で区切ったものとなっています。
1番目の数値は飛ばして、
2番目の 00000800 が Encoder Delay の先頭のサンプル数、
3番目の 00000200 が終端の無音部分のサンプル数、
4番目の 0000000001E60A00 がエンコード前の元データのサンプル数です。

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

$ echo $((16#00000800))

fdkaac / HE-AAC の場合は、サンプル数は 2048 であることが分かりました。

以下、fdkaac の場合の先頭サンプル数。
fdkaac : LC-AAC2048
fdkaac : HE-AAC2048
fdkaac : HE-AAC v23072
サンプル数を秒数に変換
ところで、サンプル数とは何だろうと思うかもしれませんが、音声データでよく目にする 44100Hz や 22050Hz という値は、「一秒間のサンプル数」です。

音声データの中身は、PC 上で扱えるバイナリデータに過ぎないので、数値を元に音を出しているわけですが、音を構成する一番最小の単位のデータのことを「サンプル」と呼びます。
16bit WAV データの場合は、16bit の数値データが最小の単位なので、1サンプルは 2byte です。

Encoder Delay のデータ長さはサンプル単位ですが、このままではわかりにくいので、秒数 (ms) に変換してみます。

ms = delay_samples * 1000 / samplerate(Hz)

samplerate は、44100 などの音声のサンプリングレート値。
delay_samples は、Encoder Delay のサンプル数。
結果は、ミリセカンド (1/1000 秒) 単位です。

44100Hz で 2048 サンプルなら、46.4399.. となります。
22050Hz で 2048 サンプルなら、92.8798.. です。

MP4 結合に MP4Box などを使う場合、音声遅延は ms 単位で指定しなければならないので、その場合はサンプル数を秒数に変換した値を使います。
MP4 結合時に音声遅延を指定する
動画の音声として AAC を使う場合、再生側で Encoder Delay はカットされないので、先頭の無音部分をスキップさせないと、映像と音声が合わなくなります。
そこで、映像と音声を結合して MP4 を作成する時に、MP4 に、「この音声を再生する時は再生開始時間を指定時間分ずらす」という情報を書き込みます。
この情報があれば、再生時に無音部分の長さ分を進めた位置から再生されるので、音ズレはなくなります。

L-SMASH ツールを使う

muxer コマンドで、音声のソース指定時に、「?encoder-delay=<サンプル数>」を追加します。
サンプル単位で指定できるので、便利です。
ただし、MP4 内部では秒数で記録されるので、結局単位の変換は行われます。

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

MP4Box を使う

L-SMASH 以外のツールでは、基本的に ms 単位で指定するので、サンプル数を秒数に変換しなければなりません。
MP4Box コマンドを使う場合は、音声のソース指定時に「:delay=-<ms 秒数>」を追加します。
この時、数値はマイナスを付けて負の値にしてください。
音声の開始を早めて先頭の無音部分を飛ばす必要があるので、開始位置はマイナスにしなければなりません。
正の値にすると、逆に音が遅れてしまうので注意。

$ MP4Box -add out.mp4 -add out.m4a:delay=-46 -new res.mp4
ffprobe で情報を見てみる
MP4 ファイルに遅延時間が正しく設定されているか確認したい場合は、ffmpeg 用のツール ffprobe コマンドで、動画の情報を表示してみてください。

$ ffprobe res.mp4

音声遅延が設定されていると、以下のような行が存在します。
Duration: 00:19:45.01, start: -0.046440

Duration が全体の長さで、start が音声遅延の時間 (秒) です。
上記は、muxer コマンドで、44100Hz、delay 2048 サンプルの AAC を結合した動画です。
計算では 46.4399.. ms となっていましたが、四捨五入して 46.440 ms なので、秒数にすると 0.046440 で、正しく値が記録されているのがわかります。