ホームチュートリアルプログラム
1-セットアップ 4-サウンド 7-FIFO インベーダー
2-フレームバッファ 5-SRAM 8-割り込み DSEmu
3-キー入力 6-ファイルシステム 9-マイク  
10-拡張回転バックグラウンド

Homebrew任天堂DS開発 Part 6

ファイルシステム・キー・サウンド

チュートリアル4では、私たちはプログラム内にサウンドファイルを含んでいるという問題がありました。そのチュートリアルで行われた方法はサンプリングサウンドを'objcopy'を使用して、それをプログラムにリンクするバイナリーファイルに変換することでした。

このアプローチに関する問題は貴重なメモリを浪費するということです。 DevKitPROでは、ARM7プログラムコードは最大64KBの制限があります。そのため多くのサンプリングサウンドを使用することができません。ARM9にはもっと多くのサウンドを持たせることはできますが、それでもまだ制限は存在しています。

私たちがこのチュートリアルで使用するアプローチはファイルシステムを使用することです。 ファイルシステムは標準のOSファイルシステムと同様にファイルを格納する方法です。 あなたは、ファイルの一覧を取得し、ファイル名でファイルを参照することができます。 ファイルはプログラムコードとは別途に格納され、貴重なRAMを消費しません。それらは、代わりにカートリッジに格納されて、必要に応じてアクセスされます。

このアプローチの利点はあなたがゲームカートリッジのリ容量だけによって制限される多くのファイルを持つことができるということです。 不都合はあなたのプログラムをゲームカートリッジに格納しなければならないということです。 あなたはカートリッジ自体を使用しない'ダウンロード可能な'ゲームにこの方法を使用することができません。

また、私たちはどのようにARM9からARM7にサンプリングサウンドの再生要求指示を送信するかをカバーするつもりです。あなたがチュートリアル4を思い出すなら、私たちは、XとYボタンが押された時に、ARM7でサンプリングサウンドを再生するかを制御しました。今回、ARM9側でボタンの押下を検出し、そして、サンプリングサウンドを再生するためのメッセージをARM7に送ります。

最終的に私たちはキーの押下を検地する前に、ユーザが複数回にわたってキーを押してしまう問題にふれます。

このチュートリアルで説明するプログラムは、選択したチャンネルでそのサンプリングサウンドを再生しながら、ファイルシステム上のサンプルのリストからサンプリングサウンドを選択できるようにします。そして、既存のサンプルがまだ再生している間に チャンネルを変えて、他のサンプルを再生します。

ゲームボーイアドバンス ファイルシステム: GBFS

任天堂DSのファイルシステムは、'NDS'ファイルフォーマットで構築されています。'ndstool'プログラムを使用して、それにファイルを追加・削除することができます。そして、DSハードウェアからこのファイルシステムを読み込めますが、残念ながら、homebrewの利用可能なライブラリは存在していません。

それでも、ゲームボーイアドバンスのためのGBFSと呼ばれるすばらしいファイルシステムライブラリがあります。しして、これはDS上でもちゃんと動作しています。このライブラリは Damian Yerrick によって書かれ、ここからダウンロードできます。また、ローカルコピーもここにあります。

ダウンロードの中に含まれる Readme ファイルには、これの使用方法が記載されています。基本的に、あなたはすべての実行可能なファイルをどこかの特定のパスに置きたいでしょう。しして、'gbfs.exe'を使用してファイル中にファイルシステムを作成し、'lsgbfs.exe'でファイルシステム中の内容の一覧を取得できます、このファイルはファイルシステムを含有し、プログラムバイナリに追加することができます。ツールの実行例は以下の通りです。

d:\source\>gbfs filesystem.gbfs InvHit.raw BaseHit.raw
      4080 InvHit.raw
      6548 BaseHit.raw
d:\source\>lsgbfs filesystem.gbfs
      4080 InvHit.raw
      6548 BaseHit.raw
