FM音源プログラム (21) -- オペレータ (14)
オペレータについては、ほぼ説明しつくしたと思いますので、最後にプログラムの最適化について検討してみます。
オペレータは、フェーズジェネレータ+EG+サイン波テーブルがセットになった、モジュール性の高い要素ですから、C++ などのオブジェクト指向言語で書くのに適しています。
しかし、FM音源プログラムを多様なマイコン上で実現したいということから、C++ ではなく、C 言語で書いてあります。
実際に、ADuC7026 版、V850 版で使っているコードを下に示します。
#define SIGNBIT 0x1000 #define OSIGN (SIGNBIT<<1) int16_t op_ws0(op_prop_t *o_p) { uint32_t phh; int s0; phh = ((o_p->mod_in + (o_p->phacc)) >> 16); o_p->phacc += o_p->ph_inc; // update phase s0 = stab[phh & (OSIGN-1)]; if (phh & OSIGN) s0 = -s0; o_p->opout = ( (s0 * (o_p->ol_lin >> 16)) >> 15); return( o_p->opout ); } // int16_t op_ws0()
WS=0 つまり、普通のサイン波出力のオペレータ関数です。
「op_prop_t」は、まだ説明していませんが、「operator property type」の意味で、フェーズアキュムレータや音色パラメータなどのオペレータ関連の変数を集めた構造体の型名です。
オペレータ関数は、引数に渡された op_prop_t 構造体へのポインタを使って、各変数にアクセスします。
EG は、別の 1ms ごとに起動されるプログラムで処理しています。
サンプリング周期で計算されるオペレータ関数は、処理時間を短くすることが重要です。
2オペレータ構成では、1サンプリング周期内に同時発音数に2をかけた回数だけオペレータ関数が呼ばれます。 たとえば、同時発音数 6 なら、12 回オペレータ関数が呼ばれますから、オペレータ関数で1サイクル短縮できれば、1サンプリング周期の合計では 12 サイクルの短縮になります。
V850 版のコンパイル結果のアセンブリ・コードを下に示します。
_op_ws0: # 41: uint32_t phh; # 42: int s0; # 43: phh = ((o_p->mod_in + (o_p->phacc)) >> 16); // high word of phase acc ld.w [r6], r12 ld.w 8[r6], r18 add r12, r18 shr 16, r18 # 44: o_p->phacc += o_p->ph_inc; // update phase ld.w 4[r6], r15 add r12, r15 st.w r15, [r6] # 45: s0 = stab[phh & (OSIGN-1)]; // look up sine table andi 0x1fff, r18, r16 shl 1, r16 mov #_stab, r11 add r16, r11 ld.h [r11], r11 # 46: if (phh & OSIGN) s0 = -s0; // if b29 == 1, change sign and 0x2000, r18 je .L222 not r11, r11 add 1, r11 .L222: # 47: o_p->opout = ( (s0 * (o_p->ol_lin >> 16)) >> 15); ld.w 28[r6], r10 sar 16, r10 mul r11, r10, r0 shr 15, r10 st.h r10, 58[r6] # 48: return( o_p->opout ); sxh r10 jmp [lp] --1 # 49: } // int16_t op_ws0()
このコンパイル結果は、ほぼ期待通りです。
ADuD7026 版のコンパイル結果を下に示します。(ARM7TDMI の THUMB モード)
0008A4E0 op_ws0?T: ; FUNCTION START 39: int16_t op_ws0(op_prop_t *o_p) 0008A4E0 B410 PUSH {R4} 0008A4E2 1C01 MOV R1,R0 ; o_p 0008A4E4 ---- Variable 'o_p' assigned to Register 'R1' ---- 40: { 0008A4E4 ; SCOPE-START 43: phh = ((o_p->mod_in + (o_p->phacc)) >> 16); // high word of phase acc 0008A4E4 1C08 MOV R0,R1 ; o_p 0008A4E6 6884 LDR R4,[R0,#0x8] 0008A4E8 1C08 MOV R0,R1 ; o_p 0008A4EA 6802 LDR R2,[R0,#0x0] ; o_p 0008A4EC 18A4 ADD R4,R2 0008A4EE 0C24 LSR R4,R4,#0x10 0008A4F0 ---- Variable 'phh' assigned to Register 'R4' ---- 44: o_p->phacc += o_p->ph_inc; // update phase 0008A4F0 1C08 MOV R0,R1 ; o_p 0008A4F2 6840 LDR R0,[R0,#0x4] 0008A4F4 1812 ADD R2,R0 0008A4F6 1C08 MOV R0,R1 ; o_p 0008A4F8 6002 STR R2,[R0,#0x0] ; o_p 45: s0 = stab[phh & (OSIGN-1)]; // look up sine table 0008A4FA 1C22 MOV R2,R4 ; phh 0008A4FC 480E LDR R0,[R15,#56] ; PoolRef @0x8A538 0008A4FE 4002 AND R2,R0 0008A500 0052 LSL R2,R2,#0x1 0008A502 480E LDR R0,[R15,#56] ; PoolRef @0x8A53C ; stab 0008A504 5A80 LDRH R0,[R0,R2] 0008A506 0400 LSL R0,R0,#0x10 0008A508 1400 ASR R0,R0,#0x10 0008A50A 1C03 MOV R3,R0 0008A50C ---- Variable 's0' assigned to Register 'R3' ---- 46: if (phh & OSIGN) s0 = -s0; // if b29 == 1, change sign 0008A50C 1C20 MOV R0,R4 ; phh 0008A50E 4A0C LDR R2,[R15,#48] ; PoolRef @0x8A540 0008A510 4210 TST R0,R2 ; phh 0008A512 D000 BEQ L_1 ; T=0x0008A516 0008A514 425B NEG R3,R3 ; s0 0008A516 L_1: 47: o_p->opout = ( (s0 * (o_p->ol_lin >> 16)) >> 15); 0008A516 1C08 MOV R0,R1 ; o_p 0008A518 69C0 LDR R0,[R0,#0x1C] 0008A51A 1400 ASR R0,R0,#0x10 0008A51C 1C1A MOV R2,R3 ; s0 0008A51E 4342 MUL R2,R0 0008A520 13D2 ASR R2,R2,#0xF 0008A522 0412 LSL R2,R2,#0x10 0008A524 0C12 LSR R2,R2,#0x10 0008A526 1C08 MOV R0,R1 ; o_p 0008A528 8742 STRH R2,[R0,#0x3A] 48: return( o_p->opout ); 0008A52A 1C08 MOV R0,R1 ; o_p 0008A52C 8F40 LDRH R0,[R0,#0x3A] 0008A52E 0400 LSL R0,R0,#0x10 0008A530 1400 ASR R0,R0,#0x10 0008A532 ; SCOPE-END 49: } // int16_t op_ws0() 0008A532 BC10 POP {R4} 0008A534 4770 BX R14 0008A536 ; END 'op_ws0?T'
一応、最適化レベルは最高の 8 に設定しているのですが、これは、ちょっと残念なコードです。
特に、C ソースの 47 行目に対するコードで、
0008A522 0412 LSL R2,R2,#0x10 0008A524 0C12 LSR R2,R2,#0x10
によってゼロ拡張しているのは、続く「STRH」命令で下位 16 ビット分しかストアしていないので、全く無駄です。
また、「o_p->opout」にストアした結果が R2 に残っているのに、
0008A52A 1C08 MOV R0,R1 ; o_p 0008A52C 8F40 LDRH R0,[R0,#0x3A]
によって、再びロードしているのは、これまた無駄です。
ATmega 版では、C で書いた場合には、効率的には受け入れられないものだったので、オペレータ関数はアセンブラで書きました。
ATmega の場合は、スピード、RAM 容量、ROM 容量、いずれも制約となりますから、当初は 9 ビット PWM 出力だったこともあり、サイン波テーブルは 1/4 周期タイプの 129 エントリ、8 ビットデータのものを使っています。
データ幅が小さいので、現在のリニア値の代わりに、lb 値、つまり対数値を使うタイプにすることを検討しています。