この記事はGame Boy Advent Calendar 2019の9日目とされています。
私は普段LSDjを使ってゲームボーイに音楽を打ち込んでいるのですが、他の可能性も探ってみたいと考えています。例えば「ゲームボーイのchiptuneで「ファミコン音源に歌わせる方法」のような表現がそこまで流行っていないのは、LSDjでTableやWAVの波形数を制限がある もしくは 実装が煩雑だからだ」「そのためGB音源でも同様の表現はできるはず」という仮説を持っています。
そのため今年は「何らかの音楽ソフトを作ってみる」ことを目標にしてゲームボーイプログラミングをやってみていました。ちょっとしたものしかできていませんが、これまでに学んだことをまとめておきます。
他言語のプログラマーも、組み込みプログラミングの雰囲気を知っていただけると思います。ただ、門外漢なので組み込み系や(当時の)ゲームプログラマーにとっては常識すぎることや、間違ったことを書いている可能性があります。もし何かあればご指摘頂けると嬉しいです。
チップチューンのすべて All About Chiptune: ゲーム機から生まれた新しい音楽
- 作者:田中 治久(hally)
- 出版社/メーカー: 誠文堂新光社
- 発売日: 2017/05/11
- メディア: 単行本
開発環境について
ゲームボーイソフトの開発に取り組むにはまず、こちらのgistを読みましょう。
Writeup discussing programming toolchains, coding practices, and languages, for GB and GBC dev.(日本語での一部訳)
- アセンブリではなくGBDKというC言語のコンパイラで開発
- そんなに複雑な処理はしないのと、Little Sound DjでもGBDKが利用されているのが理由
- Macで開発する際にはhomebrew用のフォークを利用するのが良いと思います
- エミュレータはOpenEmu + Gambatteを利用
- 以前はKiGBを利用していましたが、凝った音を鳴らそうとすると実機と音が違ってしまうことがありました
- 実機で動かす場合はGB USB 64M Smart Cardを利用していますが、売り切れているので他のカートを探しましょう
また私はMacOSを利用していますが、Windowsのほうが開発ツールが揃いやすいです。この辺の事情は組み込み系や他のレトロゲーム開発でも同様のようです。
The overhead is also due to C being a stack-oriented language [if you don't know what that means, ignore this paragraph], whereas the Game Boy's CPU is rather built for a register-oriented strategy. This most notably makes passing function arguments a lot slower.
おそらくこれは関数呼び出し時のコールスタックを支援する機構がゲームボーイのCPUに用意されておらず、関数呼び出しのオーバーヘッドが大きいのでC言語よりアセンブリを推奨するという意味だと思っています。ちなみにアドバンスでこの機構が用意されたそうです。(このあたりあまり理解できていないので、もし詳しい方がいたらコメントください)
レジスタに書き込んで音を鳴らす
まず「なにかボタンを押すとビープ音を鳴らす」実装をしましょう。ゲームボーイの音源チップの概要については、ニコニコ大百科のGB音源の記事が一番分かりやすかったです。
このコードはgbdk_playgroundのコードを引用します。レジスタにいろいろ初期設定した後、 joypad_state
に値が存在するときに NR51
のレジスタに値を書き込んで発音するという内容です。
#include <gb/gb.h> #include <gb/drawing.h> void main() { NR50_REG = 0xFF; NR51_REG = 0xFF; NR52_REG = 0x80; gotogxy(1, 1); gprintf("====== Beep ======"); gotogxy(2, 3); gprintf("Press any button"); while(1) { UBYTE joypad_state = joypad(); if(joypad_state) { NR10_REG = 0x38U; NR11_REG = 0x70U; NR12_REG = 0xE0U; NR13_REG = 0x0AU; NR14_REG = 0xC6U; NR51_REG |= 0x11; delay(200); } } }
また NR51_REG |= 0x11;
のように、組み込み系分野ではビット演算を多用するそうです。個人的には、普段のプログラミングでは意識しない点なので面白く感じました。
上記の他にも、OSやデバイスドライバなどハードウェアに近いプログラムでは、少ないメモリで高速な処理を行うためにビット列を使って状態などを表現することが多くあります。また、DVDデッキや炊飯器などの電化製品でROMに焼いて実装される、いわゆる組込み系のシステムでも、限られたメモリ容量で高速な処理を行うためにビット演算が多用されます。
組み込みプログラムでは、このように「初期設定の処理」と「無限ループさせる処理」を含むコードになります。ちなみにArduinoだとそれぞれ setup()
と loop()
の関数を書くといい感じにやってくれるそうです。
ちなみに、無限ループさせる関数ではいくつもの処理をいい感じに切り替える必要があるため、他言語のプログラマーなら「async/awaitがあればいいのに…」と感じると思います。Rustという言語で最近async/awaitの機能がstableになり、OSの無い組み込み環境でも使えると聞いて驚いています。
タイマー割り込みで一定テンポを保つ
無限ループの中で音を鳴らす処理を書いてもいいのですが、他の処理(例えば画面を動かすとか)をする場合に「正確なテンポで音を鳴らす」ことができません。タイマー割り込みで一定時間おきに音を鳴らす必要があります。
私が試しに実装したソフトでは、以下のような実装をしていました。
void initInterrupts(void) { disable_interrupts(); add_TIM(updateMusic); enable_interrupts(); TMA_REG = 0x00U; /* Set clock to 4096 Hertz */ TAC_REG = 0x04U; set_interrupts(VBL_IFLAG | TIM_IFLAG); } void updateMusic(void) { count++; if (count > 0x03U) { Sound_play(&sound); count = 0; } }
ただ、音の周波数を配列でハードコーディングして、
static const UWORD score1[] = { 1379, 1379, 1452, 1452, 1339, 1339, 1452, 1452, 1517, 1452, 1379, 1339, 1339, 1379, 1452, 0, 1379, 1379, 1452, 1452, 1339, 1339, 1452, 1452, 1517, 1452, 1379, 1339, 1339, 1297, 1339, 0 }; static const UWORD score2[] = { 1253, 0, 854, 0, 1155, 0, 854, 0, 1253, 0, 854, 0, 1155, 1102, 1046, 1155, 1253, 0, 854, 0, 1155, 0, 854, 0, 1253, 0, 854, 0, 1102, 0, 1102, 1155, };
音を鳴らす関数でそれを受け取って鳴らすような実装をしていました。
void beep1(UWORD f) { NR10_REG = 0x06; NR11_REG = 0x40; NR12_REG = 0x73; NR13_REG = (UBYTE)(f & 0x00FF); NR14_REG = (UBYTE)((f >> 8) & 0x00FF) + 0x80; } void beep2(UWORD f) { NR21_REG = 0x40; NR22_REG = 0x73; NR23_REG = (UBYTE)(f & 0x00FF); NR24_REG = (UBYTE)((f >> 8) & 0x00FF) + 0x80; }
ただ、これでは音の高さ以外指定できない上、もしエンベロープなどを指定できるよう構造体を作ったとしても、それはそれで音源作成が大変なので諦めていました。
ただ、Githubで公開されているTobu Tobu Girl(参考記事: 2017年になってゲームボーイ用新作ソフトがまさかの新発売)の実装を見たところ、タイマー割り込みで updateMusic
という関数がセットされ、その中でmmlgbというサウンドドライバ用のライブラリが呼ばれているようです。MMLであればある程度は人が読みやすく実装できそうです。
C言語の設計について
ある程度コードベースが大きくなると、オブジェクト指向的なコードを書きたくなります。ただ以前書いた通り、ASNI C準拠であるため構造体をreturnできません。
また、(今のところ不要であるものの)ポリモルフィズムが必要になった場合、マクロや関数ポインタを駆使する必要がありそうです。Joel Spolskyが「SchemeとCとHaskellがちゃんと書けるプログラマーは強い」って言ってましたが、そりゃ普段からこういうことしてたら強くなるわって思いました。
今後やりたいこと
自分はあくまで良い音楽環境を作ることが目標です。以下の2つを試してみようと思います。
①mmlgbを使ってみる
Tobu Tobu Girlの実装を参考にしつつ、ちょっとしたミニゲームを作ってみたいです。今までサウンドドライバの実装がきれいにいかずに悩んで止まっていたので。またサウンドドライバの実装がどうなってるのかちゃんと追いたいです。
②GameBoy Music Compilerを試す
LSDj以外の表現ということで、GameBoy Music Compilerも試してみたいです。Windows前提のようですが、ソースコードを見る限りrgbdsが使われているようなので時間をかければなんとかなりそうです。
2019.10.03版が出ているので、実は最近アップデートされていたようです。