4-5 BMSメインデータロード

ここではメインデータ(音符)のロード方法について説明しています。

■実データのロード

ようやく一番重要なデータのロード方法ですが、その前にちょっとおさらいをします。

BMS形式のページを見ればすぐですが、まずBMS形式というのがどういうものだったかを考えます。
例えば以下のようなBMSデータがあったとします。

#00111:01010101

この時の音符は4分音符が1小節内に4つありました。
つまり楽譜で書くと下のような感じです。


※特に「ミ」である必要はなくあくまでも長さのサンプルです

これを踏まえて説明をします。

データは1つのデータを1つの構造体に入れていく方法で定義するとして、まずは以下のような決まりごとを作ります。

1小節は9600カウントとする。
ここではこれをBMSカウント値と言う。

まず何をするかというと、この数を元にデータを分割して音符1つごとに開始カウントを求めるということをします。

さきほどのサンプルデータ
01010101というのを見てみると、文字数は8、つまりデータ数は4個となります。
ということは1小節を4で割ってあげれば、1つ分のカウントというのが求められるということになります。

文字数を取得する関数は簡単なのであとで説明するとして、まずは1つずつのカウント数を手動で求めてみましょう。

なお、1つずつ見るのも面倒なのでここでは一気に結果を書いてみます。
ちなみに上のデータは実際には2小節目のデータなのですでに1小節分の9600を加算した値としています。

音符ID BMSカウント値
0 9600
1 12000
2 14400
3 16800

よく見るとカウント値が2400ずつ増えていますが、これを数式で表してみます。

まずはじめに行うことは音符1つ分のカウント値の算出ですが、これは以下の計算式で求めることが出来ます。

w = 9600 ÷ ( 文字数 ÷ 2 )

今回の文字数というのは8なので、まずこの文字数を2で割ると4となり、これを1小節のカウントから割ると9600÷4となり、
つまり音符1つは2400カウントというのが求められます。

次に各音符のカウント値を求めます。

count = w × (音符ID)

音符IDというのはここでは4つ分のデータがあるので、単純に0、1、2、3と4回ループするだけです。
これで音符ごとのカウント値を算出することが出来ます。


さて、上のサンプルでは4つとも全てが音符でしたが実際はに音符が無い部分もあるため、
このチェックを入れて存在しないデータはスキップするようにします。

ちなみに難しいことを書いているようですが実際は特に難しいことは無く、まず文字列は2文字が1つのデータなので
先ほど計算した(文字数÷2)でループ数を求めこの数だけforでループさせます。

そして上記のデータ
01010101は分解すると、01 01 01 01となりますが、その区切りごと数値化してこれが00であるかどうかを判断し、
00ならデータが存在しないのでスキップ、それ以外ならデータが存在するということでリストに追加するだけです。


最後に先ほどから使っている文字数というのを求める関数を紹介します。
この関数は標準のCライブラリに必ず含まれるので必ず覚えておきましょう。

size_t strlen( const char *string );
stringにはNULLで終わっている文字列のバッファポインタを入れます。
結果にはその文字列の数を返します。
size_tというのはunsigned intとなっています。
なお、日本語の場合は2バイト文字になるため、たとえば「あ」と入れた場合は2が返ります。

ちなみにこの関数を使うには以下のヘッダが必要です。
<string.h>

まずはこれで基本的な概念は終了として、次はこれをプログラムにします。


■DATA部分のプログラム

それではいままでの内容をプログラムにしてみます。

ちなみにこのサンプルでは説明を簡単にするため、1つのチャンネル(11番)のみを見ています。
最終的にはすべてのチャンネルに対応するという処理が必要です。

また、小節幅の変更は考慮していません(必ず4/4拍子として計算)。
例のごとくサンプルなので眺める程度にしておきましょう。

これらをサポートするルーチンは次章のCBmsProクラスの説明で行います。

ヘッダファイル

////////////////////////////////////////////////////////////////////////
// BMS.H
////////////////////////////////////////////////////////////////////////

// BMSデータ情報
typedef struct _BMSDATA {
    LONG        lTime;          // 鳴らすタイミングデータ
    LONG        lData;          // 鳴らすデータ(0~255)
} BMSDATA,*LPBMSDATA;



// プロトタイプ(実体はヘッダ部分のプログラムを参照)
int GetCommand( const char *s );                       // コマンド番号を返す
BOOL GetCommandString( const char *src,char *dst );    // パラメータ文字列を取得
int atoi1610( const char *s );                         // 16進数文字列を数値に変換


