FFT なスペクトラム可視化に音階をくっつけて(4)

ここで見せる動画をみて誤解されちゃいそうで怖いんだけれど。

以下再生前にちょっとボリュームを下げといて、ちとボリューム大きいので:

まぁなんだか「凄そう」に見える人もいるかもしれんけれど、そうじゃないので注意。つまり「リアルタイムでやってない」。

リアルタイムでこの手のことをしたい人は、ワタシのものよりかは この人がやってること とかから真似してった方が近いんじゃないかな、多分。自分でやってないからわからんけど。

俺的興味は単に「自分がやってることが正しいかどうか」の検証だけ。つまり「ちゃんと FFT してそこから音階拾えてますかいね」てこと。だけど(3)のような「動かない」解析だと確証持ちずらいでしょ、だから「擬似的でもリアルタイム「的」」なことをして、実際に鳴ってる音との対応で見たいわけよね。

あ、(3) でちょっと触れた「本当は contour を使いたくない」の理由がわかる事象が出てるよね? 縦の青線が引かれちゃうようにみえてるのはこれは無論 contour を使っているからで。

さて。どうやってこの動画を作ったかは単純で、

  1. (3)を連続静止画を吐くように書き換え
  2. 出来た静止画を ffmpeg で mp4 動画に
  3. 1. の入力音声と 2. の結果動画を ffmpeg で結合して mp4 動画に

これだけ。1. はこれ:

tiny_wave_wrapper については Python の wave モジュール例の殴り書き(5) を見てちょ
  1 import numpy as np
  2 import matplotlib.pyplot as plt
  3 import matplotlib.ticker as ticker
  4 from tiny_wave_wrapper import WaveReader, WaveWriter
  5 
  6 #
  7 _SCALES_L = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
  8 
  9 def _nn2scale(d):
 10     return int(d / 12) - 2, _SCALES_L[int(d) % 12]
 11 
 12 def _nn2freq(d):
 13     return np.power(2, (d - 69) / 12.) * 440
 14 
 15 def _freq2nn(f):
 16     return 69 + 12 * np.log2(f / 440.)
 17 
 18 class _nnformat(object):
 19     def __init__(self, minor):
 20         if minor:
 21             self._fmt = lambda oc, sc: "  " * (_SCALES_L.index(sc) % 2) + sc
 22         else:
 23             self._fmt = lambda oc, sc: "%s  (%d)" % (sc, oc)
 24 
 25     def __call__(self, f, pos):
 26         if f > 0:
 27             nn = int(_freq2nn(f))
 28             oc, sc = _nn2scale(nn)
 29             return self._fmt(oc, sc)
 30         return ""
 31 
 32 def _setup_locator(ax):
 33     _all = np.arange(24, 144)
 34     _maj = np.array([v for v in _all if v % 12 == 0])
 35     _min = np.array([v for v in _all if v % 12 in (2, 4, 5, 7, 9, 11)])
 36     ax.yaxis.set_major_locator(ticker.FixedLocator(_nn2freq(_maj)))
 37     ax.yaxis.set_minor_locator(ticker.FixedLocator(_nn2freq(_min)))
 38 
 39 def _saveasgraph(F, fnum, args):
 40     freq = np.fft.fftfreq(F[0].shape[1], 1./rate)
 41     X = np.arange(F[0].shape[0])
 42 
 43     fig, ax_ = plt.subplots()
 44     for chn in range(2):
 45         ax = plt.subplot(1, 2, chn + 1)
 46 
 47         Y = freq[:len(freq) // 2]
 48         Z = F[chn].T[:len(freq) // 2,:]
 49         if args.upper_limit_of_view:
 50             ind = (Y <= args.upper_limit_of_view)
 51             Y, Z = Y[ind], Z[ind, :]
 52         if args.lower_limit_of_view:
 53             ind = (Y >= args.lower_limit_of_view)
 54             Y, Z = Y[ind], Z[ind, :]
 55 
 56         ax.contour(X, Y, Z, cmap='jet')
 57         ax.yaxis.set_major_formatter(ticker.FuncFormatter(_nnformat(False)))
 58         ax.yaxis.set_minor_formatter(ticker.FuncFormatter(_nnformat(True)))
 59         ax.set_xticks([])
 60         _setup_locator(ax)
 61         ax.grid(True)
 62 
 63     fig.tight_layout()
 64     fig.savefig("out_%05d.jpg" % (fnum))
 65     plt.close(fig)
 66 
 67 
 68 if __name__ == '__main__':
 69     import argparse
 70     parser = argparse.ArgumentParser()
 71     parser.add_argument("-s", "--step", type=int)
 72     parser.add_argument("-u", "--upper_limit_of_view", help="Hz", type=int)
 73     parser.add_argument("-l", "--lower_limit_of_view", help="Hz", type=int)
 74     parser.add_argument("target")
 75     args = parser.parse_args()
 76 
 77     with WaveReader(args.target) as fi:
 78         nchannels, width, rate, nframes, _, _ = fi.getparams()
 79         raw = np.fromstring(fi.readframes(nframes), dtype=np.int16)
 80         channels = raw[::2], raw[1::2]
 81 
 82     if args.step:
 83         step = args.step
 84     else:
 85         step = rate // 8
 86     nframes = len(channels[0])
 87     nframes -= (nframes % step)  # drop the fraction frames
 88     tmp = np.fft.fft(channels[0][:step])  # FIXME: to be more smart.
 89     F = np.array([
 90         np.zeros((rate // step * 10, len(tmp))),
 91         np.zeros((rate // step * 10, len(tmp)))
 92         ])
 93     p = 0
 94     for i in range(0, nframes, step):
 95         for chn in range(len(channels)):
 96             channel = channels[chn]
 97             f = np.abs(np.fft.fft(channel[i:i + step]))
 98             f = f / f.max()
 99             F[chn][p] = f
100         _saveasgraph(F, i // step, args)
101         p += 1
102         if p >= F[0].shape[0]:
103             p = 0
104             F[:,:,:] = 0

(3)より少し単純化した部分もあるけど、基本的には「時間の進行の都度静止画に」という構造に変えている。

で、今の場合「step = rate // 8」、つまりフレームレート 8 の動画相当の静止画が出来る。のでこんなで動画に:

1 me@host: ~$ ffmpeg -framerate 8 -i out_%05d.jpg -c:v libx264 out.mp4

入力の音声 test_CEG_with_adsr.wav と out.mp4 を結合:

1 me@host: ~$ ffmpeg -i out.mp4 -i test_CEG_with_adsr.wav omix.mp4

ちぅわけで、「だからどーした」って言われると困っちゃうんだけれど、よーするに「オレ的に何度かは必要になりそうだけど、都度編み出すのはさすがにそこそこ大変なのでメモメモ」ってだけのハナシ。(matplotlib もしばらく離れるとすぐに思い出せなくなるし、ffmpeg は複雑だし。)