スーパーファミコンの音源モジュール「SHVC-SOUND」でMIDI演奏
スーパーファミコンにはBGMやSEなどを演奏するための音源モジュール(APU)が入っています。特に前期型のスーファミはAPUが独立して取り外すことができます。
中期型や後期型の場合、APUはメインボードに直付けされているため、演奏目的には向きません。(S-SMPやS-DSPなどの部品を取って、使用する事はできます。QFPのはんだ付けに自信がある方向けです。)
1chipのスーファミの場合、APU内の部品をほぼ1つにまとめたS-APUが使われています。
ただし、1chipのスーファミは中古でも高いので演奏目的で部品を取って使うのはお勧めしません。
メリットは、多数のAPUを基板に乗せるとき、省スペースで済むところでしょうか。
今回は、MIDIを受信してAPUで発音するプログラムを書いてみます。
APUは前期型を用います。
前期型APUにもバージョンの違いがありますが、問題なく使用できると思います。
初期型スーファミのAPU
左が「S-DSP A」バージョン、右が「S-DSP」バージョン
中期型以降のAPUは、メインボードに直付けされています。
APU
スーファミの音源は、あらかじめ記録された音声波形を再生するPCM音源の一種です。
同時発音数は8chで、エンベロープやエコー、ノイズ生成、ピッチモジュレーションなど様々なエフェクトをハードウェアで実現できます。
このスーファミのAPUの最大の特徴は、APU内に独立したCPUを持つことです。
つまり、スーパーファミコンの中には、メインのCPU(5A22)とAPU内のCPU(SPC700)の2つが入っているということです。
ヤマハのYM2413やYM2608、AY-3-8910などのFM音源ICやPSG音源ICなどは、レジスタをたたけば音を出すことができます。しかし、スーファミのAPUは内部にCPU(SPC700)を持つため、まずSPC700用のプログラムを書かなければならず、そう簡単には音は出せません。
マイコンでよく音源ICを使う方々には、手を出しにくい音源でもあります。
手を出しにくい要因はもう一つあります。
それは、APUが再生できる専用のフォーマットの音声波形データを用意することです。
FM音源ICやPSG音源ICの場合、発音波形はパラメータで決まります。
しかし、PCM音源の場合はメモリに蓄えた波形データを再生して発音するため、波形データを用意してあげないと望み通りの音は出ません。
スーファミのAPUの場合、この波形データを独自の圧縮方式で圧縮した「BRRサンプルデータ」でなければいけません。(BRRについては解説しませんので各自調べてみてください。)WAVからBRRデータへ変換するソフトもありますが、ループ点の設定がとても面倒です。
この2つの壁を乗り越えてやっとAPUで音を出す準備が整います。
・APUの回路図
回路は、以下を参考にしました。
SFC Development Wiki - Schematics, Ports, and Pinouts
・APUのブロック図
APUには、CPU(SPC700)やSRAM(64kB)(実際はPSRAMですが便宜上SRAMとしています。)、DSP(発音専用DSP)、DAC、OPAMP等入っています。いわば音を出すことに特化したシングルボードコンピュータといえます。
SRAMには、音声波形データのほかにSPC700プログラムデータなどを入れます。もちろん発音に必要なレジスタ等も同じアドレス空間にあります。
スーファミのメインのCPUである5A22と通信するためのポートレジスタや、CPUの動作を決定するレジスタ、DSPへ指示をだすレジスタなど、様々な種類のレジスタがあります。
APUを扱う上でまず最初の壁となるのは、CPU(SPC700)用のプログラムの作成とプログラムの書き込み(APUのSRAMへプログラムを転送)です。
・SPC700アドレス空間
アドレス | 概要 | 説明 |
---|---|---|
$0000~$00EF | ページ0領域 | ページ0領域です。 ページ0をページ領域に設定すると、SPC700はページ0領域のデータを低サイクルで読み書きすることや、このデータ領域に対して様々な演算命令を実行出来ます。 ※ページ0は、実際$0000~$00FFまでの範囲です。表の都合上分けています。 通常この領域($0000~$00EF)は、SPC700でプログラムを書く時の変数領域として使用します。 |
$00F0 |
コンフィグレジスタ | CPUのクロックなどを変更できます。詳細不明です。 |
$00F1 | コントロールレジスタ | IPLの有効無効、タイマーの有効無効、ポートのリセットを行うレジスタです。 |
$00F2 | DSPアドレスレジスタ | DSP内部レジスタへアクセスするときにセットします。 |
$00F3 | DSPデータレジスタ | DSP内部レジスタへアクセスするときにセットします。 書き込みの場合は、「DSPアドレスレジスタ」→「DSPデータレジスタ」の順で書き込みます。 読み込みの場合は、読み込むアドレスを「DSPアドレスレジスタ」へ書き込んでから「DSPデータレジスタ」を読み込みます。 |
$00F4 | ポート0 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F5 | ポート1 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F6 | ポート2 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F7 | ポート3 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F8 | 内部ポート4 | APU内部ポート4レジスタです。 APU回路図中のIC1 S-SMPの27~34番ピンに割り当てられています。 RAMとして振る舞いますが、使わないほうが良さそうです。 |
$00F9 | 内部ポート5 | APU内部ポート5レジスタです。 APU回路図中のIC1 S-SMPの18~25番ピンに割り当てられています。 RAMとして振る舞いますが、使わないほうが良さそうです。 |
$00FA | タイマー0 | 8bitタイマー0の最大値を指定します。 8bitタイマー0がこのレジスタの値になると「カウンタ0」レジスタをインクリメントして「8bitタイマー0」をリセットします。 8bitタイマー0は、125μsおきに自動的にインクリメントされます。 |
$00FB | タイマー1 | 8bitタイマー1の最大値を指定します。 8bitタイマー1がこのレジスタの値になると「カウンタ1」レジスタをインクリメントして「8bitタイマー1」をリセットします。 8bitタイマー1は、125μsおきに自動的にインクリメントされます。 |
$00FC | タイマー2 | 8bitタイマー2の最大値を指定します。 8bitタイマー2がこのレジスタの値になると「カウンタ2」レジスタをインクリメントして「8bitタイマー2」をリセットします。 8bitタイマー2は、15.625μsおきに自動的にインクリメントされます。 |
$00FD | カウンタ0 | 4bitカウンタです。「タイマー0」の値と「8bitタイマー0」が一致すると、このレジスタがインクリメントされます。 ただし、カウントビット幅が4bitなので0x0Fの次は0x00となりますので注意が必要です。 |
$00FE | カウンタ1 | 4bitカウンタです。「タイマー1」の値と「8bitタイマー1」が一致すると、このレジスタがインクリメントされます。 ただし、カウントビット幅が4bitなので0x0Fの次は0x00となりますので注意が必要です。 |
$00FF | カウンタ2 | 4bitカウンタです。「タイマー2」の値と「8bitタイマー2」が一致すると、このレジスタがインクリメントされます。 ただし、カウントビット幅が4bitなので0x0Fの次は0x00となりますので注意が必要です。 |
$0100~$01FF | ページ1領域 | ページ1領域です。 ページ1をページ領域に設定すると、SPC700はページ1領域のデータを低サイクルで読み書きすることや、このデータ領域に対して様々な演算命令を実行出来ます。 このページ1はSPC700のスタック専用領域となっています。 プッシュを介さない書き込みは、推奨しません。 |
$0200~$FFBF | 多目的領域 | SPC700のプログラムやBRRサンプルデータ、BRRサンプルベクタ、エコーバッファなど多目的に使用できます。 ただし、SPC700の命令の一部は、実行できるアドレス空間が細かく限られています。詳細は各自で調べてみてください。 |
$FFC0~$FFFF | 多目的領域、IPL領域 | 通常使用する場合は、「$0200~$FFBF」と同様の多目的領域です。 ただし、コントロールレジスタでIPLを有効にすると、この領域の読み込みがIPL(データ転送用プログラム)のデータとなります。 |
APUへプログラムを書き込む
スーパーファミコンのAPUは、CPU(SPC700)やSRAM(64kB)、DSP(発音専用DSP)等を1つのモジュールに押し込めたものです。
CPUがあるということは、もちろんプログラムも必要になるんですが、残念ながらIPL(ブート用ROM)を除いてプログラムは用意されていません。
良く言えば演奏の自由度がとても高いが、反面、演奏用のプログラムをしっかり組まないと狙った通りに鳴ってくれないということです。
ブートROMには、APUのSRAMへプログラムやPCMデータ(BRR)、演奏データ等をAPU外部のCPU・マイコンから転送できるプログラムが格納されています。
APUへ演奏プログラムを転送して、初めて音を鳴らす準備ができます。
また、SRAMは電源を切るとデータが消滅するため、APUを起動させるごとにデータの転送が必要になります。
では、APU(SPC700&SRAM)へデータを転送する手順を見ていきましょう。
①APUの起動
SPC700が起動またはリセットがかかると、内部にあるブートROM(IPL)内のプログラムが開始します。
ブートプログラムはSPC700のレジスタ等を初期化したのち、初期化終了を通知するため、
SPC700はポート0($F4)に0xAA、ポート1($F5)に0xBBをセットします。
@@@@@@@@@@@@@@@@@@@
ポート SPC700
PORT0 ← 0xAA
PORT1 ← 0xBB
@@@@@@@@@@@@@@@@@@@
②APUへ演奏プログラム&PCMデータの転送開始
SPC700を制御するマイコンは、SPC700初期化終了通知(PORT0=0xAA, PORT1=0xBB)を読み出せたら、APUのSRAMへ演奏データの転送を開始できます。
マイコンはまず、転送したいデータの転送先先頭アドレスとモードをSPC700へ書き込みます。
転送したいデータの転送先先頭アドレスは2バイト($0000~$FFFF)で指定します。
アドレス下位8bit(A7~A0)はポート2へ、上位8bit(A15~A8)はポート3へ書き込みます。
モードは0以外(0x01~0xFF)をポート1へ書き込みます。(0を書き込むとポート2,3に書き込んだアドレスへジャンプし、ユーザープログラムが開始します。)
次に、SPC700への転送先先頭アドレスセット完了を通知するために、マイコン側はポート0へ0xCCを書き込みます。ポート0に0xCCが書き込まれていることをSPC700が確認すると、SPC700は演奏プログラム&PCMデータ転送シーケンスへ移行します。
@@@@@@@@@@@@@@@@@@@
MCU ← (PORT0=0xAA, PORT1=0xBB) チェックOK
↓
MCU ポート
0以外(0x01~0xFF) → PORT1
転送したいデータの転送先先頭アドレス(下位8bit(A7~A0)) → PORT2
転送したいデータの転送先先頭アドレス(上位8bit(A15~A8)) → PORT3
↓
MCU ポート
0xCC → PORT0
@@@@@@@@@@@@@@@@@@@
③APUへ演奏プログラム&PCMデータのブロック転送
ポート0に0xCCが書き込まれていることをSPC700が確認すると、SPC700は演奏プログラム&PCMデータ転送シーケンスへ移行します。
その際、SPC700はポート0へ書き込まれたデータをポート0へエコーバックします。(この場合、SPC700はポート0に0xCCを書き込みます。)
マイコン側は、SPC700ブロック転送開始通知(PORT0=0xCC)を読み出せたら、SPC700は入力待ち状態となり、転送するデータの1つ目をSPC700へ転送できます。
@@@@@@@@@@@@@@@@@@@
ポート SPC700
PORT0 ← 0xCC
↓
MCU ← (PORT0=0xCC) チェックOK
@@@@@@@@@@@@@@@@@@@
--1--
まず、SPC700のポート1に書き込みたいデータの0番目をセットします。
次に、SPC700のポート0に書き込みたいデータが、ブロック転送中何番目のデータなのかをセットします。(この場合、最初のデータなので、SPC700のポート1に0x00を書き込みます。)
@@@@@@@@@@@@@@@@@@@
MCU ポート
DATA[n=0](n=0番目の転送したいデータ) → PORT1
n=0(n=0番目:nは0x00~0xFF) → PORT0
@@@@@@@@@@@@@@@@@@@
--2--
SPC700はポート0へ書き込まれたデータをポート0へエコーバックします。
マイコン側は、SPC700がポート0へエコーバックしたデータが、先ほどポート0へセットした値と一致しているかチェックします。
もし、一致しているならば、SPC700は入力待ち状態となり、マイコン側は次のデータを書き込むことができます。
まず、SPC700のポート1に書き込みたいデータの0番目をセットします。
次に、SPC700のポート0に書き込みたいデータが、ブロック転送中何番目のデータなのかをセットします。(下位8bitのみ書き込みます。例:0x0345のとき→0x45を書き込む)
(ポート0には以前ポート0に書き込んだ値+1を書き込んでください。もし、以前ポート0に書き込んだ値以下ならば、SPC700は入力待ち状態状態を保持します。また、以前ポート0に書き込んだ値+1より大きい値ならば、SPC700はこのブロックの転送を終了します。)
@@@@@@@@@データブロック転送(継続)@@@@@@@@@@
MCU ← (PORT0=(以前ポート0に書き込んだ値)) チェックOK
↓
MCU ポート
DATA[n](n(下位8bit)番目の転送したいデータ) → PORT1
↓
n(n番目:nは0x00~0xFF) → PORT0
@@@@@@@@@@@@@@@@@@@
@@@@@@@データブロック転送(途中でやめる)@@@@@@@@
MCU ← (PORT0=(以前ポート0に書き込んだ値)) チェックOK
↓
MCU ポート
モード(1~255:ブロック転送モード,0:ユーザープログラム開始モード) → PORT1
転送したいデータの転送先先頭または、ユーザープログラム開始アドレス(下位8bit(A7~A0)) → PORT2
転送したいデータの転送先先頭または、ユーザープログラム開始アドレス(上位8bit(A15~A8)) → PORT3
↓
n+1(n+1番目) → PORT0
@@@@@@@@@@@@@@@@@@@
--3--
「--2--」で、以前ポート0に書き込んだ値+1を書き込んだ場合、引き続き次のデータをSPC700へ転送できます。「--2--」へ行きます。
以前ポート0に書き込んだ値+1より大きい値ならば、SPC700はこのブロックの転送を終了します。通常ブロックの転送を終了する場合、以前ポート0に書き込んだ値+2を書きます。
ブロック転送終了前には、ポート2,3にブロック開始アドレスまたはユーザープログラム開始アドレスを、ポート1にモード(1~255:ブロック転送モード,0:ユーザープログラム開始モード)を必ず書き込んでください。
@@@@@@@@@@@@@@@@@@@
例:ポート0にn=[0,1,2,3,4,5,7]の順で書き込んだ場合
SPC700にはDATA[0]~DATA[5]の6バイトが書き込まれます。n=6の書き込みを飛ばしているので、n=5でこのブロック転送を終了します。
このとき、ポート0にn=5を書く前に、ポート2,3にブロック開始アドレスまたはユーザープログラム開始アドレスを、ポート1にモード(1~255:ブロック転送モード,0:ユーザープログラム開始モード)を必ず書き込んでください。
@@@@@@@@@@@@@@@@@@@
③APUへ演奏プログラム&PCMデータのブロック転送終了
ブロックの転送を終了すると、ブロック転送モードまたはユーザープログラム開始モード
へ移行します。
ブロックの転送を終了前に、ポート1に書き込んだモード(1~255:ブロック転送モード,0:ユーザープログラム開始モード)がブロック転送モードの場合、「③SPC700へ演奏プログラム&PCMデータのブロック転送」の「--1--」へ行きます。
このとき、ブロックの転送終了前に転送したいデータの転送先先頭アドレスをポート2,3へ書きこんでおきます。
また、ユーザープログラム開始モードの場合、ブロックの転送終了前にポート2,3へ書き込んだアドレスへジャンプします。
APUへ書き込むプログラムの作成
・Lチカプログラムの作成
では次に、プログラム作成します。
SPC700はスーパーファミコンのメインCPUである5A22(65816)に近い命令群を持っています。
ネット上にはいろいろ情報が出回っており、適時参考にするとよさそうです。
基本的にアセンブラでプログラムを記述します。
この章では、アセンブラに少しだけ慣れてもらうため、Lチカのプログラムを組んでみます。
MIDIのプログラムではないので、興味ない方は読み飛ばしてもよいです。
Lチカは、単にポートのビットを一定時間ごとにHとLを切り替える動作をします。
SPC700はf=1.024MHzで動作するので、待ち時間なしでビット反転するプログラムの場合、人の目ではLEDの点滅がわかりません。そこで、あえてSPC700で無駄な処理を行い、時間稼ぎをして点滅がわかるようにします。
具体的な流れとして、
///////////////////////////////////////////
ポート0のbit0をHにする
↓
無駄な処理
↓
ポート0のbit0をLにする
↓
無駄な処理
↓
ポート0のbit0をHにする
↓
無駄な処理
↓
ポート0のbit0をLにする
↓
無駄な処理
↓
…
///////////////////////////////////////////
を繰り返します。
問題は、「無駄な処理」をどうするかです。
今出回っているほとんどのCPUには「何もしない」という命令が備わっており、実行するとCPUクロック何サイクル分(CPUによって異なります)何もしない時間を稼げます。
ただし、この命令はとても短い時間しか稼げません。なので多く時間稼ぎをしたい場合、この命令を連続して実行する必要があります。
SPC700の場合、「NOP」(オペコード:0x00)がこれに相当します。
サイクル数N(実行に必要なCPUクロック数)は2なので、稼げる時間Twは、
Tw = N * 1 / f
= 2 * 1 / (1.024M)
= 1.953μs
f:SPC700の動作周波数f=1.024MHz
となります。
たとえばT=0.5秒稼ぐとしたら、
Ne = T / Tw
= 0.5 / 1.953μ
= 524.7k
となるので、約52万5000回「NOP」を連続で実行しなければいけません。また、プログラムのサイズも500kB以上必要となり、そもそもSRAM64kB内に収まりません。現実的ではないですね。
そこで、ループを使って待ち時間を稼ぐ手法が、簡単なプログラムでよく使われています。c言語でいうとforとかです。
アセンブラの場合、ループ処理は複数の命令を組み合わせて使用します。
ループ処理で重要になるのが、条件分岐処理です。
具体例を挙げてみます。
/////////////////////////////////////////////////
MOV A, #$07 ;アキュムレータへ0x07を書き込む
loop:
DEC A ;アキュムレータデクリメント
BNE loop ;ゼロフラグがクリアされているときloopへ
/////////////////////////////////////////////////
命令は3つ使用しています。
最初の「MOV A, #$07」はアキュムレータ初期化です。
アセンブラの場合、「$定数」でアドレス[定数(16進数)]内にある値を参照し、「#$定数」で16進定数を表します。
アキュムレータに定数0x07を書き込みます。
このプログラムの場合、条件分岐処理は「BNE loop」です。
「BRA」は-128~+127バイトの範囲で相対アドレスジャンプできます。
分岐の条件は、ゼロフラグがクリアされていることです。ゼロフラグはその名の通り、算術結果等でその値が0になるとHとなるフラグです。
今回は、「BNE loop」の前の命令「DEC A」を実行したとき、アキュムレータの値が0となる場合、ゼロフラグがHとなります。
つまり、アキュムレータを減算していきアキュムレータが0になるまでループする処理となります。
アキュムレータの値の変化は次のようになります。
@@@@@@@@@@@@@@@@@@@
アキュムレータの値の変化
0x07 //初期化
0x06 //3行目 DEC A
0x05 //3行目 DEC A
0x04 //3行目 DEC A
0x03 //3行目 DEC A
0x02 //3行目 DEC A
0x01 //3行目 DEC A
0x00 //3行目 DEC A
処理終了
@@@@@@@@@@@@@@@@@@@
アキュムレータの初期値が0x07の場合、「loop」内を7回実行します。
では、実際にどれくらいの時間がかかるのでしょうか?
まずは、「 MOV A, #$07」です。
この命令はNmova=2サイクルで実行します。
「DEC A」
この命令もNdeca=2サイクルで実行します。
「BNE loop」
分岐しない場合Nbne_nb=2サイクル、分岐する場合Nbne_b=4サイクルで実行します。
実行に必要な時間Twは、
Tw = (Nmova + (Ndeca + Nbne_b)(nloop-1) + (Ndeca + Nbne_nb)) * 1 / f
= (2 + (2 + 4)(7-1) + (2 + 2)) * 1 / (1.024M)
= 41.02μs
nloop:アキュムレータ初期値(ループ回数)
ループ回数と実行に必要な時間の関係をもう少しわかりやすくすると、
Tw = (6 * nloop) / f
= (6 * nloop) / (1.024M)
となります。
アキュムレータ(ループ回数)にセットできる最大値はnloop=0xFF=255なので、このループ処理で稼げる最大時間Twmaxは、
Twmax = (6 * nloop) / f
= (6 * 255) / (1.024M)
= 1.494ms
となります。
たった3つの命令で約1.5ms稼ぐことができました。ですが、1.5msのLEDの点滅ではまだまだ速すぎます。
そこで、ループを重ねてより多く時間を稼ぎます。
/////////////////////////////////////////////////
MOV $00, #$07 ;$0000へ0x07を書き込む
loop1:
MOV A, #$07 ;アキュムレータへ0x07を書き込む
loop2:
DEC A ;アキュムレータデクリメント
BNE loop2 ;ゼロフラグがクリアされているときloop2へ
DEC $00 ;$0000デクリメント
BNE loop1 ;ゼロフラグがクリアされているときloop1へ
/////////////////////////////////////////////////
ループが2つ使われていますね。
基本的にループが入れ子になっている以外、大差はありません。
唯一違うところはカウンタ変数としてSRAM上の領域を使用しているところです。
SPC700のアドレス空間は0x0000~0xFFFFまであります。
そのうち、0x0000~0x00FFをページ0、0x0100~0x01FFをページ1という特別な領域があります。この特別な領域をダイレクトページとすることができます。
ダイレクトページでは算術命令による直接アクセスが可能となります。
さらに、ダイレクトページ同士(ページ0→ページ0 or ページ1→ページ1)の値の出し入れも直接行えます。
ダイレクトページは常にページ0またはページ1のどちらかしか指定できません。
ダイレクトページの指定は「CLRP」命令または「SETP」命令で指定します。
「CLRP」を実行するとページ0(0x0000~0x00FF)をダイレクトページ、「SETP」を実行するとページ1(0x0100~0x01FF)をダイレクトページに設定します。
SPC700の場合、ページ1をスタック領域として設定されているため、ページ0をダイレクトページとしておくことが多いようです。
今回のプログラム例ではページ0(0x0100~0x01FF)をダイレクトページとしています。
この2つのループを処理する時間を計算します。
loop2部分の所要時間Tw_loop2は、
Tw_loop2 = (6 * nloop2) / f
= (6 * nloop2) / (1.024M)
nloop2:アキュムレータ初期値(ループ2の回数)
となります。この部分はループ1個の時と同様です。
loop1の部分の所要時間Tw_loop1はどうなるでしょう?
「 MOV $00, #$05」ではNmovdp=5サイクルで実行します。アキュムレータへ書き込むときより3サイクル増加しています。
「DEC $00」
この命令はNdecdp=4サイクルで実行します。アキュムレータのデクリメントの時より、2サイクル増加しています。
loop1の部分のみ実行に必要な時間Tw_loop1は、
Tw_loop1 = (Nmovdp + (Ndecdp + Nbne_b)(nloop1-1) + (Ndecdp + Nbne_nb)) * 1 / f
= (5 + (4 + 4)(7-1) + (4 + 2)) * 1 / (1.024M)
= 57.62μs
nloop1:アドレス0x0000にある値の初期値(ループ1の回数)
アキュムレータの時より大分時間がかかります。
ループ回数と実行に必要な時間の関係をもう少しわかりやすくすると、
Tw_loop1 = (8 * nloop1 + 3) / f
= (8 * nloop1 + 3) / (1.024M)
となり、二つのループの合計所要時間Twは、
Tw = Tw_loop1 + Tw_loop2 * nloop1
= ((8 * nloop1 + 3) / (1.024M)) + ((6 * nloop2) / (1.024M)) * nloop1
= ((8 * nloop1 + 3) + (6 * nloop2)* nloop1) / (1.024M)
= ((8 * 7 + 3) + (6 * 7)* nloop1) / (1.024M)
= 344.7μs
となります。
アキュムレータ(ループ回数)と$0000にセットできる最大値はnloop1=nloop2=0xFF=255なので、このループ処理で稼げる最大時間Twmaxは、
Twmax = Tw_loop1 + Tw_loop2 * nloop1
= ((8 * nloop1 + 3) + (6 * nloop2)* nloop1) / (1.024M)
= ((8 * 255 + 3) + (6 * 255)* nloop1) / (1.024M)
= 383.0ms
約380msのLEDの点滅なのでLED点滅が十分わかります。
0.5秒で点滅させたい時はさらにループを入れ子にします。
/////////////////////////////////////////////////
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3:
DEC A ;アキュムレータデクリメント
BNE loop3 ;ゼロフラグがクリアされているときloop3へ
DEC $01 ;$0001デクリメント
BNE loop2 ;ゼロフラグがクリアされているときloop2へ
DEC $00 ;$0000デクリメント
BNE loop1 ;ゼロフラグがクリアされているときloop1へ
/////////////////////////////////////////////////
ループ処理で稼げる時間Twは、
Tw = Tw_loop1 + (Tw_loop2 + Tw_loop3 * nloop2) * nloop1
= ((8 * nloop1 + 3) +((8 * nloop2 + 3) + (6 * nloop3)* nloop2) * nloop1) / (1.024M)
= ((8 * 10 + 3) +((8 * 67 + 3) + (6 * 126)* 67) * 10) / (1.024M)
= 500.0ms
さて、あとはLEDが接続されたポート0への書き込みと無限ループを組み合わせるだけです。
/////////////////////////////////////////////////
main_loop:
MOV $F4, #$01 ;$00F4(ポート0)へ0x01を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1A:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2A:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3A:
DEC A ;アキュムレータデクリメント
BNE loop3A ;ゼロフラグがクリアされているときloop3Aへ
DEC $01 ;$0001デクリメント
BNE loop2A ;ゼロフラグがクリアされているときloop2Aへ
DEC $00 ;$0000デクリメント
BNE loop1A ;ゼロフラグがクリアされているときloop1Aへ
MOV $F4, #$00 ;$00F4(ポート0)へ0x00を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1B:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2B:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3B:
DEC A ;アキュムレータデクリメント
BNE loop3B ;ゼロフラグがクリアされているときloop3Bへ
DEC $01 ;$0001デクリメント
BNE loop2B ;ゼロフラグがクリアされているときloop2Bへ
DEC $00 ;$0000デクリメント
BNE loop1B ;ゼロフラグがクリアされているときloop1Bへ
BRA main_loop
/////////////////////////////////////////////////
「BRA」は無条件分岐命令です。-128~+127バイトの範囲で相対アドレスジャンプできます。
めでたくアセンブラコードが完成しました。
次に機械後へ翻訳します。
アセンブラのニーモニックは機械語のオペコードに対応しています。
では早速変換していきましょう。
変換にはSPC700オペコード表を用います。ネット上に多く情報があるので、各自で調べてみると良いでしょう。
/////////////////////////////////////////////////
main_loop:
"$8F" $F4, #$01 ;$00F4(ポート0)へ0x01を書き込む
;0.5秒ウェイト
"$8F" $00, #$0A ;$0000へ0x0Aを書き込む
loop1A:
"$8F" $01, #$43 ;$0001へ0x43を書き込む
loop2A:
"$E8", #$7E ;アキュムレータへ0x7Eを書き込む
loop3A:
"$9C" ;アキュムレータデクリメント
"$D0" loop3A ;ゼロフラグがクリアされているときloop3Aへ
"$8B" $01 ;$0001デクリメント
"$D0" loop2A ;ゼロフラグがクリアされているときloop2Aへ
"$8B" $00 ;$0000デクリメント
"$D0" loop1A ;ゼロフラグがクリアされているときloop1Aへ
"$8F" $F4, #$00 ;$00F4(ポート0)へ0x00を書き込む
;0.5秒ウェイト
"$8F" $00, #$0A ;$0000へ0x0Aを書き込む
loop1B:
"$8F" $01, #$43 ;$0001へ0x43を書き込む
loop2B:
"$E8" A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3B:
"$9C" ;アキュムレータデクリメント
"$D0" loop3B ;ゼロフラグがクリアされているときloop3Bへ
"$8B" $01 ;$0001デクリメント
"$D0" loop2B ;ゼロフラグがクリアされているときloop2Bへ
"$8B" $00 ;$0000デクリメント
"$D0" loop1B ;ゼロフラグがクリアされているときloop1Bへ
"$2F" main_loop
/////////////////////////////////////////////////
ニーモニックをオペコードへ置き換えました。もはやコメントがないとどんな処理をしているかわからないレベルです。
今度は、このプログラムを羅列します。このとき、オペランドが2バイトある命令は、オペランドの順を入れ替える必要があります。今回は、オペコード"8F"の後2バイトを入れ替えます。
/////////////////////////////////////////////////
main_loop:
8F, 01, F4, 8F, 0A, 00,
loop1A:
8F, 43, 01,
loop2A:
E8, 7E,
loop3A:
9C, D0, loop3A, 8B, 01, D0, loop2A, 8B, 00, D0, loop1A, 8F, 00, F4, 8F, 0A, 00,
loop1B:
8F, 43, 01,
loop2B:
E8, 7E,
loop3B:
9C, D0, loop3B, 8B, 01, D0, loop2B, 8B, 00, D0, loop1B, 2F, main_loop
/////////////////////////////////////////////////
だいぶすっきりしてきました。最後に残っているのは「BNE」、「BRA」命令の相対ジャンプ量です。この数値は、((ジャンプ先の実行したい命令アドレス) - (現在のオペランドアドレス))-1を指定します。
(※「-1」というのは、CPUが命令実行後にプログラムカウンタを自動的にインクリメントするため、あらかじめ1引いています。)
負の相対アドレスを指定する時は2の補数表現を用います。
@@@@@@@@@@@@@@@@@@@@
例:
X0, X1, X2, X3, X4, X5, X6, "BRA", NN, X9, XA, XB, XC, XD, XE
のプログラムがある時
NNに0x01が入った時、"BRA"実行後、"XA"が実行されます。
NNに0x03が入った時、"BRA"実行後、"XC"が実行されます。
NNに0xFA(-6)が入った時、"BRA"実行後、"X3"が実行されます。
NNに0xF8(-8)が入った時、"BRA"実行後、"X1"が実行されます。
@@@@@@@@@@@@@@@@@@@@
相対ジャンプ量-1を計算します。
loop1A = -15 -1 = -16 = 0xF0
loop2A = -8 -1 = -9 = 0xF7
loop3A = -2 -1 = -3 = 0xFD
loop1B = -15 -1 = -16 = 0xF0
loop2B = -8 -1 = -9 = 0xF7
loop3B = -2 -1 = -3 = 0xFD
main_loop = -45 -1 = -46 = 0xD2
/////////////////////////////////////////////////
main_loop:
8F, 01, F4, 8F, 0A, 00,
loop1A:
8F, 43, 01,
loop2A:
E8, 7E,
loop3A:
9C, D0, loop3A, 8B, 01, D0, loop2A, 8B, 00, D0, loop1A, 8F, 00, F4, 8F, 0A, 00,
loop1B:
8F, 43, 01,
loop2B:
E8, 7E,
loop3B:
9C, D0, loop3B, 8B, 01, D0, loop2B, 8B, 00, D0, loop1B, 2F, main_loop
/////////////////////////////////////////////////
↓
/////////////////////////////////////////////////
main_loop:
8F, 01, F4, 8F, 0A, 00,
loop1A:
8F, 43, 01,
loop2A:
E8, 7E,
loop3A:
9C, D0, FD, 8B, 01, D0, F7, 8B, 00, D0, F0, 8F, 00, F4, 8F, 0A, 00,
loop1B:
8F, 43, 01,
loop2B:
E8, 7E,
loop3B:
9C, D0, FD, 8B, 01, D0, F7, 8B, 00, D0, F0, 2F, D2
/////////////////////////////////////////////////
さらに羅列して完成です。
/////////////////////////////////////////////////
8F, 01, F4, 8F, 0A, 00, 8F, 43, 01, E8, 7E, 9C, D0, FD, 8B, 01,
D0, F7, 8B, 00, D0, F0, 8F, 00, F4, 8F, 0A, 00, 8F, 43, 01, E8,
7E, 9C, D0, FD, 8B, 01, D0, F7, 8B, 00, D0, F0, 2F, D2
/////////////////////////////////////////////////
合計46バイトの0.5秒おきにLチカするプログラムが完成しました。
このコードをAPUのSRAMに書き込めば、Lチカプログラムが実行します。
SPC700へプログラムを転送するのにはArduinoを使用します。
・回路図
・Arduinoのプログラム(スケッチ)
//SPC700でLチカプログラム
//©oy
//https://oykenkyu.blogspot.com/2021/10/spc700-midi.html
//ユーザープログラムのサイズ(Byte)
#define PROGSIZE 46
//SPC700プログラム書き始めアドレス
#define WRITE_ADR 0x0200
//SPC700プログラム開始アドレス
#define START_ADR 0x0200
//SPC700Lチカプログラム
unsigned char progdata[PROGSIZE] = {
0x8F, 0x01, 0xF4, 0x8F, 0x0A, 0x00, 0x8F, 0x43, 0x01, 0xE8, 0x7E, 0x9C, 0xD0, 0xFD, 0x8B, 0x01,
0xD0, 0xF7, 0x8B, 0x00, 0xD0, 0xF0, 0x8F, 0x00, 0xF4, 0x8F, 0x0A, 0x00, 0x8F, 0x43, 0x01, 0xE8,
0x7E, 0x9C, 0xD0, 0xFD, 0x8B, 0x01, 0xD0, 0xF7, 0x8B, 0x00, 0xD0, 0xF0, 0x2F, 0xD2
};
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr);
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata);
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len);
//セットアップ
void setup()
{
PORTB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
DDRB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
pinMode(14, OUTPUT); //pin14_APU_reset
digitalWrite(14, HIGH);//pin14_APU_reset
//SPC700プログラム書き込み
prog_write(WRITE_ADR, progdata, PROGSIZE);
//SPC700ユーザープログラム開始
write_apu(3, START_ADR >> 8);
write_apu(2, START_ADR & 0xFF);
write_apu(1, 0x00);//SPC700ユーザープログラム開始モード
write_apu(0, PROGSIZE + 1);//データ転送終了&ユーザープログラム開始
//Lチカ用にarduinoデータピンを開放、APUポート0を選択、APUのRDを常にL、
DDRB = 0x0F; //pin8~pin11_output,pin12~pin13_(D0~D1)input
DDRD &= ~0xFC;//pin2~pin7_(D2~D7)input
PORTB = (~0x03 & PORTB) | (0 & 0x03);//APU_アドレス0
digitalWrite(10, LOW);//pin10_APU_we
}
void loop()
{
}
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr)
{
unsigned char readdata;
DDRB = 0x0F;//arduino_data_pin_input
DDRD &= ~0xFC;//arduino_data_pin_input
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
digitalWrite(10, LOW);//pin10_APU_RD
delayMicroseconds(1);
readdata = ((PINB & 0x30) >> 4) | (PIND & 0xFC);
digitalWrite(10, HIGH);//pin10_APU_RD
return readdata;
}
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata)
{
DDRB = 0x3F;//arduino_data_pin_output
DDRD |= 0xFC;//arduino_data_pin_output
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
PORTD = (~0xFC & PORTD) | (writedata & 0xFC); //APU_data_write
PORTB = (~0x30 & PORTB) | (((writedata & 0x03) << 4) & 0x30); //APU_data_write
digitalWrite(11, LOW);//pin11_APU_WE
delayMicroseconds(1);
digitalWrite(11, HIGH);//pin11_APU_WE
}
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len)
{
//APUリセット
digitalWrite(14, LOW);
delay(10);
digitalWrite(14, HIGH);
while(!((read_apu(0) == 0xAA) && (read_apu(1) == 0xBB))){
//APUが初期化を完了するまで待機
}
write_apu(3, adr >> 8);
write_apu(2, adr & 0xFF);
write_apu(1, 1);//APUユーザープログラム転送モード
write_apu(0, 0xCC);
while(read_apu(0) != 0xCC){
//APUが転送準備を完了するまで待機
}
for(unsigned int i = 0;i < len;i++)
{
write_apu(1, writedata[i]);
write_apu(0, i & 0xFF);
while(read_apu(0) != (i & 0xFF)){
//APUが転送データ1バイトを書き込むまで待機
}
}
}
0.5秒おきにLEDが点滅しています。
SNESのAPUでLチカ#SPC700 pic.twitter.com/khgyBjzb5F
— oy (@0x6f_0x79) October 17, 2021
どうでしょう?
人力で機械語に変換するのは、とても大変ですね。
プログラムサイズが大きくなると、アセンブラから機械語の変換は、人力ではもはや手に負えなくなるので、通常はソフトで変換します。
WLA-DXというソフトを使うとこの面倒な作業をやってくれます。
参考
ゲームギア開発:アセンブラで開発できる「WLA-DX」を動かしてみる
;//spc700_LB.asm////////////////WLA-DX SPC700////////////////
.MEMORYMAP
DEFAULTSLOT 0
SLOTSIZE $10000
SLOT 0 $0000
.ENDME
.ROMBANKMAP
BANKSTOTAL 1
BANKSIZE $10000
BANKS 1
.ENDRO
.ORG $200 ;開始アドレス
main_loop:
MOV $F4, #$01 ;$00F4(ポート0)へ0x01を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1A:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2A:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3A:
DEC A ;アキュムレータデクリメント
BNE loop3A ;ゼロフラグがクリアされているときloop3Aへ
DEC $01 ;$0001デクリメント
BNE loop2A ;ゼロフラグがクリアされているときloop2Aへ
DEC $00 ;$0000デクリメント
BNE loop1A ;ゼロフラグがクリアされているときloop1Aへ
MOV $F4, #$00 ;$00F4(ポート0)へ0x00を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1B:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2B:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3B:
DEC A ;アキュムレータデクリメント
BNE loop3B ;ゼロフラグがクリアされているときloop3Bへ
DEC $01 ;$0001デクリメント
BNE loop2B ;ゼロフラグがクリアされているときloop2Bへ
DEC $00 ;$0000デクリメント
BNE loop1B ;ゼロフラグがクリアされているときloop1Bへ
BRA main_loop
;/////////////////////////////////////////////////////////////
;//spc700_LB.link///////////////////////////////////////////////
[objects]
C:\Users\hogehoge\spc700_LB.o
;/////////////////////////////////////////////////////////////
上記「spc700_LB.asm」「spc700_LB.link」を任意の場所(例ではC:\Users\hogehoge\)に作成し、「wla-spc700.exe」「wlalink.exe」をダウンロードしてきて同ディレクトリに配置します。
その後次の2つのコマンドをコマンドプロンプトで1つずつ実行します。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
"C:\Users\hogehoge\wla-spc700.exe" -v "C:\Users\hogehoge\spc700_LB.asm"
"C:\Users\hogehoge\wlalink.exe" -v C:\Users\hogehoge\spc700_LB.link C:\Users\hogehoge\spc700_LB.bin
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Free space at $0000-$01ff.
Free space at $022e-$ffff.
Bank 00 has 65490 bytes (99.93%) free.
65490 unused bytes of total 65536.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
こんな感じの結果が返ってきて「spc700_LB.bin」が作成されたら成功です。
「spc700_LB.bin」のサイズは64Kバイトあり、HEXエディタで覗くと開始アドレス$0200からプログラムが入っているのがわかります。もちろん先ほど行った手動ビルドの結果と同じはずです。
開始アドレスは「spc700_LB.asm」の「.ORG $200」で設定しています。
実際SPC700で実行するときは、Arduino側で開始アドレスを決めて書き込むので「.ORG $00」でも良いです。
・MIDI演奏するためのデータ転送プログラムの作成
さて、本題に戻ってスーパファミコンのAPUで音を出すプログラムを書いていきましょう。
まず、検討するのは、APUで直接MIDIを受信できるかどうかです。
MIDIはB=31250bpsの非同期シリアル通信です。シリアル通信は、0と1の時間変化でデータのやりとりをします。
受信側は1秒間に31250回以上で0,1の状態を読み取る必要があります。
Arduinoなどに使われているATmega328pは、シリアルデータを受信する専用のハードウェアを持っていますが、APUにはありません。プログラムを書いてソフトウェアで読み取る必要があります。
APUのCPUであるSPC700はf=1.024MHzで動作します。このとき、シリアルデータ1bitを読み取るのに余裕があるCPUクロック数Nrmは、
Nrm = f / B = 1.024M / 31250
= 32.77 ≂ 32クロック
です。
SPC700は1命令実行するのに2~8ほどクロックが必要なので、だいぶ余裕がないと分かります。なのでAPUで直接MIDIを受信するのはあきらめましょう。
基本的にAPUを使うには、APUのSRAMへユーザープログラム等のデータを書くための別のマイコンが必要です。
そこで、MIDIの受信はその別のマイコンに任せるとします。
DSPとSRAMのデータをマイコンとのやり取り用としてのみ、SPC700を使います。
その上で、APUのポートの特性をよく理解する必要があります。
・APUのポートの回路図
※回路図上では1ポート分のみ描かれているので、実際はこれがあと3つ加わり合計4組のラッチ回路が構成されています。
APUのポートまわりの回路図を見てもらうと、データを保持するラッチが読み込みと書き込みで別々に用意されています。
つまり、SPC700がポートに値を書き込んだあと、そのデータはSPC700側で読みだすことはできません。
逆にマイコンがAPUのポートに値を書き込んだあと、そのデータはマイコン側で読みだすことはできません。
この回路は、マイコン-SPC700間のデータのやり取りする時、なんか便利そうに見えますが、「SPC700へデータを書き込んだことを知らせる」という点では致命的です。
例えば、マイコンからAPUのポート1へデータを書き込む場合を考えてみましょう。
マイコンがAPUのポート1へ"AA"→"BB"→"CC"→"DD"→"EE"を順に書き込場合、SPC700側はポート1を随時読み取り、ポート1の値の変化から値が書き込まれたことを検知できます。
では、マイコンがAPUのポート1へ"AA"→"AA"→"AA"→"AA"→"AA"を順に書き込場合どうでしょう。SPC700側はポート1の値の変化を読み取れないため、書き込まれたことを検知できません。
そこで、ポート0を用いてSPC700側へデータを書き込んだことを通知します。
マイコン側からポート1へデータを書いたのち、マイコン側でポート0の値を変化させます。
ポート0の特定のビットをフラグとして使用することで、SPC700へ任意のデータを書き込めるようにします。
そこで、問題になるのがデータを保持するラッチが読み込みと書き込みで別々に用意されていることです。
ポート0の特定のビットをフラグとして使用した場合、SPC700はポート1のデータ受け取り後に、ポート0のフラグビットを戻す必要があります。ですがそれがハードウェア上できません。
そこで、ポート0のフラグビットの反転があった時に、SPC700はデータを読み取り&内部SRAMまたはレジスタ書き込みを実行するプログラムとしなければいけません。
・プログラム
SPC700のプログラムは基本的にDSPとSRAMのデータをマイコンとのやり取り用する機能のみとします。ですので、プログラムの規模は小さいです。
コード内のコメントで、どんなを処理をしているかを書いていますが、ニーモニックの詳しい解説はしていませんので各自で調べてみてください。
先ほどのLチカプログラムと大きく違うのは、サブルーチンを使用しているところです。
アセンブラ特有の「スタック」の説明はしないので、各自で調べてみてください。
/////////////////////////////////////////////////
.MEMORYMAP
DEFAULTSLOT 0
SLOTSIZE $10000
SLOT 0 $0000
.ENDME
.ROMBANKMAP
BANKSTOTAL 1
BANKSIZE $10000
BANKS 1
.ENDRO
.org $200
;変数
;$00,$01 アドレス保存
;
;$04 以前のポート0(フラグ用)の状態を保存
;
;$F2 DSPアドレスレジスタ
;$F3 DSPデータレジスタ
;$F4 ポート0_読み書きフラグ用(0,0,L:SRAM/H:DSPアドレス,アドレスオートインクリメント:L-No_H-Yes,読み込み完了,書き込み完了,読み込み要求,書き込み要求)
;$F5 ポート1_データやり取り用
;$F6 ポート2_アドレス下位8bitやり取り用
;$F7 ポート3_アドレス上位8bitやり取り用
MOV $00, #$00 ;$00,$01(アドレス16bit保存用)初期化
MOV $01, #$00 ;
MOV $F5, #$00 ;ポート1_データやり取り用初期化
MOV $F6, #$00 ;ポート2_アドレス下位8bitやり取り用初期化
MOV $F7, #$00 ;ポート3_アドレス上位8bitやり取り用初期化
MOV A, $F4
AND A, #$3F ;上位2ビットは使わないので0
MOV $04, A ;$04(以前のフラグ用保存用)へポート0(フラグ用)を書き込む
MOV $F4, $04 ;ポート0(フラグ用)へ$04(以前のフラグ用保存用)を書き込む
main_loop:
;書き込み要求があるときの判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
EOR A, $04 ;以前のポート0の状態と今回取得したポート0の状態に差がある時、各ビットが1
AND A, #$01 ;書き込み要求のフラグのみ抽出
BNE write ;ゼロフラグが0(書き込み要求のビット反転があった時)書き込みサブルーチンへ
;読み込み要求があるときの判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
EOR A, $04 ;以前のポート0の状態と今回取得したポート0の状態に差がある時、各ビットが1
AND A, #$02 ;読み込み要求のフラグのみ抽出
BNE read ;ゼロフラグが0(読み込み要求のビット反転があった時)読み込みサブルーチンへ
BRA main_loop ;メインループ先頭へ
write: ;ポート書き込みルーチン
;アドレスオートインクリメント有効か判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$10 ;アドレスオートインクリメントのフラグのみ抽出
BEQ no_aai_w ;ゼロフラグが1(アキュムレータが0,オートインクリメント)
;オートインクリメント有効の時
OR $04, #$10 ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をHにする
CALL !write_data ;データ書き込みサブルーチンへ
INCW $00 ;$00,$01(アドレス16bit保存用)インクリメント
BRA aai_exit_w ;aai_exit_wへ
no_aai_w: ;オートインクリメント無効の時
AND $04, #$EF ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をLにする
MOVW YA, $F6 ;Yレジスタとアキュムレータへポート3,4(アドレス)を書き込む
MOVW $00, YA ;$00,$01へポート3,4(アドレス)を書き込む
CALL !write_data ;データ書き込みサブルーチンへ
aai_exit_w:
EOR $04, #$05 ;$04(以前のフラグ用保存用)のbit2(書き込み完了通知)とbit0(書き込み要求)を反転
MOVW YA, $00 ;Yレジスタとアキュムレータへ$00,$01を書き込む
MOVW $F6, YA ;ポート3,4(アドレス)へ$00,$01を書き込む
MOV $F4, $04 ;ポート0(フラグ用)へ$04(以前のフラグ用保存用)を書き込む
BRA main_loop ;メインループ先頭へ
write_data: ;sram/dspデータ書き込みサブルーチン
;書き込み先がSRAM/DSPか判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$20 ;書き込み先SRAM/DSPのフラグのみ抽出
BEQ sel_ram_dsp_w ;ゼロフラグが1(アキュムレータが0,書き込み先SRAM)
OR $04, #$20 ;$04(以前のフラグ用保存用)のbit5(書み込み先SRAM/DSPのフラグ)をHにする
MOV $F2, $00 ;DSPアドレスレジスタへ$00内にセットされているアドレスを書き込む
MOV $F3, $F5 ;DSPデータレジスタへポート1(データ用)を書き込む
BRA sel_ram_dsp_exit_w
sel_ram_dsp_w:
AND $04, #$DF ;$04(以前のフラグ用保存用)のbit5(書み込み先SRAM/DSPのフラグ)をLにする
MOV Y, #$00 ;Yレジスタへ0x00を書き込む
MOV A, $F5 ;アキュムレータへポート1(データ用)を書き込む
MOV [$00]+Y, A ;$00,$01に内にセットされているアドレスへポート1(データ用)を書き込む
sel_ram_dsp_exit_w:
RET ;サブルーチン終了
read: ;ポート読み込みルーチン
;アドレスオートインクリメント有効か判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$10 ;アドレスオートインクリメントのフラグのみ抽出
BEQ no_aai_r ;ゼロフラグが1(アキュムレータが0,オートインクリメント)
;オートインクリメント有効の時
OR $04, #$10 ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をHにする
CALL !read_data ;データ書き込みサブルーチンへ
INCW $00 ;$00,$01(アドレス16bit保存用)インクリメント
BRA aai_exit_r ;aai_exit_rへ
no_aai_r: ;オートインクリメント無効の時
AND $04, #$EF ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をLにする
MOVW YA, $F6 ;Yレジスタとアキュムレータへポート3,4(アドレス)を書き込む
MOVW $00, YA ;$00,$01へポート3,4(アドレス)を書き込む
CALL !read_data ;データ書き込みサブルーチンへ
aai_exit_r:
EOR $04, #$0A ;$04(以前のフラグ用保存用)のbit3(読み込み完了通知)とbit1(読み込み要求)を反転
MOVW YA, $00 ;Yレジスタとアキュムレータへ$00,$01を書き込む
MOVW $F6, YA ;ポート3,4(アドレス)へ$00,$01を書き込む
MOV $F4, $04 ;ポート0(フラグ用)へ$04(以前のフラグ用保存用)を書き込む
BRA main_loop ;メインループ先頭へ
read_data: ;sram/dspデータ読み込みサブルーチン
;読み込み先がSRAM/DSPか判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$20 ;読み込み先SRAM/DSPのフラグのみ抽出
BEQ sel_ram_dsp_r ;ゼロフラグが1(アキュムレータが0,読み込み先SRAM)
OR $04, #$20 ;$04(以前のフラグ用保存用)のbit5(読み込み先SRAM/DSPのフラグ)をHにする
MOV $F2, $00 ;DSPアドレスレジスタへ$00内にセットされているアドレスを書き込む
MOV $F5, $F3 ;ポート1(データ用)へDSPデータレジスタを書き込む
BRA sel_ram_dsp_exit_r
sel_ram_dsp_r:
AND $04, #$DF ;$04(以前のフラグ用保存用)のbit5(読み込み先SRAM/DSPのフラグ)をLにする
MOV Y, #$00 ;Yレジスタへ0x00を書き込む
MOV A, [$00]+Y ;ポート1(データ用)へ$00,$01に内にセットされているアドレス内の値を書き込む
MOV $F5, A ;ポート1(データ用)へアキュムレータの値を書き込む
sel_ram_dsp_exit_r:
RET ;サブルーチン終了
/////////////////////////////////////////
このプログラムをSPC700が実行することで、マイコン側はSPC700を意識せずにSRAMやDSPとデータのやり取りができます。
このプログラムの具体的な機能は、3つあります。
・SRAM上(SPC700上のアドレス$0000~$FFFF)のデータの読み書き
SPC700上のアドレス$0000~$FFFFの読み書きができます。$00F0~$00FFにあるSPC700のレジスタのアクセスも、このSRAMアクセスモードで読み書きします。
($00,$01,$04,$F4,$F5,$F6,$F7,$100~$300はプログラム内で使用しているため書き込み禁止です。)
・DSP上(DSP上のアドレス$00~$FF)のデータの読み書き
本来DSPのアクセスには$F2,$F3レジスタを通さなければいけませんが、演奏する際は頻繁にアクセスするので、直接アクセスできるようにしました。
DSPアクセスモードでアクセスすると、読み書き手順が少なくなり、少しだけ高速に読み書きができます。
・読み書き後、次点読み書きアドレスオートインクリメント機能
有効無効が切り替えられます。連続アドレスのアクセスで使用できます。BRRサンプル書き込み時など、いちいちアドレスの指定をすることなく連続したデータの書き込みができます。
・DSPにデータを書き込む
さて、ようやくAPUのSRAMやDSPにデータをマイコン側から書き込む準備が整いました。
DSPは音を出すための処理をするLSIで、DSPに何かしらの指示を出すと音が出ます。
APU内で言うと音源ICに相当します。(DSPはSRAMから音源データを読み込んで再生するため、厳密にはDSP単体で音源ICとは呼べないでしょう。)
音を出すにはまず、音の波形データが必要です。このデータはサンプリング周波数32KHz・16bitの波形データBRR圧縮した音波形データを入手する必要があります。
BRR圧縮については説明を省きます。「snes brr」等で検索をかけるといろいろ情報があるので各自調べてみてください。
ネット上にはwavをBRR圧縮してくれるソフトを作っている方がいるようなので、BRR圧縮した音波形データの入手には、そのソフトを利用します。
DSP用にBRR圧縮されたデータは9バイトで1ブロックとなっています。1ブロックにはステレオ16bitで8サンプル分のデータとヘッダ情報が含まれています。複数のブロックを連続してSRAM上に配置することで、1つの楽器(音声)のサンプルデータとなります。
・DSPの主な仕様
同時発音数:ステレオ8チャンネル
サンプリング周波数32KHz・16bitでBRR圧縮されたデータの再生
ノイズジェネレータ、ハードウェアエンベロープ、ピッチモジュレーション、エコー等
・DSPのレジスタ
DSPのレジスタは、SPC700の16bitアドレス空間とは別に、8bitアドレス($00~$FF)空間を持っています。
各レジスタの機能
アドレス | レジスタ名 | 機能 |
---|---|---|
$X0 | VOL_X_L | ch_Xの左音量レジスタ |
$X1 | VOL_X_R | ch_Xの右音量レジスタ |
$X2 | PTC_X_L | ch_Xの音程レジスタ(下位8bit) |
$X3 | PTC_X_H | ch_Xの音程レジスタ(上位8bit) |
$X4 | SRCN_X | ch_XのBRRソース番号(楽器サンプル番号)レジスタ(0~255を指定) |
$X5 | ENV1_X | ch_XのエンベロープADレジスタ |
$X6 | ENV2_X | ch_XのエンベロープSRレジスタ |
$X7 | GAIN_X | ch_Xのソフトウェアエンベロープ使用時のゲインレジスタ |
$X8 | DSP_ENV_X | ch_XのDSPエンベロープ制御作業用レジスタ(読み込み専用) |
$X9 | DSP_OUT_X | ch_XのDSP音量制御作業用レジスタ(読み込み専用) |
$0C | MAIN_VOL_L | メイン左音量レジスタ |
$1C | MAIN_VOL_R | メイン右音量レジスタ |
$2C | ECHO_VOL_L | エコー左音量レジスタ |
$3C | ECHO_VOL_R | エコー右音量レジスタ |
$4C | KEY_ON | キーオンレジスタ |
$5C | KEY_OFF | キーオフレジスタ |
$6C | DSP_CONF | DSP設定レジスタ |
$7C | END_SMPL | BRRサンプル終端通知レジスタ |
$0D | ECHO_FB | エコーフィードバックレジスタ |
$2D | PTC_MD | ピッチモジュレーションON/OFFレジスタ |
$3D | NOISE_EN | ノイズON/OFFレジスタ |
$4D | ECHO_EN | エコーON/OFFレジスタ |
$5D | SR_DIR | BRRサンプルデータソースディレクトリレジスタ |
$6D | ECHO_BUF_ADR | エコー用SRAM領域の開始アドレスレジスタ |
$7D | ECHO_DLY | エコー遅延時間設定レジスタ |
$XF | ECHO_FIR_X | ch_XのエコーFIRフィルタ係数設定レジスタ |
レジスタマップ
$X0 | $X1 | $X2 | $X3 | $X4 | $X5 | $X6 | $X7 | $X8 | $X9 | $XA | $XB | $XC | $XD | $XE | $XF | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
$0X | VOL_0_L | VOL_0_R | PTC_0_L | PTC_0_H | SRCN_0 | ENV1_0 | ENV2_0 | GAIN_0 | DSP_ENV_0 | DSP_OUT_0 | MAIN_VOL_L | ECHO_FB | ECHO_FIR_0 | |||
$1X | VOL_1_L | VOL_1_R | PTC_1_L | PTC_1_H | SRCN_1 | ENV1_1 | ENV2_1 | GAIN_1 | DSP_ENV_1 | DSP_OUT_1 | MAIN_VOL_R | ECHO_FIR_1 | ||||
$2X | VOL_2_L | VOL_2_R | PTC_2_L | PTC_2_H | SRCN_2 | ENV1_2 | ENV2_2 | GAIN_2 | DSP_ENV_2 | DSP_OUT_2 | ECHO_VOL_L | PTC_MD | ECHO_FIR_2 | |||
$3X | VOL_3_L | VOL_3_R | PTC_3_L | PTC_3_H | SRCN_3 | ENV1_3 | ENV2_3 | GAIN_3 | DSP_ENV_3 | DSP_OUT_3 | ECHO_VOL_R | NOISE_EN | ECHO_FIR_3 | |||
$4X | VOL_4_L | VOL_4_R | PTC_4_L | PTC_4_H | SRCN_4 | ENV1_4 | ENV2_4 | GAIN_4 | DSP_ENV_4 | DSP_OUT_4 | KEY_ON | ECHO_EN | ECHO_FIR_4 | |||
$5X | VOL_5_L | VOL_5_R | PTC_5_L | PTC_5_H | SRCN_5 | ENV1_5 | ENV2_5 | GAIN_5 | DSP_ENV_5 | DSP_OUT_5 | KEY_OFF | SR_DIR | ECHO_FIR_5 | |||
$6X | VOL_6_L | VOL_6_R | PTC_6_L | PTC_6_H | SRCN_6 | ENV1_6 | ENV2_6 | GAIN_6 | DSP_ENV_6 | DSP_OUT_6 | DSP_CONF | ECHO_BUF_ADR | ECHO_FIR_6 | |||
$7X | VOL_7_L | VOL_7_R | PTC_7_L | PTC_7_H | SRCN_7 | ENV1_7 | ENV2_7 | GAIN_7 | DSP_ENV_7 | DSP_OUT_7 | END_SMPL | ECHO_DLY | ECHO_FIR_7 |
それでは、音を出すための手順をみてみましょう。
まずは、ただ発音出来ればよいのでエコーなどの機能の説明は省きます。
発音手順をおおまかに2つのグループに分けます。1つ目のグループは、一度設定したらそうそう変更しないパラメータで、2つ目のグループはMIDIのノートオン・ノートオフで頻繁に変更があるパラメータです。
1グループ目・APU起動後の発音準備処理(BRRサンプルデータの配置)~各パラメータ設定
①BRR圧縮されたデータをSRAM上に配置②BRRサンプルデータソースディレクトリアドレスに、BRR開始ブロックアドレスとループアドレスを登録(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte),…,サンプルPCM_N開始ブロックアドレス(2Byte),サンプルPCM_Nループアドレス(2Byte)の順に配置)③BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00に固定されています。)④BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号PCM_Nを指定⑤エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定⑥メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定⑦DSP設定レジスタ(DSP_CONF($6C))の設定
2グループ目・DSP発音手順
①チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット②音程レジスタ(PTC_X_L($X2), PTC_X_H($X3))のセット③キーオンレジスタ(KEY_ON($4C))のセット
詳しく見ていきましょう
APU起動後の発音準備処理(BRRサンプルデータの配置)
①BRR圧縮されたデータをSRAM上に配置
まずはサンプリングされたデータの配置です。
BRR圧縮されたデータは9バイトで1つのブロックなので、このブロックをSRAM上に連続して配置します。配置開始アドレスは基本的に自由に決められますが、$00
00~$01FF(ページ0,1)は、SPC700の変数&スタック領域なので配置できません。
また、 $0200~$0300らへんもユーザープログラムで使用するので配置できません。
例を挙げてみましょう。
BRRデータサイズが72byte(ブロックサイズ8)のサンプリングデータ「サンプル1」を配置します。
SRAM上のアドレス$8000から、サンプル1のBRRデータを書き込み、$8047がデータの終端になります。
同様にBRRデータサイズが36byte(ブロックサイズ4)のサンプリングデータ「サンプル2」を配置します。SRAM上のアドレス$8100から、サンプル2のBRRデータを書き込み、$8124がデータの終端になります。
②BRRサンプルデータソースディレクトリアドレスに、BRR開始ブロックアドレスとループアドレスを登録(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte),…,サンプルPCM_N開始ブロックアドレス(2Byte),サンプルPCM_Nループアドレス(2Byte)の順に配置)
BRRサンプルデータソースディレクトリアドレスをどこかに決めておきます。このアドレスからのデータは、DSPがBRRサンプリングデータを再生する際、アドレスで指定せずサンプル番号で指定するためのベクタテーブルです。このアドレスの下位8bitは$00で固定となりますので注意してください。
例を挙げてみましょう。
「サンプル1」の開始アドレスは$8000、「サンプル2」の開始アドレスは$8100です。「サンプル1」のループ先は2ブロック目、「サンプル2」のループ先は1ブロック目(先頭)とします。
BRRサンプルデータソースディレクトリアドレスを$0400と決めます。開始ブロックアドレス(2Byte),ループアドレス(2Byte)の順で書き込んでいきます。
(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte))
[$0400] 00, 80, 09, 80, 00, 81, 00, 81
③BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00に固定されています。)
BRRサンプリングデータのベクタ先頭のありかをDSPに教えてあげます。
BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00で固定)します。
例を挙げてみましょう。
BRRサンプルデータソースディレクトリアドレス先頭は$0400と決めたので、SR_DIR($5D)には$04をセットします。
SR_DIR($5D) ← $04
④BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号PCM_Nを指定
チャンネルごとに再生したいサンプル番号をDSPに指定します。
BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号を指定します。
例を挙げてみましょう。
チャンネル0の音色をサンプル1に設定、チャンネル1の音色をサンプル2に設定
SRCN_0($04)← $01
SRCN_1($14)← $02
⑤エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定
エンベロープ(音量の時間変化)を設定します。
エンベロープのモードには3種類あり、1.「ADSRハードウェアエンベロープ」、2.「単純増減」、3.「ゲインパラメータ直接指定」が利用できます。
1.「ADSRハードウェアエンベロープ」にはアタック(Attack)、ディケイ(Decay)、サスティン(Sustain)、リリース(Release)の4つのパラメータがあります。
アタック(Attack)は、キーオンした時に音量が0からMAXになるまでの設定時間です。
ディケイ(Decay)は、アタックがMAXになった後にサスティン音量になるまでの設定時間です。
サスティン(Sustain)は、ディケイ時間が終了し、一定の音量となる時の設定音量です。
リリース(Release)は、キーオフ後のサスティン音量から0になるまでの設定時間です。
___最大音量/|\
/ | \ _____ __サスティン音量/ | | |\__/ | | | \ _____ ___音量0| | | | |
↑ ↑ ↑
| ディケイ時間 リリース時間
アタック時間| |
↓ ↓ ↓
| | | | |
__| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|____________ キー状態
キーオン キーオフ
「ADSRハードウェアエンベロープ」を利用するには、ENV1_X($X5)レジスタのbit7をHにします。
DSPのアタック(Attack)は、ENV1_X($X5)レジスタのbit0~bit3の4ビットで指定します。DSPのディケイ(Decay)は、ENV1_X($X5)レジスタのbit4~bit6の3ビットで指定します。
DSPのサスティン(Sustain)は、ENV2_X($X6)レジスタのbit5~bit7の3ビットで指定します。
DSPのリリース(Release)は、ENV2_X($X6)レジスタのbit0~bit4の5ビットで指定します。
アタック(Attack)、ディケイ(Decay)、リリース(Release)時間は、設定した値が小さいほど時間が長いです。
サスティン(Sustain)は、最大音量との比率で設定し、設定した値が小さいほど音量が小さくなる比率になります。
2.「単純増減」には、「線形増加」、「線形減少」、「曲線増加」、「曲線減少」の4つの種類があります。
「単純増減」を利用するには、ENV1_X($X5)レジスタのbit7をLかつ、GAIN_X($X7)レジスタのbit7をHにします。GAIN_X($X7)レジスタのbit6で増加か減少を選択します。(bit6が1で増加、0で減少)GAIN_X($X7)レジスタのbit5で曲線か線形を選択します。(bit5が1で曲線、0で線形)GAIN_X($X7)レジスタのbit0~bit4の5ビットで、所要時間を指定します。この設定値が小さいほど、音量が最大or0になるまでの時間が長いです。(0にすると無限)3.「ゲインパラメータ直接指定」を利用するには、ENV1_X($X5)レジスタのbit7をLかつ、GAIN_X($X7)レジスタのbit7をLにします。GAIN_X($X7)レジスタのbit0~bit6の7ビットで音量を指定します。(0~127)
例を挙げてみましょう。
チャンネル0のエンベロープを「線形減少」に設定、チャンネル1のエンベロープを「ADSRハードウェアエンベロープ」に設定します。
チャンネル0のエンベロープは有限時間の最長時間に設定します。
チャンネル0のエンベロープはアタック(Attack)=15、ディケイ(Decay)=0、サスティン(Sustain)=3、リリース(Release)=31
ENV1_0($05) ← $00(bit7=L(ADSRエンベロープ無効))
GAIN_0($07) ← $81(bit7=H(単純増減),bit6=L(現象),bit5=L(線形),bit0~bit4=$01(所要時間(最長)))
ENV1_1($15) ← $0F(bit7=L(ADSRエンベロープ有効),bit4~bit6=$00(ディケイ),bit0~bit3=$0F(アタック))
ENV2_1($16) ← $7F(bit5~bit7=$03(サスティン),bit0~bit4=$1F(リリース))
⑥メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定
メイン音量レジスタは、APUから出力される音量を決めるレジスタです。
LとRで別々に設定できるため、左右のバランス調整として使えそうです。設定音量値は符号付き8bit(-128~127)です。
例を挙げてみましょう。左右のメイン音量を127にします。MAIN_VOL_L($0C) ← $7FMAIN_VOL_R($1C) ← $7F
⑦DSP設定レジスタ(DSP_CONF($6C))の設定
DSP設定レジスタは、「リセット」、「ミュート」、「エコー有無」、「ノイズ周波数」の4つの設定ができます。
「リセット」は、DSP_CONF($6C)のbit7をHすると、すべてのチャンネルが、キーオンへの待機中となり、ミュートがかかります。「ミュート」は、DSP_CONF($6C)のbit6をHすると、全てのチャンネルの出力にミュートがかかります。「エコー有無」は、DSP_CONF($6C)のbit5をLすると、エコーがかかります。エコー用に確保されたSRAM領域の読み書きが行われるため、必ず「エコー用SRAM領域の開始アドレスレジスタ:ECHO_BUF_ADR($6D)」でSRAM領域を指定してください。「ノイズ周波数」は、DSP_CONF($6C)のbit0~bit4の5ビット(Nbit=0~31)で設定します。 ノイズは、ドラムのスネアやシンバル系の音の表現などで利用できます。
ノイズ周波数fnは、1~29までは、2 ^ ((1/3)(11+Nbit)) [Hz]Nbit=0:fn=0、Nbit=30:fn=16KHz、Nbit=31:fn=32KHzとなるようです。
例を挙げてみましょう。「リセット」=解除、「ミュート」=解除、「エコー有無」=無効、「ノイズ周波数」=0にします。DSP_CONF($6C) ← $20(bit7=L(リセット解除),bit6=L(ミュート解除),bit5=H(エコー無効),bit0~bit4=$00(ノイズ設定値Nbit=0))
DSP発音手順
①チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット
チャンネルごとの音量を設定します。メイン音量の設定と同様、LとRで別々に設定でき、設定音量値は符号付き8bit(-128~127)です。
例を挙げてみましょう。チャンネル0の左右の音量を127に、チャンネル1の左右の音量を63にします。
VOL_0_L($00) ← $7F
VOL_0_R($01) ← $7F
VOL_1_L($10) ← $3F
VOL_1_R($11) ← $3F
②音程レジスタ(PTC_X_L($X2), PTC_X_H($X3))のセット
発音する音のピッチ(PCM再生速度)を設定します。設定値Pは14ビット(0~16383)で、上位8bitはPTC_X_H($X3)レジスタへ、下位8bitはPTC_X_L($X2)レジスタへ入れます。ピッチPは、4096($1000)で32kHz(BRR標準のサンプリング周波数)でサンプルの再生をします。(原音再生)ピッチP、録音サンプリング周波数fs、サンプルされた楽器の音の周波数fとするとき、APUで発音した楽器の音の出力周波数foutは、fout = f * (32000 / fs) * P / (2^12) [Hz]
となります。例えばサンプリング周波数fs=32000Hz、サンプルされた楽器の音の周波数f=440Hz(A4)で録音した場合で、ピッチPを8192($2000)にすると、
fout = f * (32000 / fs) * P / (2^12)
= 440 * (32000 / 32000) * 8192 / (2^12)
= 880 Hz (A5)
となります。
出力周波数foutから、ピッチPを求める場合は、
P = fout * (fs / 32000)* (2^12) / fとなります。サンプリング周波数fs=32000Hz、サンプルされた楽器の音の周波数f=440Hz(A4)で録音した場合のピッチP設定値と音程Tn (Tn=69で440Hz(A4))の関係は、P = (2^12) * (2 ^ ((Tn-69)/12))= (2^7) * (2 ^ ((Tn-9)/12))
= (2 ^ (Tn/12)) * (2 ^ (7 - (9/12)))= (2 ^ (Tn/12)) * 76.1093となります。最大の音程はP=16383,Tn=93 (1760Hz(A6))で、最小音程はP=76でTn=0(27.5Hz(A0))になります。これより高い音程を出したい場合は、録音する時の楽器の音程をあげます。ただし、楽器の音は倍音を含んでいるため、倍音成分の周波数がサンプリング周波数の半部の周波数を超えないようにしなければいけません。楽器にもよりますが、倍音周波数は基音周数の8~10倍付近まであるため、録音時の楽器の音程は、せいぜい+2オクターブが限界でしょう。
例を挙げてみましょう。チャンネル0の音程をA4(Tn=69,P=4096=$1000)に、チャンネル1の音程をE5(Tn=76,P=6137=$17F9)にします。PTC_0_L($02) ← $00PTC_1_L($12) ← $F9
PTC_0_H($03) ← $10
PTC_1_H($13) ← $17
③キーオンレジスタ(KEY_ON($4C))のセット
最終的に音を出すには、キーオンレジスタ(KEY_ON($4C))を操作します。キーオンレジスタの各ビットが、チャンネルのキーオンに対応しています。KEY_ON($4C)のbit0はチャンネル0に割り当てられ、ビットをHにするとキーオンし、音がでます。ほかのチャンネル1~7もKEY_ON($4C)のbit1~bit7に割り当てられています。キーオフしたい場合は、KEY_OFF($5C)の各ビットをHにします。このレジスタのビット割り当てもKEY_ON($4C)と同様です。
例を挙げてみましょう。チャンネル0とチャンネル1をキーオンします。KEY_ON($4C) ← $03(bit0とbit1をキーオン)
チャンネル0とチャンネル1をキーオフします。KEY_OFF($5C) ← $03(bit0とbit1をキーオフ)
これらの手順でDSPのレジスタにデータを書き込むと音が鳴ります。
ただし、これ以外の未使用のレジスタ(エコー系やノイズ系などのレジスタ)については、電源投入時に不定値となるため、初期化が必要です。
初期化を忘れると、音が鳴らなかったり、期待した音が出なかったりします。
今回の例で、未使用のレジスタで音を出すのに直接かかわるものは、具体的に、
・エコー左音量レジスタ(MAIN_VOL_L($0C))
・エコー右音量レジスタ(MAIN_VOL_R($1C))
・ピッチモジュレーションON/OFFレジスタ(PTC_MD($2D))
・ノイズON/OFFレジスタ(NOISE_EN($3D))
・エコーON/OFFレジスタ(ECHO_EN($4D))
などがあります。
エコー音量レジスタは、音量0に、ON/OFFレジスタは各チャンネルに対応するビットをすべて0にすると無効にできます。
例を挙げてみましょう。
MAIN_VOL_L($0C) ← $00(エコー左音量0)
MAIN_VOL_R($1C) ← $00(エコー右音量0)
PTC_MD($2D) ← $00(チャンネル0~7のピッチモジュレーション無効)
NOISE_EN($3D) ← $00(チャンネル0~7のノイズ発音無効)
ECHO_EN($4D) ← $00(チャンネル0~7のエコー無効)
この未使用レジスタの初期化の処理は、「APU起動後の発音準備処理(BRRサンプルデータの配置)」の「⑦DSP設定レジスタ(DSP_CONF($6C))の設定」の前までに行う必要があります。
ようやくAPUで音を出すのに必要な最低限の手順・プログラムが完成しました。まだMIDIを受信して演奏する機能はありませんが、ひとまず音を出してみましょう。
・回路図
Lチカの時とほぼ同じ回路です。
Lチカの場合は、APU内のSPC700のみでLチカを行っているのを実感できるように、Arduinoとの切り離しが可能回路としました。
今回は、Arduinoとの切り離しはしないため、プルアップ・プルダウン抵抗を取り除きました。(Lチカの時の回路でも音を出すことができます。)
ただし、データバスについては、Arduinoのプログラム上でゲート開放となるタイミングがあるので、プルアップ・プルダウン抵抗を付けておくことをおすすめします。
APU内のオペアンプの電源電圧は、本来9Vなのですが回路の簡略化のため、5Vを使用しています。気になる方は、「A_Vcc」を5Vから9Vへ変えてみてください。
・Arduinoのプログラム
MMLを演奏します。
プログラム内でTimerOneライブラリを使用しています。
BRRサンプルデータは、
のピアノサンプル「piano1」と「piano8」を用いました。Arduinoのプログラムには、著作権の都合上BRRサンプルを同胞していません。
各自用意するか、上記サイトからBRRファイルをダウンロードしてみてください。
(上記サイトのBRRデータの初めの2バイトは、ループ開始位置(BRRデータのバイト数)を示しています。3バイト目以降をArduinoのプログラムメモリに配置してください。)
BRRファイル内のバイナリデータをCプログラムの配列記述にするには、Linuxのxxdコマンドを使います。
Windowsの場合は、
を使います。
//SPC700でMML演奏プログラム
//©oy
//https://oykenkyu.blogspot.com/2021/10/spc700-midi.html
#include <TimerOne.h> //TimerOne.hをインクルード
#include "avr/io.h"
#include "avr/interrupt.h"
//このプログラム(スケッチ)ではTimerOneを使用しています。
//ですのでTimerOneをダウンロードしてインクルードしてください。
#define XTAL 16000000//水晶振動子の周波数(MML演奏速度に影響)
#define SERIALSPEED 38400//UARTのボーレート(デバッグ用)
//ユーザープログラムのサイズ(Byte)
#define PROGSIZE 172
//SPC700プログラム書き始めアドレス
#define WRITE_ADR 0x0200
//SPC700プログラム開始アドレス
#define START_ADR 0x0200
//BRRサンプルベクタテーブルアドレス
//このアドレスは、BRRサンプルベクタの場所を示しています。
#define BRR_VCT 0x0700
//DSPレジスタ名
#define VOL_X_L 0x00
#define VOL_X_R 0x01
#define PTC_X_L 0x02
#define PTC_X_H 0x03
#define SRCN_X 0x04
#define ENV1_X 0x05
#define ENV2_X 0x06
#define GAIN_X 0x07
#define DSP_ENVX_X 0x08
#define DSP_OUTX_X 0x09
#define MAIN_VOL_L 0x0C
#define MAIN_VOL_R 0x1C
#define ECHO_VOL_L 0x2C
#define ECHO_VOL_R 0x3C
#define KEY_ON 0x4C
#define KEY_OFF 0x5C
#define DSP_CONF 0x6C
#define END_SMPL 0x7C
#define ECHO_FB 0x0D
#define PTC_MD 0x2D
#define NOISE_EN 0x3D
#define ECHO_EN 0x4D
#define SR_DIR 0x5D
#define ECHO_BUF_ADR 0x6D
#define ECHO_DLY 0x7D
#define ECHO_FIR_X 0x0F
//DSPチャンネル名
#define CH0 0x00
#define CH1 0x10
#define CH2 0x20
#define CH3 0x30
#define CH4 0x40
#define CH5 0x50
#define CH6 0x60
#define CH7 0x70
//DSPレジスタのビット
#define DSP_RESET_EN 0x80
#define DSP_MUTE_EN 0x40
#define DSP_ECHO_DEN 0x20
#define DSP_ENV_ADSR_EN 0x80
#define MML_MAX_TR 8//MML演奏最大トラック数(SNESのAPUはMAX 8チャンネル)
char timeout = 0;//APU書き込みタイムアウト検出用
//デフォルトオクターブ[ch0,ch1,ch2,…,]
//増やすとオクターブが高くなります。(出力オクターブ = brr_oct + (mmlのオクターブ))
char brr_oct[8] = {0, 0, 2, -1, -1, -1, -1, 0};
//MML演奏用
//ATmega328だとdoubleでも精度はfloatと同様
volatile double mml_time = 0;//MML演奏用時間カウンタ
volatile double mml_time_ms = 0;//MML演奏用時間カウンタms
volatile char mml_time_cnt_en = 0;//MML演奏用時間カウント_有効無効(0で無効、1で有効)
volatile int tempo = 120;//テンポ
//ユーザープログラム
//SRAM・DSPとマイコン間データ転送プログラム
//プログラム詳細
//https://oykenkyu.blogspot.com/2021/10/spc700-midi.html
unsigned char progdata[PROGSIZE] = {
0x8F, 0x00, 0x00, 0x8F, 0x00, 0x01, 0x8F, 0x00, 0xF5, 0x8F, 0x00, 0xF6, 0x8F, 0x00, 0xF7, 0xE4,
0xF4, 0x28, 0x3F, 0xC4, 0x04, 0xFA, 0x04, 0xF4, 0xE4, 0xF4, 0x44, 0x04, 0x28, 0x01, 0xD0, 0x0A,
0xE4, 0xF4, 0x44, 0x04, 0x28, 0x02, 0xD0, 0x43, 0x2F, 0xEE, 0xE4, 0xF4, 0x28, 0x10, 0xF0, 0x0A,
0x18, 0x10, 0x04, 0x3F, 0x50, 0x02, 0x3A, 0x00, 0x2F, 0x0A, 0x38, 0xEF, 0x04, 0xBA, 0xF6, 0xDA,
0x00, 0x3F, 0x50, 0x02, 0x58, 0x05, 0x04, 0xBA, 0x00, 0xDA, 0xF6, 0xFA, 0x04, 0xF4, 0x2F, 0xC8,
0xE4, 0xF4, 0x28, 0x20, 0xF0, 0x0B, 0x18, 0x20, 0x04, 0xFA, 0x00, 0xF2, 0xFA, 0xF5, 0xF3, 0x2F,
0x09, 0x38, 0xDF, 0x04, 0x8D, 0x00, 0xE4, 0xF5, 0xD7, 0x00, 0x6F, 0xE4, 0xF4, 0x28, 0x10, 0xF0,
0x0A, 0x18, 0x10, 0x04, 0x3F, 0x91, 0x02, 0x3A, 0x00, 0x2F, 0x0A, 0x38, 0xEF, 0x04, 0xBA, 0xF6,
0xDA, 0x00, 0x3F, 0x91, 0x02, 0x58, 0x0A, 0x04, 0xBA, 0x00, 0xDA, 0xF6, 0xFA, 0x04, 0xF4, 0x2F,
0x87, 0xE4, 0xF4, 0x28, 0x20, 0xF0, 0x0B, 0x18, 0x20, 0x04, 0xFA, 0x00, 0xF2, 0xFA, 0xF3, 0xF5,
0x2F, 0x09, 0x38, 0xDF, 0x04, 0x8D, 0x00, 0xF7, 0x00, 0xC4, 0xF5, 0x6F
};
//BRRサンプル0
//著作権の関係上ここには同胞していません。
//演奏デモでは「https://www.smwcentral.net/?p=section&a=details&id=17248」の"piano2 g#2-d#3.brr"を使用
//上記brrデータの先頭2バイトは、ループ先アドレスを示すので、3バイト目以降を下の配列に入れてください。
unsigned int brrsample0_len = 5634;//BRRサンプル0サイズ
const unsigned char PROGMEM brrsample0[5634] = {
};
//BRRサンプル1
//著作権の関係上ここには同胞していません。
//演奏デモでは「https://www.smwcentral.net/?p=section&a=details&id=17248」の"piano8 c6-e6.brr"を使用
//上記brrデータの先頭2バイトは、ループ先アドレスを示すので、3バイト目以降を下の配列に入れてください。
unsigned int brrsample1_len = 5634;//BRRサンプル1サイズ
const unsigned char brrsample1[5634] PROGMEM = {
};
//MIDIのノート番号からDSPのピッチを求めるテーブル(BRRサンプリング周波数32000Hzで440Hz(A)の音程の楽器を録音)
const unsigned int noteFreq[128] = {
//A, A#, B, C, C#, D, D#, E, F, F#, G, G#
/**/ /**/ /**/ 76, 81, 85, 91, 96, 102, 108, 114, 121,
128, 136, 144, 152, 161, 171, 181, 192, 203, 215, 228, 242,
256, 271, 287, 304, 323, 342, 362, 384, 406, 431, 456, 483,
512, 542, 575, 609, 645, 683, 724, 767, 813, 861, 912, 967,
1024, 1085, 1149, 1218, 1290, 1367, 1448, 1534, 1625, 1722, 1825, 1933,
2048, 2170, 2299, 2435, 2580, 2734, 2896, 3069, 3251, 3444, 3649, 3866,
4096, 4340, 4598, 4871, 5161, 5468, 5793, 6137, 6502, 6889, 7298, 7732,
8192, 8679, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596,
15464, 16383, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596,
15464, 16383, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596,
15464, 16383, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777
};
//演奏のデータ
//簡易MML
unsigned int mml_data_len = 0xFFFF;//トラック終端「;」が入るため、適当な数値で良い
//mmlのトラック終端には必ず「;」を入れてください。
//使えるコマンド
//「;」 :トラック終端コマンド(トラック終端に必ず入れてください。)
//
//・全チャンネル(トラック)でパラメータを共有
// 「t」+ 数値:テンポを整数で指定
//
//・各チャンネル(トラック)でパラメータの独立指定が可能
// 「v」+ 数値 :ベロシティ(音量)を整数で指定
// 「p」+ 数値 :ベロシティ(音量)を整数で指定
// 「o」+ 数値 :デフォルトオクターブを整数で指定
// 「l」+ 数値 :デフォルト音長を整数で指定
// 「<」または「>」 :オクターブを上げる、下げる
// 「^」 :タイ(タイの次の音程コマンドでの発音は無視され、音長のみ取得します。)
//
//
//・音程コマンド
// (音程)+(「+」または「-」)+(音長)+(「.」):音程はc,d,e,f,g,a,b,rで指定、+または-で半オクターブ移動、音長は整数で指定、ドットの数で(音調+(音調/2)+(音調/4)+(音調/8)+…)符点音長を追加指定
// 例:
// 「a4」 : 音階Aで4分音符
// 「c+16」 : 音階C#で16分音符
// 「g-1.」 : 音階G♭で符点1分音符(1分音符+2分音符の長さ)
// 「r.」 : 休符で符点デフォルト音長(デフォルト音長の初期値は4分音符です。変更したい場合は「l」コマンドで指定してください。)
// 「r2...」 : 休符で3符点2分音符(複符点2分音符)(2分音符+4分音符+8分音符+16分音符の長さ)
//
//
//よいまちカンターレ
const unsigned char PROGMEM mml_data[]=
"t171l16r1r1r8o7d8>a8<degf+ed>a8>a8r8<a8<d8>a8<gf+8.d8e8r8d8>a8<degf+ed>a8>a8r8<a8<d8>a8<gf+8.e8d8r8d8>b8<degf+ed>a8>a8r8<a8<d8>a8<gf+8.d8e8r8d8>b8<degf+ed>a8b8r8<d8e8f8>b<c+deff+gg+a4.r2r8>b8b8r2.b8b8r2r8>e4r8f+8r8g8r8g+8r8<<d8d8.c+64c32>b64r2..<abr2r8a8a8br2r8.d8g+8ar2r8.>e8<d8r2.g+8g+8r2..a8ar2r>b4a+8b8<c+4d+8e8f8d8d8.c+64c32>b64r2r8<e8d8dr2r8.d8rar2.e8d8<dr2r8.>>>>a4.r8g+2g4.r8b8.ra8b8a2<c8>a8<c8>a8b4<drdr8.>grb8r8a8<<e32r32e32r32a8r8>>a8g8d8<<a32r32e32r32>>f+8.<<e32r32a8r8>>f+4<a+8ra+32r32ar8.b8.rar8.f+rerb8e4.g8r8er8.<c+r8.eree>ererc+8r8>a+2r8<frf8.r>a+8r8<a+4a8.<dega4.g+32g32f+32f32e32r1r1r4...>>a4a8a8r8a8g+8a8b4b8br2r8.<<g+2..b4.r8<c4.>g+8<c+2.>g+8<c+8d1^d8>a2<c+2>b2<c2>e2..f+2g+4g+8g+8g+8e2..f+2a4.c+ef+g+b4.<c2>g+1^g+8a1g+1b4>b4<e4>b4c2d2r2.g+8.r>b8.b8rb8.rg+8b8b3r6g+8g+8<c4c+4.<<e4d+4>b4a1b1<c1^c1e1e4>b4<e4g+4r1r1r1r1r1r1r1r1r1r1r1r1r2..cr8.c8>g8<c8c32r32c32r32>b32r32b32r8r32d8rddc+64c64>b64a+64g+64f+64e64d+64r8.e4f+4r2<g+2..b4.r8<c4.>g+8<c+2.>g+8<c+8d1^d8>a2<c+2>b2<c2>e2..f+2g+4g+8g+8g+8e2..f+2a4.c+ef+g+b4.<c2>g+1^g+8a1g+1b4>b4<e4>b4<e4.r8f+4.r1r1r1r4.>e2.r8e8e8e4r8;l16r1r1r8o6d8>a8<degf+ed>ar>ar8.<a8<d8>a8<gf+8.d8er8.d8>a8<degf+ed>a8>a4<a8<d8>a8<gf+8re8d8r8d8>b8<degf+ed>a8>a8.r<a8<d8>a8<gf+8rd8e8r8d8>b8<degf+ed>a8b8.r<drerfr>b<c+deff+gg+ara8.^a64g+64g64f+64e64d+64d64c64>a+64a64r2r32a.r32a.r2.r32a.r32a.r4...r8.a4a8b8b8<c8c8c+8c+8r8>drd8>br<d8rdr8d8r8drdr>b32r32b<f+32r32f+re8rd8r8drdr>br<d8rd8.e8g8.rf+8.re8.rd4r8drd8>br<d8rdrd32r32d8r8drdr>br<d8rdr8drd4r2r8g4f+8g8a4b8<c8c+8r8>drd8>br<d8rdr8e8r8d32r32d32r32dr>br<f+8.e8rd8r8drdr>br<d8rd8.e8g8.rf+re8.rd4r4drd8>br<d8re8rf+8a8.r>br8.<d8rc+8rdrd4r4d8e8f+8g8a2r4d4a8.g8.f+4.g8.rf+8.rf+8.rb8.rf+8.re8..r32e8d2^d8r2d8.e8.f+ra8.rc+rc+rd8.c+6r48c+8d1r2e8rd8ra4r1r1r2.e8.re8er8.e8g+re8b8.rb8e8g+8.rf+re8f+re8.r<c+8.r>b8<c+8r8>g+8..r32g+rf+8e8f+8e8f+8g+8e8.r<c+8.r>b8<c+r>g+rg+8.rg+8f+8e8f+8e8g+8r8c+8.r4re8.r4rf+4r4g+4r4<c+8.r>b.r32b8g+.r32e8r8b8.r<c+8r8>g+8.rg+.r32f+.r32e.r32e8.r<c+8.r>b8<c+8r8>g+8.rg+rf+8e8f+8e8f+8g+.r32g+8.r8.<e8.rd+8.re2^e6r6r24>c+8d+8e8.rere4f+8.re8.re.r32e8>b8<e8.f+8.g+8b2..r4b8b8e8drerf+4e8r2r8b8.rb8.a8rg+8.rg+8<c+8>b3r6b8b8d+4e2^e6r6r24c+8d+8e4..r8.c+8c+8d+8e3r24e8.r>b8<e8f+8a8.g+8.e2..r4e8rer8b1^b1^b8r1r1r1e8r8<c+8.r>b8<c+8r8>g+8r8g+8r8erf+8e8f+8g+8e8.r<c+8.r>b8<c+8r8>g+8.rg+8f+8e8f+8.rg+8.rc+8.r4re8.r4rf+8.r4rg+8.r4r<c+8r8>brb8r8g+4b8.r<c+4>g+4g+rf+8.e1r1r1r1.re8.r<c+8.r>b8<c+8r8>g+4g+rf+8e8f+8e8f+8g+8e8.r<c+8.r>b8<c+r>g+rg+8.rg+8f+8e8f+8.rg+8.rc+8.r4re8.r4rf+4r4g+4r4<c+8.r>b8b8g+8e8r8b8.r<c+8r8>g+8.rg+8f+8e8e8.r<c+8.r>b8<c+8r8>g+8.rg+rf+8e8f+8e8f+8g+rg+8.r8.<e4d+8.re2^e6r6r24>c+8d+8e4ere4f+4e4e8e8>b8<e8.f+8.g+rb2..r4b8r8ererf+e32f32f+32e32d+32f32f+.r32e8.r1r1r1r>b2.r8b8b8b4r8;l16r1r1o3d8d8r4.>f+4a4r8a8a+4r8a+8b8b8b8r4.b8<e8d4d8r8>f+4f+8f+8g8g8g8g8g8g8g8g8f+8f+8f+8f+8f8f8f8f8f8e8e8e8f+8f+8f+8f+8g+8g+8g+8g+8a+8a+8a+8a+8f+8a8a8g+8gf+<g8>f+8g8g+8a8a8g+8gf+<g8>f+8g8g+8a8a8g+8gf+<g8>f+8g8g+8a8a8b8b8<c8c8c+8c+8d8d8r4d8rd8r8.>g+8g+8r4g+8rg+8r8.g8g8r4g8rg8r8.g8g8g8g8d8<d8>d+8<d+8>e8e8>e8r8<e8re8>e8r<a8a8>a8r<a8.ra8.d+8d8d8c8>a8<d8c8d8g4f+8r8a4<f+8r4d8d8>d8d8d8d8d8d8<d8d8>d8d8d8d8d8d8<d8d8>d8d8d8d8d8d8<d8d8d8d8a8d8f+4>b4b8b8a+4a+8a+8a4a8a8a8.a8.a8d4d8<d8d8d8>a8g+8g4g8g8<d8>g8<d8>g8g4d8d8d8g8<d8c+8>f+4f+8f+8a+4a+8a+8b4b8b8f+8f+8g+8a+8e4e8e8b8e8b8e8f+4f+8f+8<c+8>f+8<c+8>g8g4g8g8g8g8g8g8g4g8g8g8g8g8a8a8a8g+8g<g8r>f+8g8g+8a8a8g+8g<g8r>f+8g8g+8a8a8g+8g<g8r>f+8e8a8a8a8a8r8a8g+8a8b4b8b8r2r8e8<e8>e8<e8>e8<e8>g8b4b8b8<c4c8>g+8<c8c+8c+8c+8c+8c+8>g+8g8e8e8<e8>e8<e8>e8<e8>e8f8f+4f+8g+8a8a8a8a+8b4b8b8<c4>b8<c8c+8r8c+8c4c8c8>b4b8r8a+4a+8<d8>a+8a8a8a8a8a8a8a+8b4b8b8a4a8a8a8g+8g+8g+8<c4c8c8c+4c+8c+8>e4<e8>e8<e8>f+4f+8f+8f+4f+8f+8g+4g+8g+8g+4g+8g+8a+8a+8a+8a+8a+8a+8a+8a+8<c4c4d4d8c+8>a+8a+8a+8a+8a+8a+8a+8a+8a4a8a8a8a8a8a8g+8<g+8>g+8g+8<c8c8c4c+8c+8c+8e4c+8>g+8g8f+8f+8f+8f+8f+8f+8f+8g8g+8g+8g+8g+8g+8g+8g+4a8a8a8a8a8<a8>a8<a8>a8<a8>a8<a8>a8<<e8>>a8a+8b4<b8>b8b8b8b8b8b8b8b8b8b8b8b8b8<<d2>b2<c2>a2r1r1r1r1r1r1r1r1r1r1r4.>c8<c32c+64d64d+64e64f+64g32.f+64f64e64d+64d32c+32c32>b<c8c8>g8<c8g8c8>b8<c8d4d8r4c4d4c8>f+g+a+b<c+e>e8<e8>e8<e8>e8<e8>g8b4b8b8<c4c8>g+8<c8c+8c+8c+8c+8c+8>g+8g8e8e8<e8>e8<e8>e8<e8>e8f8f+4f+8g+8a8a8a8a+8b4b8b8<c4>b8<c8c+8r8c+8c4c8c8>b4b8b8a+4a+8<d8>a+8a8a8a8a8a8a8a+8b4b8b8a4a8a8a8g+8g+8g+8<c4c8c8c+4c+8c+8>e4<e8>e8<e8>f+4f+8f+8f+4f+8f+8g+8g+8g+8g+8g+8g+8g+8g+8a+8a+8a+8a+8a+8a+8a+8a+8<c2d2>a8a8g+8gf+<g8>f+8g8g+8a8a8g+8gf+<g8>f+8g8g+8a8a8g+8gf+<g8>f+8g8g+8r4e2.r8e8e8e4.;l16r1r1r8o4a8r2r8a4.r8<a+3r24a+8b8r1c4>a8<c4<<<c64>b64a64g32f64e64d64c64>b64a64g64f32e64d64c64>b32a32g32.r2..a2g+2r8g4r8f+4.d8f+4.r8a+2^a+6r1r1r1r1r1r1r1r1r1r1r2....r4.g+24a48a+48b32<c48c+48d48>a96a+96<c96c+96d96d+96e96f+96g96g+96a96a+96b96<c96c+96>d+96e96f+96g96g+96a96a+96b96<c96c+96d48d+96e96f96f+96g96g+96r96a96a+96b96<c96c+96d96d+96f96r1r1r1..>>f+4<d4>a8.r<d8.r>g+8.rg4r1r8ar<d4c8.r>f+rd8f+8<d8r2r8>a8f+4<c+8c+8>b8.rc+4a8e8c+8r8f+8.rb8b8f+4<drd8c+8r4>e8g8b8<d8>e8r4.f+8a8<c+8r8>f+8c+8r8<f8r4>>a+8<<e8.r4r>f8.ra+8>a+8<a8.rg8a8r1r1r2..>>e4e8e8r8e8d+8e8f+4f+8f+8r2r8<e8>b8<e8e8>b4<e8>b4b4<c4.c4e4g+4.g+4>b4r8<g+8r8>b8<e8>b8g+8<c+4.r8>f+4.r8f+4.r8<g+4<g+r8.<g+8e8>b8<g+8r8e8>b8<f+8r8>a8f+8<d.r64<a64g64f64e64d64c64>b64a64g32f64e64d64c64>b64a32g64f64e64d32c32>a+64a32g32ee4c+8e4c+8e8f+4f+4f+4.d+8r8b4g+8<c4.r8c+4.e8>g+4e8e8e8f+4f+8f+8f+4f+8f+8e8e8e8e8e8e8e8e8<<b8e8>b8<e8e8.f+8.a8<c8>>e4.f+2>e4.g+4.g+4b4.b4.b4d+4r4g+4.r8c+4.g+4.r4>f+4.f+4.f+4g+4g+8g+4.g+4<<<c2f+2e2c2>>>f+4.f+4.f+4f+4.b2^b8<<<e8c8>g8.re8b8<f+8.r>>c2>a2r4<<<c+8>g+4f+8e8f+8>>e2..b2<c2^c8>c+2..e2b4e4.f+2a2b2<c2>c+4.c2>a+2b2..r1.r8<<c4c8c8r8<<c8>b8<c8d8.rd8r4>>>g4a4r<<ef+g+ab<c+d+>>e8>b8<e8e8>b4<e8>b4b4<c4.c4e4g+4.g+4>b4r8<g+8r8>b8<e8>b8g+8<c+4.r8>f+4.r8f+4.r8<g+4<g+r8.<g+8e8>b8<g+8r8e8>b8<f+8r8>a8f+8<d.r64<a64g64f64e64d64c64>b64a64g32f64e64d64c64>b64a32g64f64e64d32c32>a+64a32g32ee4c+8e4c+8e8f+4f+4f+4.d+8r8b4g+8<c4.r8c+4.e8>g+4e8e8e8f+4f+8f+8f+4f+8f+8e8e8e8e8e8e8e8e8<<b8e8>b8<e8e8.f+8.a8>>g8r4.f+3r1r1r1r4r6b2.r8b8b8b4r8;l16r1r1o7a24f+12df+>ardrdf+a<d<d>ae>ae<ec+>beaa+<c+>a+<c+g+f+48g+24>>a+<de>a+<<f+d>a24<d24f+24>>ar<degra+r<d>agf+a<ec>a>a<deg<e24a24e24>f+rgrdrbgb24<f+12bf+d>b<d>aba<f+d>br<eec+48>a24<e>c+24e24a24<c24d24e24>>g+8<ce<<d>ad>af+ed>bf+b<deec+>af+<<<dc>ba>>g+8<<c24d24f24bfcf>af<af>af+>a+<f+g+rg+24a24r2.r24g+r2...g+r1rc+r1r1r1r1r1r1r2...r4.>a32r32a32r32a32r32a32r32a32r1r4...<d32r32e32r32g32r32a32r3r96e32r32a32r4...f32r.a32r4r32f32r32a32r3r96b32r.f+32r32a32r4r32c32r32d32r32a32r1r8..f+32r.e32r3r96a32r.g+32r.f+32r1r.d>f+r8d24f+24a24<d24a24<c+24d24r12f+24r12e24f+24r24f+24g24a24f+24a24b24<c+24>b24r24<c+24>a24f+24drbaf+r8.ab>c+4f+8<e8e>ea<c+>e>ar8<e>>c+8.<<arer>a+8f+a+<f+>f+<c+8>a8>b<df+ed>b<af+>b<f+<c+>baf+<<e>>g<e24r12e32g32b32r32f+32b32<d32r32d24r48>b24r48g24r48>>bb<ge>b<<c+24e24g+24f+24c+r48>c+ec+>f+c+8<f+>f+<c+24f+24<f+24a24f+24>b24>gda+a<d8>g8d<d>a+ga+gd8a+g<dgg8>g<g>d8dg8ar8r32<e32<eer1r1r1r1r2.r>g+rbr<erer>>br<g+rerg+rg+r<g+rd+r>g+r<d+d+g+rg+rd+d+>g+r<g+g+>g+ec+>g+r8<g+r8eg+r>br<erg+r>br8.g+<e>bg+br4rf+r<er8<<c+32f+32ac+>>>f+rar<f+b<f+b>f+>b<erg+r>g+r<g+>b<erg+r>g+r<cr>g+<cr>g+<crcr>ar<<ef+af+>>br<fr>fa+<fa+<dc>a+eg+ec+g+ec+>aer32<<e32g+32e32g+32r.>e>bf+r8.bf+<ebf+a<c+>baf+ec+>af+<g+f+32b32<d+r>crcrcg+32<d+32f+32r32f+32r32>>g+r<er<<g+d+c+>bg+ec+>bag+ec+>b<c+ec+>f+<c+f+a<c+f+a<c+e>ac+>af+c+>ag+<eeg+b<eg+e<e>bg+>beg+e>bg+<a+rg+rerg+g+<<e>bec+>bg+ec<grecgrcra>f+d>a<<arerdr>eg+<er>g+r<c+>g+<g+e>g+r<c+r>br<f+rer>br<erf+d+>br<br>d+rg+rd+rbr<cr>>g+r<crd+rerc+rc+rg+r8.c+rererer>f+r<<ec+>af+ec+>f+c+>a<c+f+a<brd+r>g+r<d+rg+rd+>g+d+rg+r<ar>ar<cr>erer<cr>erar<er>>ar<crercrer4r>br<f+rbr<erf+r<cr>f+r>brf+b<f+a<cec>a>b<f32r32f+32r32g32r32g+32r32a32r32a+32r32b32r32<e2<e2e2f+2r4<c+8>g+4f+8e8f+8>b2er>>e4<b4>b4<<c8>g+8c8<d+r8.c+4.r4>>c+4<d4>e4r8<<dr4..>f+4<f+r8.c+4>>a4<b4<br8.c4>d+4<g+4>>c+8<<g+4>>c4<<g+4>>>a+4<<<c2^c8<e6c6>a6e6c6>a6e4c4r1..<<ed+c+>bgec+>bagec+r2g+rbr<erer>>br<g+rerg+rg+r<g+rd+r>g+r<d+d+g+rg+rd+d+>g+r<g+g+>g+ec+>g+r8<g+r8eg+r>br<erg+r>br8.g+<e>bg+br4rf+r<er8<<c+32f+32ac+>>>f+rar<f+b<f+b>f+>b<erg+r>g+r<g+>b<erg+r>g+r<cr>g+<cr>g+<crcr>ar<<ef+af+>>br<fr>fa+<fa+<dc>a+eg+ec+g+ec+>aer32<<e32g+32e32g+32r.>e>bf+r8.bf+<ebf+a<c+>baf+ec+>af+<g+f+32b32<d+r>crcrcg+32<d+32f+32r32f+32r32>>g+r<e<<<c+>g+d+c+>bg+ec+>bag+ec+>b<c+ec+>f+<c+f+a<c+f+a<c+e>af+>af+c+>ag+<eeg+b<eg+e<e>bg+>beg+e>bg+<a+rg+rerg+g+<<e>bec+>bg+ec<gdrcgrcra>f+d>a<<arerdr1....ebag+ebag+e>b<ed+e>b<d+er4e>b<er>b<eg+e>b<eg+e>br<b>b<ef+g+>b<er8.;l16r1r1o4d4r4.>f+4<e4.r8f+2r8>b4r4.b8<e8d4.r8f+2^f+8g2..c+2c2r8e4f+4f+4g+2^g+8a+2^a+8>a8a8r2.a8a8r2.a8a8r2r8a4a8b8b8<c8c8c+8c+8d8d8r2.>g+8g+8r2.g8g8r2.g8g8r2.e8e8r2.a8a8r2.d8d8r4d8ddddg4f+8g8a4b8<c8c+8d8d8r2.>a+8a+8r2.<d8d8r2.d8d8r2.>b4.r8a+4.r8a4.r8a8.a8.a8<d2r2>g1^g4d2r4f+2a+2b1e1f+1<<<fdfa+<dr>>a+<<d>e>a+<d>a+<ede>a+<fdfa+<dr>>a+<<d>f>a<d>a>>g8a8e8<e8g+>f+>a<<<g>>>g8a<gg8g+8e8<e8g+>f+>a<<<g>>>g8a<gg8g+8e8<e8g+>f+>a<<<g>>>g8a<ee8<<<e8.re8ere24f48f+e8d+8e8f+8.rf+8f+8r2r8>>>e2.g8b4.r8<c4.>g+8<c8c+2^c+8>g+8g8e8e1f+4.r8a4.r8b2<c2c+4.c2>b4.r8a+4.<c+8>a+8a2.a+8b2a2^a8g+4.<c2c+2e2^e8>f+1g+1c+2..r8<c2d2<e1>b1g+2<c2e4.>e2^e8c+1d+1e1^e1b1^b1r1r1r1r1r1r1r1r1r1r1r1r1r2..c8r8c8c8r8c8>b8<c8d4d8r4c4d4r2>e2.g8b4.r8<c4.>g+8<c8c+2^c+8>g+8g8e8e1f+4.r8a4.r8b2<c2c+4.c2>b4.r8a+4.<c+8>a+8a2.a+8b2a2^a8g+4.<c2c+2e2^e8>f+1g+1c+2..r8<c2d2e2r1r1r8>c+8d8d+8r4e2.r8e8e8e8r4;l16r1r1r2r8o3f+4a4r4a+4r8a+8b8r2.b8d4.r8f+2r8g2..f+2f2^f8e4r8f+4.r8g+4.r8a+4.r4<e.g32e8>>g+f+g<c8r>e8f8f+8<<e8e8b>>fg<c8r>e8f8f+8<<e8e8>>g+f+g<c8r>e8f8<a4.b4<c4c+4f+r8.c8r8dr8.dr8.c8c8r4g+r8g+8r8.>b8b8e8eeg8ggr8e8a+8a+8e8e8<er8e8r8.e8g8>>b8bb<e8re8e8r<e8e8er8.er8e8r8.d8d8dr8.d8d8dr>g4f+8g8a4b8<c8c+8d8>d8d4d8d<d>dd<c+dr8drd>dfa<dr>d<d>dd<c+d>d8d8d8c+dd8d<d>dd<c+d>>a8g8<ard8ard8f+rd8b8<f+rbrf+r>a+8<drg+r>a+r>a8<ar>a8<ar>a2<a2<d8>a8<c8>a8r4<d8.dr4d4r4>a8a4.r4<c+4.r8f+8r8f+4f+4f+8.f+r4f+4r4e8e4e8r4c+4c+8c+4f+8c+8r8>g1..r4ar<er>f+regr<e>f+rgrg+rar<er>f+regr<e>f+rgrg+r8e<er>f+regr<e>f+rgra4a8a8r8a8<d+8>a8b8.rb8b8r2r8<e4<e4.>b4<g+4.r8g+2..g+3r24g+8.rg+4.r2.>c+4f+4a4<e8r8>b4<f+8e8>g+4r4g+4r8g+4r4f+2>a+4.r4<e2..b4r8b8a4.r4g+4g+8<c4r4c+4.r8>>e4.r4<f+1g+1a+2..r8>c4..rd4..ra+8a+8a+8a+8a+8a+8a+8a+8a8a8a8a8a8a8a8a8g+8g+8g+8g+8<c8c8c8c8c+8c+8c+8>e4<c+8>g+8g8f+4f+8f+f+f+8f+8f+8ggg+4g+8g+g+g+8g+8r8g+8a8a8a8a8a8a8a8a8a8a8a8a8a8e8a8a+8b4b8b8b4b4b1r1r1r1r1r1r1r1r1r1r1r1r1r2..<<g8r8f8g8<c8c8>b8r8d4r4.>c8.rd8.r2re4<e4.>b4<g+4.r8g+2..g+3r24g+8.rg+4.r2.>c+4f+4a4<e8r8>b4<f+8e8>g+4r4g+4r8g+4r4f+2>a+4.r4<e2..b4r8b8a4.r4g+4g+8<c4r4c+4.r8>>e4.r4<f+1g+1a+2..r1r4.>>g+f+g<c8r>e8f8f+8r4rfg<c8r>e8f8f+8r4g+f+g<c8r2r8.e2d+dc+32c32>br8<e8e8d+32d32c+32c32>b32a+32ar8;l16r1r1o5d4r4.>d4c+4.r8a+4.a+4<f+4r4.>f+4f+4.r8a2^a8b2..f+2f2^f8b2.r8<d2>f2^f8<c+4r2.c+4r2.c+4r2r8>a4a8b8b8<c8c8c+8c+8r1r1r1r1r1r1r2..>g4f+8g8a4b8<c8c+8>>>d1^d1^d1..r4<<<f+2f2e2d2>f+1<f+1e4.e4.r4e4.r8>a+2<a4f+2^f+6r12d4.d2r8e4.e4.r4a+4.f2^f8g2f4g8c+8e4r1r1r2r8<b8.rb8br8.arg+rarb8r8b8b8r2..g+8g+g+r8g+8r8g+8r8g+8r8g+8r8g+8r8g+g+e8rg+g+8eg+r8g+8r8g+8r8g+8r8g+8r8g+8r4>a8<f+8c+8f+8c+8a8f+8a8f+8b8f+8e8g+8<c8>g+8<c8r8>e8r4.e8r8f+8r8a8r8<d8r2.>e8eer8g+8r8g+8r8g+8r8f+8r8f+8r8f+8b4r8g+8r8f+8r8g+8r8g+8e8g+8r8g+8e8g+8r8a8r8a8r8a8r8a8r8g+8r8e8r8b8r8b8r8e8r8e8r8e8r8e8>g4.r8f+2<g+4e8e8r8g+8r8e8r8d+8r8d+8r8f+8>b8<f+8>b8rbbrbb<c8ccr8ccr8g+8e8g+8r8g+8e8r8c+re8c+8eer8e8r8eer8f+8d+8f+8r8f+8r8d+d+e8cc>a8<cce8c8r8>aa<e8c8>a8<ccr8c8r8>a8<e8e8r8e8r8e8r8e8r8e8e8eer8e8e4>g4.r8a4.r8a+4.r8b2r1g+2..g+2g+2^g+8e2..g+2d4.>b4<a2f+2<d+2>g+2e4.e2e2e2^e6r1.r3a+4a+8a+8r8<c8>b8<c8>g4g8r4g4a4ref+g+ab<c+d+r4g+8g+g+r8g+8r8g+8r8g+8r8g+8r8g+8r8g+g+e8rg+g+8eg+r8g+8r8g+8r8g+8r8g+8r8g+8r4>a8<f+8c+8f+8c+8a8f+8a8f+8b8f+8e8g+8<c8>g+8<c8r8>e8r4.e8r8f+8r8a8r8<d8r2.>e8eer8g+8r8g+8r8g+8r8f+8r8f+8r8f+8b4r8g+8r8f+8r8g+8r8g+8e8g+8r8g+8e8g+8r8a8r8a8r8a8r8a8r8g+8r8e8r8b8r8b8r8e8r8e8r8e8r8e8r1r1r1.r8>>>g+8a8a+8r4<e2.r8e8e8e8r4;";
/////////////////////////////関数
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr);
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata);
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len);
//~~ユーザプログラム用関数/////////////////////////////
//~~APU-ATmega328p間転送系関数
//APU(ユーザープログラム動作中)のSRAMへ書き込み
void write_sram(unsigned int adr, unsigned char sram_writedata);
//APU(ユーザープログラム動作中)のDSPへ書き込み
void write_dsp(unsigned char adr, unsigned char dsp_writedata);
//APU(ユーザープログラム動作中)のSRAMから読み込み
unsigned char read_sram(unsigned int adr);
//APU(ユーザープログラム動作中)のDSPから読み込み
unsigned char read_dsp(unsigned int adr);
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)
void write_sram_st(unsigned int adr, unsigned char *sram_writedata, unsigned int len);
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)(プログラム用フラッシュメモリのデータをAPUへ連続転送する場合)
void write_sram_st_prg(unsigned int adr, unsigned char *sram_writedata, unsigned int len);
//~~DSPに書き込み系関数
//BRRベクタにサンプルベクタ登録
void etr_brr_vct(unsigned char smp_num, unsigned int start_vct_adr, unsigned int loop_vct_adr);
//ADSRエンベロープ登録
void etr_adsr_env(unsigned char ch, unsigned char attack, unsigned char decay, unsigned char sustain, unsigned char release);
//音程のセット
void ptc_set(unsigned char ch, unsigned int notenum);
//メインの音量とパンのセット
void main_vel_pan_set(unsigned char vel,unsigned char pan);
//チャンネルの音量とパンのセット
void vel_pan_set(unsigned char ch, unsigned char vel,unsigned char pan);
//チャンネルノートオン
void note_on(unsigned char ch);
//チャンネルノートオフ
void note_off(unsigned char ch);
//MML演奏用関数/////////////////////////////////////
//簡易MMLデータより音を鳴らす
void mml_play(unsigned char *play_data, unsigned int len);
//簡易mmlデータ1バイト取得
int get_mml_data(unsigned char *mml_data, unsigned int len, unsigned int pos);
//mmlコマンド1つ取得
void get_mml_com(unsigned char *mml_data, unsigned int len, int *pos, int *ret_data);
//mmlデータから数値を返す
int get_mml_num(unsigned char *mml_data, unsigned int len, int *pos, int *dp_num);
//mmlデータ配列内のトラック数をカウントする。(「;」の数)
int get_mml_tr_num(unsigned char *mml_data, unsigned int len);
//mmlデータ配列内のトラック先頭位置配列に、トラック数分の先頭位置を入れる
int get_mml_tr_pos(unsigned char *mml_data, unsigned int len, unsigned int tr_max, int *pos_tr);
//タイマー割り込み~~~~~~~~~~~~~~~~~~~~~~~~~
void tim1()//MML演奏用
{
if(mml_time_cnt_en == 1){
//MML演奏用時間カウント有効の時、カウント
mml_time_ms++;//1msごとのカウント
//mml_time = mml_time_ms * (9*8192.0 * tempo / (120.0 * 2000.0));//下式と等価
mml_time = mml_time_ms * (3*64.0 * tempo / 625.0); //msをMMLカウンタ値に変換(トラックごとの演奏ずれをできるだけ抑制するため)
}
}
//セットアップ
void setup()
{
//デバッグメッセージ用
Serial.begin(SERIALSPEED);//シリアル通信開始
Serial.print("\r\npwr_on_ok\r\n");
//APU関係
PORTB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
DDRB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
digitalWrite(14, HIGH);//pin14_APU_reset
pinMode(14, OUTPUT); //pin14_APU_reset
//SPC700プログラム書き込み
prog_write(WRITE_ADR, progdata, PROGSIZE);
//SPC700ユーザープログラム開始
write_apu(3, START_ADR >> 8);
write_apu(2, START_ADR & 0xFF);
write_apu(1, 0x00);//SPC700ユーザープログラム開始モード
write_apu(0, PROGSIZE + 1);//データ転送終了&ユーザープログラム開始
Serial.print("Start user program\r\n");
delay(100);//SPC700ユーザープログラム開始までの待ち時間
//SPC700でユーザープログラムを実行しているため、
//SRAMアドレス中の
//$0000,$0001,$0004 (変数用領域)
//$00F4 ~ $00F7 (APUポート領域)
//$0100 ~ $01FF (スタック領域)
//$0200 ~ $0300 (ユーザープログラム領域)
//これらのアドレスへのデータの書き込みは禁止です。
//※SPC700のユーザープログラム側で、これらのアドレスへの書き込みのプロテクトはしていません。書き込まないように注意してください。
//MML演奏用タイマー
Timer1.initialize((int)((float)(1000*(XTAL/16000000.0))));// 1000μs毎にtim1( )割込み関数を呼び出す
Timer1.attachInterrupt(tim1); // タイマー割り込み開始
}
void loop()
{
//①BRR圧縮されたデータをSRAM上に配置
//BRRをAPUのSRAMへ配置します。
write_sram_st_prg(0x1500, brrsample0, brrsample0_len);//配置開始アドレス,配列(ATmega328pのプログラムメモリ上)の先頭アドレス,配列サイズ
write_sram_st_prg(0x7500, brrsample1, brrsample1_len);//配置開始アドレス,配列(ATmega328pのプログラムメモリ上)の先頭アドレス,配列サイズ
//②BRRサンプルデータソースディレクトリアドレスに、BRR開始ブロックアドレスとループアドレスを登録(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte),…,サンプルPCM_N開始ブロックアドレス(2Byte),サンプルPCM_Nループアドレス(2Byte)の順に配置)
//BRRサンプルアドレス登録関数で開始アドレスとループアドレスを指定。
etr_brr_vct(0, 0x1500, 0x1500+5346);//サンプル番号, 開始アドレス(2バイト), ループアドレス(2バイト)(ステレオサンプル数をNとするときで、ループ先サンプル点LNとするとき、ループアドレスは((LN+1)/2)*(9/8)となります。)
etr_brr_vct(1, 0x7500, 0x7500+5445);//サンプル番号, 開始アドレス(2バイト), ループアドレス(2バイト)(ステレオサンプル数をNとするときで、ループ先サンプル点LNとするとき、ループアドレスは((LN+1)/2)*(9/8)となります。)
//③BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00に固定されています。)
write_dsp(SR_DIR, BRR_VCT >> 8);//SR_DIRに指定するソースディレクトリアドレスは上位8bitなので8ビット右へシフトする。
//④BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号PCM_Nを指定
write_dsp(SRCN_X | CH0, 1);
write_dsp(SRCN_X | CH1, 1);
write_dsp(SRCN_X | CH2, 0);
write_dsp(SRCN_X | CH3, 1);
write_dsp(SRCN_X | CH4, 1);
write_dsp(SRCN_X | CH5, 1);
write_dsp(SRCN_X | CH6, 1);
write_dsp(SRCN_X | CH7, 1);
//⑤エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定
etr_adsr_env(CH0, 15, 3, 4, 15);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH1, 15, 2, 6, 0);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH2, 15, 1, 2, 15);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH3, 15, 3, 4, 15);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH4, 15, 3, 4, 15);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH5, 15, 7, 7, 1);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH6, 15, 7, 7, 1);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
etr_adsr_env(CH7, 15, 7, 7, 1);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
//⑥メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定
main_vel_pan_set(127,64);//音量, パン
//未使用のレジスタの初期化
write_dsp(ECHO_VOL_L, 0);//エコー左音量0
write_dsp(ECHO_VOL_R, 0);//エコー右音量0
write_dsp(ECHO_EN, 0);//エコー無効
write_dsp(PTC_MD, 0);//ピッチモジュレーション無効
write_dsp(NOISE_EN, 0);//ノイズ無効
//一応キーオフもする
write_dsp(KEY_OFF, 0xFF);//全チャンネルキーオフ
write_dsp(KEY_OFF, 0x00);//全チャンネルのキーオフフラグをもどす
//⑦DSP設定レジスタ(DSP_CONF($6C))の設定(DSPリセット処理も行う)
write_dsp(DSP_CONF, DSP_RESET_EN | DSP_ECHO_DEN);//DSPリセット状態、エコー無効
write_dsp(DSP_CONF, DSP_ECHO_DEN);//DSPリセット解除、エコー無効
//音量初期設定(MML内でも記述可能です)
//①チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット
vel_pan_set(CH0, 20, 64);//チャンネル0, 音量, パン
vel_pan_set(CH1, 35, 64);//チャンネル1, 音量, パン
vel_pan_set(CH2, 50, 60);//チャンネル2, 音量, パン
vel_pan_set(CH3, 35, 64);//チャンネル3, 音量, パン
vel_pan_set(CH4, 20, 58);//チャンネル4, 音量, パン
vel_pan_set(CH5, 35, 68);//チャンネル5, 音量, パン
vel_pan_set(CH6, 40, 64);//チャンネル6, 音量, パン
vel_pan_set(CH7, 20, 70);//チャンネル7, 音量, パン
Serial.print("DSP setup ok\r\n");
while(1)
{
//MML演奏
mml_play(mml_data, mml_data_len);
}
}
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr)
{
unsigned char readdata;
DDRB = 0x0F;//arduino_data_pin_input
DDRD &= ~0xFC;//arduino_data_pin_input
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
digitalWrite(10, LOW);//pin10_APU_RD
delayMicroseconds(1);
readdata = ((PINB & 0x30) >> 4) | (PIND & 0xFC);
digitalWrite(10, HIGH);//pin10_APU_RD
return readdata;
}
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata)
{
DDRB = 0x3F;//arduino_data_pin_output
DDRD |= 0xFC;//arduino_data_pin_output
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
PORTD = (~0xFC & PORTD) | (writedata & 0xFC); //APU_data_write
PORTB = (~0x30 & PORTB) | (((writedata & 0x03) << 4) & 0x30); //APU_data_write
digitalWrite(11, LOW);//pin11_APU_WE
delayMicroseconds(1);
digitalWrite(11, HIGH);//pin11_APU_WE
}
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len)
{
//APUリセット
digitalWrite(14, LOW);
delay(10);
digitalWrite(14, HIGH);
while(!((read_apu(0) == 0xAA) && (read_apu(1) == 0xBB))){
//APUが初期化を完了するまで待機
static int i_t_c=0;
delayMicroseconds(1);
if(i_t_c>=1000){
Serial.print("Time out : APU not found : PORT0=0xAA and PORT1=0xBB did not return\r\n");
delay(1000);
asm volatile (" jmp 0");//タイムアウトしたら(APUへの読み書き失敗)Arduinoをリセットする。
}
i_t_c++;
}
write_apu(3, adr >> 8);
write_apu(2, adr & 0xFF);
write_apu(1, 1);//APUユーザープログラム転送モード
write_apu(0, 0xCC);
while(read_apu(0) != 0xCC){
//APUが転送準備を完了するまで待機
static int i_t_c=0;
delayMicroseconds(1);
if(i_t_c>=1000){
Serial.print("Time out : PORT0=0xCC did not return\r\n");
delay(1000);
asm volatile (" jmp 0");//タイムアウトしたら(APUへの読み書き失敗)Arduinoをリセットする。
}
i_t_c++;
}
for(unsigned int i = 0;i < len;i++)
{
write_apu(1, writedata[i]);
write_apu(0, i & 0xFF);
while(read_apu(0) != (i & 0xFF)){
//APUが転送データ1バイトを書き込むまで待機
static int i_t_c=0;
delayMicroseconds(1);
if(i_t_c>=1000){
Serial.print("Time out : Failed to transfer the user program\r\n");
delay(1000);
asm volatile (" jmp 0");//タイムアウトしたら(APUへの読み書き失敗)Arduinoをリセットする。
}
i_t_c++;
}
}
}
//~~ユーザプログラム用関数/////////////////////////////
//APU(ユーザープログラム動作中)のSRAMへ書き込み
void write_sram(unsigned int adr, unsigned char sram_writedata)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAM_への書き込み手順
//1.アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.データをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
unsigned char temp_port0;
int i = 0;
//Serial.print("apu_port0_read:0x");//デバッグ用
//Serial.print(read_apu(0), HEX);//デバッグ用
//Serial.print("\r\n");//デバッグ用
write_apu(2, adr & 0xFF);
write_apu(3, (adr>>8) & 0xFF);
write_apu(1, sram_writedata);
temp_port0 = read_apu(0);
write_apu(0, (temp_port0 ^ B00000001) & B11001111);
for(i = 0;i < 100;i++){
//Serial.print("apu_port0_read:0x");//デバッグ用
//Serial.print(read_apu(0), HEX);//デバッグ用
//Serial.print("\r\n");//デバッグ用
if(((read_apu(0) ^ temp_port0) & B00000100) != 0){
return;
}
}
timeout = 1;
Serial.print("Timeout : write_sram_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
}
//APU(ユーザープログラム動作中)のDSPへ書き込み
void write_dsp(unsigned char adr, unsigned char dsp_writedata)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//DSP_への書き込み手順
//1.アドレス0~7bitをAPUのポート2に書き込む
//2.データをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=H(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
unsigned char temp_port0;
int i = 0;
write_apu(2, adr);
write_apu(1, dsp_writedata);
temp_port0 = read_apu(0);
write_apu(0, ((temp_port0 ^ B00000001) | B00100000) & B11101111);
for(i = 0;i < 100;i++){
if(((read_apu(0) ^ temp_port0) & B00000100) != 0){
return;
}
}
timeout = 1;
Serial.print("Timeout : write_dsp_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
}
//APU(ユーザープログラム動作中)のSRAMから読み込み
unsigned char read_sram(unsigned int adr)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAMから読み込み手順
//1.アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit1(読み込み要求)を反転させて、APUのポート0に書き込む
//3.APUのポート0を読み取り、bit3(読み込み完了通知)が反転するまで待つ
//4.APUのポート1(データ)のデータを読み取る
unsigned char temp_port0;
int i = 0;
write_apu(2, adr & 0xFF);
write_apu(3, (adr>>8) & 0xFF);
temp_port0 = read_apu(0);
write_apu(0, (temp_port0 ^ B00000010) & B11001111);
for(i = 0;i < 100;i++){
if(((read_apu(0) ^ temp_port0) & B00001000) != 0){
return read_apu(1);
}
}
timeout = 1;
Serial.print("Timeout : read_sram_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
return 0;
}
//APU(ユーザープログラム動作中)のDSPから読み込み
unsigned char read_dsp(unsigned int adr)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//DSPから読み込み手順
//1.アドレス0~7bitをAPUのポート2に書き込む
//2.APUのポート0を読み取り、bit5=H(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit1(読み込み要求)を反転させて、APUのポート0に書き込む
//3.APUのポート0を読み取り、bit3(読み込み完了通知)が反転するまで待つ
//4.APUのポート1(データ)のデータを読み取る
unsigned char temp_port0;
int i = 0;
write_apu(2, adr);
temp_port0 = read_apu(0);
write_apu(0, ((temp_port0 ^ B00000010) | B00100000) & B11101111);
for(i = 0;i < 100;i++){
if(((read_apu(0) ^ temp_port0) & B00001000) != 0){
return read_apu(1);
}
}
timeout = 1;
Serial.print("Timeout : read_dsp_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
return 0;
}
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)
void write_sram_st(unsigned int adr, unsigned char *sram_writedata, unsigned int len)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAM_への連続書き込み手順
//1.開始アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.最初のデータをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//5.次のデータをAPUのポート1に書き込む
//6.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント有効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//7.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//8.「5.次のデータをAPUのポート1に書き込む」へ行く。終了する場合はそのまま終了
unsigned char temp_port0;
int i = 0;
write_sram(adr,sram_writedata[0]);//0番目のデータを書いてもまだオートインクリメントはしない。
for(unsigned int j = 0;j < len;j++){//もういちど0番目のデータを書いた後、アドレスをオートインクリメントする。
write_apu(1, sram_writedata[j]);
temp_port0 = read_apu(0);
write_apu(0, ((temp_port0 ^ B00000001) | B00010000) & B11011111);
for(i = 0;i < 200;i++){
if(((read_apu(0) ^ temp_port0) & B00000100) != 0){
break;
}
}
if(i >= 100){
timeout = 1;//タイムアウト検出用
return;
}
}
}
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)(プログラム用フラッシュメモリのデータをAPUへ連続転送する場合)
void write_sram_st_prg(unsigned int adr, unsigned char *sram_writedata, unsigned int len)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAM_への連続書き込み手順
//1.開始アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.最初のデータをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//5.次のデータをAPUのポート1に書き込む
//6.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント有効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//7.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//8.「5.次のデータをAPUのポート1に書き込む」へ行く。終了する場合はそのまま終了
unsigned char temp_port0;
int i = 0;
write_sram(adr,pgm_read_byte_near(sram_writedata));//0番目のデータを書いてもまだオートインクリメントはしない。
for(unsigned int j = 0;j < len;j++){//もういちど0番目のデータを書いた後、アドレスをオートインクリメントする。
write_apu(1, pgm_read_byte_near(sram_writedata + j));
temp_port0 = read_apu(0);
write_apu(0, ((temp_port0 ^ B00000001) | B00010000) & B11011111);
for(i = 0;i < 200;i++){
if(((read_apu(0) ^ temp_port0) & B00000100) != 0){
break;
}
}
if(i >= 100){
timeout = 1;//タイムアウト検出用
return;
}
}
}
//BRRベクタにサンプルベクタ登録
void etr_brr_vct(unsigned char smp_num, unsigned int start_vct_adr, unsigned int loop_vct_adr)
{
//BRRベクタ登録関数
//BRRサンプルデータ開始アドレスとループアドレス、サンプル番号を指定すると、BRRベクタに登録します。
//sample0_start_Lbyte, sample0_start_Hbyte, sample0_loop_Lbyte, sample0_loop_Hbyte
write_sram(BRR_VCT + (smp_num * 4), start_vct_adr & 0xFF);
write_sram(BRR_VCT + (smp_num * 4) + 1, (start_vct_adr >> 8) & 0xFF);
write_sram(BRR_VCT + (smp_num * 4) + 2, loop_vct_adr & 0xFF);
write_sram(BRR_VCT + (smp_num * 4) + 3, (loop_vct_adr >> 8) & 0xFF);
}
//ADSRエンベロープ登録
void etr_adsr_env(unsigned char ch, unsigned char attack, unsigned char decay, unsigned char sustain, unsigned char release)
{
//エンベロープ登録関数(この関数を実行すると自動的にADSRエンベロープが有効になります。)
//エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定
write_dsp(ENV1_X | ch, DSP_ENV_ADSR_EN | ((0x07 & decay) << 4) | (0x0F & attack));
write_dsp(ENV2_X | ch, ((0x07 & sustain) << 5) | (0x1F & release));
}
//音程のセット
void ptc_set(unsigned char ch, unsigned int notenum)
{
//音程レジスタ(PTC_X_L($X2), PTC_X_H($X3))のセット
//MIDIのノート番号からDSPのピッチを求めるテーブルより、ピッチを求めてチャンネルchにセット
write_dsp(PTC_X_L | ch, noteFreq[notenum] & 0xFF);
write_dsp(PTC_X_H | ch,(noteFreq[notenum] >> 8) & 0x3F);
}
//メインの音量とパンのセット
void main_vel_pan_set(unsigned char vel,unsigned char pan)
{
//音量, パン
//メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定
//音量(0~127)とパン(0~127)を乗算して、128で割る(最大出力は127*127/128=126)
//パンは、64で中央、1で左端、127で右端 (0は1として処理)
if(pan == 0){
pan = 1;
}
write_dsp(MAIN_VOL_L, (unsigned char)(((unsigned int)vel * (128 - pan)) >> 7));
write_dsp(MAIN_VOL_R, (unsigned char)(((unsigned int)vel * pan) >> 7));
}
//チャンネルの音量とパンのセット
void vel_pan_set(unsigned char ch, unsigned char vel,unsigned char pan)
{
//チャンネル, 音量, パン
//チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット
//音量(0~127)とパン(0~127)を乗算して、128で割る(最大出力は127*127/128=126)
//パンは、64で中央、1で左端、127で右端 (0は1として処理)
if(pan == 0){
pan = 1;
}
write_dsp(VOL_X_L | ch, (unsigned char)(((unsigned int)vel * (128 - pan)) >> 7));
write_dsp(VOL_X_R | ch, (unsigned char)(((unsigned int)vel * pan) >> 7));
}
//チャンネルノートオン
void note_on(unsigned char ch)
{
//チャンネル(0~8を左4ビットシフトしたもの)
//キーオンレジスタ(KEY_ON($4C))のセット
write_dsp(KEY_OFF, 0);//ふたたびノートオフできるようにする
write_dsp(KEY_ON, 1 << (ch >> 4));
}
//チャンネルノートオフ
void note_off(unsigned char ch)
{
//チャンネル(0~8を左4ビットシフトしたもの)
//キーオフレジスタ(KEY_OFF($5C))のセット
write_dsp(KEY_OFF, 1 << (ch >> 4));
}
//MML演奏用関数/////////////////////////////////////
//簡易MMLデータより音を鳴らす
void mml_play(unsigned char *play_data, unsigned int len)
{
int mml_com[4] = {0, 0, 0, 0 };//mmlコマンド取得用[コード, 数値1, 時間, 数値2(音長表現のドットの数)]
int mml_st[MML_MAX_TR];//mml演奏状態(0でmml読み取り終了、1でmml読み取り中)、チャンネルごとに保存
char mml_tie[MML_MAX_TR];//タイ有効無効フラグ[^]
char mml_oct[MML_MAX_TR];//オクターブ[o]
char mml_def_dly[MML_MAX_TR];//デフォルト音調[l]
char mml_pan[MML_MAX_TR];//パン[p]
char mml_vel[MML_MAX_TR];//ベロシティ[v]
double mml_next_tim[MML_MAX_TR];//次のMMLコマンド実行時間(絶対時間)
int mml_st_sum = 0;//トラック演奏状態チェック用(フラグを加算して0になったらすべてのトラックの演奏が終了)
////////////////トラック数の取得と各トラック先頭位置の取得//////
unsigned int tr_pos[MML_MAX_TR];//トラック先頭位置保存用(先頭位置)
int tr_num = get_mml_tr_pos(mml_data, mml_data_len, MML_MAX_TR, tr_pos);//各トラック先頭位置をtr_posに入れ、トラック数を返す
//トラックごとの配列初期設定
for(int mml_setup = 0;mml_setup < tr_num; mml_setup++){
mml_st[mml_setup] = 1;//mml演奏状態フラグ(0でmml読み取り終了、1でmml読み取り中)
mml_tie[mml_setup] = 0;//タイ状態有効無効フラグ
mml_oct[mml_setup] = 4;//オクターブ
mml_def_dly[mml_setup] = 4;//デフォルト音調(四分音符)
mml_next_tim[mml_setup] = 0;//最初のMMLコマンドは無条件で読み取る。
mml_pan[mml_setup] = 64;//パン[p]
mml_vel[mml_setup] = 64;//ベロシティ[v]
}
//Serial.print("tr_num:");//デバッグ用
//Serial.print(tr_num);//デバッグ用
//Serial.print("\r\n");//デバッグ用
mml_time_ms = 0;//MML演奏用時間msカウンタ初期化
mml_time_cnt_en = 1;//MML演奏用時間カウント_開始
while(1){//すべてのトラックが演奏終了のとき、mml演奏関数終了
mml_st_sum = 0;//トラック演奏状態チェック用初期化
//トラックごと処理
for(int mml_st_tr = 0;mml_st_tr < tr_num; mml_st_tr++){
//Serial.print("mml_st[mml_st_tr]:");//デバッグ用
//Serial.print(mml_st[mml_st_tr]);//デバッグ用
//Serial.print("\r\n");//デバッグ用
if(mml_st[mml_st_tr] == 0){//演奏が終了したトラックは無視
continue;
}
//Serial.print("mml_time:");//デバッグ用
//Serial.print(mml_time);//デバッグ用
//Serial.print("mml_next_tim[mml_st_tr]:");//デバッグ用
//Serial.print(mml_next_tim[mml_st_tr]);//デバッグ用
//Serial.print("\r\n");//デバッグ用
if((unsigned long)mml_next_tim[mml_st_tr] <= (unsigned long)mml_time){ //次回読み取り時間カウンタが時間カウンタの値以上になったらMMLコードを読み取る。
////////////////////////////
//mmlコマンド取得(pos自動インクリメント)
get_mml_com(play_data, len, (tr_pos + mml_st_tr), mml_com);//返しは、[コード, 数値1, 時間, 数値2(音長表現のドットの数)]の配列ポインタ
if(mml_com[2] == -2){//MMLの音調が省略されていたらデフォルト音長をセット
mml_com[2] = mml_def_dly[mml_st_tr];
}
//MMLコマンド表示(多少不安定になります)//デバッグ用
/*Serial.print("ch:");
Serial.print(mml_st_tr);
Serial.print("_pos:");
Serial.print(tr_pos[mml_st_tr]);
Serial.print("_");
Serial.write(mml_com[0]);
Serial.print(mml_com[1]);
Serial.print("_dlytim:");
Serial.print(mml_com[2]);
Serial.print("_dot:");
Serial.print(mml_com[3]);
Serial.print("\r\n");*/
switch(mml_com[0]){
case 'n'://ノートオン
if(mml_tie[mml_st_tr] == 0){//タイ無効の時音を鳴らす
ptc_set(mml_st_tr << 4, ((mml_oct[mml_st_tr] + brr_oct[mml_st_tr]) * 12) + mml_com[1]);//チャンネル, 音程
note_on(mml_st_tr << 4);//ノートオン
}
mml_tie[mml_st_tr] = 0;//タイ無効
break;
case 'r'://ノートオフ
note_off(mml_st_tr << 4);
break;
case 'o'://オクターブセット
mml_oct[mml_st_tr] = mml_com[1];
break;
case '>'://オクターブ下げ
mml_oct[mml_st_tr]--;
break;
case '<'://オクターブ上げ
mml_oct[mml_st_tr]++;
break;
case '^'://タイ
mml_tie[mml_st_tr] = 1;//タイ有効
break;
case 'l'://デフォルト音長
mml_def_dly[mml_st_tr] = mml_com[1];//音長
break;
case 'v'://ベロシティ
mml_vel[mml_st_tr] = mml_com[1];
vel_pan_set(mml_st_tr << 4, mml_vel[mml_st_tr], mml_pan[mml_st_tr]);//チャンネル, 音量, パン
break;
case 'p'://パン
mml_pan[mml_st_tr] = mml_com[1];
vel_pan_set(mml_st_tr << 4, mml_vel[mml_st_tr], mml_pan[mml_st_tr]);//チャンネル, 音量, パン
break;
case 't'://テンポ
tempo = mml_com[1];
break;
case ';'://トラック終端
mml_st[mml_st_tr] = 0;//現在のトラックは演奏終了
break;
default:
break;
}
//時間計算
if(mml_com[2] == 0){
//待ち時間なし
}else{
//delay(500);
//待ち時間計算(ms)
double dly_tim_dot_sum = 0;
for(int d_time = 0; d_time <= mml_com[3];d_time++)//mmlのドットの数分ディレイを入れる
{
dly_tim_dot_sum += (double)(9.0*8192.0 / (mml_com[2]*pow(2, d_time)));
//delay((int)((2000.0 / (mml_com[2]*pow(2, d_time))) * (120.0 / tempo)));
}
//次回のMMLコマンド実行時間を加算する
mml_next_tim[mml_st_tr] += dly_tim_dot_sum;
}
////////////////////////////
}
mml_st_sum += mml_st[mml_st_tr];//トラック演奏状態チェック用(フラグを加算して0になったらすべてのトラックの演奏が終了)
//Serial.print("mml_st_sum:");//デバッグ用
//Serial.print(mml_st_sum);//デバッグ用
//Serial.print("\r\n");//デバッグ用
}
//トラック演奏状態チェック
if(mml_st_sum == 0){//すべてのトラックが演奏終了のとき、mml演奏関数終了
break;//mml演奏関数終了
}
}
mml_time_cnt_en = 0;//MML演奏用時間カウント_停止
}
//簡易mmlデータ1バイト取得
int get_mml_data(unsigned char *mml_data, unsigned int len, unsigned int pos)
{
//posは0~(len - 1)までを指定
//データ範囲外で -1 を返す。
if(pos >= len){
return -1;
}
//return mml_data[pos]; //MMLデータをSRAMに配置した時
return pgm_read_byte_near(mml_data + pos); //MMLデータをPROGMEMに配置した時
}
//mmlコマンド1つ取得
//返しは、[コード, 数値1, 時間, 数値2(音長表現のドットの数)]の配列ポインタ
void get_mml_com(unsigned char *mml_data, unsigned int len, int *pos, int *ret_data)
{
unsigned int pos_temp = *pos;//読み取り位置を記憶
unsigned char temp_get_mml_byte;
int ret_code = 0;
int ret_num1 = 0;
int ret_tim = 0;
int ret_num2 = 0;
switch(get_mml_data(mml_data, len, pos_temp)){
case 'b'://
ret_num1 += 2;
case 'a'://
ret_num1 += 2;
case 'g'://
ret_num1 += 2;
case 'f'://
ret_num1 += 1;
case 'e'://
ret_num1 += 2;
case 'd'://
ret_num1 += 2;
case 'c'://
if(get_mml_data(mml_data, len, pos_temp + 1) == '+'){
pos_temp++;
ret_num1 += 1;//半音上げ
}else if(get_mml_data(mml_data, len, pos_temp + 1) == '-'){
pos_temp++;
ret_num1 -= 1;//半音下げ
}
pos_temp++;
ret_code = 'n';
ret_tim = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
if(ret_tim == 0){//音長が省略されていたら
ret_tim = -2;
}
break;
case 'r'://ノートオフ
ret_code = 'r';
pos_temp++;
ret_tim = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
if(ret_tim == 0){//音長が省略されていたら
ret_tim = -2;
}
break;
case '<'://
ret_code = (int)'<';
pos_temp++;
break;
case '>'://
ret_code = (int)'>';
pos_temp++;
break;
case '^'://タイ
ret_code = (int)'^';
pos_temp++;
break;
case 'o'://音程
ret_code = (int)'o';
pos_temp++;
ret_num1 = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
break;
case 'v'://ベロシティ
ret_code = (int)'v';
pos_temp++;
ret_num1 = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
break;
case 'p'://ベロシティ
ret_code = (int)'p';
pos_temp++;
ret_num1 = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
break;
case 'l'://デフォルト音長
ret_code = (int)'l';
pos_temp++;
ret_num1 = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
break;
case '['://ループ開始(使用不可)
pos_temp++;
break;
case ']'://ループ終了(使用不可)
pos_temp++;
break;
case 't'://テンポ
ret_code = (int)'t';
pos_temp++;
ret_num1 = get_mml_num(mml_data, len, &pos_temp, &ret_num2);//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
break;
case ';'://終端
ret_code = ';';
//pos_temp=len;
break;
default://未定義コマンドが来るとストップ
break;
}
*pos = pos_temp;//戻し用現在の読み取り位置更新
ret_data[0] = ret_code;
ret_data[1] = ret_num1;
ret_data[2] = ret_tim;
ret_data[3] = ret_num2;
}
//mmlデータから数値を返す
//mmlデータ配列, mmlデータ配列の長さ, 読み取り位置のポインタ, ドットの数戻り値用ポインタ
int get_mml_num(unsigned char *mml_data, unsigned int len, int *pos, int *dp_num)
{
//※読み取り位置は配列外になっても加算されます。
unsigned int pos_temp = *pos;//読み取り位置を記憶
int keta = 0;//読み取る数値の桁数(0で数字以外,1で1桁,2で2桁)の読み取り位置インクリメント用
int ret_num = 0;//戻り値用数字
int ret_dp_num = 0;//ドットの数戻り値用
int mml_data_byte = get_mml_data(mml_data, len, pos_temp);
//数値読み取り
while((mml_data_byte >= '0') && (mml_data_byte <= '9')){
//ret_num += (mml_data_byte - '0')*pow(10, keta);//数字と桁数の乗算値を加算
ret_num = ret_num * 10 + (mml_data_byte - '0');
keta++;
mml_data_byte = get_mml_data(mml_data, len, pos_temp + keta);
}
//ドットの数を数える
mml_data_byte = get_mml_data(mml_data, len, pos_temp + keta);
while(mml_data_byte == '.'){
ret_dp_num++;
mml_data_byte = get_mml_data(mml_data, len, pos_temp + keta + ret_dp_num);
}
*pos += keta + ret_dp_num;//現在の読み取り位置に読み取った数分、加算
*dp_num = ret_dp_num;//ドットの数書き換え
return ret_num;
}
//mmlデータ配列内のトラック数をカウントする。(「;」の数)
int get_mml_tr_num(unsigned char *mml_data, unsigned int len)
{
int get_data_temp = 0;
unsigned int tr_cnt = 0;//トラック数カウント
for(int c_tr = 0; c_tr < len; c_tr++){
get_data_temp = get_mml_data(mml_data, len, c_tr);
if(get_data_temp == ';'){
tr_cnt++;//「;」の数カウント
}else if((get_data_temp == 0) || (get_data_temp == -1)){
//終端判定
//文字列配列終端(0x00)か配列サイズ外でデータ終了
break;
}
}
return tr_cnt;
}
//mmlデータ配列内のトラック先頭位置配列に、トラック数分の先頭位置を入れる
//mmlデータ配列, mmlデータ配列の長さ, 取得トラック数, トラック先頭位置配列の戻り値用ポインタ
//戻り値は、先頭位置取得トラック数
int get_mml_tr_pos(unsigned char *mml_data, unsigned int len, unsigned int tr_max, int *pos_tr)
{
int get_data_temp = 0;
unsigned int tr_cnt = 0;//トラック数カウント
unsigned int get_temp_tr_pos = 0;//トラック先頭アドレス一時保存
for(int c_tr = 0; c_tr < len; c_tr++){
get_data_temp = get_mml_data(mml_data, len, c_tr);
if(get_data_temp == ';'){
pos_tr[tr_cnt] = get_temp_tr_pos;//前回の「;」の次のアドレスを入れる
get_temp_tr_pos = c_tr + 1;
tr_cnt++;//「;」の数カウント
}
if((get_data_temp == 0) || (get_data_temp == -1) || (tr_max <= tr_cnt)){
//終端判定
//文字列配列終端(0x00)か配列サイズ外でデータ終了
break;
}
}
return tr_cnt;
}
SNESのAPUの演奏テスト#よいまちカンターレ#SPC700 pic.twitter.com/OL3QkyI7Xk
— oy (@0x6f_0x79) January 3, 2022
・MIDIを受信する
最後に、MIDIを受信するプログラムを追加して完成です。
・回路図
MIDIを受信するための回路を追加しました。
MIDI規格推奨応用事例(RP)/MIDI規格追記事項(CA)
APU内のDSPのバージョンは、「S-DSP」や「S-DSP A」などの種類がありますが、どちらも同じ回路で演奏できます。
[2022/5/30] 追記 : 回路図中のフォトカプラの記号にミスがあったので、修正しました。
※1個の場合、APUの「PA6_CS1」はArduinoに接続せず、5V直結でもかまいません。
2個以上のAPUを使う場合の回路
[2022/5/30 追記] : 回路図中のフォトカプラの記号にミスがあったので、修正しました。
・プログラム
今回作成したMIDIを受信するプログラムは、音程によってBRRサンプルを変更しています。
デモで使用したBRRサンプルでは、ピッチが若干ずれているので気になる方は、音程テーブルを修正して利用してください。
また、APUを複数接続して発音チャンネルを増やせるようにしました。
エコーの有効化もできます。
//SPC700でMIDI演奏プログラム
//©oy
//https://oykenkyu.blogspot.com/2021/10/spc700-midi.html
#include "avr/io.h"
#include "avr/interrupt.h"
#define DB_MESS 0 //デバッグメッセージ有効無効
#define AUTO_RESET 0 //APUユーザプログラム書き込み失敗時にリセット有効無効
#define XTAL 16000000//水晶振動子の周波数(MIDI受信速度に影響)
//プログラムの変更をせずに、Arduinoの水晶振動子を19.660MHzに変更すると、38400bpsで受信できます。
//#define SERIALSPEED_DEF 38400 //UARTのボーレート
#define SERIALSPEED_DEF 31250//ボーレート(MIDIは31250bps)
#define SERIALSPEED ((long)((long long)SERIALSPEED_DEF * 16000000.0 / XTAL))
//受信バッファサイズを64バイトから256バイトにするとMIDI受信バッファあふれを抑制できるかも
//[C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino\HardwareSerial.h]内の「#define SERIAL_RX_BUFFER_SIZE 64」を「#define SERIAL_RX_BUFFER_SIZE 256」にする
//SNES_APUの数(1基で8チャンネル分発音できます)
#define APU_NUM 1
//MIDI演奏最大トラック数(SNESのAPUは1器で8チャンネル分発音できます。)
#define MIDI_MAX_TR 8
//SNES_APUの~csピン番号(連番)
#define APU_CS_PIN 16//16,17,18,19
//ユーザープログラムのサイズ(Byte)
#define PROGSIZE 172
//SPC700プログラム書き始めアドレス
#define WRITE_ADR 0x0200
//SPC700プログラム開始アドレス
#define START_ADR 0x0200
//BRRサンプルベクタテーブルアドレス
//このアドレスは、BRRサンプルベクタの場所を示しています。
#define BRR_VCT 0x0700
//エコー用メモリ領域の先頭アドレス(※エコーバッファが小さいと、ユーザプログラム領域やBBRの書き換えが発生する可能性があります。)
#define ECHO_ADR 0x8700
//DSPレジスタ名
#define VOL_X_L 0x00
#define VOL_X_R 0x01
#define PTC_X_L 0x02
#define PTC_X_H 0x03
#define SRCN_X 0x04
#define ENV1_X 0x05
#define ENV2_X 0x06
#define GAIN_X 0x07
#define DSP_ENVX_X 0x08
#define DSP_OUTX_X 0x09
#define MAIN_VOL_L 0x0C
#define MAIN_VOL_R 0x1C
#define ECHO_VOL_L 0x2C
#define ECHO_VOL_R 0x3C
#define KEY_ON 0x4C
#define KEY_OFF 0x5C
#define DSP_CONF 0x6C
#define END_SMPL 0x7C
#define ECHO_FB 0x0D
#define PTC_MD 0x2D
#define NOISE_EN 0x3D
#define ECHO_EN 0x4D
#define SR_DIR 0x5D
#define ECHO_BUF_ADR 0x6D
#define ECHO_DLY 0x7D
#define ECHO_FIR_X 0x0F
//DSPチャンネル名
#define CH0 0x00
#define CH1 0x10
#define CH2 0x20
#define CH3 0x30
#define CH4 0x40
#define CH5 0x50
#define CH6 0x60
#define CH7 0x70
//DSPレジスタのビット
#define DSP_RESET_EN 0x80
#define DSP_MUTE_EN 0x40
#define DSP_ECHO_DEN 0x20
#define DSP_ENV_ADSR_EN 0x80
char timeout = 0;//APU書き込みタイムアウト検出用
char echo_en = 1;//エコー有効
//エコー遅延量(4bitで指定、指定値*16msの遅延が得られます。ただし、遅延量が大きいとエコーバッファも大きくしなければいけません。エコーバッファがBRRサンプルデータやユーザプログラムを破壊しないように注意してください。)
char echo_delay = 12;
char echo_fg = 64;//エコーフィードバックゲイン(大きくし過ぎると発振します。)
char echo_vel_l = 16;//エコー左音量
char echo_vel_r = 16;//エコー右音量
char echo_fir[8] = {127, 0, 0, 0, 0, 0, 0, 0};//エコーFIRフィルタ係数
//デフォルトオクターブ[sample0,sample1,sample2,…,]
//増やすとオクターブが高くなります。(出力オクターブ = brr_oct + (mmlのオクターブ))
int brr_oct[4] = {3, 2, 1, 0};
int brr_tone[4] = {-2, 3, -4, 1};
//MIDI受信用
char midi_main_vel[16];//メインベロシティ(midiチャンネルが配列要素順)
char midi_pan[16];//パン(midiチャンネルが配列要素順)
char midi_vel[MIDI_MAX_TR];//ベロシティ(APUチャンネルが配列要素順)
unsigned char midi_buf[256];
int ex_mess_en=0; //midi_read()で使用
int dat_ph=0; //midi_read()で使用
unsigned char read_buf_h; //midi_read()で使用
int stop_byte=0; //midi_read()で使用
unsigned char read_buf=0; //midi_read()で使用
int comp_read_buf=0; //midi_read()で使用
unsigned char running_ch=0x90;//ランニングステータスチャンネル
unsigned char midinote_tn[16]; //MIDIノートオン音程保存
unsigned char midinote_ch[16]; //MIDIノートオンチャンネル保存
unsigned char midinote[16]; //MIDIノートオン判定保存
unsigned int midinote_off[16]; //MIDIノートオフ時配列計算用保存
unsigned char midiprog[16]; //MIDIプログラムチェンジ保存
unsigned char sas_en = 0; //サスティン有効フラグ
//ユーザープログラム
//SRAM・DSPとマイコン間データ転送プログラム
//プログラム詳細
//https://oykenkyu.blogspot.com/2021/10/spc700-midi.html
unsigned char progdata[PROGSIZE] = {
0x8F, 0x00, 0x00, 0x8F, 0x00, 0x01, 0x8F, 0x00, 0xF5, 0x8F, 0x00, 0xF6, 0x8F, 0x00, 0xF7, 0xE4,
0xF4, 0x28, 0x3F, 0xC4, 0x04, 0xFA, 0x04, 0xF4, 0xE4, 0xF4, 0x44, 0x04, 0x28, 0x01, 0xD0, 0x0A,
0xE4, 0xF4, 0x44, 0x04, 0x28, 0x02, 0xD0, 0x43, 0x2F, 0xEE, 0xE4, 0xF4, 0x28, 0x10, 0xF0, 0x0A,
0x18, 0x10, 0x04, 0x3F, 0x50, 0x02, 0x3A, 0x00, 0x2F, 0x0A, 0x38, 0xEF, 0x04, 0xBA, 0xF6, 0xDA,
0x00, 0x3F, 0x50, 0x02, 0x58, 0x05, 0x04, 0xBA, 0x00, 0xDA, 0xF6, 0xFA, 0x04, 0xF4, 0x2F, 0xC8,
0xE4, 0xF4, 0x28, 0x20, 0xF0, 0x0B, 0x18, 0x20, 0x04, 0xFA, 0x00, 0xF2, 0xFA, 0xF5, 0xF3, 0x2F,
0x09, 0x38, 0xDF, 0x04, 0x8D, 0x00, 0xE4, 0xF5, 0xD7, 0x00, 0x6F, 0xE4, 0xF4, 0x28, 0x10, 0xF0,
0x0A, 0x18, 0x10, 0x04, 0x3F, 0x91, 0x02, 0x3A, 0x00, 0x2F, 0x0A, 0x38, 0xEF, 0x04, 0xBA, 0xF6,
0xDA, 0x00, 0x3F, 0x91, 0x02, 0x58, 0x0A, 0x04, 0xBA, 0x00, 0xDA, 0xF6, 0xFA, 0x04, 0xF4, 0x2F,
0x87, 0xE4, 0xF4, 0x28, 0x20, 0xF0, 0x0B, 0x18, 0x20, 0x04, 0xFA, 0x00, 0xF2, 0xFA, 0xF3, 0xF5,
0x2F, 0x09, 0x38, 0xDF, 0x04, 0x8D, 0x00, 0xF7, 0x00, 0xC4, 0xF5, 0x6F
};
//BRRサンプル0
//piano1
//著作権の関係上ここには同胞していません。
//演奏デモでは「https://www.smwcentral.net/?p=section&a=details&id=17248」の"piano1 ---g2.brr"を使用
//上記brrデータの先頭2バイトは、ループ先アドレスを示すので、3バイト目以降を下の配列に入れてください。
unsigned int brrsample0_len = 5634;//BRRサンプル0サイズ
const unsigned char PROGMEM brrsample0[5634] = {
};
//BRRサンプル1
//piano1
//著作権の関係上ここには同胞していません。
//演奏デモでは「https://www.smwcentral.net/?p=section&a=details&id=17248」の"piano2 g#2-d#3.brr"を使用
//上記brrデータの先頭2バイトは、ループ先アドレスを示すので、3バイト目以降を下の配列に入れてください。
unsigned int brrsample1_len = 5634;//BRRサンプル1サイズ
const unsigned char brrsample1[5634] PROGMEM = {
};
//BRRサンプル2
//piano
//著作権の関係上ここには同胞していません。
//演奏デモでは「https://www.smwcentral.net/?p=section&a=details&id=17248」の"piano5 g#4-b4.brr"を使用
//上記brrデータの先頭2バイトは、ループ先アドレスを示すので、3バイト目以降を下の配列に入れてください。
unsigned int brrsample2_len = 5634;//BRRサンプル2サイズ
const unsigned char PROGMEM brrsample2[5634] = {
};
//BRRサンプル3
//piano1
//著作権の関係上ここには同胞していません。
//演奏デモでは「https://www.smwcentral.net/?p=section&a=details&id=17248」の"piano6 c5-f5.brr"を使用
//上記brrデータの先頭2バイトは、ループ先アドレスを示すので、3バイト目以降を下の配列に入れてください。
unsigned int brrsample3_len = 5634;//BRRサンプル3サイズ
const unsigned char PROGMEM brrsample3[5634] = {
};
//MIDIのノート番号からDSPのピッチを求めるテーブル(BRRサンプリング周波数32000Hzで440Hz(A)の音程の楽器を録音)
const unsigned int noteFreq[128] = {
//A, A#, B, C, C#, D, D#, E, F, F#, G, G#
/**/ /**/ /**/ 76, 81, 85, 91, 96, 102, 108, 114, 121,
128, 136, 144, 152, 161, 171, 181, 192, 203, 215, 228, 242,
256, 271, 287, 304, 323, 342, 362, 384, 406, 431, 456, 483,
512, 542, 575, 609, 645, 683, 724, 767, 813, 861, 912, 967,
1024, 1085, 1149, 1218, 1290, 1367, 1448, 1534, 1625, 1722, 1825, 1933,
2048, 2170, 2299, 2435, 2580, 2734, 2896, 3069, 3251, 3444, 3649, 3866,
4096, 4340, 4598, 4871, 5161, 5468, 5793, 6137, 6502, 6889, 7298, 7732,
8192, 8679, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596, 15464,
16383, 8679, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596, 15464,
16383, 8679, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596, 15464,
16383, 8679, 9195, 9742, 10321, 10935, 11585, 12274, 13004, 13777, 14596
};
/////////////////////////////関数
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr, unsigned char cs);
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata, unsigned char cs);
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len, unsigned char cs);
//~~ユーザプログラム用関数/////////////////////////////
//~~APU-ATmega328p間転送系関数
//APU(ユーザープログラム動作中)のSRAMへ書き込み
void write_sram(unsigned int adr, unsigned char sram_writedata, unsigned char cs);
//APU(ユーザープログラム動作中)のDSPへ書き込み
void write_dsp(unsigned char adr, unsigned char dsp_writedata, unsigned char cs);
//APU(ユーザープログラム動作中)のSRAMから読み込み
unsigned char read_sram(unsigned int adr, unsigned char cs);
//APU(ユーザープログラム動作中)のDSPから読み込み
unsigned char read_dsp(unsigned int adr, unsigned char cs);
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)
void write_sram_st(unsigned int adr, unsigned char *sram_writedata, unsigned int len, unsigned char cs);
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)(プログラム用フラッシュメモリのデータをAPUへ連続転送する場合)
void write_sram_st_prg(unsigned int adr, unsigned char *sram_writedata, unsigned int len, unsigned char cs);
//~~DSPに書き込み系関数
//BRRベクタにサンプルベクタ登録
void etr_brr_vct(unsigned char smp_num, unsigned int start_vct_adr, unsigned int loop_vct_adr, unsigned char cs);
//ADSRエンベロープ登録
void etr_adsr_env(unsigned char ch, unsigned char attack, unsigned char decay, unsigned char sustain, unsigned char release, unsigned char cs);
//音程のセット
void ptc_set(unsigned char ch, unsigned int notenum, unsigned char cs);
//メインの音量とパンのセット
void main_vel_pan_set(unsigned char vel,unsigned char pan, unsigned char cs);
//チャンネルの音量とパンのセット
void vel_pan_set(unsigned char ch, unsigned char vel,unsigned char pan, unsigned char cs);
//チャンネルノートオン
void note_on(unsigned char ch, unsigned char cs);
//チャンネルノートオフ
void note_off(unsigned char ch, unsigned char cs);
//MIDI受信関係
//1バイトずつmidi_readを実行し、1グループのmidiメッセージを受信し終えるとmidi_comを実行
void midi_read(char read_buf);
//MIDIデータ入力処理
inline void midi_com(unsigned char *in_midi_mess);
//セットアップ
void setup()
{
//デバッグメッセージ用
Serial.begin(SERIALSPEED);//シリアル通信開始
Serial.print("\r\npwr_on_ok\r\n");
//APU関係
PORTB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
DDRB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
pinMode(14, OUTPUT); //pin14_APU_reset
digitalWrite(14, HIGH);//pin14_APU_reset
//APUリセット
digitalWrite(14, LOW);
delay(10);
digitalWrite(14, HIGH);
for(int apu_cs = 0; apu_cs < APU_NUM;apu_cs++)
{
timeout = 0;
pinMode(APU_CS_PIN + apu_cs, OUTPUT); //pin16,17,18..._APU_cs
digitalWrite(APU_CS_PIN + apu_cs, HIGH);//pin16,17,18..._APU_cs
//SPC700プログラム書き込み
prog_write(WRITE_ADR, progdata, PROGSIZE, apu_cs);
if(timeout != 0){
Serial.print("APU");
Serial.print(apu_cs);
Serial.print("_not found\r\n");
delay(100);
continue;
}
//SPC700ユーザープログラム開始
write_apu(3, START_ADR >> 8, apu_cs);
write_apu(2, START_ADR & 0xFF, apu_cs);
write_apu(1, 0x00, apu_cs);//SPC700ユーザープログラム開始モード
write_apu(0, PROGSIZE + 1, apu_cs);//データ転送終了&ユーザープログラム開始
Serial.print("APU");
Serial.print(apu_cs);
Serial.print("_Start user program\r\n");
delay(100);//SPC700ユーザープログラム開始までの待ち時間
//メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))を0にする
main_vel_pan_set(0,64, apu_cs);//音量, パン
}
//SPC700でユーザープログラムを実行しているため、
//SRAMアドレス中の
//$0000,$0001,$0004 (変数用領域)
//$00F4 ~ $00F7 (APUポート領域)
//$0100 ~ $01FF (スタック領域)
//$0200 ~ $0300 (ユーザープログラム領域)
//これらのアドレスへのデータの書き込みは禁止です。
//※SPC700のユーザープログラム側で、これらのアドレスへの書き込みのプロテクトはしていません。書き込まないように注意してください。
//MIDI受信用変数初期化
for(int i=0;i<MIDI_MAX_TR;i++){
midi_vel[i] = 100;//ベロシティ
}
for(int i=0;i<16;i++){
midi_main_vel[i] = 100;//メインベロシティ
midi_pan[i] = 64;//パン
}
}
void loop()
{
for(int apu_cs = 0; apu_cs < APU_NUM;apu_cs++){
timeout = 0;
//①BRR圧縮されたデータをSRAM上に配置
//BRRをAPUのSRAMへ配置します。
unsigned int sample0_adr =0x1500;
unsigned int sample1_adr =0x1500 + brrsample0_len + 1;
unsigned int sample2_adr =0x1500 + brrsample0_len + brrsample1_len + 2;
unsigned int sample3_adr =0x1500 + brrsample0_len + brrsample1_len + brrsample2_len + 3;
write_sram_st_prg(sample0_adr, brrsample0, brrsample0_len, apu_cs);//配置開始アドレス,配列(ATmega328pのプログラムメモリ上)の先頭アドレス,配列サイズ
write_sram_st_prg(sample1_adr, brrsample1, brrsample1_len, apu_cs);//配置開始アドレス,配列(ATmega328pのプログラムメモリ上)の先頭アドレス,配列サイズ
write_sram_st_prg(sample2_adr, brrsample2, brrsample2_len, apu_cs);//配置開始アドレス,配列(ATmega328pのプログラムメモリ上)の先頭アドレス,配列サイズ
write_sram_st_prg(sample3_adr, brrsample3, brrsample3_len, apu_cs);//配置開始アドレス,配列(ATmega328pのプログラムメモリ上)の先頭アドレス,配列サイズ
//②BRRサンプルデータソースディレクトリアドレスに、BRR開始ブロックアドレスとループアドレスを登録(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte),…,サンプルPCM_N開始ブロックアドレス(2Byte),サンプルPCM_Nループアドレス(2Byte)の順に配置)
//BRRサンプルアドレス登録関数で開始アドレスとループアドレスを指定。
etr_brr_vct(0, sample0_adr, sample0_adr + (unsigned int)0x14E2, apu_cs);//サンプル番号, 開始アドレス(2バイト), ループアドレス(2バイト)(ステレオサンプル数をNとするときで、ループ先サンプル点LNとするとき、ループアドレスは((LN+1)/2)*(9/8)となります。)
etr_brr_vct(1, sample1_adr, sample1_adr + (unsigned int)0x1545, apu_cs);//サンプル番号, 開始アドレス(2バイト), ループアドレス(2バイト)(ステレオサンプル数をNとするときで、ループ先サンプル点LNとするとき、ループアドレスは((LN+1)/2)*(9/8)となります。)
etr_brr_vct(2, sample2_adr, sample2_adr + (unsigned int)0x15C3, apu_cs);//サンプル番号, 開始アドレス(2バイト), ループアドレス(2バイト)(ステレオサンプル数をNとするときで、ループ先サンプル点LNとするとき、ループアドレスは((LN+1)/2)*(9/8)となります。)
etr_brr_vct(3, sample3_adr, sample3_adr + (unsigned int)0x1584, apu_cs);//サンプル番号, 開始アドレス(2バイト), ループアドレス(2バイト)(ステレオサンプル数をNとするときで、ループ先サンプル点LNとするとき、ループアドレスは((LN+1)/2)*(9/8)となります。)
//③BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00に固定されています。)
write_dsp(SR_DIR, BRR_VCT >> 8, apu_cs);//SR_DIRに指定するソースディレクトリアドレスは上位8bitなので8ビット右へシフトする。
//④BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号PCM_Nを指定
write_dsp(SRCN_X | CH0, 1, apu_cs);
write_dsp(SRCN_X | CH1, 1, apu_cs);
write_dsp(SRCN_X | CH2, 1, apu_cs);
write_dsp(SRCN_X | CH3, 1, apu_cs);
write_dsp(SRCN_X | CH4, 1, apu_cs);
write_dsp(SRCN_X | CH5, 1, apu_cs);
write_dsp(SRCN_X | CH6, 1, apu_cs);
write_dsp(SRCN_X | CH7, 1, apu_cs);
//⑤エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定
for(int j=0;j<MIDI_MAX_TR;j++){
etr_adsr_env(j << 4, 15, 1, 4, 8, apu_cs);//チャンネル(4ビット左シフトしたもの限定),Attack ,Decay ,Sustain ,Release
}
//⑥メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定
main_vel_pan_set(127,64, apu_cs);//音量, パン
//未使用のレジスタの初期化
write_dsp(ECHO_VOL_L, 0, apu_cs);//エコー左音量0
write_dsp(ECHO_VOL_R, 0, apu_cs);//エコー右音量0
write_dsp(ECHO_EN, 0, apu_cs);//エコー無効
write_dsp(PTC_MD, 0, apu_cs);//ピッチモジュレーション無効
write_dsp(NOISE_EN, 0, apu_cs);//ノイズ無効
//一応キーオフもする
write_dsp(KEY_OFF, 0xFF, apu_cs);//全チャンネルキーオフ
write_dsp(KEY_OFF, 0x00, apu_cs);//全チャンネルのキーオフフラグをもどす
///////エコー関係
if(echo_en == 1){
//エコー有効
write_dsp(ECHO_BUF_ADR, ECHO_ADR >> 8, apu_cs);//エコーバッファ先頭アドレス
write_dsp(ECHO_DLY, echo_delay, apu_cs);//エコー遅延量(4bitで指定、指定値*16msの遅延が得られます。ただし、遅延量が大きいとエコーバッファも大きくしなければいけません。)
write_dsp(ECHO_FB, echo_fg, apu_cs);//エコーフィードバックゲイン
write_dsp(ECHO_VOL_L, echo_vel_l, apu_cs);//エコー左音量
write_dsp(ECHO_VOL_R, echo_vel_r, apu_cs);//エコー右音量
//エコーFIRフィルタ係数セット
for(char efir = 0;efir < 8;efir++)
{
write_dsp(ECHO_FIR_X | (efir << 4), echo_fir[efir], apu_cs);
}
write_dsp(ECHO_EN, 0xFF, apu_cs);//エコー全チャンネル有効
}
//⑦DSP設定レジスタ(DSP_CONF($6C))の設定(DSPリセット処理も行う)
write_dsp(DSP_CONF, DSP_RESET_EN | DSP_ECHO_DEN, apu_cs);//DSPリセット状態、エコー無効
write_dsp(DSP_CONF, ((echo_en == 1) ? 0 : DSP_ECHO_DEN), apu_cs);//DSPリセット解除
//音量初期設定
//①チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット
vel_pan_set(CH0, 64, 64, apu_cs);//チャンネル0, 音量, パン
vel_pan_set(CH1, 64, 64, apu_cs);//チャンネル1, 音量, パン
vel_pan_set(CH2, 64, 64, apu_cs);//チャンネル2, 音量, パン
vel_pan_set(CH3, 64, 64, apu_cs);//チャンネル3, 音量, パン
vel_pan_set(CH4, 64, 64, apu_cs);//チャンネル4, 音量, パン
vel_pan_set(CH5, 64, 64, apu_cs);//チャンネル5, 音量, パン
vel_pan_set(CH6, 64, 64, apu_cs);//チャンネル6, 音量, パン
vel_pan_set(CH7, 64, 64, apu_cs);//チャンネル7, 音量, パン
}
Serial.print("DSP setup ok\r\n");
while (1)
{
if (Serial.available() > 0)
{
midi_read(Serial.read());//イベント処理
}
}
}
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr, unsigned char cs)
{
unsigned char readdata;
DDRB = 0x0F;//arduino_data_pin_input
DDRD &= ~0xFC;//arduino_data_pin_input
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
digitalWrite(APU_CS_PIN + cs, HIGH);//APU_CS
delayMicroseconds(1);
digitalWrite(10, LOW);//pin10_APU_RD
delayMicroseconds(1);
readdata = ((PINB & 0x30) >> 4) | (PIND & 0xFC);
digitalWrite(10, HIGH);//pin10_APU_RD
digitalWrite(APU_CS_PIN + cs, LOW);//APU_CS
return readdata;
}
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata, unsigned char cs)
{
DDRB = 0x3F;//arduino_data_pin_output
DDRD |= 0xFC;//arduino_data_pin_output
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
PORTD = (~0xFC & PORTD) | (writedata & 0xFC); //APU_data_write
PORTB = (~0x30 & PORTB) | (((writedata & 0x03) << 4) & 0x30); //APU_data_write
digitalWrite(APU_CS_PIN + cs, HIGH);//APU_CS
delayMicroseconds(1);
digitalWrite(11, LOW);//pin11_APU_WE
delayMicroseconds(1);
digitalWrite(11, HIGH);//pin11_APU_WE
digitalWrite(APU_CS_PIN + cs, LOW);//APU_CS
}
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len, unsigned char cs)
{
while(!((read_apu(0, cs) == 0xAA) && (read_apu(1, cs) == 0xBB))){
//APUが初期化を完了するまで待機
static int i_t_c=0;
delayMicroseconds(1);
if(i_t_c>=1000){
Serial.print("Time out : APU");
Serial.print(cs);
Serial.print(" not found : PORT0=0xAA and PORT1=0xBB did not return\r\n");
delay(1000);
if(AUTO_RESET == 1){
asm volatile (" jmp 0");//タイムアウトしたら(APUへの読み書き失敗)Arduinoをリセットする。
}
timeout = cs + 1;
return;
}
i_t_c++;
}
write_apu(3, adr >> 8, cs);
write_apu(2, adr & 0xFF, cs);
write_apu(1, 1, cs);//APUユーザープログラム転送モード
write_apu(0, 0xCC, cs);
while(read_apu(0, cs) != 0xCC){
//APUが転送準備を完了するまで待機
static int i_t_c=0;
delayMicroseconds(1);
if(i_t_c>=1000){
Serial.print("Time out : APU");
Serial.print(cs);
Serial.print(" not found : PORT0=0xCC did not return\r\n");
delay(1000);
if(AUTO_RESET == 1){
asm volatile (" jmp 0");//タイムアウトしたら(APUへの読み書き失敗)Arduinoをリセットする。
}
timeout = cs + 1;
return;
}
i_t_c++;
}
for(unsigned int i = 0;i < len;i++)
{
write_apu(1, writedata[i], cs);
write_apu(0, i & 0xFF, cs);
while(read_apu(0, cs) != (i & 0xFF)){
//APUが転送データ1バイトを書き込むまで待機
static int i_t_c=0;
delayMicroseconds(1);
if(i_t_c>=1000){
Serial.print("Time out : APU");
Serial.print(cs);
Serial.print(" not found : Failed to transfer the user program\r\n");
delay(1000);
if(AUTO_RESET == 1){
asm volatile (" jmp 0");//タイムアウトしたら(APUへの読み書き失敗)Arduinoをリセットする。
}
timeout = cs + 1;
return;
}
i_t_c++;
}
}
}
//~~ユーザプログラム用関数/////////////////////////////
//APU(ユーザープログラム動作中)のSRAMへ書き込み
void write_sram(unsigned int adr, unsigned char sram_writedata, unsigned char cs)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAM_への書き込み手順
//1.アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.データをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
unsigned char temp_port0;
int i = 0;
//Serial.print("apu_port0_read:0x");//デバッグ用
//Serial.print(read_apu(0), HEX);//デバッグ用
//Serial.print("\r\n");//デバッグ用
write_apu(2, adr & 0xFF, cs);
write_apu(3, (adr>>8) & 0xFF, cs);
write_apu(1, sram_writedata, cs);
temp_port0 = read_apu(0, cs);
write_apu(0, (temp_port0 ^ B00000001) & B11001111, cs);
for(i = 0;i < 100;i++){
#if DB_MESS
Serial.print("apu_port0_read:0x");//デバッグ用
Serial.print(read_apu(0), HEX);//デバッグ用
Serial.print("\r\n");//デバッグ用
#endif
if(((read_apu(0, cs) ^ temp_port0) & B00000100) != 0){
return;
}
}
timeout = cs + 1;
#if DB_MESS
Serial.print("Timeout , APU:");
Serial.print(cs);
Serial.print(" , write_sram_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
#endif
}
//APU(ユーザープログラム動作中)のDSPへ書き込み
void write_dsp(unsigned char adr, unsigned char dsp_writedata, unsigned char cs)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//DSP_への書き込み手順
//1.アドレス0~7bitをAPUのポート2に書き込む
//2.データをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=H(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
unsigned char temp_port0;
int i = 0;
write_apu(2, adr, cs);
write_apu(1, dsp_writedata, cs);
temp_port0 = read_apu(0, cs);
write_apu(0, ((temp_port0 ^ B00000001) | B00100000) & B11101111, cs);
for(i = 0;i < 100;i++){
if(((read_apu(0, cs) ^ temp_port0) & B00000100) != 0){
return;
}
}
timeout = cs + 1;
#if DB_MESS
Serial.print("Timeout , APU:");
Serial.print(cs);
Serial.print(" , write_dsp_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
#endif
}
//APU(ユーザープログラム動作中)のSRAMから読み込み
unsigned char read_sram(unsigned int adr, unsigned char cs)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAMから読み込み手順
//1.アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit1(読み込み要求)を反転させて、APUのポート0に書き込む
//3.APUのポート0を読み取り、bit3(読み込み完了通知)が反転するまで待つ
//4.APUのポート1(データ)のデータを読み取る
unsigned char temp_port0;
int i = 0;
write_apu(2, adr & 0xFF, cs);
write_apu(3, (adr>>8) & 0xFF, cs);
temp_port0 = read_apu(0, cs);
write_apu(0, (temp_port0 ^ B00000010) & B11001111, cs);
for(i = 0;i < 100;i++){
if(((read_apu(0, cs) ^ temp_port0) & B00001000) != 0){
return read_apu(1, cs);
}
}
timeout = cs + 1;
#if DB_MESS
Serial.print("Timeout , APU:");
Serial.print(cs);
Serial.print(" , read_sram_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
#endif
return 0;
}
//APU(ユーザープログラム動作中)のDSPから読み込み
unsigned char read_dsp(unsigned int adr, unsigned char cs)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//DSPから読み込み手順
//1.アドレス0~7bitをAPUのポート2に書き込む
//2.APUのポート0を読み取り、bit5=H(L:SRAM/H:DSP)に、bit4=L(オートアドレスインクリメント無効)に、bit1(読み込み要求)を反転させて、APUのポート0に書き込む
//3.APUのポート0を読み取り、bit3(読み込み完了通知)が反転するまで待つ
//4.APUのポート1(データ)のデータを読み取る
unsigned char temp_port0;
int i = 0;
write_apu(2, adr, cs);
temp_port0 = read_apu(0, cs);
write_apu(0, ((temp_port0 ^ B00000010) | B00100000) & B11101111, cs);
for(i = 0;i < 100;i++){
if(((read_apu(0, cs) ^ temp_port0) & B00001000) != 0){
return read_apu(1, cs);
}
}
timeout = cs + 1;
#if DB_MESS
Serial.print("Timeout , APU:");
Serial.print(cs);
Serial.print(" , read_dsp_A:0x");
Serial.print(adr, HEX);
Serial.print("\r\n");
#endif
return 0;
}
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)
void write_sram_st(unsigned int adr, unsigned char *sram_writedata, unsigned int len, unsigned char cs)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAM_への連続書き込み手順
//1.開始アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.最初のデータをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//5.次のデータをAPUのポート1に書き込む
//6.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント有効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//7.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//8.「5.次のデータをAPUのポート1に書き込む」へ行く。終了する場合はそのまま終了
unsigned char temp_port0;
int i = 0;
write_sram(adr,sram_writedata[0], cs);//0番目のデータを書いてもまだオートインクリメントはしない。
for(unsigned int j = 0;j < len;j++){//もういちど0番目のデータを書いた後、アドレスをオートインクリメントする。
write_apu(1, sram_writedata[j], cs);
temp_port0 = read_apu(0, cs);
write_apu(0, ((temp_port0 ^ B00000001) | B00010000) & B11011111, cs);
for(i = 0;i < 200;i++){
if(((read_apu(0, cs) ^ temp_port0) & B00000100) != 0){
break;
}
}
if(i >= 100){
timeout = cs + 1;//タイムアウト検出用
return;
}
}
}
//APU(ユーザープログラム動作中)のSRAMへ書き込み(連続)(プログラム用フラッシュメモリのデータをAPUへ連続転送する場合)
void write_sram_st_prg(unsigned int adr, unsigned char *sram_writedata, unsigned int len, unsigned char cs)
{
//ユーザープログラム動作中のみこの関数が実行できます。
//SRAM_への連続書き込み手順
//1.開始アドレス0~7bitと7~15bitをAPUのポート2と3に書き込む
//2.最初のデータをAPUのポート1に書き込む
//3.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント無効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//4.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//5.次のデータをAPUのポート1に書き込む
//6.APUのポート0を読み取り、bit5=L(L:SRAM/H:DSP)に、bit4=H(オートアドレスインクリメント有効)に、bit0(書き込み要求)を反転させて、APUのポート0に書き込む
//7.APUのポート0を読み取り、bit2(書き込み完了通知)が反転するまで待つ
//8.「5.次のデータをAPUのポート1に書き込む」へ行く。終了する場合はそのまま終了
unsigned char temp_port0;
int i = 0;
write_sram(adr,pgm_read_byte_near(sram_writedata), cs);//0番目のデータを書いてもまだオートインクリメントはしない。
for(unsigned int j = 0;j < len;j++){//もういちど0番目のデータを書いた後、アドレスをオートインクリメントする。
write_apu(1, pgm_read_byte_near(sram_writedata + j), cs);
temp_port0 = read_apu(0, cs);
write_apu(0, ((temp_port0 ^ B00000001) | B00010000) & B11011111, cs);
for(i = 0;i < 200;i++){
if(((read_apu(0, cs) ^ temp_port0) & B00000100) != 0){
break;
}
}
if(i >= 100){
timeout = cs + 1;//タイムアウト検出用
return;
}
}
}
//BRRベクタにサンプルベクタ登録
void etr_brr_vct(unsigned char smp_num, unsigned int start_vct_adr, unsigned int loop_vct_adr, unsigned char cs)
{
//BRRベクタ登録関数
//BRRサンプルデータ開始アドレスとループアドレス、サンプル番号を指定すると、BRRベクタに登録します。
//sample0_start_Lbyte, sample0_start_Hbyte, sample0_loop_Lbyte, sample0_loop_Hbyte
write_sram(BRR_VCT + (smp_num * 4), start_vct_adr & 0xFF, cs);
write_sram(BRR_VCT + (smp_num * 4) + 1, (start_vct_adr >> 8) & 0xFF, cs);
write_sram(BRR_VCT + (smp_num * 4) + 2, loop_vct_adr & 0xFF, cs);
write_sram(BRR_VCT + (smp_num * 4) + 3, (loop_vct_adr >> 8) & 0xFF, cs);
}
//ADSRエンベロープ登録
void etr_adsr_env(unsigned char ch, unsigned char attack, unsigned char decay, unsigned char sustain, unsigned char release, unsigned char cs)
{
//エンベロープ登録関数(この関数を実行すると自動的にADSRエンベロープが有効になります。)
//エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定
write_dsp(ENV1_X | ch, DSP_ENV_ADSR_EN | ((0x07 & decay) << 4) | (0x0F & attack), cs);
write_dsp(ENV2_X | ch, ((0x07 & sustain) << 5) | (0x1F & release), cs);
}
//音程のセット
void ptc_set(unsigned char ch, unsigned int notenum, unsigned char cs)
{
char sample_num = 1;
//音程によってBRRサンプルを変更する
if(notenum <= 33){
sample_num = 0;
}else if(notenum <= 47){
//sample_num = 1;
sample_num = 1;
}else if(notenum <= 60){
//sample_num = 2;
sample_num = 2;
}else if(notenum <= 67){
//sample_num = 3;
sample_num = 2;
}else if(notenum <= 71){
//sample_num = 4;
sample_num = 2;
}else if(notenum <= 77){
//sample_num = 5;
sample_num = 3;
}else if(notenum <= 83){
//sample_num = 6;
sample_num = 3;
}else if(notenum <= 88){
//sample_num = 7;
sample_num = 3;
}else if(notenum <= 94){
//sample_num = 8;
sample_num = 3;
}else{
//sample_num = 9;
sample_num = 3;
}
write_dsp(SRCN_X | ch, sample_num, cs);
//音程レジスタ(PTC_X_L($X2), PTC_X_H($X3))のセット
//MIDIのノート番号からDSPのピッチを求めるテーブルより、ピッチを求めてチャンネルchにセット
write_dsp(PTC_X_L | ch, (noteFreq[brr_tone[sample_num] + notenum + ((brr_oct[sample_num]) * 12)]) & 0xFF, cs);
write_dsp(PTC_X_H | ch,((noteFreq[brr_tone[sample_num] + notenum + ((brr_oct[sample_num]) * 12)]) >> 8) & 0x3F, cs);
}
//メインの音量とパンのセット
void main_vel_pan_set(unsigned char vel,unsigned char pan, unsigned char cs)
{
//音量, パン
//メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定
//音量(0~127)とパン(0~127)を乗算して、128で割る(最大出力は127*127/128=126)
//パンは、64で中央、1で左端、127で右端 (0は1として処理)
if(pan == 0){
pan = 1;
}
write_dsp(MAIN_VOL_L, (unsigned char)(((unsigned int)vel * (128 - pan)) >> 7), cs);
write_dsp(MAIN_VOL_R, (unsigned char)(((unsigned int)vel * pan) >> 7), cs);
}
//チャンネルの音量とパンのセット
void vel_pan_set(unsigned char ch, unsigned char vel,unsigned char pan, unsigned char cs)
{
//チャンネル, 音量, パン
//チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット
//音量(0~127)とパン(0~127)を乗算して、128で割る(最大出力は127*127/128=126)
//パンは、64で中央、1で左端、127で右端 (0は1として処理)
if(pan == 0){
pan = 1;
}
write_dsp(VOL_X_L | ch, (unsigned char)((unsigned int)((unsigned int)vel * (128 - pan)) >> 7), cs);
write_dsp(VOL_X_R | ch, (unsigned char)((unsigned int)((unsigned int)vel * pan) >> 7), cs);
}
//チャンネルノートオン
void note_on(unsigned char ch, unsigned char cs)
{
//チャンネル(0~8を左4ビットシフトしたもの)
//キーオンレジスタ(KEY_ON($4C))のセット
write_dsp(KEY_OFF, 0, cs);//ふたたびノートオフできるようにする
write_dsp(KEY_ON, 1 << (ch >> 4), cs);
}
//チャンネルノートオフ
void note_off(unsigned char ch, unsigned char cs)
{
//チャンネル(0~8を左4ビットシフトしたもの)
//キーオフレジスタ(KEY_OFF($5C))のセット
write_dsp(KEY_OFF, 1 << (ch >> 4), cs);
}
//MIDI受信用関数/////////////////////////////
//1バイトずつmidi_readを実行し、1グループのmidiメッセージを受信し終えるとmidi_comを実行
void midi_read(char read_buf)
{
if (ex_mess_en == 1)
{
if (read_buf == 0xf7)
{ //エクスクルーシブ・メッセージ終了
ex_mess_en = 0;
dat_ph = stop_byte;
}
}
else if (((read_buf >> 7) & 0x01) == 1)
{
//データ始まり検出したら
dat_ph = 0;
ex_mess_en = 0;
read_buf_h = 0xF0 & read_buf;
if ((0x80 <= read_buf_h) && (0xB0 >= read_buf_h) || (read_buf_h == 0xE0))
{
//3バイト読み取り
running_ch = read_buf; //ランニングステータスチャンネルセット
stop_byte = 2;
}
else if ((read_buf_h == 0xC0) || (read_buf_h == 0xD0))
{
//2バイト読み取り
running_ch = read_buf;
stop_byte = 1;
}
else
{
switch (read_buf)
{
case 0xF0: //エクスクルーシブ・メッセージ
ex_mess_en = 1;
stop_byte = 255;
break;
case 0xF1: //MIDIタイムコード
case 0xF3: //ソング・セレクト
stop_byte = 1;
break;
case 0xF2: //ソング・ポジション・ポインター
stop_byte = 2;
break;
case 0xF6: //チューン・リクエスト
case 0xF7: //エンド・オブ・エクスクルーシブ
case 0xF8: //タイミング・クロック(MIDIクロック)
case 0xFA: //スタート
case 0xFB: //コンティニュー
case 0xFC: //ストップ
case 0xFF: //システム・リセット
stop_byte = 0;
break;
case 0xFE: //アクティブ・センシングc
stop_byte = 0;
break;
}
}
}
else if (dat_ph == 0)
{ //ランニングステータス//////
stop_byte = 2;
midi_buf[0] = running_ch;
dat_ph++;
//////////////////////////
}
midi_buf[dat_ph] = read_buf; //受信データ格納
if (dat_ph >= stop_byte)
{ //指定バイト数受信完了
stop_byte = 3;
ex_mess_en = 0;
if((midi_buf[0]==0xFE) && (dat_ph == 0))
{
dat_ph = 0;//アクティブセンシングは処理しない
}
else
{
midi_com(midi_buf);
}
dat_ph = 0;
midi_buf[0] = 0;
return;
}
if ((((midi_buf[0] >> 7) & 0x01) == 1) || (ex_mess_en == 1))
{
//1バイト目がメッセージ以外読み取りしない
dat_ph++;
}
}
//MIDIデータ入力処理
inline void midi_com(unsigned char *in_midi_mess)
{
unsigned char in_midi_data_buf_h0 = in_midi_mess[0] >> 4; //上位4ビットのみを右へ4ビットシフト
unsigned char in_midi_data_buf_l0 = in_midi_mess[0] & 0x0f; //下位4ビットのみ チャンネル
unsigned char in_midi_data_buf_1 = in_midi_mess[1];
unsigned char in_midi_data_buf_2 = in_midi_mess[2];
const char temp = 0; //0の代わり
//ドラムパートは無視
if (in_midi_data_buf_l0 == 0x09)
{
return;
}
//80
else if (in_midi_data_buf_h0 == 0x08)
{
//ノートオフ
for (int i = 0; i < MIDI_MAX_TR; i++)
{
if (midinote[i] == 1)
{
if ((midinote_ch[i] == in_midi_data_buf_l0) && (midinote_tn[i] == in_midi_data_buf_1))
{
//発音中と同じノートナンバーを見つけたら発音停止
if (sas_en == 0)
{
midinote[i] = 0; //フラグ
note_off((i & 0x07) << 4,i >> 3);
}
else //サスティンon
{
midinote[i] = 2; //フラグ
}
}
}
}
}
//90
else if (in_midi_data_buf_h0 == 0x09)
{
//ノートオフ
if (in_midi_mess[2] == 0)
{
for (int i = 0; i < MIDI_MAX_TR; i++)
{
if ((midinote_tn[i] == in_midi_data_buf_1) && (midinote_ch[i] == in_midi_data_buf_l0))
{
//音量0で発音中と同じノートナンバーを見つけたら発音停止
if (sas_en == 0)
{
midinote[i] = 0; //フラグ
note_off((i & 0x07) << 4,i >> 3);
}
else //サスティンon
{
midinote[i] = 2; //フラグ
}
}
}
}
else
{
//ノートオン
for (int i = 0; i < MIDI_MAX_TR; i++)
{
/*if (i == 9)
{
//ドラムは無視
continue;
}*/
if ((midinote_tn[i] == in_midi_data_buf_1 ) && (midinote_ch[i] == in_midi_data_buf_l0))
{
//同じ音が来たら
midinote[i] = 1; //フラグ
//一旦ノートオフ
note_off((i & 0x07) << 4, i >> 3);
//ノートオン
ptc_set((i & 0x07) << 4, in_midi_mess[1], i >> 3);//チャンネル, 音程
midi_vel[i] = in_midi_mess[2];
vel_pan_set((i & 0x07) << 4, ((int)((int)midi_vel[i] * (int)midi_main_vel[midinote_ch[i]] )>>7), midi_pan[midinote_ch[i]], i >> 3);//チャンネル, 音量, パン
note_on((i & 0x07) << 4, i >> 3);//ノートオン
break;
}
else if (midinote[i] == 0)
{
//和音空きがあったときノートオン
midinote[i] = 1; //フラグ
midinote_ch[i] = in_midi_mess[0] - 0x90; //ch
midinote_tn[i] = in_midi_mess[1]; //tn
//ノートオン
ptc_set((i & 0x07) << 4, in_midi_mess[1], i >> 3);//チャンネル, 音程
midi_vel[i] = in_midi_mess[2];
vel_pan_set((i & 0x07) << 4, ((int)((int)midi_vel[i] * (int)midi_main_vel[midinote_ch[i]] )>>7), midi_pan[midinote_ch[i]], i >> 3);//チャンネル, 音量, パン
note_on((i & 0x07) << 4, i >> 3);//ノートオン
break;
}
}
}
}
else if (in_midi_data_buf_h0 == 0x0A)
{
//ポリフォニックキープレッシャー
//何もしない
}
else if (in_midi_data_buf_h0 == 0x0B)
{
//コントロールチェンジ
switch (in_midi_mess[1])
{
case 0x07: //チャンネルボリューム
midi_main_vel[in_midi_data_buf_l0] = in_midi_mess[2];
//vel_pan_set(midinote_ch[in_midi_data_buf_l0] << 4, ((int)((int)midi_vel[midinote_ch[in_midi_data_buf_l0]] * (int)midi_main_vel[midinote_ch[in_midi_data_buf_l0]] )>>7), midi_pan[midinote_ch[in_midi_data_buf_l0]]);//チャンネル, 音量, パン
break;
case 0x0A: //パン
midi_pan[in_midi_data_buf_l0] = in_midi_mess[2];
//vel_pan_set(midinote_ch[in_midi_data_buf_l0] << 4, ((int)((int)midi_vel[midinote_ch[in_midi_data_buf_l0]] * (int)midi_main_vel[midinote_ch[in_midi_data_buf_l0]] )>>7), midi_pan[midinote_ch[in_midi_data_buf_l0]]);//チャンネル, 音量, パン
break;
case 0x78:
case 0x79:
for (int i = 0; i < MIDI_MAX_TR; i++)
{
//発音停止
if (midinote[i] == 1)
{
midinote[i] = 0;
note_off((i & 0x07) << 4, i >> 3);
}
}
break;
case 0x40: //サスティン
if (in_midi_mess[2] == 0x7F)
{
sas_en = 1;
}
else
{
sas_en = 0;
for (int i = 0; i < MIDI_MAX_TR; i++)
{
if (i == 9)
{
//ドラムは無視
continue;
}
if ((midinote[i] == 2)||(midinote[i] == 0))
{
note_off((i & 0x07) << 4, i >> 3);
midinote[i] = 0; //フラグ
}
}
}
break;
default:
break;
}
}
else if (in_midi_data_buf_h0 == 0x0C)
{
//プログラムチェンジ
midiprog[in_midi_data_buf_l0]=in_midi_mess[1];
}
else if (in_midi_data_buf_h0 == 0x0E)
{
//ピッチベンド
//何もしない
}
else if (in_midi_data_buf_h0 == 0x0F)
{
//システムメッセージ
if (in_midi_mess[0] == 0xFE)
{
//アクティブセンシング
//midiout(0xFE);
}
if (in_midi_mess[0] == 0x00)
{
//エクスクルーシブ・メッセージ
if (in_midi_mess[1] == 0x7E && in_midi_mess[2] == 0x7F)
{
//ユニバーサル
if (in_midi_mess[3] == 0x09)
{
//GM
if (in_midi_mess[4] == 0x01)
{
//GMシステム・オン
for (int i = 0; i < MIDI_MAX_TR; i++)
{
midinote[i] = 0;
note_off((i & 0x07) << 4, i >> 3);
}
}
if (in_midi_mess[4] == 0x02)
{
//GMシステム・オフ
for (int i = 0; i < MIDI_MAX_TR; i++)
{
midinote[i] = 0;
note_off((i & 0x07) << 4, i >> 3);
}
}
}
}
}
}
}
ATmega328のプログラムメモリが32KBなので、BRRサンプルデータはそう大きく出来ません。APU内のSRAMは64KBあるのでかなり余らしてしまいます。(今回のMIDI受信プログラムでは、余らせた分のRAMはエコーバッファとして利用しています。)
外部にEEPROMやSDカードを接続してそこからBRRサンプルデータを読み取るとよさそうです。
・まとめ
スーパーファミコンのAPUは、サンプルデータを用意する手間はありますが、手軽でちょっといい感じの音源モジュールです。
エコーやハードウェアエンベロープ、ピッチモジュレーション等が備わっているので、
本格的な音作りができます。
スーパファミコンは中古で多く出回っており、入手性はとても良いです。
また、前期型のスーファミに入っているAPUは、モジュールとして取り外すことができるため、部品取りの難易度は低く、おすすめです。
参考サイト
SPC700の命令、DSPのレジスタ、APUの使い方全般
SFC Development Wiki - SPC700 Reference
SPC700の命令、DSPのレジスタ、APUの使い方全般
SNES Spec
SNES回路図
SFC Development Wiki - Schematics, Ports, and Pinouts
APU-Arudino周りの回路
SHVC-SOUNDをPCへ接続する - えぬえす工房
APU-Arudino周りの回路
SNES APU - SNES Hardware page
SPC700コンパイラWLA-DXの使い方等
ゲームギア開発:アセンブラで開発できる「WLA-DX」を動かしてみる
BRRサンプルデータ(ピアノ)
Details for Acoustic Grand Piano - SMW CENTRAL
MIDI受信回路
エコーの関連
FIR Filter - SNESLAB
0 件のコメント:
コメントを投稿