ソースファイル

////////////////////////////////////////////////////////////////////////
// BMS.CPP
////////////////////////////////////////////////////////////////////////

// グローバル変数
LPBMSDATA pBmsData = NULL;          // 1つのチャンネルのデータ配列(11のみを見ると仮定)
int       iBmsData = 0;             // データの数


// BMSデータの読み込み
BOOL LoadBmsData( const char *file )
{
    // ファイルをロード
    FILE *fp = fopen( file,"r" );
    if( !fp )
        return FALSE;

    while(1) {
        // 1行を読みこむ
        char buf[1024];
        ZeroMemory( buf,1024 );
        fgets( buf,1024,fp );
        if( buf[0]==NULL && feof(fp) )
            break;

        // コマンド以外なら次の行へ
        if( buf[0]!='#' )
            continue;

        // コマンドの種類を取得
        int cmd = GetCommand( buf );
        if( cmd!=-1 )
            continue;                           // データではない場合は次の行へ

        // パラメータ文字列を取得
        char data[1024];
        ZeroMemory( data,sizeof(data) );
        if( !GetCommandString(buf,data) ) {
            fclose(fp);
            return FALSE;                       // 文字列の取得が失敗ならエラー
        }

        // チャンネル番号の取得
        char tmp[4];
        ZeroMemory( &tmp,sizeof(tmp) );
        tmp[0] = buf[4];                        // チャンネル番号
        tmp[1] = buf[5];                        // #001xx:******* のxx部分
        int ch = atoi( tmp );                   // 数字化

        // チャンネル番号のチェック(このサンプルのみ)
        if( ch!=0x11 )
            continue;

        // 小節の倍率変更命令の場合はキャンセル
        if( ch==2 )
            continue;

        // 小節番号を取得
        ZeroMemory( &tmp,sizeof(tmp) );
        tmp[0] = buf[1];                        // 小節番号
        tmp[1] = buf[2];                        // #xxx11:******* のxxx部分
        tmp[2] = buf[3];
        int line = atoi( tmp );                 // 数字化

        // データが偶数かチェック
        if( strlen( data )%2==1 ) {
            fclose( fp );
            return FALSE;                       // データが奇数ならエラー
        }

        // データ数
        int len = strlen( data ) / 2;                               

        // 1音符の長さ
        int tick = 9600 / len;

        // 実際のデータの追加
        for( int i=0;i<len;i++ ) {
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = data[i*2  ];
            tmp[1] = data[i*2+1];
            int data = atoi1610( tmp );         // 16進数

            // データが0で無い場合はメモリに追加していく
            if( data>0 ) {
                iBmsData++;
                pBmsData = (LPBMSDATA)realloc( pBmsData,sizeof(BMSDATA)*iBmsData );     // メモリを再確保する
                pBmsData[ iBmsData-1 ].lTime    = line * 9600 + i * tick;               // 現在の小節数+オフセット
                pBmsData[ iBmsData-1 ].lData    = data;                                 // 取得した値(鳴らすデータID)を保存
            }
        }
    }

    fclose( fp );
    return TRUE;
}

このプログラムを簡単に説明します。

まず指定のファイルをオープンし1行ずつ検索をします。

たとえば「#
00111:0102000300040400」のようなデータの行であれば、
小節番号(この場合
001)とチャンネル番号(この場合11)を取得します。

そして、小節番号からその小節の始点のカウント数を求めます。
1小節は9600と決め打ちしているので単純に9600をかけます。
さらにデータ数を9600から割ることで1音符のカウント数を得ます。

データ部をforでループさせ1つずつデータを数値化し、
データが存在する場合は現在のループに1音符分のカウント値をかけ、
それをその小節のカウント値に加算します。

この例では8つのデータで定義されているため、9600を8で割ると1音符は1200カウントになります。

現在の音符のカウント数が分かったら、あとはそれを構造体に設定しメモリバッファに追加していきます。
ここではメモリの再確保でやったようなやり方を使用しデータを追加します。


最終的にこの行を解析した後は、以下のような配列になっています。
なお、この例では小節番号が001なので実際には2小節目の定義となります。
そのため最初から9600が加算されている状態となっています。

pBmsData [0] [1] [2] [3] [4]
llTime 9600 10800 13200 15600 16800
lData 01 02 03 04 04


これで最低限必要なBMSプログラムの説明は終わりです。
次章はもっと高度な機能(小節幅変更やテンポの途中変更など)を盛り込んだプログラムについて説明します。