Wizard Notes

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

Pythonで12音平均律の各音高をカラフルにライブプロット

f:id:Kurene:20210627190411g:plain

PyQtGraphの複数の線グラフプロットを利用して,12音平均律で音高ごとに色を変えてライブプロットしたら綺麗&役立つかなと思い作ってみました.

もう少しブラッシュアップしようと思ったのですが,後述の理由でお蔵入りになったので,供養としてソースコードと実装方法を公開します.

デモ動画・ソースコード

github.com

https://github.com/Kurene/pyqtgraph-app/blob/main/pqg_pitchlines.py

実装の流れ

PyQtGraphやスペクトル算出といった実装の流れとしては,以下の記事で紹介した方法と同じです.

www.wizard-notes.com

以下では,差分である2点について説明します.

PyQtGraphにおける複数のカーブプロット

このビジュアライズでは,イメージプロットではなく複数のカーブプロットを利用しています.

PyQtGraphで実現するには,まず,複数のplotdataitemオブジェクトと作成します.

class PQGPitchLines():
    def __init__(self):
        ...
        self.plots = []
        for k in range(self.n_chroma):
            self.plots.append(
                self.plotitem.plot(pen=pg.mkPen((k, self.n_chroma), width=3)) 
            )
        ... 

ここで,12音のカーブプロットを区別できるように,pyqtgraph.mkPenを使って色を変えています.

プロットデータのアップデートも同様に,12個のplotdataitemについて,plotdataitem.setData()で新しいデータをセットしています.

class PQGPitchLines():
    def update(self):
        ...
        for k in range(self.n_chroma):
            alpha = self.y[idx,k] * 0.9
            alpha = alpha if pw > 1e-3 else 0.0
            self.plots[k].setAlpha(alpha, False)
            self.y[idx,k] += k
            self.plots[k].setData(
                self.x, 
                np.r_[self.y[pos:self.n_frames,k],self.y[0:pos,k]]
                )
        ...

ここで,self.y[:,k]に音高kのエネルギー値が入っています.

可視化のポイントとしては,現在の音高kのエネルギー値self.y[idx,k]に応じて線の透明度を設定しています.その結果,エネルギー値が高い音高の線のみ見えるようになっています.

12音平均律のエネルギー抽出

今回はメルフィルタバンクではなく,12音のエネルギー抽出(クロマ)フィルタバンクを利用しています.

具体的な算出方法は、以下の記事を参考にしてください.

www.wizard-notes.com

www.wizard-notes.com

リアルタイムの場合,以下のようなy=Axという線形写像np.dotで毎フレーム計算するイメージです.

f:id:Kurene:20210627185032p:plain

class PQGPitchLines():
    def update(self):
        ...
        self.sig_mono[:] = 0.5 * (self.sig[0] + self.sig[1])
        pw = np.sqrt(np.mean(self.sig_mono**2))
        self.sig_mono[:] = self.sig_mono[:] * self.window
        self.specs[:] = np.abs(self.fft(self.sig_mono))**2
        self.chroma[:] = np.dot(self.chromafb, self.specs)
        self.chroma[:] = self.chroma / (np.max(self.chroma)+1e-16)
        self.chroma[:] = 0.3*self.chroma+0.7*self.chroma_pre
        self.y[idx]    = self.chroma[:]
     ...

実装ノウハウ

  • フレームごとに,12音のエネルギーの最大値で正規化
    • どの音高が鳴っているか見やすくするため
  • 過去のクロマベクトル self.chroma_preブレンド
    • そのまま使うと感度が高すぎるので,鈍らせるため

お蔵入り理由

  • 単一音源ならともかく,楽曲だと12音高すべてがアクティブになりがち
    • 倍音や打楽器音の影響
    • 事前にHPSSするといったような工夫が必要になる
    • 音高判定は,誤判定への対処や高精度化の問題がある
  • 12個のカーブプロットの意味を人間が理解するのは難しい
    • クロマグラムのほうが見やすい