ソフトシンセ(の基本)

注意:ここではある程度デモの話,C言語の基礎,WinAPIがわかるという前提で話を進めていきます。
その辺の話からよくわからない方はそういうサイトを回ってからの方が良いと思われます。
あと、VisualC++(以下VC)限定なので他の方にはあまり有効でないかもしれません。



今回は前回スルーしたフィルタ(ローパスフィルタとか)などの話をしておきたいと思います。
前回作成した音がなるサンプルではいわゆる矩形波の音が鳴っていましたが
コレでデモの曲をつくっても、あまりカッコよくはないでしょう。(oldskoolなら話は別ですが・・・
というわけで、簡単に紹介と軽い実装です。


●加算式シンセサイザ と 減算式シンセサイザ
あらゆる波形はテイラー展開することですべてsinとcosで表すことができる。
と、昔を偉い人は発見したわけで、世の中のいろんな楽器の音を作るには、音をテイラー展開して
sinの式にあらわせば、最終的にはすべての楽器の音をsinの足し算であらわすことができるわけだが
sin自体まだまだコンピュータの演算速度が遅くてボトルネックになったりしているのに
そんなものを無限回足し算なんてやってられない。
そこで、簡単な式で生成できる波形に着目したところ、矩形波や三角波やノコギリ波には
多くのsin波が規則的に多く含まれているとがわかり、これらを利用して
sin波(単一の周波数)を加算するのではなく、矩形波や三角波から不要な周波数を引き算する
という方法をとることで、加算(演算)回数を増やさずに多くの周波数成分を含めることが可能となる。
そしてその引き算というのが以下のフィルタというものに通すことで特定の周波数をカット(引き算)
することが可能となる。


●アナログフィルタ と デジタルフィルタ
アナログフィルタというのは電気回路などで実現されたフィルタでずっと昔からあります。
いわゆる回路素子のコンデンサ(C)やコイル(L)をつかって構成された物理的なフィルタです。
極端な話、低い周波数と高い周波数が混じった電圧波形をローパスフィルタに通すと
低い周波数だけ取り出すことができます。(詳しくは電気回路の本を見てください・・。)
めちゃくちゃ高いオーディオ機器にめちゃくちゃいいアナログフィルタが搭載されてるでしょう。
デジタルフィルタというのは、コンピュータで音などをデジタル(離散的)に処理するために
考えられたフィルタで、まぁコンピュータでやってれば全部コレですね(w
FFT(高速フーリエ変換)などよって行われるフィルタがこれですね。

それぞれのフィルタは特徴があり、FFTを使ったフィルタでのローパスフィルタはアナログフィルタのそれに
比べて、任意の周波数以上のカットがきれいにできます。(アナログフィルタではローパスフィルタに
通しても完全には高周波を除去できない)
もちろん、それぞれのフィルタを多段に使うことでそれぞれのフィルタの性質を真似ることは可能です。
なんだか、プログラムをする人にとってみると、これだけ聞いた感じだとデジタルフィルタのほうが
よさそうな感じなんですが、どうも作曲する人たちはデジタルフィルタよりもアナログフィルタのほうを
好んで使うほうが多いようです。
(人間の感性は単に周波数をカットすればいいとかいうもんじゃないんですねぇ・・)
(まぁ音の大きさは、人間の耳がlogで感じたりするのとかと関係してるんでしょうねぇ)

では、FFTをつかってコードをかいていくのかというと、そうではありません。
FFTのコードはフーリエ展開をやるだけとはいっても4kIntroなどに対してはサイズがかなり問題
になってきます。また、前述したように音楽に関しては正確な周波数カットのフィルタよりも
多少カットしきれないアナログフィルタのほうが好まれることが多いのも事実です。
そこで、今回はアナログフィルタをベースにしてコードに落としたFIR,IIRフィルタというものを使います。


●FIRフィルタとIIRフィルタ
アナログフィルタをプログラム的にのせる方法としてFIRとIIRというものがあります。
(デジタルフィルタもFIR,IIRで表すことができます。)
FIRはFinite Inpluse Responseフィルタの略でインパルス応答が有限時間内に収束するようなフィルタです。
コード的には
 
 (x[t] - 入力信号)
 (y[t] - 出力信号)
 long t;
 for(t=0; t<BUFFER_SIZE;  t++){  
     y[t] = x[t]*A(t) + x[t-1]*A(t-1) + ・・・;
 }
このように、入力信号->出力信号の流れのみでループの入るフィルタです。

IIRはInfinite Inpluse Responseフィルタの略でインパルス応答が無限に続くようなフィルタです。
コード的には
 
 (x[t] - 入力信号)
 (y[t] - 出力信号)
 long t;
 for(t=0; t<BUFFER_SIZE;  t++){  
     y[t] = x[t]*A(t) + x[t-1]*A(t-1) + ・・・ + y[t]*B(t) + y[t-1]*B(t-1) + ・・・;
 }
このように入力信号とフィルタによって過去に出力された信号とあわせて、出力信号をつくります。

このように、FIR,IIRフィルタは単純なループで書くことができるので
サイズ的にもFFTなどをつかって書くよりも単純に書くことができます。

これらは電気回路からデザインされたアナログフィルタをもとに式を書くことでつくることができます。



●では、どうやってフィルタを実装するのか
1. 電気回路の本を読破して、電気回路をマスターした上で、回路方程式を立てて、ガリガリコーディング
2. 世界のWebサイトからおいしいとこだけをいただいてきて、適当に組み合わせて、ぽいやつを作る。
3. 自分ではまったく作れる気がしないので、人が作ったものを使う。

もし、完全に自分のオリジナルな世界にひとつだけのフィルタを作りたいのであれば1.をお勧めします。
2.も悪くないですが、私もいろいろ探してみたのですが、たしかに部分的にフィルターのソースはのって
たりするのですが、やっぱり組み合わせるとなるとそう簡単にできないような気がします。だいたい、
そういうところのサイトの解説が電気回路に基づいた解説であることが多いです・・・。
3.はある意味 面白くないですが、現実的です(w
デモを作っているチームなどがソフトシンセを公開しているので、完成しているそれを使う
というのもひとつの手でしょう。farbrauschのkbが作ったV2 SynthesizerやANDのXSynthなどがそれでしょう。
( SystemKのKSynth2とか(w )
それぞれツールが公開されているので使うことができます。
あと、でもやっぱりある程度オリジナリティがほしいのであれば、DirectXのDirectSoundのフィルタを使うのも
手でしょう。ローパスフィルタ、ハイパスフィルタやディレイなど一通りのフィルタが実装されています。
コレを利用して、一部にオリジナリティを加えてシンセを作るというのもなかなか実用的かもしれません。


●フィルタの種類
軽く覚え書き程度に列挙しておきます。
詳しく知りたければ名前でググッてください。ここではどんなのかがわかる雰囲気を重視しておきます。
ローパス - 高い周波数の音をカットする
ハイパス - 低い周波数の音をカットする
バンドパス - 特定の周波数付近の音のみ取り出す
エリミネートパス - 特定の周波数付近の音をカット
レゾナンス - 音に癖をつける
ディレイ - 音が遅れて聞こえてくる。残響効果
リバーブ・コーラス - 音に厚みをつける

・・・その他いろいろ


●フィルタの実装
さて、ここまでの原理を解説したサイトは結構ある。日本語でもかなり豊富である。市販のシンセサイザの
取扱説明書や、DTMソフトのマニュアルを読むと大量に解説してある。
がしかし、そのフィルタの実装(プログラムの処理)になると、とたんに解説がない。
いや、ないわけではないのだが、どれも解説が電気回路の知識にもともとづいた解説で
電気回路の知識がなければどくはすることはとうてい難しい話ばかりである。
もちろん私自身、回路の知識をもっていくつか解説をみてきたわけだが
デモで使う自前のソフトシンセくらいで、正確な周波数を計算してそれに計算しつくした
フィルタを使って、計算されつくされた音をだしてもなぁ・・・と思った。
ぶっちゃけ、らしく鳴ればいい のではないだろうか。そもそも高速化のために
計算のはしょりがの多いデモプログラムに正確な必要はないと思う。

というわけで、もっとわかりやすく、誰でも作れる(と思う)レベルまで掘り下げて
実装の話をしたいと思います。
(電気回路に詳しい人は非常に物足りない話になると思いますので、そういう方は
回路の微分方程式からどうぞ・・・)


▼ディレイ
ディレイは残響効果ですね。ということはつまり、一度鳴らした音が周りの障害物に当たって跳ね返ってきて
さらに、鳴らした音と重なって聞こえるというものですね。てことは、時間的にずらして、過去の音を足し算
すればいいわけです。
式が結構簡単な割りにいい感じに音が変わるのでつい書いてしまいます(w
 
 DELAY_TIME = 残響音の遅れ時間(サンプル時間)
 DELAY_LEVEL = 残響音の減衰量(0.0 - 1.0)
 long t;
 for(t=0; t<BUFFER_SIZE;  t++){  
    if(t>=DELAY_TIME) out[t] = in[t] + out[t-DELAY_TIME]*DELAY_LEVEL;
    else                 out[t] = in[t];
 }
としてやればいいわけです。
DELAY_LEVELが1.0より大きいと跳ね返ってくる音が元の音より大きくなってますから
いわゆるハウリングのようなの状態になってしまうので注意。

▼ローパス
ローパスは高い周波の音をカットをして低い周波の音を通すようなフィルタです。
高い周波は波の振幅が時間的に大きく変わる、低い周波は振幅が時間的にあまり変わらない
というものです。
考え方としては、一つ前のサンプリングの値に対して急激に変化するあたいを小さくする
というもので、

 
 LOW_PASS = ローパス率(0.0 - 1.0) 
 (1.0で高周波をカットしません。 0.0だと波形が変化しません(周波数が0になる))
 long t;
 for(t=1; t<BUFFER_SIZE;  t++){  
     out[t] = in[t] + (out[t-1]-in[t])*LOW_PASS;
 }
wave_data[t]を決定する時はwave_data[t-1]との差分をちいさくすることで
大きく変化する(高い周波数)波形を減らします。

▼ハイパス
ハイパスは低い周波の音をカットをして高い周波の音を通すようなフィルタです。
今度は先ほどのローパスフィルタの逆です。

 
 HIGH_PASS = ハイパス率(0.0 - 1.0) (0.0で低周波をカットしません。)
 long t;
 for(t=1; t<BUFFER_SIZE;  t++){
     out[t] = in[t] - in[t-1]*HIGH_PASS;
 }
前後の差分をとることで変化の大きいものだけとりだし、
高い周波数成分だけを取り出すことができます。


▼レゾナンス
特定の周波数付近を共振させて音に癖を出します。
コレを用いることでシンセサイザーらしい音が出ます
(というかコレを使うと変わった音が出るのでシンセサイザでよく使われる・・。)

 const float LOW_PASS = ローパス率(0.0 - 1.0)(1.0で高周波をカットしません)
 const float RESO     = レゾナンス(0.0 - 1.0)(1.0に近いときあまり発振しません
 ただし、1.0だと波形を全部打ち消してしまうのでつかえません。)  
 float v0=0,v1=0;
 for(t=1; t<BUFFER_SIZE; t++){
     v0 = (1.0f - RESO*LOW_PASS)*v0 + (in[t] - v1)*LOW_PASS;
     v1 = (1.0f - RESO*LOW_PASS)*v1 +  LOW_PASS*v0;
     out[t] = v1;
 }
これはデジタル的に説明するのは難しいんですねぇ・・。
回路的にはコンデンサとコイルが直列に入っていることで共振するので
ローパスも同時に入ってしまいます。
v0がコイルにかかる電圧,v1がコンデンサにかかる電圧です。
(なんていえばいいんだろ・・・)


●まとめ
フィルタ作成については簡単に説明したサイトがほとんどなかったので、こんなんあったらいいなぁ的なノリ
で作成したわけですが、いかがだったでしょうか?
だんだん回路の話し抜きには進めそうにないので、この辺で終わっときます(w
前回中身は書かないとかいっておきながら、今回はフィルタを実装していますが
私が思うに、この辺は100人いれば90人以上同じ意味のコードを書くと思います。
(もちろん違った特性を作るために、コードを変えることもあるでしょう)
音にオリジナリティが出てくるのは、これらのフィルタをいかに組み合わせて使うか
そして、むしろ誰もが作ってない効果的なあたらしいフィルタの実装に意味があると思います。

いかなるフィルタでもFIR,IIRの基本は現在よりも前の時間の値をつかってどうにかする。

ということですね
というわけで、ぜひ皆さんもオリジナルのフィルタをつくってかっこいいソフトシンセで
デモをばんばんつくっちゃってください。
(とくに4KBはサイズが限られてくるので少ないコードで以下に効果的なフィルタを作るかが鍵です。)

というわけで、今回でソフトシンセの解説は終わります。


フィルタのサンプルソースをおいときます。




感想・質問・間違い指摘はBBSまで・・・