FPGA 版 FM 音源 (22) -- YMF262 測定 (14)

ソフトウェアや FPGA で EG を実現する場合、必ずしも実際のチップである YMF262 の EG 出力波形を全く忠実に再現する必要はありません。
基本的な構成は踏襲するとしても、リソース削減の要求は強くはないので、リソースが多少増加しても性能の向上を目指したり、ソフトウェアや FPGA 向きの構成にすることが考えられます。
そのひとつとして、EG 出力のビット幅を増やして、エンベロープ・カーブの「ギザギザ」を減らすことがあります。
ここでは、サイン波発生部と同等になるよう、lb 値の小数部を 5 ビット幅から 8 ビット幅に拡張してみようと思います。
AVR のような 8 ビット・マイコンでも実現しやすいように EG アキュムレータを 15 ビット幅とし、8 ビット境界も意識した回路を下に示します。

アタックとディケイ/リリースの切り替えは、1 の補数をとる exor ゲートだけを示し、アキュムレータに加算する値を切り換える部分は省略して、アタックの計算部分だけを示しています。
レート・マルチプライアは廃止し、Rof はフィードバックする値自体に数値として乗算します。
上の回路で AR/DR/RR = 14 から 10 までの範囲のレートに対応しています。
AR = 15 は、立ち上がり時間ゼロ、つまり、-96 dB でスタートした次のサンプルで 0 dB のピークに達するような仕様となっていますから、それを計算回路で実現するというよりも、EG のシーケンスのコントロールで 1 サンプル後に強制 0 dB とする方法を取ります。
AR/DR/RR = 9 以下は、「プリスケーラ」により計算回路の「クロック」を分周し、AR/DR/RR = 10 の設定にした上の回路に供給するという形で実現します。 プリスケーラ部分は示してありません。
アタック計算は、まず EG アキュムレータの上位 7 ビットのみをバレル・シフタにより AR の値により左 4 ビット・シフトからシフトなしの状態のいずれかとし、Rof の値に「4」を足したものとの乗算を行って 1 の補数器に入力しています。
この乗算は、具体的には 7 + 5 = 12 ビットの「長い数」と、Rof の 4、5、6、7 の「短い数」の乗算ということになります。
この「短い数」は 3 ビット幅ですから、基本的な「シフト・アンド・アッド」演算 3 回で実現できますから、「乗算」といっても、それほど大きな負担にはなりません。
上の回路では、EG アキュムレータの上位 7 ビットをバレル・シフトしていますが、これを、

  • Rof の 4、5、6、7 をバレル・シフト、
  • EG アキュムレータの上位 7 ビットはそのまま、

両者を乗算する方式に変えても実現できます。
ただし、この場合には、Rof 側も 4 + 3 = 7 ビット幅となり、 7 ビット x 7 ビットの乗算が必要になります。
8 ビット・マイコンである ATmega でも 8 ビット x 8 ビットの乗算を 2 サイクルで実行できますから、ソフトウェアによる実現では、こちらの方が好都合です。
後に示す C プログラムでも、こちらの方法を取っています。
上の回路の、バレル・シフタの下側についている NOR ゲート (負論理入力の AND ゲート) は、EG アキュムレータの上位ビットが「ゼロ」となり、ピーク位置に近づいたことを検出して、フィードバック量を切り替える回路です。
EG アキュムレータのビット数を増やし、EG カーブの変化がなめらかになるようにしたので、より「ゴンペルツ曲線」に近づくこととなり、そのままでは、ピーク付近が「平坦」になってアタック時間が長くかかるようになってしまいます。
この切り替え回路なしで、EG アキュムレータの上位ビットのマスクもない状態での出力のグラフを下に示します。

ピーク付近の平坦な部分に滞留する時間が、アタックの立ち上がりに要する時間とあまり変わらないレベルとなっています。
切り替え回路および EG アキュムレータの上位ビット・マスク付きでの出力のグラフを次に示します。

最初に示した回路では、AR = 9 からプリスケーラが有効になるので、AR = 9 から 14 までを示してあります。
ピーク付近で、lb ドメインで直線となるように切り替えています。
リニア値としては、ディケイ/リリースの逆の変化となりますから、「直線」ではなく、「下に凸」なカーブとなります。
EG アキュムレータの上位ビット・マスクの値や、切り換えた「増分」などを調整し、切り替え部分で大きな「段差」とならないようにしてあります。

ピークに達するまでの、トータルのアタック・タイムは、YMF262 の回路のものより若干長くなり、Rof の値に換算すると、「2」程度の違いが生じています。
たとえば、サンプル・インデクス 25 付近でピークになる設定を探すと、

  • YMF262 の回路での AR = 13、Rof = 2 と、
  • この回路での AR = 14、Rof = 0

とが、ほぼ等しくなっています。

最後に、これらのグラフ出力のために使った C プログラムを示します。

// 15-bit EG acc, lb(4.8) output EG (AR/DR)
// 2011/02/07

#include <stdio.h>
#include <inttypes.h>
#include <math.h>

