音にあわせてシェイクするような動画を作りたいと思うも…(ffmpegとpyavで)

ffmpeg の話でもあり久々の pyav の話でもある。

しかも「完遂させずに満足しちゃった」話。

物足りない…と言ったうちの一つが、要するにこれなのだわ。つまりさ、「視覚効果」として音を利用出来ないだろうか、てことなの。

やりたいことの発想は例えばさ、「波形をピクセルずらしの元ネタにする」てことさ。これを直接的に実現する手段として、ワタシはまだ remap しか見つけられてない。この remap の仕様がさ、利用者にとってはかなりエグいもなのよ:

Remap pixels using 2nd: Xmap and 3rd: Ymap input video stream.

Destination pixel at position (X, Y) will be picked from source (x, y) position where x = Xmap(X, Y) and y = Ymap(X, Y). If mapping values are out of range, zero value for pixel will be used for destination pixel.

Xmap and Ymap input video streams must be of same dimensions. Output video stream will have Xmap/Ymap video stream dimensions. Xmap and Ymap input video streams are 16bit depth, single channel.

「16bit depth, single channel」が何ゆえ「エグい」か、つーと、普通どんなどマイナーフォーマットでも 8bit だからね。「ピクセル位置」を表現するんだから 16bit でないと表現できないのは当たり前なんだけれど、なのでここで要求されているのはたとえば「pix_format=gray16be」とか「pix_format=gray16」な。

で、そのフォーマット問題もさることながら、「そのさー、リマップファイルっていかにして作るのぞ」てことだよさ。

「ffmpeg 自身で」で簡単に出来そうならいいんだけれど、そうじゃないよなぁ、と直感的に思うわけだ。出来ない気もする。例えば、「identical」(つまり何もしない)を指示するためのリマップは、numpy で表現するとすればたとえばこんななはずなのよ:

