VSTiの作り方
2012/11/05
7.おまけ (ADSRの実装)
ADSRの実装
ADSRと言えばシンセを扱う人なら誰でも知っている音の音量カーブを決めるパラメータですが...Twitterの方でモハヨナオさんの発言「ADSRエンベロープの実装って真面目にやるとキーオフのタイミングをめぐってややこしくなるけど、キーオンとキーオフ、二つのエンベロープに分割するとすごく楽になる 」というのがあって、なんか面白かったので自分のコードを眺めなおしたりしてたのだけど、なんか毎回ちょっとずつ違う実装してたりしてまとまりがないのだ。
正直ADSRのカーブを見ながらソフトで実装しようとすると変に複雑になりがちなんで、モハヨナオさんのアイデアとはまた違うのですが、この際、自分なりのADSRの実装について考察してみます。
ハードウェアとして一般的な回路がどうなってるかは武田さんのページなんかからたどれば色々でてきます。
http://www.aleph.co.jp/~takeda/radio/hms6.html
http://www.uni-bonn.de/~uzs159/adsr2.png
単純に書くとこんな構造です。
基本的にはこれをシミュレートする方向で考えると、回路的にはタイマーIC(NE555)一個だけでできているのですね。つまりADSR内部で必要なのはTimerOnのフラグ1つあれば良くて、出力される電圧は結局の所、タイマーICに繋がっている1つのコンデンサに対する充放電のカーブになっています。
ダイオードで充放電の方向が決まるのですが、動作を言葉で説明するとこうなります。出力するレベルは 0.0-1.0 の範囲で、Key信号はオン=1.0、オフ=0.0です。
■TimerはKeyの立ち上がりでOnして出力が1.0になったらOff (途中でKeyが下がっても強制Off)
■Timer On期間はAttackレートでピークに向かって充電
■Timer Off期間は出力がSustain Levelより大きいければSustain Levelに向かってDecayレートで放電
■出力がKey Levelより高ければKey Levelに向かって放電
これだけです。これは言葉で書いたほうがすっきりしてプログラムに落とすのも簡単な気がしますね。
ちなみに「xxに向かって充電(放電)」て言うのは、一定の周期で
現在値=目標値 + (現在値 - 目標値)×係数
を繰り返して目標値に漸近するカーブになります。
で、まあ、実は音の性格を決める所でアタックのカーブの取り方が結構重要な気がします。「ピークに向かって充電」と書きましたけど、1.0に漸近する曲線ではいつまでたっても1.0にならないわけで、このピーク付近のカーブの違いでアタック部分の音の図太さが変わっちゃいます。もう1つパラメータを追加するならAHDSRの「Hold」に相当する部分ですね。
NE555の仕様に忠実に行くならば、タイマーのスレッショルドは
続・ADSRの実装
さて、ADSRはハードウェア回路としてはコンデンサの充放電によるカーブでできていて、それをソフトウェアで実現するために現在値=目標値 + (現在値 - 目標値) × 係数
という計算でいけるよと説明しましたけど、まずこのあたりについて。
式の説明
話を簡単にするために0に向かって減少するリリースの場合だと、この式は単に現在値 = 現在値 × 係数(例えば0.99とか)
になるわけですが、こんな単純でいいんでしょうか? という話。
コンデンサの充放電って要するにRC回路の過渡応答なわけで、電気回路の教科書を探せば
V(t)=V×(1-exp(-t/RC))
なんてゆう公式が見つかるかと思います。これはRC回路に電圧Vをかけた時のコンデンサの電圧の上がり方を表しています。リリースのカーブを想定して現在値1Vで0Vに向かって減少するという場合なら、1Vを基準点(=0)と置いて-1Vで充電すると考えればよいです。つまり
V(t) = 1 + (-1× (1-exp(-t/RC))) = exp(-t/RC)
なんだ結局指数関数 exp(-t/RC) で減少してるだけじゃん、て事ですね。指数の符号がマイナスですから指数曲線の0から左に向かって動いています。
さてこれを一定周期毎に計算するわけです。つまり単位時間あたりの差分を計算するのですが、「指数の微分は指数」ですからね。言い換えれば、「今の地点の傾きは今の地点の大きさ」って事です。
つまり現在値に比例した差分を差っぴいていけば良いだけの話なので
現在値 = 現在値 × 係数
で良いのです。この式って指数関数なのですよ。
電荷がコンデンサから抜けて行くのは水の溜まった水槽の一番下に穴を開けたみたいな感じで、最初は圧があるので勢い良く減りますがだんだんゆっくりになるのをイメージすれば直感的に理解できるかも知れません。水槽に残っている水の量に比例した速さで抜けて行くわけです。
係数の話
さてじゃあ、この係数はどうやって決めれば良いの? という話。結構な頻度で繰り返し掛け算していくので 0.99... みたいな1に近い値が必要である事はわかると思います。
Attack Time とか Release Timeとか時間で指定したい所ですが、漸近してゆくだけでいつまで経っても目標値にはなりませんので、例えば「目標値の95%に近づくまでの時間」のような決め方をします。
つまり 1.0 => 0.05 になるまでの時間。仮に1mSec周期で計算してReleaseTime を1秒にしたいなら 1000回掛け算をして 0.05になるわけですから、これは
「0.05 の1000乗根」
です。C言語なら pow(0.05, 1.0/1000) とかいう感じで計算できますね。
更に更に、この辺の人間の感覚は対数的なのでツマミの反応カーブ自体が指数的(対数目盛り)であって欲しいですね。ハードウェアで作るならボリュームにAカーブを使う所です。ツマミのx=0.0~1.0に対して、最速10mSec~最長10Secとか。これは
pow(1000,x)*0.01 (sec) という感じですね。
毎秒n回計算するとしてこれを上の式と一緒にすると
係数=pow(0.05, 1.0 / (pow(1000, x) * 0.01 * n))
こんな感じになります。 結構重そうだけど、係数を計算するのはツマミをいじった時だけですのであまり問題にはならないですね。
でましたデノーマル
こんな感じでADSRは実装できるのですが・・・ただし!! ここで1つだけ気をつけなければいけない事があります。それが「デノーマルの罠」です。 「現在値 = 現在値 × 係数」 で計算を繰り返していくと、だんだん0に近づいていって、これを放置しておくと「デノーマル数」という厄介な問題に突入してパフォーマンスがガタ落ちになります。詳しくは下のページの方にも書いてますので参照して欲しいのですが、ほぼ0になったらどこかで計算を打ち切ってやる必要があります。
http://www.g200kg.com/jp/docs/makingvst/03.html#3.3
Javascriptで書いてると見落とし勝ちですが、先日確認した所ではこの問題はNativeだけじゃなくてJavascriptでも同じように発生するので要注意です。
g200kg