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

うーむ。

matplotlib ネタなのかもなこれは。

とりあえず 一つ前のものよりは「マシ」にしたヤツ:

  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.pow(2, (d - 69) / 12.) * 440
 14 
 15 def _freq2nn(f):
 16     return 69 + 12 * np.log2(f / 440.)
 17 
 18 def _nnformat(f, pos):
 19     if f > 0:
 20         nn = _freq2nn(f)
 21         oc1, sc1 = _nn2scale(int(nn))
 22         oc2, sc2 = _nn2scale(int(np.ceil(nn)))
 23         return r"""%.2f [%s(%d) $\sim$ %s(%d)]""" % (
 24             nn, sc1, oc1, sc2, oc2)
 25     return ""
 26 
 27 
 28 if __name__ == '__main__':
 29     import argparse
 30     parser = argparse.ArgumentParser()
 31     parser.add_argument("mode", choices=["show", "pdf"])
 32     parser.add_argument("-s", "--step", type=int)
 33     parser.add_argument("-u", "--upper_limit_of_view", help="Hz", type=int)
 34     parser.add_argument("-l", "--lower_limit_of_view", help="Hz", type=int)
 35     parser.add_argument("-c", "--channel", choices=["both", "left", "right"])
 36     parser.add_argument("target")
 37     args = parser.parse_args()
 38 
 39     with WaveReader(args.target) as fi:
 40         nchannels, width, rate, nframes, _, _ = fi.getparams()
 41         raw = np.fromstring(fi.readframes(nframes), dtype=np.int16)
 42         channels = raw[::2], raw[1::2]
 43 
 44     if args.step:
 45         step = args.step
 46     else:
 47         step = rate // 8
 48     fig, ax = plt.subplots()
 49     if args.mode == "pdf":
 50         fig.set_size_inches(16.53 * 4, 11.69 * 2)
 51     for chn in range(len(channels)):
 52         if args.channel == "left":
 53             if chn == 1:
 54                 continue
 55             ax1 = ax
 56         elif args.channel == "right":
 57             if chn == 0:
 58                 continue
 59             ax1 = ax
 60         else:
 61             ax1 = plt.subplot(2, 1, chn + 1)
 62         plt.setp(ax1.get_xticklabels(), fontsize=8)
 63         plt.setp(ax1.get_yticklabels(), fontsize=8)
 64         #
 65         F = np.array([])
 66         channel = channels[chn]
 67         nframes = len(channel)
 68         nframes -= (nframes % step)  # drop the fraction frames
 69         for i in range(0, nframes, step):
 70             f = np.abs(np.fft.fft(channel[i:i + step]))
 71             f = f / f.max()
 72             F = np.vstack((F, f)) if len(F) else f
 73 
 74         freq = np.fft.fftfreq(F.shape[1], 1./rate)
 75         X = np.arange(F.shape[0]) / (rate / step) * (2 / width)
 76         Y = freq[:len(freq) // 2]
 77         Z = F.T[:len(freq) // 2,:]
 78         if args.upper_limit_of_view:
 79             ind = (Y <= args.upper_limit_of_view)
 80             Z = Z[ind, :]
 81             Y = Y[ind]
 82         if args.lower_limit_of_view:
 83             ind = (Y >= args.lower_limit_of_view)
 84             Z = Z[ind, :]
 85             Y = Y[ind]
 86         ax1.contour(X, Y, Z, cmap='jet')
 87         ax1.set_ylabel("in Hz")
 88         ax1.grid(True)
 89 
 90         ax2 = ax1.twinx()
 91         plt.setp(ax2.get_xticklabels(), fontsize=8)
 92         plt.setp(ax2.get_yticklabels(), fontsize=8)
 93         ax2.contour(X, Y, Z, cmap='jet')
 94         ax2.yaxis.set_major_formatter(ticker.FuncFormatter(_nnformat))
 95         ax2.set_ylabel("in scale (approx)")
 96         ax2.grid(True)
 97 
 98     if args.mode == "pdf":
 99         plt.savefig(args.target + ".pdf", bbox_inches="tight")
100     else:
101         plt.tight_layout()
102         plt.show()

フォントサイズの変更みたいなのはすぐに理解出来るでしょ。抜本的な部分は「クリップ出来るように」と「片方チャンネルだけ見れるように」。ある「電子ピアノ曲の右チャンネルのみの 3000Hz 以下」への適用例:

これ「ピアノビギナーズ向け練習動画」だったヤツなので、綺麗に周波数が分解される。

「log2 スケール」なんてのを描ければ期待に近いもの(音階が主役のスケール)になりそうなのだが…。log スケール自体は matplotlib で出来るけれど、無論これは log10 スケール。うーん、ないか? 自力ないとダメかも。