d:\source\>cat myprog_tmp.nds filesystem.gbfs >myprog.nds

私は、DSのためにDevKitPROでコンパイルするために、GBFSソースコードに1つのマイナーチェンジを加えました。それは 'libgfs.c' の119行目で、'bsearch' の結果を '(const GBFS_ENTRY*)' でキャストしました。

  here = (const GBFS_ENTRY*)bsearch(key, dirbase,
                 n_entries, sizeof(GBFS_ENTRY),
                 namecmp);

この修正を行わないと、以下のエラーメッセージが表示されます。

libgbfs.c:121: error: invalid conversion from `void*' to `const GBFS_ENTRY*'
make: *** [libgbfs.o] Error 1

GBFSファイルの追加

GBFSファイルがいったん作成されると、それを .ndsファイルに追加する必要があります。それは、256バイト境界に合わせて追加する必要があるので、.ndsファイルにする最初のことはDevKitProから'padbin'を使用して.ndsファイルを256バイト単位に広げて、次に'cat'コマンドを使用して、GBFSを.ndsに追加することです:

  ndstool -c gbfs_demo1.nds -9 arm9.bin -7 arm7.bin
  padbin 256 gbfs_demo1.nds
  cat gbfs_demo1.nds sounds.gbfs >gbfs_demo1_tmp.nds
  mv gbfs_demo1_tmp.nds gbfs_demo1.nds

ファイルシステムへのアクセス

'libgbfs.c'は任天堂DSプログラムに対して、数多くの機能を提供します。ファイルシステムを使用するためには、これをコンパイルしてファイルシステムを使用する実行可能コードにリンクする必要があります。今回の例ではARM9からファイルシステムを使用しますので、ARM9実行可能コードにリンクします。

ファイルシステムは私達のNDSファイルの後ろに位置していて、GBAカートリッジROM領域に格納されます。チュートリアル5では、このメモリ領域がメモリアドレス0x8000000から開始していて、0x0A000000からはカートリッジSRAMエリアが開始していることを示しました。私達はこのエリアをARM9からアクセスできるようにマップする必要があります。チュートリアル5では WAIT_CR レジスタを使用することでこれを実現できることを示しました。

  // ゲームカートリッジ メモリをを ARM9 にマップ
  WAIT_CR &= ~0x80;

メモリ了以が一度マップされると、私達は、ファイルシステムの先頭のファイルの場所を見つけるように、'find_first_gbfs_file'を呼び出します。呼び出しの結果は、GBFS_FILEタイプのオブジェクトへのポインタで、C言語の stdio (標準入出力)の FILE と同等の働きをします。

'find_first_gbfs_file' ファイルシステムを見つけるためにメモリを順番に探索します。それはメモリ内の開始位置のポインタを必要とします。私はマップしたカートリッジROMの先頭を使用しました。

  /* カートリッジメモリの先頭から探索を開始 */
  GBFS_FILE const* gbfs_file = 
    find_first_gbfs_file((void*)0x08000000);

いったん GBFS_FILE へのポインタを入手すると、私達は特定のファイルを検索することができます。ファイル名を指定して、'gbfs_get_obj' を呼び出すとそのファイルの記憶場所のポインタを戻します。また、オプションでそのファイルの長さも取得することができます。

    uint32 length = 0;
    uint8* data    = (uint8*)gbfs_get_obj(gbfs_file, 
			  	         "BaseInv.raw",
				         &length);

ファイルのデータはポインタを使用して直接操作することも、操作のためにRAMにコピーすることもできます。その他の使用できるGBFS機能に関しては、すべてGBFSと一緒に配布されたReadmeファイルに記載されています。

キーの押下のハンドリング

チュートリアル3では、どのようにボタンの押下を検出するかを解説しました。残念ながら、この方法でボタンをチェックし、それに基づいて動作するのは、ここで使用するサンプルプログラムでは、うまく動作しません。

