RakuChordのPWM解説
RakuChordのコードベースは10年前くらいに書いたものなのですが、改めてなんでこれで音が鳴っているのか、よくわからなくなってきたので調べなおしました。
ソースコードはこちら。
soundSetup
タイマー周りのセットアップを行う。
RakuChordで使うのはArduinoのDigital 9番ピン。まずはOUTPUTに設定する。
次にTimerの設定を行う。
9番ピンはPWM信号出力に使うことができ、Timer1と関係するレジスタOC1Aで設定する。
TCCR1AとTCCR1Bで下記のような設定を行う
- WGM10 (WGM13~10 = 0001)
-- 8bit位相基準PWM
--- TCNT1が 0 -> 0xFF -> 0 -> 0xFF ... といったふうにジグザグに動く
--- カウントアップ時にTCNT1がOCR1Aの値を超えると、9番ピンがLOWに、カウントダウン時にTCNT1がORC1Aの値より下がると9番ピンがHIGHになる。
- COM1A1 (COM1A1~0 = 10)
-- カウントアップ一致比較でLOW,カウントダウン比較一致でHIGH
- CS10 (CS12~10 = 001)
-- 分周なし
--- つまり16MHzでTCNT1が動く
TIMSK1にTOIE1ビットを立ててタイマー1の割り込みを有効にする。
TIMER1_OVF_vect
TIMASK1を設定することでこの割込みが呼ばれる。
これはTCNT1が0xFFになったタイミングで呼ばれる割込みルーチン。
ここまでの設定で16MHzでカウンタが動き、0->0xFF -> 0 となるのが1周期と考えると・・
> tex
$\frac{1}{16MHz} \times (FF \times 2) 秒$ <<
の間隔でこの割込みが呼び出されることになる。
(ここでTCNT2にtimerLoadValueを代入しているのは何の意味もなさそう・・)
基本的にはこの割込みルーチンではdnxにdxを足していく。 xは0~4の5通りあり、これたオシレータを表している。独立した5つの音を制御しているということ。
最終的にはこのdnxから波形テーブルを引いて、5つあるそれぞれの結果をすべて足し合わせてOCR1Aに代入する。 dn, dと音程
dnx, dxはunsigned intです。これは16ビットです。 先ほど見たように割込みルーチンでdnxにdxをどんどん足していく。 このdxはRakuChordの鍵盤の押下により音程に対応した値が代入されている。 このソースコードを見ると、音程に対応した値というのは周波数そのものである。
(NOTE_A4が440, NOTE_A5が880となっていることからも、これが良く知っている周波数と同じであることがわかる)
一方、dnxの値は割込みルーチンのなかで、上位6ビットを使ってテーブルを引いている。 波形テーブルは64sampleで1波形を表している。dnxは16bitだが、その上位6ビットを取り出すことでこのsampleを読みだしている。 dxの値をを変数とし、dnxが何回の割込みで1周するかを計算する。 > tex
<<
割込みの発生間隔は以前求めたので、それを掛け合わせることで波形1周期にかかる時間が計算できる
> tex
$\frac{FFFF}{dn} \times \frac{1}{16MHz} \times (FF \times 2)秒$ <<
式を変形する
> tex
$\frac{FFFFFF \times 2}{dn \times 16MHz} = \frac{1}{dn} \times \frac{FFFFFF \times 2}{16MHz} = \frac{1}{dn} \times 2.097152秒$ <<
波形1周期にかかる周波数を求める(上の逆数)
> tex
<<
これによりdnは周波数をだいたい2で割ったものとなる。1オクターブ音が低くなるのと、少し誤差が発生することに目をつぶればこれでよさそう。