The jonki

呼ばれて飛び出てじょじょじょじょーんき

PyQtGraphでソフトウェア・シンセサイザーを作って遊んでみた

f:id:jonki:20220220215242p:plain

ピアノは弾けないのに最近シンセサイザーに興味があり,KORG Volcaシリーズで遊んでいました.今年は勉強がてら遊びも入れたコードを書きたいなと思っていたので,どうせなら勉強がてらソフトウェアシンセサイザーを書いてみようと思い書き始めました.できたのがこんなアプリで,MIDIキーボードから操作しています.

github.com

www.youtube.com

やったこと(やりたかったこと)

今回やりたかったのはこんなところです.ステップ・シーケンサまで出来ると良かったのですが,GUI含めてかなり大変そうだったので今回はやっていません.シンセのフィルター機能も無限に色々あるのですが,かなり単純なところを実装しました(といっても後述紹介するブログのシンセコードをほぼそのまま使っています).またPyQtやPyQtGraphのGUIコーディングや信号処理周りに不慣れだったので中々大変でした.

  • 信号処理周りのコーディング
  • インタラクティブに音を鳴らして楽しくシンセ遊びができる
    • 基本的な信号波形の選択と再生
    • エンベロープ・ジェネレータ(Attack Decay Sustain Release)を可視化しつつ反映
    • Low Frequency Oscillator で信号を揺らす
  • ステップ・シーケンサ機能(音楽上のステップのシーケンスと音の記録による簡易的な作曲機能のようなもの) // 今回断念

ざっくりと振り返り

ちなみに実は今回のコードのシンセ部分は下記の素晴らしいブログを使っているだけです.全3回ですが,かなり読み応えあって面白いのでおすすめです.細かい解説はそちらにゆずるとして,ざっくりとここでは開発経緯やシンセの内容に触れます.書いといてなんですが,エンベロープジェネレータやLFOのコードに関してはここの記事だけをみても多分よく分からないかなと思います.

python.plainenglish.io

プログラミング言語及びGUIの選択

これは結構悩んだのですがPythonPyQtPyQtGraphという構成にしました.実は今回の勉強を除く用途だとWebが最も適しています.GUIの自由度は高いし,Web Audio APIがかなり優秀なのでこれを使ったほうが絶対に良いです.今回は直接信号を生成したり編集したかったので使わなかったので書き慣れたpythonを選択しました.が,やはりGUIpythonだと厳しいです.そもそもGUIライブラリが充実しておらず,高フレームレートで複雑な描画ができるのはPyQtGraphぐらいしか見当たりませんでした.今回Qtベースにしましたが,Webに比べるとGUIパーツの選択肢の狭さや使い勝手の癖など個人的にはあまり楽しい感じのものではなかったです.ただ見た目はさておきGUIとして求められる基本的な機能は十分でした.1点注意として,PyQtはソフトウェアライセンス的にはGPLでコードの公開が必要とのことで,その問題を回避したPySideというのが機能がほぼ同じ?であるようです.今回はプライベートなのでフル公開でまったく問題ないですが,企業などで使う場合は注意が必要です.

ソース波形の生成

Oscillatorが基本となる波形を生成します.学校だと普通勉強するのはsin波だと思いますが,これは単一周波数成分しか乗っておらず聞いても面白くないです.音楽的にはのこぎり波,矩形波三角波のような倍音が多く乗った成分の方が派手で楽しいです.

これは最終的に使っているコードではないのですが,仕組みとしてはここのsinオシレータがわかりやすいです.物理現象としては分かっていてもコードにどう落とすんだ?となるところですが,pythonだとitertools.countが無限にステップを刻んでくれるところを値を指定個数分yieldする形で実装します.

def get_sin_oscillator(freq, amp=1, phase=0, sample_rate=44100):
    phase = (phase / 360) * 2 * math.pi
    increment = (2 * math.pi * freq)/ sample_rate
    return (math.sin(phase + v) * amp for v in itertools.count(start=0, step=increment))

osc = get_sin_oscillator(freq=1, sample_rate=512)
samples = [next(osc) for i in range(512)]

エンベロープ・ジェネレータ

エンベロープ・ジェネレータとは波形の概要を形成するものです.Attack時間,Decay時間,Sustainレベル,Release時間が決まれば,音の鳴り始め〜鳴り終わりを定義できます.今回は最も単純なエンベロープを適用していて,GUIと連動して分かりやすくしています.ちなみにSutainだけは時間でなく相対的な音量レベルです.鍵盤をどれぐらい押し続けるかは分かりませんからね.

f:id:jonki:20220220221650p:plain
ADSR

エンベロープ・ジェネレータは Envelopeクラスで実装されています.ads_stepperr_stepper というのが肝です.ここでもitertools.count を使うことで.指定したAttack/Decay/Releaseの時間を刻むフレームを作っています.ここではads_stepperのコードを持ってきました.Attack → Decayへとwhileループとyieldを使って切り替えています.

    def ads_stepper(self):
        attack_stepper = itertools.count(
            start=0, step=1 / (self.attack_duration * self.sample_rate))
        decay_stepper = itertools.count(
            start=1,
            step=-(1 - self.sustain_level) /
            (self.decay_duration * self.sample_rate))
        while True:
            if attack_stepper:
                val = next(attack_stepper)
                if val > 1:
                    attack_stepper = None
                    val = next(decay_stepper)
            elif decay_stepper:
                val = next(decay_stepper)
                if val <= self.sustain_level:
                    val = self.sustain_level
                    decay_stepper = None
            else:
                val = self.sustain_level
            self.val = val
            yield val

LFO

Low Frequency Oscillatorと言われるもので,ベースの波形に対して時間的な変化(振幅や周波数)を与えるものです.LFOが出来ると所謂シンセっぽい音になるので,ここが一番楽しいかもしれません.例えば振幅をsin波で揺らせば「ホワンホワンホワン」という音になり,矩形波で揺らせば「ピーポーピーポー」とサイレンっぽくなります.更に周波数も何らかの波で揺らせばかなり複雑な音に変化します.これは ModulatedOscillatorクラスが担当します.指定したモジュレーション関数に対して値を投げて波形を変化させています.

    def _modulate(self, mod_vals):
        if not mod_vals:
            return
        if self.amp_mod is not None:
            new_amp = self.amp_mod(self.oscillator.init_amp, mod_vals[0])
            self.oscillator.amp = new_amp

        if self.freq_mod is not None:
            if self._modulators_count == 2:
                mod_val = mod_vals[1]
            else:
                mod_val = mod_vals[0]
            new_freq = self.freq_mod(self.oscillator.init_freq, mod_val)
            self.oscillator.freq = new_freq

やり残していること

これもシンセによくあるローパスフィルター(LPF)を実施したのですが,フィルター適用により結構ノイズが乗ってしまっています.周波数特性を考えるにあたりノイズや計算量のバランスなど考えてフィルタを設計する必要があると思うのですが,今回はそこまで時間が取れず次回の課題にしようと思っています.LPFといってもいろいろなフィルターがあり奥が深いです. - Python NumPy SciPy : デジタルフィルタ(ローパスフィルタ)による波形整形 | org-技術

まとめ

中々不慣れなことが多く大変でしたが,1つキリが良いところでまとめられてよかったです.シンセサイザーモジュレーションに関しては組み合わせは無限にあり,そのコーディング部分などなかなかトリッキーに実装していて面白かったです.