8×5というヘンなサイズだと仮定して
1 >>> import numpy as np
2 >>> a = np.kron(np.arange(5), np.ones(8)).reshape((5, 8))
3 >>> a
4 array([[ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
5        [ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.],
6        [ 2.,  2.,  2.,  2.,  2.,  2.,  2.,  2.],
7        [ 3.,  3.,  3.,  3.,  3.,  3.,  3.,  3.],
8        [ 4.,  4.,  4.,  4.,  4.,  4.,  4.,  4.]])

この行列に対して、音声の波形(というかまさに振幅)を「ズラし」として足し合わせれば、目的のリマップになるはずだ、てわけだ。

ffmpeg フィルタ全般に言えるんだけれど、フィルタ間のデータの行き来はとてもじゃないが「シームレス」とは程遠くて、この「音声データの振幅」をダイレクトに remap に流し込む方法は多分ないんじゃないかと思う。なので、「リマップファイルを作る」のと「それを使う」のは原則として別プロセス(別作業)となるはず。まぁ「「エフェクトファイル」みたいなのを用意してから適用するんだぜっ」て説明の仕方なら納得すんのかもしらんけどね。

つーわけで、「リマップファイル、作ろうぜ」のアプローチとして、pyav と numpy、Pillow がいいんだろうなぁと:

失敗作(後述)
 1 # -*- coding: utf-8 -*-
 2 from __future__ import division
 3 from __future__ import unicode_literals
 4 
 5 import sys
 6 import numpy as np
 7 from PIL import Image
 8 import av
 9 
10 #
11 container = av.open(sys.argv[1])
12 #
13 stream = next(s for s in container.streams if s.type == 'audio')
14 resampler = av.AudioResampler(
15     format=av.AudioFormat("s16p"), layout="mono", rate=44100)  # stereo
16 #
17 
18 #
19 video_width = 1920
20 video_height = 1080
21 ocont_remap_xy = av.open("remap_xy.mp4", "w")
22 ocont_remap_yx = av.open("remap_yx.mp4", "w")
23 vstream_xy = None
24 vstream_yx = None
25 vrate = 25  #
26 for packet in container.demux(stream):
27     for frame in packet.decode():
28         rf = resampler.resample(frame)
29         # get amplitudes
30         from_audio_raw = np.fromstring(rf.planes[0].to_bytes(), dtype=np.int16)  # normally, shape is (1024,)
31 
32         # translate amplitudes into pixel dispositions
33         from_audio_y = np.floor((from_audio_raw / np.abs(from_audio_raw.max()) + 1.0) * video_height)
34 
35         # build remap data
36         remap_xy = np.kron(np.arange(video_height), np.ones(video_width)).reshape((video_height, video_width))
37         xdimdiff = remap_xy.shape[1] - from_audio_y.shape[0]
38         remap_xy[:,int(xdimdiff / 2):int(xdimdiff / 2) + from_audio_y.shape[0]] += from_audio_y
39         remap_xy %= video_height
40         remap_yx = np.kron(np.arange(video_width), np.ones(video_height)).reshape((video_width, video_height))
41 
42         # build static image as "I" (32-bit signed integer pixels)
43         remap_xy_img = Image.fromarray(remap_xy, "I")
44         remap_yx_img = Image.fromarray(remap_yx, "I")
45 
46         # encode image to video stream
47         if vstream_xy is None:
48             vstream_xy = ocont_remap_xy.add_stream('h264', rate=vrate)
49             vstream_xy.width = video_width
50             vstream_xy.height = video_height
51             #vstream_xy.pix_fmt = 'gray16be'
52             vstream_xy.pix_fmt = 'yuv420p'
53         vframe_xy = av.VideoFrame.from_image(remap_xy_img)
54         for p in vstream_xy.encode(vframe_xy):
55             ocont_remap_xy.mux(p)
56         #
57         if vstream_yx is None:
58             vstream_yx = ocont_remap_yx.add_stream('h264', rate=vrate)
59             vstream_yx.width = video_width
60             vstream_yx.height = video_height
61             #vstream_yx.pix_fmt = 'gray16be'
62             vstream_yx.pix_fmt = 'yuv420p'
63         vframe_yx = av.VideoFrame.from_image(remap_yx_img)
64         for p in vstream_yx.encode(vframe_yx):
65             ocont_remap_yx.mux(p)
66 ocont_remap_xy.close()  # MUST!
67 ocont_remap_yx.close()  # MUST!

失敗作と言っている意味は何種類もある。

一つには「致命的に処理が遅い」ことだが、真の致命傷は「pyav が gray16be を決して受け容れてくれない」こと。なのでたとえこれが高速に処理出来て満足出来たとしても完成はしない。

もう一つは「同期」。動画は 25fps で書き出そうとしてる。音声は 44100Hz で入ってるが、一回のデコードで 1024 のサンプルが得られる。この関係性をちゃんと把握して絵にしないと、たとえば showwaves がみせてくれるようなものにはならない。

完成させたければ、同期の件をちゃんとしつつ、pyav でリマップ動画を作るのを諦めていったん連続静止画にして ffmpeg で再結合すればいい。

てわけで、ワタシの興味はここで途切れてしまった。「あ、出来るね、けどまぁいいや…」。

つーわけで、やってみたくてエロい人は続けてみちくり。個人的にこのアプローチが「最善」であって欲しいわけはなくて、もっと直接的で簡単な方法があると信じたいので、このやり方で突き進めたくないてことだわ。だけど「これでもたぶん出来るはずだよ」てことで。


2019-01-25追記: 代わりといってはなんだが、な続き → 音にあわせて光るような動画を作りたい…ですの? (ffmpegで)

あと、今さっき気付いたんだけれど、pyav 使ってる時点で、もう remap なんか全く必要ないんだよね。だとすると。remap を作ったお方は、いったいどんなユースケースを考えて作った? どう考えても用途ない。つまり「その remap ファイルを作れるんなら作る過程で remap 前に目的のものを作れる、ふつー」。

うーん、その考え方で同じネタやり直してみてもいいかもしれんな。もはやほぼ ffmpeg ネタではないけれど。