音響信号処理言語"Faust"の勉強会に参加してきました
はじめに
先日、こちらの勉強会に参加してきました。
"Faust"は関数型音響信号処理言語で、慣れれば爆速でDSPアルゴリズムの試作・検討ができるようです。 しかしながら、日本語による情報はまだまだ少ないのが現状とのことなので、 勉強会に参加したばかりで言語仕様もほとんど理解できていない状態ではありますが、 ざっとFaustを触ってみた感想などをここに残しておこうかと。
Faustのここがすごかった
・記述量が少なくてすむ
例えば下記のような、エコーエフェクトの実装を考えてみます。
これを以下のようにFaustで記述してみました。
一方、これと同様のものをC++ & JUCEで記述してみると以下のようになるでしょうか。
その差は一目瞭然ですね。。。なるほど。
・出音の確認が容易
では、先に書いたFaustのコードの出音を確認してみます。
方法はいくつか存在するようですが、ここでは"FaustLive"というツールを使用してみました。
下記の画像のように、FaustLive上に先のdspファイルをドロップするだけで、すぐに出音の確認ができます。
さらに、現在FaustLiveに読み込まれているソース (ここではecho.dsp)を修正・更新すると、すぐに変更が出音に反映されます。
いや、これすごい便利ですね。"ソースの変更⇔出音の確認"というサイクルを手軽に回すことができます。
C++ & JUCEの場合だと、ソースコードの変更→VSTとしてビルド→DAWに読み込ませる→出音の確認という工程を踏まなければならないことを考えると、その差は大きいですね。
・信号の流れをブロック線図で確認できる
Faustで書かれた処理は、下記のようなブロック線図で確認することができます。 入力信号がそれぞれ異なる遅延時間・減衰係数を持つブロックに分配され、 処理後にマージされ1つの信号になって出力されるという流れがわかります。
おわりに
以上、素人がFaustを少し触ってみて特に印象に残った部分についてざっくりと書いてみました。 また、今回の勉強会の主催・講師をされた松浦さん(@tomoya_nonymous)が、当日の資料を公開してくださってます。基本的な文法を含め一通りの解説がなされているので、はじめてFaust触るマンとして大変参考になります。
また、Faustの環境構築については、Connpassにある第一回勉強会のページに記載されているものを参考にすればよいかと思います。 これらを参考にすれば、ひとまず、Fasutで遊ぶことができると思います。 こちらのFaust勉強会、次回は11/26あたりに第2回を開催予定とのことなので、ご興味を持たれた方はぜひ。https://t.co/pqFQsvnego 本日使ったスライドです。読み物的にもこれ読めばfaustについて大体わかる的な網羅的なものを目指しましたので参加できなかった人は是非 #faust_jp
— 松浦知也/Matsuura Tomoya (@tomoya_nonymous) 2017年8月26日
遊んでる #faust_jp pic.twitter.com/7g3OEHvA0v
— アヲギリ (@Aogiri_m2d) 2017年8月27日
周期波形テーブルとシングルトン
はじめに
サイン波テーブルを作成して、シングルトンでそれを使い回します。
例によって素人調べなので、間違いがありましたらご指摘いただければ幸いです。
テーブルルックアップ [1, 2]
周期波形1周期分のデータを用意し、それを使いまわす方法です。ここでは、周期波形の簡単な例としてサイン波を考えてみます。
サイン波は、上記のような周期 の周期関数です。従って、下記も成立します。
上記の右辺のsin関数の位相は、 となります。すなわち、Sin関数への入力位相 は、結局のところ の範囲に落とし込めることがわかります。
このことから、 のサイン波1周期分の波形メモリーをあらかじめ用意しておけば、入力位相を の範囲に落とし込み波形メモリーを参照することで、任意の入力位相に対するサイン関数出力を取り出すことができます。
続いて、サイン波1周期分の波形メモリーをどうやって作成するかを考えてみます。
コンピュータ上では、離散データしか扱えないので、連続した波形メモリーを用意することはできません。従って、サイン波1周期をN点でサンプリングすることで、波形メモリー数列 を作成します。
ここで、 は単位インパルス数列を表します。また、を今後、波形メモリーのアドレスと呼ぶことにします。
では、続いて任意の入力位相 から参照すべき波形メモリーのアドレスを算出する方法を考えていきます。
これは、先の入力位相を の範囲に落とし込んだやり方そのままになります。ただし、 を の範囲にスケール変換する必要があります。それを踏まえると、任意の入力位相 に対応するアドレス は下記のようにして計算できます。
これで、入力位相 をアドレス に変換できました。ところで、波形メモリーは離散データなので、アドレスが整数値の部分にしかデータが存在しません。しかしながら、先に計算した は、整数値以外の値もとりえます。
従って、 が小数値になった場合、そのアドレスに対応するデータは、前後の整数値アドレスのデータを用いて補完する必要があります。この補完方法には、様々なやり方がありますが、今回は前後1点ずつのデータを用いて行う線形補完 [2]で対処します。この線形補完を行うことで、テーブルルックアップの完成です。
実際にこのテーブルルックアップを使って波形を生成してみます。C++でコーディングしてみたものが以下になります。
以下は、生成波形をグラフ化してみたものですが、大丈夫そうですね。
波形テーブルをSingletonにする [3]
シンセサイザーの場合、サイン波のような基本波形は、メインのOSCやLFOといった様々な部分で必要になるかと思います。各モジュール毎に同じ波形テーブルのコピーを持たせてもしょうがないので、1つのSine波形テーブルをプログラム中のどこからでも、参照できるようにしてみます。
先のコードをベースにSineTableをSingleton化してみると以下のようになるでしょうか(デザインパターンは成書できちんと学んだわけでは無いので、怪しいかも...C++ベースで書かれたおすすめのデザインパターン入門書の情報ください)。
上記のようなごくごく小規模なサンプルプログラムだと、特に益が無いですね…。今回は、サイン波を例に行いましたが、バンドリミットしたノコギリ波テーブルを用いた基本波形OSCとか、もうちょっと実用的な部分もその内まとめておきたいですね。
参考文献
- Joe Wright. "Synthesising band limited waveforms using wavetables". Musicdsp.org.
http://www.musicdsp.org/files/bandlimited.pdf, (accessed 2017-07-16).
- 青木直史. C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理. オーム社, 2008.
- _narumi. "ゲームプログラマのためのデザインパターン(シングルトン)". Qiita.
http://qiita.com/narumi_/items/b205e59e7fc81695f380, (参照 2017-07-16).
手軽にフィルタ設計
はじめに
SciPyのSignalサブパッケージを用いて、IIRフィルタを設計する際の基礎的なメモです。今回は、バタワース特性を持つローパスフィルタを設計してみます。
素人理解なので、間違いがありましたらご指摘いただければ幸いです。
IIRフィルタの設計[1-3]
以下のような手順に沿って行われます。
今回設計するバタワース特性を有するローパスフィルタのは、下記のようになります(2次系)。
ここで、 及び はフィルタ系数という、カットオフ周波数とサンプリング周波数に依存した値になります。 及び を含めたの形が定まれば、これを差分方程式に直して入力信号に対するフィルタリング処理が可能になります。また、 とした時、 と がそれぞれ、振幅特性・位相特性を示します( は、一般に複素関数。 と は、角周波数とサンプリング周期)。
結局は、既存のから任意のカットオフ周波数における、フィルタ係数を含めた全体を求めることになるのですが、まっとうにこの変換作業を行うのはなかなか骨の折れる作業となります。
SciPyでデジタルフィルタの伝達関数を求める[4, 5]
Pythonの外部パッケージであるSciPyには信号処理用の関数がまとまったSignalというサブパッケージが用意されています。これを利用することで手軽に先の を求めることができます。
今回使用するSignalサブパッケージの関数は、"iirfilter"と"freqz"の2つになります。前者でフィルタ係数を、後者で を求めることができます。詳細は、後述のソースコード内のコメント及び、公式のドキュメント[5]を参照してください。
実際にこれらを使って、バタワース特性を持つローパスフィルタ(2次系)の振幅特性をプロットしてみたのが下記のソースコードになります。
以下、実行結果です(振幅特性のボード線図、フィルタ係数)。
ちゃんと、カットオフ周波数 1 kHzから減衰しています。今回は試していませんが、"iirfilter"の返り値には、極と零点を指定することもできるみたいです。
今回は2次系で行いましたが、高次系でも、同様に簡単にフィルタ係数を求めることができるのでありがたいですね。
参考文献
- 三谷政昭. 信号解析のための数学. 森北出版, 1990
- 大類重範. ディジタル信号処理. 日本理工出版会, 2001
-
鏡慎吾. "やる夫で学ぶディジタル信号処理".
http://www.ic.is.tohoku.ac.jp/~swk/lecture/yaruodsp/preface.html
(参照 2017-07-11)
- 中久喜健司. 科学技術計算のための Python入門. 技術評論社, 2016
-
The Scipy community. "Signal processing (scipy.signal)".
https://docs.scipy.org/doc/scipy-0.19.0/reference/signal.html (参照 2017-07-11)
JUCEでソフトシンセをつくる 4 ~ ポリフォニック化
はじめに
前回までで、オシレータとAmpEnvを持つモノシンセができました。
今回は、これをポリフォニックシンセに拡張してやります。
モノからポリへ拡張する際の問題点
まずは、前回までのモノフォニックシンセにおけるMIDIと波形生成の流れをざっくりと図示すると、下記のような感じでしょうか (Activity図の正しい書き方を知らないので間違ってても許していただきたい)。
入力MIDI noteのOn / offに応じて、MonoSynth内のOscillatorとAmpEvnの状態が切り替わりホストAudioBufferサイズ分の波形データを生成します。では、これがポリフォニックの場合はどうなるべきかを考えてみます。ポリフォニックでは、同時に複数の波形を生成する必要があるため、発音数分だけのMonoSynth (今後はVoiceと表記)を用意する必要があります。したがって、単純に先のモノフォニックに倣えば、下図のようになるでしょうか。
さて、この図を元にポリフォニック処理を実装しようとすると、MIDI on / off signalを受けっとた際に、どの番号のVoiceを処理すべきかという問題が生じます。MIDI onの時には、AmpEnvがRelease / Off Stateにあるボイス(今後は空のVoiceと表記)が、優先的に処理されることが期待されます。また、MIDI offの場合は、的確に"入力されたMIDI note番号と同じNote音階で鳴っているボイス"が処理される必要があります。その他にも、各ボイスのマージや何やら色々考える必要はありますが、以下、ポリフォニックにおけるMIDI入力時の処理に絞って、その実装を記述します。
ポリフォニックの実装
引き続き、前回のプロジェクトをベースに拡張していきます。ポリフォニック処理は、新たにPolySynth Classを作成し、そこに記述していきます。下記に、今回実装したPolySynth Classのインターフェイス回りを記載します。
適切なVoiceのOn / offを実現するためには、各ボイスの状態(Note番号)を常に把握しておく必要があります。そこで、各ボイスのNote番号を保持しておく、"VoiceState"構造体を導入しました。MIDI入力の度に、このVoiceStateを書き換え、On / Off状態に応じて2つのリスト間を移動させます。Voice本体は、このVoiceStateを参照して、そのふるまいを決めます。
続いて、下記にMIDI入力時の具体的な処理を記載します。
大体のことは、コード中のコメントに書いてあるので、以下は蛇足です。
まず、MIDI on入力を受け取った時の処理ですが、空のボイスの有無で処理が分岐しますが、いずれにせよ、リストの先頭ボイスを上書きし、lisOnVoiceの末尾に要素を移しています。変更のあったボイスをリストの末尾から詰めていくことで、変更履歴の古い順にソートされた状態が保たれます。これは、MIDI off入力の時も同じで、MIDI offになったボイスは、lisOnVoiceからlisNoteOffの末尾に移されます。
MIDI offになった直後のボイスは、Amp EnvがまだRelease状態にある可能性が高く、可能な限り再度On状態になるまでの時間を遅らせたいというのが自然な要求かと思います。これについては、リスト内において、Offなったタイミングが古い順に空ボイスがソートされているため、自然にクリアすることができます (MIDI onの書き込みは、lisNoteOffの先頭に対して行うようになっているため、古い空ボイスから優先して使用されます) 。
残りのMIDI入力処理以外の部分については、下記のリポジトリに全ソースコードを置いておきます。
動作確認
ようやく、シンセらしくなってきました。
うんうん。今度は、ちゃんと他のVAシンセと同じ感じでポリフォニック化できているはず。 pic.twitter.com/AMhTsvvNc0
— アヲギリ (@Aogiri_m2d) 2017年6月18日
今後は
フィルターやLFOやら、いろいろ必要なものがありますが、正直、未定です。たぶん、FMについてのあれこれになりそう。
JUCEでソフトシンセをつくる 3 ~アンプエンベロープ
前回まで
MIDI入力を通してAM・FMオシレータが鳴らせるようになりました。
今回は
ADSRエンベロープカーブを持つアンプを実装します。実装するアンプエンベロープの仕様等については、後ほど記載します。
ソースファイルの追加
下の画像のように追加します。
アンプの実装は、前回のオシレータの時と同様にパラメータ部をモジュール本体部から独立させてます ("AmpParameters", "Amplifier")。
また、オシレータもアンプもパラメータ部の実装については、共通する部分が多いので新たに"ParameterBase"クラスを設けて、それを継承する形で各モジュールのパラメータを実装していきましょう。これで、今後、さらにモジュールが増えることになっても実装をスムーズに進められるはずです。
Parameterの実装
まずは、各モジュールのParameterクラスの基底となるParameterBaseクラスを用意してやります。
これを継承してAmpParameterクラスを実装します。今回、必要なパラメータはADSR (Attack, Decay, Sustain, Release)の4種類になります。Sustain以外の時間に関するパラメータの表示上の単位次元は"秒"としておきます。Sustainに関しては無次元量としましょう (0 ~ 1.0 = 0 ~ 100 %)。
継承先では、コンストラクタでパラメータインスタンスを作製するだけです。これで、パラメータの実装はかなり楽になりました。また、前回作製した"OscParameter"も同様の形に修正しておきましょう(詳細は割愛)。
Amplifierの実装
ADSR Envを持つアンプモジュールの本体部を実装していきます。これから、実装するアンプエンベロープのイメージ図は以下のようになります。
よく見るタイプの図ですね。今回、エベロープカーブは全て直線にしてしまいます。図からわからる用に、エンベロープは、AttackStateからOffStateまでの5種類の状態をとります。基本的には、各状態に入ってからの経過サンプル時間を積算していき、パラメータADRで設定した時間を超えたら、次の状態へと遷移するような仕組みを実装します。ただし、OffState->AttackState、SustainState->ReleaseStateの遷移は、時間経過ではなく、それぞれ、Ampが"MidiOn"、"MidiOff"を受けっとたタイミングで状態の遷移が生じます。
さて、個々の状態において異なる内部パラメータは、そのカーブの傾き(sloop)になります。各状態におけるsloopを列挙すると以下のようになます。
状態が遷移する度にパラメータからsloopを計算して、1サンプル時間経過ごとに、gainにsloopの値を加算してやればよさそうです。
以上をまとめると次のようになります。
- MidiOn / MidiOffがあればAttack / Releaseへ状態遷移し、パラメータからsloop、初期gain、状態持続時間を計算。経過サンプル時間もリセット。
- gainの値に応じて、AudioBufferの信号強度を減衰させる。
- 1サンプル時間進めると同時に、gain値を更新 (sloopをgainに加算)。
- もし、状態遷移してからの経過サンプル時間がパラメータで設定した、ADRの時間を超えていれば。状態の遷移を行い、sloop、初期gain、状態持続時間を再計算、経過サンプル時間もリセット。
これを実装したものが、以下になります。
個々の状態における処理の違いはさほど複雑なものでもないので、わざわざ、StateパターンやStrategyパターンによる状態分けをする必要はないでしょう。また、先にも述べたようにSustain、OffStateは時間変化による内部パラメータの変化は起こり得ないので、勝手な遷移が起こらないように経過サンプル時間の積算を避けておきます。
SynthモジュールにAmplifierを組み込む
これまでのOscillatorの時と同様に、実装したAmplifierをOscillatorと共にSynthクラス内で走らせてやりましょう。
これまでとは違い、MidiOffを受け取ってもOscの発振を止めず、AmplifierがOffStateになったタイミングでOscの発振を止めるようにしています。
後は、いつものように、これをPluginProcessorのProcessBlockで走らせてやればOKです。
割愛した部分も含めた全ソースコードは、下記リポジトリにあるので適当に参考にして下さい。
動作確認
動画とるのがめんどくさいので、後で。
JUCEでソフトシンセをつくる 2 ~ AM・FMオシレータ
前回は
MIDI入力を通して任意音階のSine波が鳴らせるようになりました。
今回は
先のSineOsillatorから、AM・FM Oscillatorを作ります。
AM・FM波の演算式は、それぞれ以下のものとして実装してみます。
ここで、は、それぞれ、キャリアSineOscの周波数、モジュレータSineOscの周波数になります。また、は、ユーザー側がホストDAWのGUIを通じて設定できるパラメータになります。
は、MIDI入力によって決まるので、は、というパラメータを定義して、以下のようにします。
以上より、今回の目的は、
"2種類のパラメータを用意し、それをキャリア・モジュレータSine波に適用させ、AM・FM波を合成する機能を実装する"ということになります。
プロジェクトの設定
下の画像のように設定します。
前回のプロジェクトに、"OscParameters"、"ModOscillator"というソースファイルを新たに追加しています。それぞれに、パラメータとAM・FMオシレータの実装を記述していきます。
また、前回の"SineOscillator"も合わせて、オシレータ関係のソースファイルをまとめておきます。
パラメータの実装
以下のように実装します。
パラメータの実装については、以前に少し書いてたので参考にしてください。
Sine波オシレータの修正
前回実装したSineOscillatorを少し改修します。
主な変更点としては、
- MIDI-ON以外のタイミングでも周波数を変更できるよう、changeFrequency関数を追加
- メンバ変数をprotectedに変更し、各種関数の内部に空の仮想関数を追加(NVIによって挙動を拡張できるようにする)
これらは、このSineOscillator Classを派生させAM・FMオシレータを実装するための下準備になります。AM・FMもオシレータであるため、結局は、SineOscillatorと同じようなインターフェイス構成を持ちます。従って、今回は継承を使って実装することにしました。
AM・FMオシレータの実装
SineOscillator Classの派生クラスとして実装します。派生クラスにprotected継承されたメンバ変数を、キャリアオシレータのパラメータとして扱っています。
ProcessBlockでOscを走らせる
後は、前回同様にsynthソースファイルにMIDI入力の処理を書いて、ProcessBlockでModOscillatorを走らせるだけです。これについては、前回とほぼ同じ実装なので詳細は割愛します。下記に今回のAM・FMオシレータシンセの全ソースコードを置いておくので参照してください。
動作確認
ModTypeで、AM・FMモードが切り替えられます。
FMはまだしも、AMはSin波同士だと思ったよりしょっぱいですね。
次回の予定
たぶん、アンプエンベロープかポリフォニックの実装をします。
JUCEでソフトシンセをつくる 1 ~ Hello, SineとMIDI入力
はじめに
JUCEを使って、ソフトシンセを作っていきます。
とりあえず、今回はSine波オシレータとMIDI入力を実装してみます。
仕様
以下の機能を実装します。
プロジェクトの設定
下記の画像のように設定します。
- Plugin is a SynthとPlugin Midi Inputに☑を入れておきます
- SineOscillator、Synthヘッダ・ソースファイルを追加します(前者には、Sine波オシレータの実装を、後者には、オシレータで生成した波形のレンダリングやMIDI入力に関する実装を書いていきます)
SineOscillatorの実装
SineOscillator.h, cppに記述していきます。
本当はSine波の生成はテーブル・ルックアップを使った方が速い(らしい)のですが、それは、次回ということで。今回は、普通にsin関数を使っています。
波形のレンダリングとMIDI処理の実装
Synth.h, cppに記述していきます。
実際、このMIDI入力処理だと1つのMIDI入力があったらすぐにオーディオレンダリングに入っているので、1ボイスしか同時発音できません。
また、鍵盤が現在押しこまれているのかどうかを判断できないので、2鍵盤を押しこんだ状態で片方を放した時、まだ、打鍵状態にある方の音が鳴らない等という問題が残っていたりしますが、この対処については、またの機会に。
とりあえず、MIDI鍵盤を通して任意の音階を鳴らすことはできるようになりました。
オーディオバッファの取得云々等については、JUCE JAPANを読もう(ダイレクトマーケティング)。
後は、実際にこれをProcessBlockで走らせれば、Hello, sineできます。
無事、MIDI入力を通して、Hello, sineできてますね。
次の予定
ただ、Sine波を鳴らすだけではつまらないので、次は、基本波形を一式実装…といきたいのですが、Sine波以外のバンドリミット済みの基本波形テーブル作製や波形の切り替え処理の実装が意外と面倒なので、次回は、SineテーブルのみからできるFM Oscillatorを実装してみようかと思います。そのうち書きます。