FM音源プログラム (5) -- EG (5)

EG プログラムの処理方法を、簡略化した C プログラムの断片を示しながら説明していきます。
まず、現在の EG の状態を示す列挙型

enum egmode_t {OFF, ATTACK, DECAY, SUSTAIN, RELEASE};

の変数 egmode を定義して、プログラム中では

switch (egmode) {
  case ATTACK:   ... 
  case DECAY:    ...
  case SUSTAIN:  ...
  case RELEASE:  ...
  case OFF:      ...
}

のように場合分けして処理します。 実際のプログラムでは、MIDI のオールノートオフのような強制オフに対応する FORCE_DAMP のような状態が加わっています。
EG の lb ドメインでの値は 32 ビット整数の egacc に保持されています。 lb 値としては b22 の右側に小数点があるものとします。(整数部 10 ビット、小数部 22 ビット)
lb - リニア変換する場合には上位の 16 ビットだけを抜き出して、整数部 10 ビット、小数部 6 ビットの lb 値とします。
実際のプログラムでは、define によるマクロとして実現されていますが、関数として lb - リニア変換を表現すると、

int32_t LB_to_LIN(int16_t x)
{
  if ((EG_96dB >> 16) <= x) return(0);
  return( (uint32_t)lb2lin[0x3f & x] << (16 - (x >> 6)));
}

となります。 リニア値は 32 ビット幅で返され、その上位 16 ビットを使います。
ここで、EG_96dB は 32 ビット幅での -96 dB を表す数値、lb2lin[] は 2^{-f} (f は小数) のテーブル (64 エントリ、16 ビット幅、小数点位置は b15 の右) です。

ディケイ/リリースの処理

アタックからディケイにモード遷移する際に

egrate = dr + rof;
eginc  = eg_tab[egrate];
egacc  = 0;

のような準備をしておきます。
ここで、dr は音色パラメータの DR を 4 倍したもので、rof は Rate Key Scale によるオフセット値、 eg_tab[] は EG レートからアキュムレータに加算する増分値への変換に使うテーブルです。 ディケイは 0 dB から始まるので、egacc は 0 にしておきます。
リリースも同様ですが、音色パラメータの RR を使うのと、リリース時には egacc の値は変更しない部分が違います。
ディケイ/リリースともに egacc のアップデートは

egacc += eginc;

で行います。
ディケイの場合は、音色パラメータの SL を egacc での lb 表現方法に変換した値である sl_lb と、egacc の値とを比較し、サステインに移行するかどうかを決定します。
リリースの場合は、EG_96dB と、egacc とを比較し、OFF 状態に移行するかどうかを決定します。
また、OFF 以外のどのモードでも、KEYOFF の条件を検出したら、リリースに移行します。

サステインの処理 (EGT=1 の場合)

サステインに移行する際に egacc を sl_lb に再設定し、eginc はゼロにして egacc の値が変化しないようにします。

egacc = sl_lb;
eginc = 0;
アタックの処理

これまで、意識的にアタックの処理については説明を避けてきました。 それは、単純に直線的に egacc の値を最大減衰量から 0 dB に向かって減らしていくだけでは \exp(R \cdot t) の形のエンベロープとなり、不自然だからです。
egacc の値を対数関数で変化させれば、指数変換でリニア値に変換後は直線的な変化になります。
しかし、この方法では、対数テーブルが新たに必要となりますから、現在ある指数テーブル (lb2lin[]) を利用して、指数変換を2回行ってS字状のカーブを作ることにしました。
これは、ゴンペルツ (Gompertz) 曲線といって、一般に
\qquad y = c \cdot \exp ( -a \cdot \exp( -b \cdot x))
の形で表されます。
ここでは、
\qquad y = \exp ( -\ln(2) \cdot 12 \cdot \exp( -\ln(2) \cdot {\rm rate} \cdot t))
に相当する曲線を使っています。
プログラムでは、最初の指数関数の引数となる変数を新たに egacc2 として定義し、これをインクリメントします。

egacc2 += eginc;
...
egacc  = (LB_to_LIN(egacc2 >> 16) >> 7) * 3;
ol_lin =  LB_to_LIN(egacc >> 16);

グラフを下に示します。
4 倍する前の音色データの AR 値の 15 レベルに対応して 15 本のグラフが書いてあるのですが、アタック時間の短い部分に多くのグラフが重なっています。
グラフが階段状になっているのが上の方で目立つのは、振幅が大きい所ではステップサイズが大きくなるからです。

実は、「ゴンペルツ曲線」というのは、今日はじめて知りました。
苦し紛れに指数関数を2回使ってS字カーブで作ったアタックの曲線に、どうも納得がいってなかったのです。 そのため、この方法の説明をするのが、ちょっと気が重かったのです。
この記事を書くためにいろいろググッているうちに、この方法に、立派な名前が付いていることが分かりました。
また、ソフトウェアのバグの発生に関して、ゴンペルツ曲線がシスアドの試験に出るらしいこともわかりました。 ということは、私はシスアドには、なれないのかも知れません。 まあ、なる気もないですけど。