ffmpeg な「Manipulating audio channels」

あえてメモっとく意味があるかどうかが絶妙なところ。

ffmpeg にしては非常に珍しく、公式の解説が気合入っててめちゃくちゃわかりやすい:

なので基本これ読んでおけばバッチリ自由自在だぜっ、と概ね言える。話の性質上解説しやすいのかもなぁ。

ただ、なんの事前情報も持たずに上のページから離れずにすべからく理解出来る、というわけでもなくて、結局は読み方にコツがいる公式リファレンスを読む必要はある。

「バッチリ自由自在だぜっ、と概ね言える」で話は終わりかなぁと思ってやってみたのだが、まぁ言うべきことがないではなかったので、少しだけ話を続ける。

入力の例としてこんなのだと思って欲しい。入力1も2も映像はまったく同じ(か一方にだけ hardsub な字幕がある)で、音声だけ違う(一方が日本語で他方が英語、など)。公式説明はどの例も入力は全て音声ファイルなので、あえて動画を入力とする例:

2つの動画ともにステレオ
1 [me@host: ~]$ ffmpeg -y -i movJ.mp4 -i movE.mp4 \
2 >    -filter_complex "[0:a][1:a]amerge=inputs=2,pan=stereo|c0<c0+c1|c1<c2+c3[a]" \
3 >    -map '0:v' -map '[a]' \
4 >    movM.mp4

初見では呪文にしか見えないだろうし、そう見えたからといってあなたが無能であることを示すものでもない。やっていることを日本語にすると:

  1. 映像は movJ.mp4 のものをそのまま使う (-map '0:v')
  2. 音声は両ファイルのものを使う:
    • movJ.mp4 のステレオ左右チャンネルを足して出力([a])の左チャンネルにする
    • movE.mp4 のステレオ左右チャンネルを足して出力([a])の右チャンネルにする
    • 「足して」だけだと clipping noise るので「(=でなく) < で」renormalize する

少なくとも Windows 7 だとこうしたステレオを「左チャンネルだけ聞きたい」「右チャンネルだけ聞きたい」の術に欠けるので、(Windows であれば) たとえば Media Player Classic – Home Cinema (GitHub) を使うといい。「表示⇒オプション⇒音声切り替え」で以下の操作が可能:

なんにせよ「簡単じゃねーか、話を続けた意味は?」だよね、まだ。

ワタシはねぇ、とてつもなく下らない理由でハマっちまったのだ、軽く一時間ほど。以下間違い探し:

さっきのと何が違うか探してみて
1 [me@host: ~]$ ffmpeg -y -i movJ.mp4 -i movE.mp4 \
2 >    -filter_complex "[0:a][1:a]amerge=inputs=2;pan=stereo|c0<c0+c1|c1<c2+c3[a]" \
3 >    -map '0:v' -map '[a]' \
4 >    movM.mp4

この場合こういうエラーを喰らう:

1 Cannot find a matching stream for unlabeled input pad 0 on filter Parsed_pan_1

ffmpeg のエラー報告って、個人的にはかなり頭オカシイと思ってる。大抵何言われてるのかわからない。(ただまぁ、今回の場合は正解を理解したのち、かつ多少の ffmpeg 全体に対する理解があれば、答えを知った後は「確かに」なことを言われているのだけれども。)

答えはセミコロンとカンマを間違えた、てことなのだが、実はこの例を自分で試してみることをやったときも、ピリオドとコロンを間違えた。こういう「特殊な呪文」にはありがちなミス、てことだ。

まぁこの「ワタシがバカでしたぁ」告白が今回のメモの本題だったんだけれど、「個人的用事」としてはまだ少しだけ続く。

上では(例えば)「日本語が左チャンネル、英語が右チャンネル」にするというだけの用事は済んだわけだけれど、さらに優劣をつけたいならどうか、という話。つまり(たとえば)「主に英語が聞こえるが、こっそり低音量で日本語が聞こえる」としたいなら、て話。これについてはむしろリファレンスの方に例が書かれてる:

gain を変えているのだが、< (renormalize) すると意味なくなるので注意
1 [me@host: ~]$ ffmpeg -y -i movJ.mp4 -i movE.mp4 \
2 >    -filter_complex "[0:a][1:a]amerge=inputs=2,pan=stereo|c0=0.2*c0+0.2*c1|c1<c2+c3[a]" \
3 >    -map '0:v' -map '[a]' \
4 >    movM.mp4

なお、まぁなんというか「よく出来ているとは言えないドメイン固有言語」なのでこうしたいがダメ:

出来て欲しいと考えるのが人情
1 [me@host: ~]$ ffmpeg -y -i movJ.mp4 -i movE.mp4 \
2 >    -filter_complex "[0:a][1:a]amerge=inputs=2,pan=stereo|c0=0.2*(c0+c1)|c1<c2+c3[a]" \
3 >    -map '0:v' -map '[a]' \
4 >    movM.mp4