私達は'下'キーをスクリーンに表示させるチャネル番号を小さくするため、'上'キーは大きくするために使用したいと思っています。チュートリアル3のコードに基づいて、私は元々、以下のようなコードを試みました。

 while(1) {
   swiWaitForVBlank();
   [...]
   if(READ_KEYS & KEY_DOWN) {
     if(current_channel == 0)
       current_channel = 15;
     else
       --current_channel;
  [...]
 }   

ここでの問題は、ユーザーが長い間'下'キーを押すと、'while ループ'なので、チャネルを小さくするという動作が複数回実行されてしまうということです。この結果、各々の押下によって、複数回チャネルの減少が起きてしまいます。1つだけのチャネルを下げたい場合、1/60秒間のわずかな間だけボタンを押すというわずかなチャンスしかありません。

チュートリアル3ではキーの押下で1ブロックの色を変えていたので、この問題を説明しませんでした。複数回同じ色に変わっても何も問題ないからです。

これに関してはゲームボーイアドバンス時代から、よく知られている解決策があります。このすばらしいGBA 'キー' チュートリアルアドバンスド キー セクションにこれを解決する方法が記載されています。

'libnds' では keys.h で以下の実現方法と同様なシステムを持っています。

void scanKeys();
void keysInit();
u32 keysHeld();
u32 keysDown();
u32 keysUp();

必要なのは、'scanKeys'関数をループ毎に呼び出して、他の機能はキーの状況が変わったときだけ使用するという また、これらの関数を使用する前に、'keysInit' を呼び出す必要があります。前述のサンプルを以下のように修正します。

 keysInit();
 while(1) {
   swiWaitForVBlank();
   scanKeys();
   [...]
   if(keysDown() & KEY_DOWN) {
     if(current_channel == 0)
       current_channel = 15;
     else
       --current_channel;
  [...]
 }   

これで、'下'キーを長期間押されていても、チャネル番号は1つしか減少しないようになります。

プロセス間通信

チュートリアル4で解説したように、ARM7だけがサウンドレジスタにアクセスできます。そのため、ARM9からARM7にコマンドを送る方法が必要です。 これはサウンドのためだけでなく、無線や他のARM7機能を使用するためにも有用です。

私が取った方法は、GBADEV フォーラムにポストされた 'dssound' という MOD 再生デモプログラムに影響されました。

'ndslib' にも、ARM9からサウンド再生コマンドを送信する方法があります。しかし、それをこの作業のために入手することができませんでした。 私がこのチュートリアルを書き始めてから、 この使用方法のサンプルがGBADEV フォーラムにポストされました。ここで使用した方法は、サウンド以外にも簡単に拡張することができます。 しかし ndslib の方法は確実なように見えます。

コマンド

このサンプルで使用されるプロセス間通信システムは、'Command' 構造体を使用して、ARM9 から ARM7 に送信するコマンドを定義します。これは command.h で定義されています。

/* The ARM9 fills out values in this structure to tell the ARM7 what
   to do. */
struct Command {
  CommandType commandType;
  union {
    void* data;  
    PlaySampleSoundCommand playSample;    
  };
};

'Command' は discriminated union です。この中には、コマンドのタイプ(サウンドの再生、無線での接続など)を定義する 'commandType' メンバーがあります。 そして、コマンドで必要なデータの union があります。現在は、構造化されていないデータのポインタか、または、'PlaySampleSoundCommand'のインスタンスだけです。

'CommandType' は送信可能なコマンドの enum です。

/* Enumeration of commands that the ARM9 can send to the ARM7 */
enum CommandType {
  PLAY_ONE_SHOT_SAMPLE
};

コマンドタイプの enum に新しいコマンドを追加し、そして、union に新しいコマンドが必要とするデータのタイプを追加することにより、'Command' 構造体を拡張することができます。

PlaySampleSoundCommand

'PlaySampleSoundCommand'は、サウンドを再生するデータが必要です。これには RAW サウンドデータへのポインタ、再生頻度、ボリュームなどが含まれます。

/* サウンド再生のコマンドパレメータの例 */
struct PlaySampleSoundCommand
{
  int channel;
  int frequency;
  void* data;
  int length;
  int volume;
};

以下のようなコードでサウンドを再生するコマンドを作成できます。

  Command command;
  command.commandType = PLAY_ONE_SHOT_SAMPLE;
  command.playSample.channel = 0;
  command.playSample.frequency = 11127;
  command.playSample.data = [...];
  [...etc...]

CommandControl

共有メモリに格納されたコマンド用の循環配列は、ARM7とARM9の両者からアクセス可能です。新しいコマンドを追加する位置を指し示す配列のインデックスが 'currentCommand' メンバー変数です。

/* コマンドの最大数 */
#define MAX_COMMANDS 20

/* 構造体は、ARM7 と ARM9 で共有。ARM9 がコマンドを書き込み、
   ARM7 がそれを読み込んで実行
*/
struct CommandControl {
  Command command[MAX_COMMANDS];
  int currentCommand;
};

/* 共有する CommandControl 構造体のアドレス */
#define commandControl ((CommandControl*)((uint32)(IPC) + sizeof(TransferRegion)))

libnds ライブラリは、'TransferRegion' タイプの IPC 共有変数があります。'CommandControl' 共有変数はこの変数が確実に共用メモリ上に存在するので、この直後に配置します。この目的ために上記で 'commandControl' の定義を行っています。

同じ方法でシステムをフックする別のライブラリを使用すると、それらはお互いを上書きしますので、その場合には、それぞれが上書きしないようにコードを修正する必要があります。

コマンドと ARM9

ARM9から、実行するためのコマンドをコマンドの待ち行列に置くには、'currentCommand'によって位置付けられた配列要素に 'Command' を書き込み、次に 'currentCommand' を増加する必要があります。 それが MAX_COMMANDS を越している場合には、'currentCommand' を 0 にして戻すべきです。

void CommandPlayOneShotSample(
  int channel, 
  int frequency, 
  void* data, 
  int length, 
  int volume)
{
  Command* command = 
    &commandControl->command[commandControl->currentCommand];
  PlaySampleSoundCommand* ps = &command->playSample;

  command->commandType = PLAY_ONE_SHOT_SAMPLE; 
  ps->channel = channel;
  ps->frequency = frequency;
  ps->data = data;
  ps->length = length;
  ps->volume = volume;

  commandControl->currentCommand++;
  commandControl->currentCommand &= MAX_COMMANDS-1;
}

また、ComandControl共有変数を初期状態に設定するために、ARM9 から初期化関数を呼ぶ必要があります。

void CommandInit() {
  memset(commandControl, 0, sizeof(CommandControl));  
}

この ARM9 コマンドに関連したコードは command9.cpp にあります。 新しいコマンドをcommand.hに追加することで、 Commando union を更新できます。

コマンドと ARM7

ARM7 に関連したコードは command7.cppにあります。 ARM7から定期的に 'CommandProcessCommands' 関数を呼び出す必要があります。 私は垂直ブランク割り込みからこれを呼び出しています。それは CommandControl 構造体からまだ処理していないすべてのコマンドを読み出し、その中のタイプに基づいた処理を実行します。

void CommandProcessCommands() {
  static int currentCommand = -1;
  
  while(currentCommand != commandControl->currentCommand) {
    Command* command = &commandControl->command[currentCommand];
    switch(command->commandType) {
    case PLAY_ONE_SHOT_SAMPLE:
      CommandPlayOneShotSample(&command->playSample);
      break;      
    }
    currentCommand++;
    currentCommand &= MAX_COMMANDS-1;
  }
}

もしコマンドが拡張されている場合には、この部分を修正して、コマンドタイプ(command->commanType)のcase文を追加して、 新しいコマンドタイプに基づく処理を Command unin に含まれるデータを渡して実行するようにします。 現在は唯一のコマンドは、チュートリアル4で解説したARM7でサウンドを再生する 'CommandPlayOneShotSample' だけです。

static void CommandPlayOneShotSample(PlaySampleSoundCommand* ps)
{
  int channel = ps->channel;

  SCHANNEL_CR(channel) = 0;
  SCHANNEL_TIMER(channel) = SOUND_FREQ(ps->frequency);
  SCHANNEL_SOURCE(channel) = (uint32)ps->data;
  SCHANNEL_LENGTH(channel) = ps->length >> 2;  
  SCHANNEL_CR(channel) = 
    SCHANNEL_ENABLE | 
    SOUND_ONE_SHOT | 
    SOUND_8BIT | 
    SOUND_VOL(ps->volume);
}

使用方法

これまでのコードが適切なら、ARM9からサウンドを再生するには、IPCを使用してすべてを実行する 'CommandPlayOneShotSample' を単純に呼び出すだけです。

    if(keysDown() & KEY_A) {
      [...]
      CommandPlayOneShotSample(current_channel, 
			       current_file->frequency, 
			       sound_buffer, 
			       current_file->length, 
			       0x3F);
    }

まとめ

私達は GBFSファイルシステムからファイルを検索できますが。それに関して、ファイル名・ユーザーに判りやすい表示名・再生時の頻度を持つ構造体をデモプログラム内に作成しました。

/* サウンドデータへのポインタ */
struct SoundFile {
  char const* filename;
  char const* name;
  int frequency;  
  uint8*  data;
  uint32 length;
};

static SoundFile soundFiles[] = {
  { "BaseHit.raw", "Base Hit", 11127, 0, 0 },
  { "InvHit.raw", "Invader Hit", 11127, 0, 0 },
  { "Shot.raw", "Shot", 11127, 0, 0 },
  { "Ufo.raw", "Ufo", 11127, 0, 0 },
  { "UfoHit.raw", "Ufo Hit", 11127, 0, 0 },
  { "Walk1.raw", "Walk1", 11127, 0, 0 },
  { "Walk2.raw", "Walk2", 11127, 0, 0 },
  { "Walk3.raw", "Walk3", 11127, 0, 0 },
  { "Walk4.raw", "Walk4", 11127, 0, 0 },
  { 0,0,0,0,0 }
};

また、この構造体はGBFSファイルシステム内でファイルが位置するポインタと長さを持っています。 このデータは初期処理の中で、GBFSルーチンを使用してカートリッジ領域をマップする際に取得します。

static void InitSoundFiles()
{
  // ゲームカートリッジ領域を ARM9 にマップ
  WAIT_CR &= ~0x80;
  
  /* カートリッジメモリの先頭から探索を開始 */
  GBFS_FILE const* gbfs_file = 
    find_first_gbfs_file((void*)0x08000000);

  unsigned int max_length = 0;
  SoundFile* file = soundFiles;
  while(file->filename) {
    file->data = (uint8*)gbfs_get_obj(gbfs_file, 
				      file->filename, 
				      &file->length);
    if(file->length > max_length)
      max_length = file->length;
    file++;
  }
  sound_buffer = (uint8*)malloc(max_length);
  current_file = soundFiles;
}

また、私がもっとも大きいサウンドファイルの長さにあわせて動的に割り当てたメモリをポイントするサウンドバッファを作成したことに注目してください。 これは ARM7 に送信するために、カートリッジ領域からデータを複写するバッファーです。 私は、ARM7に直接カートリッジ領域からサウンドを再生することに成功しませんでした。 残念ながら、これはあなたが再生できる最大のサウンドがメモリに保持しなければならないという制限事項になります。 私はこれに関して将来のチュートリアルで十句する方法を調べるつもりです。

'main' ルーチンはキーの押下のハンドリングと垂直ブランク割り込みでテキストを表示するだけです。

void on_irq() 
{	
  if(IF & IRQ_VBLANK) {
    consoleClear();
    consolePrintf("GBFS Demo Program\n\n");
    
    consolePrintf("Press 'A' to play current file.\n");
    consolePrintf("'left/right' changes file.\n");
    consolePrintf("'up/down' changes channel.\n\n");
    
    consolePrintf("File:    %s (%d)\n", 
		  current_file->name, 
		  current_file->length);
    consolePrintf("Channel: %d\n", current_channel);

    // Tell the DS we handled the VBLANK interrupt
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
    IF |= IRQ_VBLANK;
  }
  else {
    // Ignore all other interrupts
    IF = IF;
  }
}

void InitInterruptHandler()
{
  IME = 0;
  IRQ_HANDLER = on_irq;
  IE = IRQ_VBLANK;
  IF = ~0;
  DISP_SR = DISP_VBLANK_IRQ;
  IME = 1;
}

int main(void)
{
  powerON(POWER_ALL);  
  videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE);
  vramSetBankA(VRAM_A_MAIN_BG);
  BG0_CR = BG_MAP_BASE(31);
  BG_PALETTE[255] = RGB15(31,31,31);
  lcdSwap();
  InitInterruptHandler();
  consoleInitDefault((u16*)SCREEN_BASE_BLOCK(31), (u16*)CHAR_BASE_BLOCK(0), 16);

  CommandInit();
  InitSoundFiles();
  keysInit();

  while(1) {
    swiWaitForVBlank();
    scanKeys();

    if(keysDown() & KEY_UP) {
      if(++current_channel > 15)
	current_channel = 0;
    }
    if(keysDown() & KEY_DOWN) {
      if(current_channel == 0)
	current_channel = 15;
      else
	--current_channel;
    }
    if(keysDown() & KEY_LEFT) {
      if(--current_file < soundFiles)
	current_file = soundFiles;
    }
    if(keysDown() & KEY_RIGHT) {
      if((++current_file)->filename == 0) 
	--current_file;
    }
    if(keysDown() & KEY_A) {
      dmaCopy(current_file->data, sound_buffer, current_file->length);
      CommandPlayOneShotSample(current_channel, 
			       current_file->frequency, 
			       sound_buffer, 
			       current_file->length, 
			       0x3F);
    }
  }

  return 0;
}

RAW サウンドデータは 'A'キーが押された時に DMA コピーを使用してカートリッジ領域からローカルのサウンドバッファに転送します。 そして、サウンドを再生するように要求します。DMA コピーはCPU を使用するよりも基本的に速いハードウェアのコピーです。

ARM7 側での標準テンプレートに対する唯一の修正点は、垂直ブランク割り込み中に 前述した 'CommandProcessCommands' 関数を呼び出すことだけです。

デモのビルド

完全なサンプルプログラムは、'gbfs_demo1' です。ARM9 のコードは arm9_main.cppにあります。それは チュートリアル1と同様に情報を表示するのにコンソールルーチンを使用しています。 ARM7 のコードは arm7_main.cppにあります。 すべてをビルドするための Makefile ファイルです。

完全なソースコードは gbfs_demo1.zip で、エミュレータまたは実機で動作する gbfs_demo1.ndsgbfs_demo1.nds.gba ファイルをダウンロードできます。

結論

このチュートリアルではいろいろな領域をカバーしました。それはプログラムバイナリに追加するファイルシステムからどのようにファイルをロードするかを解説しました。次にサウンドを再生するために、どのように ARM9 から ARM7 にコマンドを送るかを解説しました。 最後に同じ keypress の反復通知ないで、キーの押下をそのように検出するかを解説しました。

私は、このチュートリアルのための情報を教えてくれたGBADEV フォーラムの投稿者に、そしてプロセス間通信の手法を見せてくれた MOD 再生サンプルプログラムの作者に感謝申し上げます。

いつものように、いろいろなコメントや提案を歓迎します。以下の私の連絡先の詳細を見てください。