新版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;