「Python 言語」とか「C 言語」みたいなものを考えたらダメ。もっとダサい。


オマケ的追記:
かなり複雑な例:

 1 #! /bin/sh
 2 
 3 ffmpeg -y \
 4   -i "l1.mp4" -i "cr1.mp4" -i "cl1.mp4" -i "cr2.mp4" \
 5   -filter_complex "
 6 color=c=black:s=960x540:d=234.119[prepadv0];
 7 sine=frequency=0:sample_rate=44100:d=234.119 [prepada_l0];
 8 sine=frequency=0:sample_rate=44100:d=234.119 [prepada_r0];
 9 [prepada_l0][prepada_r0]amerge=inputs=2[prepada0];
10 [0:v]scale=960:540[v0];
11 color=c=black:s=960x540:d=38.569[postpadv0];
12 sine=frequency=0:sample_rate=44100:d=38.569 [postpada_l0];
13 sine=frequency=0:sample_rate=44100:d=38.569 [postpada_r0];
14 [postpada_l0][postpada_r0]amerge=inputs=2[postpada0];
15 [prepadv0][v0][postpadv0] concat=n=3:v=1:a=0 [vc0];
16 [prepada0][0:a][postpada0] concat=n=3:v=0:a=1 [ac0];
17 
18 [1:v]scale=960:540[v1];
19 color=c=black:s=960x540:d=44.348[postpadv1];
20 sine=frequency=0:sample_rate=44100:d=44.348 [postpada_l1];
21 sine=frequency=0:sample_rate=44100:d=44.348 [postpada_r1];
22 [postpada_l1][postpada_r1]amerge=inputs=2[postpada1];
23 [v1][postpadv1] concat=n=2:v=1:a=0 [vc1];
24 [1:a][postpada1] concat=n=2:v=0:a=1 [ac1];
25 
26 color=c=black:s=960x540:d=62.338[prepadv2];
27 sine=frequency=0:sample_rate=44100:d=62.338 [prepada_l2];
28 sine=frequency=0:sample_rate=44100:d=62.338 [prepada_r2];
29 [prepada_l2][prepada_r2]amerge=inputs=2[prepada2];
30 [2:v]scale=960:540[v2];
31 [prepadv2][v2] concat=n=2:v=1:a=0 [vc2];
32 [prepada2][2:a] concat=n=2:v=0:a=1 [ac2];
33 
34 color=c=black:s=960x540:d=60.271[prepadv3];
35 sine=frequency=0:sample_rate=44100:d=60.271 [prepada_l3];
36 sine=frequency=0:sample_rate=44100:d=60.271 [prepada_r3];
37 [prepada_l3][prepada_r3]amerge=inputs=2[prepada3];
38 [3:v]scale=960:540[v3];
39 color=c=black:s=960x540:d=41.657[postpadv3];
40 sine=frequency=0:sample_rate=44100:d=41.657 [postpada_l3];
41 sine=frequency=0:sample_rate=44100:d=41.657 [postpada_r3];
42 [postpada_l3][postpada_r3]amerge=inputs=2[postpada3];
43 [prepadv3][v3][postpadv3] concat=n=3:v=1:a=0 [vc3];
44 [prepada3][3:a][postpada3] concat=n=3:v=0:a=1 [ac3];
45 
46 [vc0][vc1]hstack[1v];
47 [vc2][vc3]hstack[2v];
48 [1v][2v]vstack[v];
49 [ac0][ac1][ac2][ac3]
50 amerge=inputs=4,
51 pan=stereo|\
52     c0 < c0 + c4   +   0.2 * c2 + 0.2 * c6 |\
53     c1 < c3 + c7   +   0.2 * c1 + 0.2 * c5
54 [a]
55 " -map '[v]' -map '[a]' -ac 2 \
56   "merged.mp4"

何を想像すればいいかというと、4つのアングルから同時撮影された4つの動画。撮影開始・終了に撮影者ごとのズレがあるのでこれを穴埋めしつつ 2×2 にスタックしているのだが、音声については、

  • 左に配置する動画の音声の左チャンネルを出力の左チャンネルにするが、右に配置する動画の音声の左チャンネル成分もちょっとだけ混ぜる
  • 右に配置する動画の音声の右チャンネルを出力の右チャンネルにするが、左に配置する動画の音声の右チャンネル成分もちょっとだけ混ぜる

ということをしている。こうすると現実の聞こえ方に近づくかしら、みたいな思惑ね。

なんであえてこれを紹介したかは「こういうことも出来るぜっ」てことを言いたいがためではなくて、「ffmpeg の filter 構文はダサい」コレクションの追加、だったりするから。上のスクリプト、空白をつける場所を変えるとエラーになったりする。「pan=stereo」部分、空白を入れちゃダメ。ここ「チャンネルレイアウト名またはチャンネル数」を書くんだけど、余分な空白があると「”stereo “」という空白入りの完全一致を試みて、「そんなレイアウトはご存知ないですぅ」とのたまう。