H8オルゴール同様、かなり短い周期で割り込みをかけ、発振器をエミュレートします。H8版は32.000KHzでしたが、CPUクロックとのかねあいで32.768KHzにしました。
H8版は16MHz÷32KHz=500で、割り込みオーバーヘッドなども考えると400クロック程度で発振器の処理をしなければいけませんでした。H8/300Hは1命令あたりのクロック数が2〜20クロックも必要なことを考えると、1回の割り込み処理の中で波形演算処理に使える命令数は極めて少なく、同時発音数は4音、波形は1種類固定でエンベロープはただ単純に減衰するだけ、というレベルの物しかできませんでした。
今回のSH7046はクロック49.152MHzなので、49.152MHz÷32.768KHz=1500で、発振器のエミュレーションに千数百クロックも余裕で使えます。また、ほとんどの命令が1クロックで実行できるということもあり、、実質的な処理能力は10倍以上になると思われます。
出力はアナログ値になるのですが、SH7046にはD/Aコンバータがないので、H8/3664版と同様にPWMで出力し、ローパスフィルタでアナログ電圧にします。PWM周期は750ペリフェラルクロック(CPUクロック1500個ぶん)で32.768KHz(つまりオシレータエミュレーション割り込みと同じ周期)です。波形の最大振幅は512とし、あえて100%変調はかけていません。これは、オペアンプの電源を5V単電源で使っているため、手軽に入手できる安いオペアンプだと0V付近と5V付近の入力信号を正しく扱えないためです。
OSCINTという割り込みルーチンで発振器をエミューレートします。いわゆる「分周型」ではなく「オーバーフロー型」です(H8版からずっとそうですが)。割り込みがかかるたびに「周波数アキュムレータ」に「周波数」を加算します。
割り込みは32768Hzの周期でかかり、周波数アキュムレータは16ビット(すなわち値は0〜65535)なので、たとえば「周波数」に1を設定していれば、周波数アキュムレータは2秒周期、すなわち0.5Hzでオーバーフローします。「周波数」に2を設定すれば1Hzでオーバーフローします。440を設定すれば、220Hzでオーバーフローします。このように、「周波数」に実際鳴らしたい音の周波数の2倍の値を設定すれば、希望の周波数が得られます。
周波数アキュムレータを15ビットにすれば、「周波数」の設定値が実際の周波数と一致してわかりやすかったのですが、低音域での周波数精度が悪くなるので1ビット増やし、あえて周波数の2倍の値を設定するようにしました。
任意の波形を出力するには、周波数アキュムレータの適当な上位何ビットかをそのまま波形メモリのアドレスとします。今回は7ビットをアドレスとしました。つまり、一周期ぶんの波形は128バイトで表すことになります。
そうやって得た波形データを、音量まで計算して8パートぶんの波形を重ね合わせるのにMAC(積和演算器)を使っています。H8は個人では入手が難しい上級チップにしかMACが乗っていないのに、SHはすべての機種にMACが乗っているからありがたいです。
H8版でやっていたディレイはとりあえずつけていません。H8版のディレイは、確かに音に余韻がつくのですが、せっかくステレオなのにもかかわらず音の定位がぼやけてしまっていました。SH版もいずれステレオ化してディレイ等のエフェクトをかけようと思っていますが、そのためには私自身がもっと音場処理などの基礎を勉強する必要がありそうです。
ENVINTという割り込みルーチンでエンベロープの処理をしています。H8版ではただ固定のレートで減衰するだけでしたが、SH版はシンセサイザーなどでよく使われるADSRでエンベロープパターンの形状を設定できます。
エンベロープパターンはアタック(A)、ディケイ(D)、サスティン(S)、リリース(R)の4つの期間から構成されます。
アタック(A)は、音の鳴り始めから最大音量に達するまでの期間です。今回のオルゴールでは音量は直線的に変化するようにしました。
ディケイ(D)は、音量が最大になってからサスティンレベルに落ちるまでの期間です。音量は直線的に変化します。
サスティン(S)は、サスティンレベルに達してから、発音停止するまでの期間です。音量は指数関数的に減衰します(減衰しないことも可能)。
リリース(R)は、発音停止後の余韻です。音量は指数関数的に減衰します。
なお、アタック、ディケイ、サスティンのいずれの期間でも、指定の音長が終了するなどして発音停止した場合、即リリースに移行します。
TONE文で指定するエンベロープパラメータは8ビットですが、エンベロープ割り込み内では精度を保つために16ビットで処理しています。(厳密に言うと、符号付き/符号無しにかかわるちょっと面倒くさい処理を安易に回避するため実質有効なのは15ビットにしていますが)
MUSINTという割り込みルーチンでMMLの解釈・実行を行っています。この手のプログラムは大抵MMLそのものではなく、何らかの中間コードに変換してから演奏するのが一般的ですが、このSHオルゴールではMMLのまま演奏しています。MMLも中間コードも同じ音楽情報を表しているはずなのに、わざわざ変換するのは2度手間と思えますし、貴重な内蔵RAMに、わざわざ中間コードバッファなどを確保するのはもったいないと思ったので。
多くの場合中間コードに変換するのは、人間にわかりやすいMMLより、機械に理解しやすい中間コードの方がテンポ、音長に合わせて正確なタイミングで発音するような、時間にシビアな処理を実現しやすいからだと思います。 |
MMLの解釈実行は割り込みが行いますので、PLAY文は演奏ポインタキューが空いているかどうか調べ、空いていればPLAY文中の各文字列のポインタをキューにおいているだけです。
PLAY文の実行時間の長さは、そのPLAY文のパートの中で一番長いパートに合わされます。したがって、各PLAY文の頭で必ず音符は揃います。たとえば、
10 PLAY "C1","E2" 20 PLAY "D2","F1" |