FPGA 版 FM 音源 (21) -- YMF262 測定 (13)

アタックからディケイへ変化するだけの、単純なエンベロープを発生する C プログラムのリストを下に示します。
アタック・レート (AR) を 15 から 11 まで、Rof を 0 から 3 まで変えながら、それぞれの設定のアタック・ディケイ・カーブを EG アキュムレータの値が 511 に到達するまで、各行に、サンプル・インデクス、リニア出力値、EG アキュムレータ値、の順で出力します。

// OPL3 EG (AR/DR) simulator
// 2011/02/02

#include <stdio.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 (0x1000)
// EG acc constant for 0dB, -96dB
#define EG_0dB  (0x000)
#define EG_96dB (0x1ff)

// rate multiplier / shift amount modulator
// look-up table
typedef int rmp_LUT_t[8];

rmp_LUT_t rmp_LUT[4] = {
// rmp_cnt
//  0  1  2  3  4  5  6  7
   {0, 0, 0, 0, 0, 0, 0, 0}, // Rof = 0
   {0, 0, 0, 0, 0, 0, 1, 1}, // Rof = 1
   {1, 1, 0, 0, 1, 1, 0, 0}, // Rof = 2
   {1, 1, 0, 0, 1, 1, 1, 1}, // Rof = 3
}; // rmp_LUT_t sft_LUT

int lb2lin_tab[256];
int eg_acc, rate, rof10, rmp_cnt;
int psc_acc, psc_inc;
int hi_rate_flag, sft_base;
eg_stat_t eg_stat;
int ar, dr, rof;

//
// 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 = (12 < rate);
  sft_base = 3 - (13 < rate);
  psc_acc = 0; // reset prescaler
  if (hi_rate_flag) { // for hi-rate
    psc_inc = 0; // no prescaler update
  } else { // for lo-rate, set prescaler incr.
    psc_inc = (1 << ((0x0f & rate) )); 
  }
} // 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
  rmp_cnt = 0; // prescaler counter
  eg_stat = EG_OFF; // set to OFF state
} // void EG_init()

int EG_module(int key_stat)
{
  int sft_sel, clk_en, sft;
  clk_en = hi_rate_flag; // continuous clock for hi-rate
  sft = sft_base; // fixed EG acc increment for lo-rate
// clock prescaler and rate multiplier  
  psc_acc += psc_inc; // increment prescaler acc
// test for prescaler carry
  if (hi_rate_flag | (PSC_CMASK & psc_acc) ) { 
    psc_acc &= (~PSC_CMASK); // clear prescaler carry
// rate multiplier / shift amount modulator  
    sft_sel = rmp_LUT[rof10][rmp_cnt]; // table lookup
    sft_sel |= (15 == rate); // always '1' for rate = 15
    if (hi_rate_flag) { // for hi-rate
      sft = sft_base - sft_sel; // shift amount mod.
    } else { // for lo-rate, clock rate modulation
      clk_en  = (sft_sel | (0 == (0x01 & rmp_cnt)));
    } // if (hi_rate_flag) ...
// update rate multiplier counter
    rmp_cnt = 0x07 & (rmp_cnt + 1); 
  } // if (hi_rate_flag | ...
// 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 {
// add 1's complement of 1/2/3-bit right shifted eg_acc
          if (0 < rate) { eg_acc += (~(eg_acc >> sft)); }
        } // 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 1, 2 or 4              
        if (0 < rate) { eg_acc += (8 >> sft); }
        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 = 15; ar > 10; ar--) { 
    dr = ar;
    for (rof = 3; rof >= 0; 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 %5d \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 = 3; ...
  } // for (ar = 13; ...
} // void main()

アタック → ディケイの遷移部分については、まだ、実際の値と十分な一致が得られていません。
エンベロープ・カーブの間には空行を 2 行挿入してあり、GNUPLOT で「データ・ブロック」の区切りとして認識されます。
レート・マルチプライアの出力ロジック部は、ソフトウェアによる実現なので、ビットごとの and / or では構成せずに、「テーブル・ルックアップ」で求めています。