新版FM音源プログラム (13)

これまでのFM音源プログラムでは、オペレータ (スロット) ひとつ分を実現する関数を定義し、それをオペレータの個数だけ呼び出していました。
より効率化するために、ひとつの関数内ですべてのオペレータの計算を行なうように変更することを考えています。
8 K エントリのウェーブ・テーブルを使うタイプのプログラムと、3.75 K エントリのウェーブ・テーブルを使うタイプのプログラムとで、実行サイクル数に大きな差はありませんでした。
そこで、ウェーブ・テーブル・サイズの削減効果を重視して、2016 年 8 月 16 日付けの記事 (→こちら) で示している 3.75 K (3840) エントリのウェーブ・テーブルを使うことにしました。

2 オペレータ・タイプのFM音源では、「アルゴリズム」としては左に示す「直列FM」と「並列加算」の 2 つのタイプしかありません。
直列FMの場合に「モジュレータ」となるオペレータ 0 に関しては、(位相) 変調入力としては、自分自身からの「セルフ・フィードバック」の系列しかなく、他のオペレータから変調されることはありません。
また、オペレータ 1 に関しては、直列FMの場合にはオペレータ 0 から変調を受けますが、並列加算の場合にはどこからも変調を受けません。
したがって、直列FMでも並列加算でも、オペレータ 0 → オペレータ 1 の順で計算を進めれば支障をきたすことはありません。
オペレータ 0 とオペレータ 1 とで変調入力の扱いが異なっているので、それぞれの処理は別々に記述し、常にオペレータ 0/1 をペアにして扱うことにします。

計算過程をブロック・ダイアグラムとして表現したものを下に示します。

青い破線で囲まれた部分がオペレータ (スロット) 自体の計算で、青い破線の外側は「前処理」や「後処理」に相当します。
プログラム中に条件分岐を含むと効率が悪くなるので、たとえば、オペレータ 1 の変調入力 (mod_in) に関してはオペレータ 0 の出力に「mod_mul」を掛けることにより変調の有無を実現しています。
変調「なし」の場合には mod_mul = 0 により mod_in をゼロにし、変調「あり」の場合には mod_mul = 0x200 の設定で mod_in に変調がかかるようになっています。
実際のプログラムを下に示します。 各種の Cortex-M シリーズのマイコンで測定した結果もコメント中に示してあります。

//
// uVision V5.21.1.0 (MDK-Lite V5.21a, armcc v5.06 update 3, -O3)
// 56 cycle / 2slot (STM32F446 (CM4) @  30 MHz, Flash latency = 0)
// 67 cycle / 2slot (STM32F446 (CM4) @ 180 MHz, Flash latency = 5)
// 56 cycle / 2slot (STM32F407 (CM4) @  30 MHz, Flash latency = 0)
// 66 cycle / 2slot (STM32F407 (CM4) @ 168 MHz, Flash latency = 5)
// 54 cycle / 2slot (STM32F303 (CM4) @  24 MHz, Flash latency = 0)
// 61 cycle / 2slot (STM32F303 (CM4) @  64 MHz, Flash latency = 2)
// 56 cycle / 2slot (STM32F100 (CM3) @  24 MHz, Flash latency = 0)
// 67 cycle / 2slot (LPC1114   (CM0) @  20 MHz, Flash wait = 0)
// 86 cycle / 2slot (LPC1114   (CM0) @  48 MHz, Flash wait = 2)
//
// Atollic TrueSTUDIO for STM32 v9.0.0 (gcc 6.3.1, -Os)
// 53 cycle / 2slot (STM32F446 (CM4) @  30 MHz, Flash latency = 0)
// 63 cycle / 2slot (STM32F446 (CM4) @ 180 MHz, Flash latency = 5)
// 53 cycle / 2slot (STM32F407 (CM4) @  30 MHz, Flash latency = 0)
// 62 cycle / 2slot (STM32F407 (CM4) @ 168 MHz, Flash latency = 5)
// 51 cycle / 2slot (STM32F303 (CM4) @  24 MHz, Flash latency = 0)
// 67 cycle / 2slot (STM32F303 (CM4) @  64 MHz, Flash latency = 2)
// 57 cycle / 2slot (STM32F100 (CM3) @  24 MHz, Flash latency = 0)
//

void calc_slot(op_prop_t *o_p,  // pointer to op_prop[]
               int num_slot)    // number of slots
{
  uint32_t phh; // phase accumulator high
  int32_t  out; // operator output
  int32_t  prev_out; // OP previous output

  do {
//--------------------
// Series FM modulator		
//--------------------
// phase generator calculation
    phh         =  o_p->ph_inc; // get phase increment
    phh        +=  o_p->ph_acc; // add to phase accum.
    o_p->ph_acc = phh;          // update phase accum.
    phh        +=  o_p->mod_in; // phase modulation input
    phh       >>=  (PH_ACC_FRAC_BITS-1); // byte offset
// sine table lookup with the Wave Select
    out   =  *(int16_t *)((uint8_t *)o_p->stab_p + (o_p->ind_mask & phh));
// operator output scaling
    out  *= o_p->ol_lin;  // multiply linear output level
    out >>= 15;           // reduce to 16-bit range
// feedback with 2-tap FIR LPF
    prev_out     = o_p->op_out; // save last output sample
    o_p->op_out  = out;         // update operator output value
    prev_out    += out;    // 1st order FIR LPF for feedback ( H(z) = (1 + z^(-1)) )
    o_p->mod_in  = (prev_out * o_p->mod_mul); // for feedback
    o_p++; // advance operator property pointer
//------------------
// Series FM carrier
//------------------
// modulator to carrier connection
    out *= o_p->mod_mul;
// phase generator calculation
    phh         =  o_p->ph_inc; // get phase increment
    phh        +=  o_p->ph_acc; // add to phase accum.
    o_p->ph_acc = phh;          // update phase accum.
    phh        +=  out;         // phase modulation input
    phh       >>=  (PH_ACC_FRAC_BITS-1); // byte offset
// sine table lookup with the Wave Select
    out   =  *(int16_t *)((uint8_t *)o_p->stab_p + (o_p->ind_mask & phh));
// operator output scaling
    out  *= o_p->ol_lin;  // multiply linear output level
    out >>= 15;           // reduce to 16-bit range
    o_p->op_out  = out;   // update operator output value
    o_p++; // advance operator property pointer
    num_slot -= 2;
  } while (0 < num_slot); // past end of slots?
} // void calc_slot()

プログラム中で参照している定数や構造体の定義は下のようになります。

// phase accumlator bit width assign
#define PH_ACC_INT_BITS  (10) // integer part (sine ROM address part)
#define PH_ACC_FRAC_BITS (9)  // fraction part
#define PH_ACC_BITS      (PH_ACC_INT_BITS + PH_ACC_FRAC_BITS)

// operator property record
typedef struct tag_op_entry_t {
  uint32_t  ph_inc;   // phase accum. increment
  uint32_t  ph_acc;   // phase accumulator
  int32_t   mod_in;   // phase modulation input
  int16_t  *stab_p;   // (full) sine table pointer
  uint16_t  ind_mask; // table index mask
  int16_t   op_out;   // operator output value
  uint32_t  ol_lin;   // linear output level
  uint16_t  mod_mul;  // feedback / modulator connection
}  op_prop_t;