Wizard Notes

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

Python: PyAudioとPyQtで作る簡易シンセサイザー

生成した音信号を気軽に鳴らせるシステムが欲しくなり、結果的にシンセサイザもどきを作ってみました。

MIDIを扱うと面倒なので、Numpy/Scipyで生成した音信号を直接オーディオ出力できるような構造になっています。

また、一応シンセサイザっぽい見た目なので、PyQt5を使ってクリック&キーボード入力可能な鍵盤を描画し、実際に音を鳴らせるようにもしてみました。

設計

今回はプロトタイプなので、VCF,VCA,といった機能すら持たない、オシレータだけのシンセサイザもどきを作ります。

そのため、必要な入力情報としては、音高発音タイミング情報になります。従って、以下のようなモジュールを作れば実現できます。

  • 音高・タイミングを入力できるインターフェース
  • 音高に従って信号(サイン波/のこぎり波)を生成
  • 生成したオーディオ出力する

今回の実装では、それぞれ以下のモジュールが対応しています。

さらに、オーディオ出力中もインターフェースでの操作を可能にするため、threadingを用いて並列処理をしています。

実装の要点

PyQt5による簡易シンセ鍵盤の作成

PyQtの情報はWeb上には少ないこともあり、以下をベースに鍵盤を作りました。

Python: PyQt5のQPushButtonで作る簡易ピアノ鍵盤 - Wizard Notes

以下、実装のポイントを列挙します。

  • ボタンを並べたいときは、QGridLayoutが便利
  • buttonClicked()に、クリック時の処理をするコールバック関数を渡す
    • 引数があるときは、functools.partial で対応
  • 終了時の処理はQWidgetcloseEvent()をオーバーライド

Numpy, Scipyによる信号生成

numpyp.sinscipy.signal.sawtoothを使います。

注意点として、PyAudioのストリームに1024サンプル程度のバッファごとに信号を送る必要があります。 そのため、現在のバッファ用信号作成の際には、前のバッファでの信号の位相を保持しておく必要があります。

今回のコードでは、愚直ですが、バッファサイズをオフセットとしています。ただし、self.period = n_chunk * rateとなったとき、位相は0となるため、オフセットself.offsetリセットしています*1

def out(self):
    x =  np.arange(self.offset, self.offset + self.n_chunk)
    chunk = self.gain * self.generator(self.pi2_t0 * x)
    self.offset += self.n_chunk
    if self.offset == self.period:
        self.offset = 0
    return chunk

なお、今回の実装では、周波数ごとにオシレータを作っています。イメージとしては、各鍵盤に対してオシレータが存在する感じです。

PyAudioによるオーディオ出力

オーディオ入出力のみの実装については、以下の記事が参考になると思います。

www.wizard-notes.com

注意点として、オーディオ出力は一つだけ開き、アクティブなオシレータが生成した信号を全て加算したバッファをstream.write()に投げます。

    def render(self):
        while self.stream.is_active():
            
            chunk = np.zeros(self.n_chunk)
            for osc in self.oscillators:
                if osc.is_run():
                    chunk += osc.out()
            self.stream.write(chunk.astype(np.float32).tostring())

また、ストリーム書き込み中もGUIは操作できるようにしたいので、threadingを使って並列処理します。

t = threading.Thread(target=self.render)
t.start()

今回の実装では、この並列処理は投げっぱなしです。そのため、オシレータが全て非アクティブのときも、要素がゼロのみの信号をストリームに書き込んでいます。

実装

github.com

補足

今回のPyQt5のキーボードは QPushButton を使っているので、クリックしている間は鳴らすというような一般的な鍵盤動作には向いていないと思います。

なので、PyQt5で真面目に演奏できる鍵盤を作ろうとすると結構大変なので、以下のものを採用するのはありだと思います。

github.com

github.com

*1:intが16-bitだとこの手法は厳しい