FPGA 版 FM 音源 (55) -- YMF297 (OPN3/OPL3) 測定 (21) -- EG シミュレーション・プログラム (1)

C 言語で書いた「フル機能」の EG シミュレーション・プログラムができました。
詳しい説明は後に回して、今回は結果のグラフとソースのみ掲載します。
(2016 年 12 月 31 日追記: プログラムに RATE = 0 の場合の動作に関するバグがあったのを修正しました)
(2017年 1 月 1 日追記: 上記バグを再修正。 「fs_en」信号を追加。)
(2017年 1 月 11 日追記: 上記とは別の RATE = 0 に関するバグを修正。)

  • AR = 15, Rof =3 から AR = 11, Rof = 0 まで変化
  • DR は AR と同じ値
  • SL = 3 (-9 dB)
  • RR = 13 固定
  • EGT = 1 (サステイン・タイプ)
  • サンプル・インデクス = 1 で KON = 1 (キーオン)
  • サンプル・インデクス = 192 で KON = 0 (キーオフ)

の設定で実行した結果のグラフを下に示します。

青いトレースが EG アキュムレータ出力の数値を 8 倍してプロットしたもので、赤いトレースが EG アキュムレータ出力を log—リニア変換した数値をプロットしたものです。
他の設定は上の場合と同一で、EGT のみ

  • EGT = 0 (減衰音タイプ)

とした場合の結果を下に示します。

EG シミュレーション・プログラムのソースを下に示します。
Verilog-HDL によるハードウェアでの実現を意識した書き方になっていて、ソフトウェアとして効率の良い書き方にはなっていません。
信号の値が変化するタイミングで計算した値を変数に記憶しておいて使い回しするのではなく、必要な値は毎回計算するようにしています。

// OPL3 EG simulator
// 2016/12/30 full function (AR/DR/SL/RR/EGT/Rof)

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

// state definition for EG state machine
typedef enum eg_states {
  EG_MUTE,    EG_ATTACK,  EG_DECAY, 
  EG_SUSTAIN, EG_RELEASE
} eg_stat_t; 

// EG accumlator 
#define EG_ACC_BITS (9)
#define EG_ACC_MASK ((1UL << EG_ACC_BITS)-1)
// EG acc constant for 0dB, -96dB, -95 dB, -93 dB
#define EG_0dB  (0x000)
#define EG_96dB (EG_ACC_MASK)
#define EG_95dB (EG_ACC_MASK-0x3)
#define EG_93dB (EG_ACC_MASK-0xf)

// rate multiplier lookup table for hi-rate
uint8_t rmp_LUT[4][8] = {
// psc_cnt[2:0]
//  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
}; // uint8_t rmp_LUT

// Rof to prescaler bitmask for lo-rate
uint8_t rof_mask[4] = {
  0x01,  // 3'b001 for Rof = 0
  0x05,  // 3'b101 for Rof = 1
  0x03,  // 3'b011 for Rof = 2
  0x07   // 3'b111 for Rof = 3
}; // uint8_t rof_mask[]

uint16_t  lb2lin_tab[256];
int       ar, dr, sl, rr, rof, egt;
uint16_t  psc_cnt, psc_z1;
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()

// Calculate prescaler clock enable by rate multiplier
uint16_t calc_psc_en(int fs_en, int rate, int rof)
{
  uint16_t psc_en;

  if (fs_en) { // prescaler update timing
    psc_z1  = psc_cnt++;
  } // if (fs_en) { ...
  psc_en = (~psc_z1 & psc_cnt);
  return(0 != (rof_mask[rof] & (psc_en >> (12 - rate))));
} // uint16_t calc_psc_en();

