FPGA 版 FM 音源 (18) -- YMF262 測定 (10)
今回は、アタック・カーブを「数値」として細かく検討し、計算回路を考えます。
lb ドメインで単純に直線を発生させればいいディケイ/リリースと違って、アタックの発生には、何らかの「演算」が必要になります。
最も簡単には、「結果」の値を ROM テーブルに書き込んでおいて、EG アキュムレータを単に +1 していくだけの「アドレス・カウンタ」として使う方法があります。
そのような構成を下の図に示します。
ディケイ/リリースで lb ドメインで「直線」を生成するための EG アキュムレータ (インクリメンタ) を ROM アドレス・カウンタとして利用しています。
左の図で、点線で示してある ROM 出力からのフィードバック・パスは、アタック途中でキー OFF となってリリースが開始される場合に、現在の EG 出力値をアキュムレータにロードするために必要です。
また、図では示してありませんが、ディケイ/リリース動作に遷移した場合には ROM をバイパスして、EG アキュムレータの内容を出力する必要があります。
この構成では、ROM を使うので、アタックのカーブの形状は任意に選べます。
後で示すように、簡単な演算回路でアタック・カーブを発生できることが分かったので、この構成を取る事はないと思います。
AR = 13、Rof = 0 より「遅い」設定のアタック・カーブでは、下の表に示す、34 種類のリニア・エンベロープ出力値しか登場しません。
AR = 13、Rof = 0 で、ちょうど 1 サンプルで 1 ステップずつ変化します。
それより速いレートについては、まだ検討していません。
index | リニア 出力 |
acc min |
acc max |
差分 min |
差分 max |
min/8 | max/8 | acc 増分 |
acc | リニア 出力 |
---|---|---|---|---|---|---|---|---|---|---|
-33 | 0 | 384 | 511 | 0 | -127 | 48 | 63 | -56 | 391 | 0 |
-32 | 2 | 334 | 351 | -33 | -177 | 41 | 43 | -49 | 342 | 2 |
-31 | 6 | 295 | 301 | -33 | -56 | 36 | 37 | -43 | 299 | 6 |
-30 | 14 | 259 | 262 | -33 | -42 | 32 | 32 | -38 | 261 | 14 |
-29 | 29 | 227 | 228 | -31 | -35 | 28 | 28 | -33 | 228 | 29 |
-28 | 54 | 199 | 199 | -28 | -29 | 24 | 24 | -29 | 199 | 54 |
-27 | 94 | 174 | 174 | -25 | -25 | 21 | 21 | -25 | 174 | 94 |
-26 | 151 | 152 | 152 | -22 | -22 | 19 | 19 | -22 | 152 | 151 |
-25 | 234 | 132 | 132 | -20 | -20 | 16 | 16 | -20 | 132 | 234 |
-24 | 338 | 115 | 115 | -17 | -17 | 14 | 14 | -17 | 115 | 338 |
-23 | 468 | 100 | 100 | -15 | -15 | 12 | 12 | -15 | 100 | 468 |
-22 | 620 | 87 | 87 | -13 | -13 | 10 | 10 | -13 | 87 | 620 |
-21 | 787 | 76 | 76 | -11 | -11 | 9 | 9 | -11 | 76 | 787 |
-20 | 978 | 66 | 66 | -10 | -10 | 8 | 8 | -10 | 66 | 978 |
-19 | 1188 | 57 | 57 | -9 | -9 | 7 | 7 | -9 | 57 | 1188 |
-18 | 1413 | 49 | 49 | -8 | -8 | 6 | 6 | -8 | 49 | 1413 |
-17 | 1645 | 42 | 42 | -7 | -7 | 5 | 5 | -7 | 42 | 1645 |
-16 | 1873 | 36 | 36 | -6 | -6 | 4 | 4 | -6 | 36 | 1873 |
-15 | 2088 | 31 | 31 | -5 | -5 | 3 | 3 | -5 | 31 | 2088 |
-14 | 2276 | 27 | 27 | -4 | -4 | 3 | 3 | -4 | 27 | 2276 |
-13 | 2482 | 23 | 23 | -4 | -4 | 2 | 2 | -4 | 23 | 2482 |
-12 | 2648 | 20 | 20 | -3 | -3 | 2 | 2 | -3 | 20 | 2648 |
-11 | 2826 | 17 | 17 | -3 | -3 | 2 | 2 | -3 | 17 | 2826 |
-10 | 3016 | 14 | 14 | -3 | -3 | 1 | 1 | -3 | 14 | 3016 |
-9 | 3150 | 12 | 12 | -2 | -2 | 1 | 1 | -2 | 12 | 3150 |
-8 | 3290 | 10 | 10 | -2 | -2 | 1 | 1 | -2 | 10 | 3290 |
-7 | 3434 | 8 | 8 | -2 | -2 | 1 | 1 | -2 | 8 | 3434 |
-6 | 3588 | 6 | 6 | -2 | -2 | 0 | 0 | -2 | 6 | 3588 |
-5 | 3666 | 5 | 5 | -1 | -1 | 0 | 0 | -1 | 5 | 3666 |
-4 | 3746 | 4 | 4 | -1 | -1 | 0 | 0 | -1 | 4 | 3746 |
-3 | 3828 | 3 | 3 | -1 | -1 | 0 | 0 | -1 | 3 | 3828 |
-2 | 3912 | 2 | 2 | -1 | -1 | 0 | 0 | -1 | 2 | 3912 |
-1 | 3998 | 1 | 1 | -1 | -1 | 0 | 0 | -1 | 1 | 3998 |
0 | 4084 | 0 | 0 | -1 | -1 | 0 | 0 | -1 | 0 | 4084 |
上の表の左端のカラムの「index」は、アタック・カーブがピークに達して、終了する時点のサンプル・インデクスを「0」として、開始方向にマイナスの値としてラベル付けしたものです。
「リニア出力」のカラムは、2 オペレータ並列アルゴリズムの設定でキャプチャしたリニア出力値を 1/2 にして、1 オペレータ当たりの値に変換したものです。
「acc min」および「acc max」のカラムは、それぞれ、出力リニア値から、EG アキュムレータに保持されているであろう「lb 値」を逆算した、「最小値」と「最大値」です。
出力レベルが大きい場合には、16 ビットリニア値に「量子化」されても、その元となる lb 値は、ただ一通りに決まりますが、レベルが小さい場合には量子化の影響で、同じリニア値を与える lb 値が複数個存在するようになるので、その最小値と最大値を求めています。
前回示したように、アタック・カーブを「波形」として見た場合、ほぼ「ゴンペルツ曲線」と見なすことができました。
「ゴンペルツ曲線」は (→こちらの記事) に示すように、「指数関数の指数関数」の形ですから、「対数ドメイン」である「lb 値」の EG アキュムレータでは、単なる指数関数
を実現すれば良いことになります。
ここで、 の微分 を求めると、
となります。
は ごとにサンプルされた値を取るものとして「微分」を「差分」に置き換えると、
となります。 ここで、 とすれば、
と表されます。
ちなみに、前回のグラフで、指数関数をフィッティングした結果は b = 0.1422 で 1/b は約 7 となっていました。
以上のことを念頭において、前の表の残りのカラムを説明します。
まず、「差分 min」と「差分 max」は、現在の EG アキュムレータ値と、一つ前のサンプルでの値との「差分」の「最小値」と「最大値」です。
「差分 min」は、
(現在の EG アキュムレータの最大値 - ひとつ前の EG アキュムレータの最小値)
であり、「差分 max」は
(現在の EG アキュムレータの最小値 - ひとつ前の EG アキュムレータの最大値)
です。
現在および一つ前の EG アキュムレータの値が一意に決まる場合は最小値と最大値は同じになります。
たとえば、サンプル・インデクス「-23」の EG アキュムレータの現在値が「100」であり、ひとつ前のサンプル・インデクス「-24」の EG アキュムレータ値が「115」ですから、その「差分」は「-15」と計算され、その値が「差分 min」と「差分 max」のカラムに書かれています。
「min / 8」と「max / 8」は、それぞれ、EG アキュムレータの最大値と最小値を「8」で割って切り捨てた (3 ビット右シフト) 値です。
ここで、「 / 8」のカラムと、「差分」のカラムを注意深く見比べると、たとえば、例に出したサンプル・インデクス「-23」の差分「-15」は、ひとつ前のサンプル・インデクス「-24」の「 /8」カラムの「14」をマイナスにして、さらに 1 を引いたものになっていることが分かります。
これはサンプル・インデクス「-23」に限ったことではなく、差分が一意に決まるサンプル・インデクス「-27」以降、「0」までのすべてのサンプル・インデクスにおいても成り立っています。
つまり、
- EG アキュムレータの値 (のコピーを) を 3 ビット右シフト
- 符号反転 (2 の補数を取る)
- 1 を引く
- 現在の EG アキュムレータに加算
という手順で、次のサンプル・インデクスでの EG アキュムレータの値が得られることが分かります。
ここで、2 の補数を取る手順は、
- 全ビット反転 (1 の補数を取る)
- 1 を足す
であることに注意すると、「1 を足す」と「1 を引く」が打ち消しあって、1 の補数を取るだけで良くなり、結局、上の手順は、
- EG アキュムレータの値 (のコピーを) を 3 ビット右シフト
- 全ビット反転 (1 の補数を取る)
- 現在の EG アキュムレータに加算
となります。
これを回路で表現すると、左の図のようになります。
EG アキュムレータの値が一意に決まらないサンプル・インデクス「-28」以前については、直接推測はできませんから、EG アキュムレータの初期値を「511」として、この方式で計算してみたところ、実際にキャプチャしたリニア値と完全に一致するアタック・カーブの系列を得ることができました。
これが、最初の表の残りの 3 つのカラムです。
「acc 増分」は、現在の値を得るために、この値をひとつ前の EG アキュムレータ値に加えたということを示し、「acc」は現在の EG アキュムレータ値、「リニア出力」は lb-リニア変換により得られたリニア値を示しています。
EG アキュムレータの初期値「511」から出発すると、
511 → 447 → 391
と変化して、最初の表のサンプル・インデクス「-33」の状態になります。
つまり、初期値の「511」はサンプル・インデクス「-35」、「447」はサンプル・インデクス「-34」の状態に対応し、36 ステップでアタック・カーブ全体が構成されることになります。
初期値を変えながら、リニア値のキャプチャ結果と完全に一致するものを探索すると、
511、449、448、447、392、391
と選んだ系列だけが完全に一致します。
449、448 は 447 に、392 は 391 に極めて近く、同じ系列と見なせますから、測定結果と完全に一致するのは初期値 511 から始まる系列ひとつと言ってもよいでしょう。
初期値を 511 から 391 まで変化させて得たアタック・カーブのグラフを下に示します。
測定結果と完全に一致はしない系列でも、ゴンペルツ曲線としての変化はしています。
最後に、C プログラムで表現したものを下に示します。
// OPL3 EG (AR) simulator // 2011/01/26 #include <stdio.h> #include <math.h> int lb2lin_tab[256]; // // lb-to-lin conversion with sign // int lb2lin(int sign, int lb) { int lin, sft; sft = (lb / 256); // integer part of lb value lb = (lb % 256); // fraction part of lb value // force 0 if too small lin = ( (15 < sft) ? 0 : (lb2lin_tab[lb] >> sft) ); return((-sign) ^ lin ); // 1's complementer } // int lb2lin() // // generate lb-to-lin table for 8 bit fraction part of lb // void gen_lb2lin_tab( void ) { int i; double v; for (i = 0; i < 256; i++) { // for 8 bit fraction // (2 ** (-x)) = exp(-log(2) * x) v = 2048 * exp(-log(2) * (i + 1) / 256.0); lb2lin_tab[i] = (int)(v + 0.5) * 2; } // for (i = 0; ... } // void gen_lb2lin_tab() void main( void ) { int i, op_out, eg_acc; gen_lb2lin_tab(); eg_acc = 511; // initial value printf("#index op_out eg_acc \r\xa"); for (i = -35; i <= 0; i++) { // convert lb to linear value op_out = lb2lin(0, (eg_acc << 3)); printf("%5d %5d %5d \r\xa", i, op_out, eg_acc); // add 1's complement of 3-bit right shifted eg_acc eg_acc += (~(eg_acc >> 3)); } // for (i = 0; ... } // void main()
EG アキュムレータの更新は、
eg_acc += (~(eg_acc >> 3));
の 1 行です。 (eg_acc >> 3) で 3 ビット右シフトした値を単項演算子「~」で全ビットの反転 (1 の補数) を行い、アキュムレータに足しこんでいます。
減算で表現したい場合は、
eg_acc -= (1 + (eg_acc >> 3));
となります。