ffmpeg な「Manipulating audio channels」

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

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 “」という空白入りの完全一致を試みて、「そんなレイアウトはご存知ないですぅ」とのたまう。

半年後にハマってみた – Error while processing the decoded data for stream #0:1

本日12/14。

上の「出来てみた」で違うヤツに対し概ねこんなエラー:

 1 ffmpeg version 3.3.2 Copyright (c) 2000-2017 the FFmpeg developers
 2   ...
 3 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'aaa.mp4':
 4   ...
 5     Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 130 kb/s (default)
 6     Metadata:
 7       handler_name    : SoundHandler
 8 Input #1, mov,mp4,m4a,3gp,3g2,mj2, from 'bbb.mp4':
 9   ...
10     Stream #1:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
11     Metadata:
12       creation_time   : 2018-11-07T11:35:20.000000Z
13       handler_name    : ISO Media file produced by Google Inc. Created on: 11/07/2018.
14 Stream mapping:
15   Stream #0:1 (aac) -> amerge:in0 (graph 0)
16   Stream #1:1 (aac) -> amerge:in1 (graph 0)
17   Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
18   pan (graph 0) -> Stream #0:1 (aac)
19 ...
20 [Parsed_amerge_0 @ 0000000006de58a0] No channel layout for input 1
21 [Parsed_amerge_0 @ 0000000006de58a0] Input channel layouts overlap: output layout will be determined by the number of distinct input channels
22 Error while filtering
23 Failed to inject frame into filter network: Cannot allocate memory
24 Error while processing the decoded data for stream #0:1
25 [aac @ 0000000000527360] Qavg: nan
26 [aac @ 0000000000527360] 1 frames left in the queue on closing
27 Conversion failed!

オリジナルの状態で一方がステレオ、もう一方がモノラルだったため、最初それが原因だろうと思ってステレオに変換後に受け取ったのがこの状態。というかエラーは何一つ変わってない。(そもそも入力を上での aaa.mp4(元からステレオのもの) を2回渡しても同じことになる。)

ffprobe で覗き込んでもなにがしかヘンなところがあるわけでないので、腑に落ちないもののいくつか変換を試みてみるも結果は同じ。相変わらず何を叱られてるのかわからない、つまりエラー報告は何一つ本質を突いてくれてない。

こうなったらもはや「理想とかけ離れた最後の手段」しかないか、と:

1 [me@host: ~]$ ffmpeg -y -i aaa.mp4 aaa.mp3
2    ...
3 [me@host: ~]$ ffmpeg -y -i bbb.mp4 -ac 2 bbb.mp3
4    ...
5 [me@host: ~]$ ffmpeg.exe -y -i aaa.mp3 -i bbb.mp3 -i bbb.mp4 -c:a aac \
6 > -filter_complex "[0:a][1:a]amerge=inputs=2,pan=stereo|c0<c0+c1|c1<c2+c3[a]" \
7 > -map '2:v' -map '[a]' result.mp4
8    ...

予想通りこれで目的のことは出来た。

aac → mp3 (lame) → aac という不可逆変換を経てしまうのでこれは劣化するだろう、たぶん。が、今の目的はどのみち左チャンネルは左チャンネルでモノラル、右チャンネルは右チャンネルでモノラル、てことをしているわけだから、許容出来ないことはないであろう、と諦める。

結局「各エンコーダがなにがしかのよきにはからってくれる」というブラックボックス的振る舞いに頼るしか手はなかった、ということ。元の aac 状態で「何がトクベツなのか」がわからんのだから「いっそ mp3 変換でよきにはからってくれる何かしら」に頼るしかないのだと。

いまだによくわからんのだが、ffprobe ってほんとに必要な情報全部ダンプしてくれているのであろうか? 「何かがトクベツ」だからこそ「何かよろしくないこと」が起こるハズなのに、ffprobe が示す情報だけでは何一つトクベツな何かしらは感じることが出来ない。ゆえ抜本的な措置のしようもないのだよな…。