int EG_module(uint16_t *eg_mem_p, int fs_en, 
              int kon_rise, int kon_fall, int egt, 
              int ar, int dr, int sl, int rr, int rof)
{
// WIREs
  int rate, rof10, hi_rate;
  int ar15, rate15, rate13, rate0;
  int eq_0dB, eq_95dB, eq_slx;
  int trans;
  int sft_sel, clk_en, sft, slx;
  int acc_clk_en;
  int A, D, S, R, M;
  int to_A, to_D, to_S, to_R, to_M;
  uint16_t lin_out, eg_add;
  int16_t  eg_inc;
// REGs  
  uint16_t eg_acc;
  uint8_t  eg_stat;

// unpack EG_state / EG_acc from slot memory
  eg_acc  = (EG_ACC_MASK & (*eg_mem_p));
  eg_stat = ((*eg_mem_p) >> EG_ACC_BITS);
//
// combinational signal (WIREs) calculation
// 
// decode SL ( 0dB..-42dB for SL=0..14, -93dB for SL=15)
  slx = (sl << 4) + (0x100 * (15 == sl));
// EG state decode
  A = (EG_ATTACK  == eg_stat);
  D = (EG_DECAY   == eg_stat);
  S = (EG_SUSTAIN == eg_stat);
  R = (EG_RELEASE == eg_stat);
  M = !(A | D | S | R);
// rate MUX
  rate  = (A ? ar : 0);
  rate |= (D ? dr : 0); 
  rate |= (R ? rr : 0); 
  rate0 = (0 == rate);
// actual_rate = rate_reg_value + rate_offset  
  if (rate0) {
    rof = 0; // force zero offset
  }
  rof10 = (0x03 & rof); // LS 2 bits of Rof
  rate += (rof >> 2);   // MS 2 bits of Rof
  if (15 <= rate) { 
    rate = 15; // clip to 15
    rof10 = 0; // no fraction
  } 
// rate flags  
  hi_rate = (13 <= rate);
  rate15  = (15 == rate);
  rate13  = (13 == rate);
  ar15    = (A & rate15);
// eg_acc value comparator  
  eq_0dB  =  (EG_0dB == eg_acc); // attack peak (0dB)
  eq_95dB = !(EG_95dB & (EG_95dB ^ eg_acc)); // decay / release bottom (-95dB)
  eq_slx  = !(EG_93dB & (slx     ^ eg_acc)); // crossing sustain level
// state machine transition flag  
  to_A  = (!A & kon_rise); // to attack
  to_D  = ( A & eq_0dB);   // attack to decay
  to_S  = ( D & eq_slx);   // decay to sustain
  to_R  = (!M & !R & kon_fall); // to release
  to_M  = ((D | R) & eq_95dB);  // decay/release to mute
  trans = (to_A | to_D | to_S | to_R | to_M); // EG state transition
// clock prescaler and rate multiplier  
  clk_en  = (!rate0) * calc_psc_en(fs_en, rate, rof10);
  clk_en |= hi_rate; // continuous clock for hi-rate
// calculate shift amount
  sft_sel  = rmp_LUT[rof10][0x07 & psc_cnt]; // table lookup
  sft_sel |= rate15;  // always '1' for rate = 15
  sft_sel &= hi_rate; // always '0' for lo-rate
  sft      = (!sft_sel + (!hi_rate | rate13));
// inhibit EG acc update at EG state transition timing
  acc_clk_en = (M | (!trans & clk_en)); 
  eg_inc =  (A       ? (~eg_acc >> 1) : 0) // attack : 1's compl. of EG acc
          | ((D | R) ?   0x04         : 0) // decay/release : fixed pattern
          ;
  eg_inc >>= sft; // barrel shift with sign
  eg_add   = (eg_acc + eg_inc); // adder output
//  
// EG accumulator update (REG)
//
  if (acc_clk_en) { // EG acc update timing
    if        (M) { // mute state
      eg_acc = EG_96dB; // force mute
    } else if (!ar15) {
      eg_acc = eg_add; // normal EG acc operation
    } else { // AR = 15
      eg_acc = 0; // force immediate peak
    } // if (M) { ...
  } // if (acc_clk_en) { ...
  eg_acc &= EG_ACC_MASK; // limit to 9 bit width
//
// EG state machine update (REG)
//
  if (trans) { // EG state trasition timing
    if        (to_A) {
      eg_stat = EG_ATTACK; 
    } else if (to_R) {
      eg_stat = EG_RELEASE;
    } else if (to_D) {
      eg_stat = EG_DECAY;
    } else if (to_S) {
      eg_stat = (egt ? EG_SUSTAIN : EG_RELEASE);
    } else if (to_M) {
      eg_stat = EG_MUTE;
    } // if (to_A) {} else { ...
  } // if (trans) { ...
// pack and save updated EG_state / EG_acc to slot memory
  *eg_mem_p = (((uint16_t)eg_stat << EG_ACC_BITS) | eg_acc);
  return(eg_acc);
} // int EG_module_v2()

void main( void )
{
  int i, eg_out, lin_out;
  int psci;
  uint16_t eg_mem[1] = {EG_96dB};

// generate lb to lin table
  gen_lb2lin_tab();
  sl  = 3;  // -9 dB
  rr  = 13;
  egt = 1;  // EG type = sustain
  for (ar = 15; ar >= 11; ar--) { 
    dr = ar;
    for (rof = 3; rof >= 0; rof--) {
      for (psci = 0; psci < 1; psci++) {   
        fprintf(stdout, "# DR=%d, Rof=%d, psc_ini=%d\r\xa", dr, rof, psci);
        psc_cnt = psci; // prescaler counter initial value
        for (i = 0; i < 512; i++) {
          printf("%5d", i); 
          eg_out = EG_module(eg_mem, 1, (1 == i), (192 == i), egt, ar, dr, sl, rr, rof);
// convert lb to linear value
          lin_out  = lb2lin(0, (eg_out << 3));
          printf(" %4d %3d", lin_out, eg_out); 
          printf("\r\xa");
        } // for (i = 0; ...
// 2 empty lines for GNUPLOT data block separator
        printf("\r\xa\r\xa"); 
      } // for (psci = 0; ...
    } // for (rof = 3; ...
  } // for (ar = 15; ...
} // void main()