Wizard Notes

Python, JavaScript を使った音楽信号分析の技術録、作曲活動に関する雑記

Python:PyQtGraphでメルスペクトログラムをリアルタイム描画

音楽再生ソフトやプラグインでよく見かける,スペクトログラムのリアルタイムプロット

どの周波数帯域で音が鳴っているのかをリアルタイムで可視化できるので非常に便利です.

Pythonでスペクトログラムのリアルタイムプロットをやろうとすると,やはりリアルタイム性が問題になってきます

Pythonの代表的なグラフプロットライブラリである matplotlibですが,表示するデータの大きさにもよりますが,仕様上,FPSだと10Hz(1秒に10回)程度の描画が限界です.

なので,リアルタイム描画にmatplotlibを使うと,どうしてもカクついた描画になってしまいます

www.wizard-notes.com

そこで今回は,Python用のリアルタイム描画ライブラリ PyQtGraph を使って,スペクトログラムの滑らかなリアルタイム描画をするスクリプトを作成しました.

PyQtGraphはまだWeb上に解説が少ないので,ソースコードと合わせて実装の流れを紹介していきたいと思います。

ソースコード・デモ動画

github.com

pyqtgraph-app/pqg_melspectrogram.py at main · Kurene/pyqtgraph-app · GitHub

PyQtGraphについて

www.wizard-notes.com

PyQtGraph におけるリアルタイム描画の実装方針

こちらの記事で詳しく解説しています.

基本的には,ループバック+PyAudioでPC上の音をリアルタイムで取得し,PyQtGraphを用いるリアルタイム描画クラスで一定の時間間隔ごとに描画しています.

www.wizard-notes.com

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乗)は,高い周波数になるほど周波数解像度(ビン数)が高すぎて,可視化に向いていません.

そこで,適度に周波数解像度を落とすのが可視化のポイントとなります.

具体的には,音高(ドレミファソラシド)や対数軸化などがありますが,今回は人間の聴感特性に合わせた周波数解像度となっているメルスペクトログラムを利用しました.

以下がメルスペクトログラム算出の流れです.

  1. 時間信号に窓をかけてFFT
  2. FFTの結果の絶対値 or 2乗を抽出
  3. 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)
       ....

メルスペクトログラムについては,以下の記事・参考ページも合わせてご覧ください.

www.wizard-notes.com

librosa.feature.melspectrogram — librosa 0.10.0 documentation