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 アキュムレータでは、単なる指数関数
\qquad\qquad f(x)\, =\, a \cdot e^{-b \cdot x}
を実現すれば良いことになります。
ここで、f(x)微分 f'(x) を求めると、
\qquad\qquad f'(x)\, =\, -b \cdot a \cdot e^{-b \cdot x}\, =\, -b \cdot f(x)
となります。
x\Delta x ごとにサンプルされた値を取るものとして「微分」を「差分」に置き換えると、
\qquad\qquad\frac{f(x + \Delta x) - f(x)}{\Delta x} \,=\, -b \cdot f(x)
となります。 ここで、\Delta x = 1 とすれば、
\qquad\qquad f(x + 1) \,=\, f(x) - b\cdot f(x)
と表されます。
ちなみに、前回のグラフで、指数関数をフィッティングした結果は 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)); 

となります。