typedef enum eg_states {
  EG_OFF, EG_AINIT, EG_ATTACK, EG_A2D, 
  EG_DECAY, EG_SUSTAIN, EG_RELEASE
} eg_stat_t; 

// prescaler carry bit mask
#define PSC_CMASK (0x0200)

// EG acc constant for 0dB, -96dB
#define EG_0dB  (0x0000)
#define EG_96dB (0x7ff8)

int      lb2lin_tab[256];
int      rate, rof10, m_rate;
int      rof_mult, rof_mult2;
int      hi_rate_flag;
uint16_t eg_acc, psc_acc, psc_inc;
int      ar, dr, rof;

eg_stat_t eg_stat;

//
// 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()

// EG rate setup
void EG_set_rate(int rate_ini, int rof_ini)
{
  rate  = (rate_ini * 4) + rof_ini; // initial rate value
  rof10 = 0x03 & rate; // LS 2 bits
  rate /= 4;           // MS 4 bits
  if (0  == rate) { rof10 = 0;}
  if (15 <= rate) { // clip to 15
    rate = 15;
    rof10 = 0; // no fractions
  } 
  hi_rate_flag = (10 <= rate);
  psc_acc = 0; // reset prescaler
  if (hi_rate_flag) { // for hi-rate
    psc_inc = PSC_CMASK; // for continuous clock
    m_rate = rate;
  } else { // for lo-rate, set prescaler incr.
    psc_inc = (0x1 << ((0x0f & rate)-1)); 
    m_rate = 10; // saturate to 10
  }
  rof_mult  = ((4 + rof10) << (m_rate-10)); 
  rof_mult2 = rof_mult * 2; 
} // void EG_set_rate()

// EG initialize
void EG_init( void )
{
  EG_set_rate(0, 0); // rate = 0 for no acc update
  eg_acc = EG_96dB; // minimum level
  eg_stat = EG_OFF; // set to OFF state
} // void EG_init()

int EG_module(int key_stat)
{
  int clk_en;
  clk_en = hi_rate_flag; // continuous clock for hi-rate
// clock prescaler and rate multiplier  
  psc_acc += psc_inc; // increment prescaler acc
// test for prescaler carry
  if (PSC_CMASK & psc_acc) { // prescaler carry?
    psc_acc &= (~PSC_CMASK); // clear prescaler carry
    clk_en = 1;
  } // if (PSC_CMASK ...
// EG accumulator update          
  if (clk_en | (EG_A2D == eg_stat) | (EG_AINIT == eg_stat)) {
    switch (eg_stat) {
      case EG_AINIT: // init for ATTACK
        EG_set_rate(ar, rof); // set attack rate and init psc
        eg_stat++; // advance to next state
        break;
      case EG_ATTACK: 
// if rate = 15, attack time is "0"      
        if (15 == rate) { eg_acc = EG_0dB; }
        if (EG_0dB == eg_acc) { // is it 0 dB?
          eg_stat++; // advance to next state
        } else {
          if (0 < rate) { 
            if (0x7f00 & eg_acc) { // far from peak
// add 1's complement of Rof multiplied eg_acc
              eg_acc += ~(rof_mult * (eg_acc >> 8)); 
            } else { // near to peak
              eg_acc += ~rof_mult; // constant decrement
              if (0x8000 & eg_acc) { // overflow?
                eg_acc = EG_0dB; // saturate to 0dB
                eg_stat++;       // advance to next state
              } // if (0x8000 & eg_acc) ...
            } // if (
          } // if (0 < rate)
        } // if (0 == eg_acc) ...
        break;
      case EG_A2D: // ATTACK to DECAY transition
        EG_set_rate(dr, rof); // set decay rate and init psc
        eg_stat++;  // advance to next state
        break;
      case EG_DECAY: case EG_RELEASE:
// increment EG acc by shifted Rof              
        if (0 < rate) { eg_acc += rof_mult2; }
        if (EG_96dB <= eg_acc) { // EG OFF?
          eg_acc  = EG_96dB;
          eg_stat = EG_OFF;
        } // if (EG_96dB <= eg_acc)
        break;
      case EG_OFF: // no EG update
        break;
      default:
        break;
    } // switch (eg_stat) { ...
  } // if (clk_en) ...
  return(eg_acc);
} // int EG_module()

void main( void )
{
  int i, eg_out, op_out;
  gen_lb2lin_tab();
  for (ar = 14; ar >= 9; ar--) { 
    dr = ar;
    for (rof = 3; rof >= 0; rof--) {
      printf("# ar=%d, rof=%d \r\xa", ar, rof);
      EG_init(); // initialize EG vars
      eg_stat = EG_AINIT;
      for (i = 0; EG_OFF != eg_stat ; i++) {
        eg_out  = EG_module(1); // compute EG 
// convert lb to linear value
        op_out  = lb2lin(0, (eg_out >> 3)); 
        printf("%5d %5d 0x%.4x \r\xa", i, op_out, eg_out); 
      } // for (i = 0; ...
// 2 empty lines for GNUPLOT data block separator
      printf("\r\xa\r\xa"); 
    } // for (rof = 0; ...
  } // for (ar = 13; ...
} // void main()