音楽再生ソフトやプラグインでよく見かける,スペクトログラムのリアルタイムプロット.
どの周波数帯域で音が鳴っているのかをリアルタイムで可視化できるので非常に便利です.
Pythonでスペクトログラムのリアルタイムプロットをやろうとすると,やはりリアルタイム性が問題になってきます.
Pythonの代表的なグラフプロットライブラリである matplotlib
ですが,表示するデータの大きさにもよりますが,仕様上,FPSだと10Hz(1秒に10回)程度の描画が限界です.
なので,リアルタイム描画にmatplotlibを使うと,どうしてもカクついた描画になってしまいます.
そこで今回は,Python用のリアルタイム描画ライブラリ PyQtGraph を使って,スペクトログラムの滑らかなリアルタイム描画をするスクリプトを作成しました.
PyQtGraphはまだWeb上に解説が少ないので,ソースコードと合わせて実装の流れを紹介していきたいと思います。
- ソースコード・デモ動画
- PyQtGraphについて
- PyQtGraph におけるリアルタイム描画の実装方針
- PyQtGraphにおける2次元データのリアルタイム描画の流れ
- メルスペクトログラムの算出方法
ソースコード・デモ動画
pyqtgraph-app/pqg_melspectrogram.py at main · Kurene/pyqtgraph-app · GitHub
PyQtGraphについて
PyQtGraph におけるリアルタイム描画の実装方針
こちらの記事で詳しく解説しています.
基本的には,ループバック+PyAudioでPC上の音をリアルタイムで取得し,PyQtGraphを用いるリアルタイム描画クラスで一定の時間間隔ごとに描画しています.
PyQtGraphにおける2次元データのリアルタイム描画の流れ
2次元データを描画する際には,いくつかのオブジェクトを作る必要があります.
特に2次元データ(イメージプロットの場合),以下のクラスが重要となります.
- ImageItem
- ViewBox
- PlotItem
ImageItem は,2次元データを格納するクラスです.
機能として,データの更新,データ描画時の色などを管理します.
https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/imageitem.html?highlight=ImageItem
ViewBox は,ユーザからの2次元データ/イメージ操作を管理します.
例えば,マウスドラッグでの子の内部スケーリング/パン操作(拡大縮小)を実現することができます.
グラフ描画というよりも,アプリケーション作成の時に重要となるクラスだと思います.
https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/viewbox.html?highlight=ViewBox
PlotItem は,グラフ描画のための2軸プロット・2次元領域の管理機能を担っています.
具体的には,軸や凡例,表示範囲などを管理します.ただし,例えば軸であれば具体的なパラメタはAxisItem
が管理しているなど,PlotItemとは別のクラスが関わっている場合もあるので,ドキュメントでAPIを探すときは注意が必要です.
以上の3つのクラスのオブジェクトに関して,PlotItemはViewBoxを,ViewBoxはImageItemを持つような構造として記述します.
それぞれ役割が異なるため,自分が追加したい機能はどのクラスの役割なのかを考えることで,ドキュメント上で該当するAPIが探しやすくなります.
class PQGMelSpectrogram(): ... def __init__(self): ## PyQtGraph の初期設定 app = QtGui.QApplication([]) win = pg.GraphicsLayoutWidget() win.resize(size[0], size[1]) win.show() ## ImageItem の設定 imageitem = pg.ImageItem(border="k") # カラーマップ取得・セット cmap = pg.colormap.getFromMatplotlib("jet") bar = pg.ColorBarItem( cmap=cmap ) bar.setImageItem(imageitem) ## ViewBox の設定 viewbox = win.addViewBox() viewbox.setAspectLocked(lock=True) viewbox.addItem(imageitem) ## 軸 (AxisItem) の設定 axis_left = pg.AxisItem(orientation="left") n_ygrid = 6 yticks = {} for k in range(n_ygrid): index = k*(self.n_mels//n_ygrid) yticks[index] = int(self.melfreqs[index]) axis_left.setTicks([yticks.items()]) ## PlotItemの設定 plotitem = pg.PlotItem(viewBox=viewbox, axisItems={"left":axis_left}) # グラフの範囲 plotitem.setLimits( minXRange=0, maxXRange=self.n_frames, minYRange=0, maxYRange=self.n_mels) # アスペクト比固定 plotitem.setAspectLocked(lock=True) # マウス操作無効 plotitem.setMouseEnabled(x=False, y=False) # ラベルのセット plotitem.setLabels(bottom="Time-frame", left="Frequency") win.addItem(plotitem) self.app = app self.win = win self.viewbox = viewbox self.plotitem = plotitem self.imageitem = imageitem pg.setConfigOptions(antialias=True)
メルスペクトログラムの算出方法
通常のスペクトログラム(FFTの2乗)は,高い周波数になるほど周波数解像度(ビン数)が高すぎて,可視化に向いていません.
そこで,適度に周波数解像度を落とすのが可視化のポイントとなります.
具体的には,音高(ドレミファソラシド)や対数軸化などがありますが,今回は人間の聴感特性に合わせた周波数解像度となっているメルスペクトログラムを利用しました.
以下がメルスペクトログラム算出の流れです.
メルフィルタバンク行列は (メルスペクトログラムの周波数ビン数,FFTスペクトログラムのビン数)となっています.
従って 3 では,FFTの各ビンの値を,いい感じに重みを付けて足し合わす(マージする),という操作をしています.
class PQGMelSpectrogram(): ... def update(self): ... # 最新をスペクトログラム格納するインデックス idx = self.iter % self.n_frames # モノラル信号算出 self.x[:] = 0.5 * (self.sig[0] + self.sig[1]) # FFT => パワー算出 self.x[:] = self.x[:] * self.window self.specs[:] = np.abs(self.fft(self.x))**2 # メルスペクトログラム算出 self.melspecs[idx, :] = np.dot(self.melfb, self.specs) ....
メルスペクトログラムについては,以下の記事・参考ページも合わせてご覧ください.
librosa.feature.melspectrogram — librosa 0.10.0 documentation