4-6 もっと複雑なBMSファイルの読み込み

この章では前回説明しなかった小節幅の変更やBPMの途中変更、
ゲーム中で表示する小節バーなどの対応も考えてのプログラムの作り方を説明します。


■BMSでの小節の長さの定義

BMSでは小節ごとに長さを変更することが出来ます。(こちらを参照)

このコマンドを使用すると、たとえば通常4/4拍子のリズムを途中で2/4に変更するということが出来ますが、
これを時間的に考えると通常の半分の時間の小節ということになります。

図にすると以下のような感じになります。

※9600や14400などの値は以前説明した1小節を9600とした場合のBMSカウント単位です


これを踏まえて前章のプログラムを見ると、小節の幅はまったく考慮されていないため、
このようなデータをロードすると以下のようなとんでもない曲になってしまいます。


2小節目が四分音符ではなく二分音符となっているため曲が伸びてしまっています。

ということでまずはこれに対応するための概要を説明します。

まず前提としてBMSの仕様ではどの場所にも命令は書けます。
つまり例えば小節番号が若いものを後方に持ってくることが出来るわけですが、
同じく小節長コマンドももしかしたら実データのあとに記述されている可能性があります。

もし小節長コマンドがあとに記述されていた場合、最初は1小節を9600のまま計算し、
そのあとで小節長コマンドにて小節長が変更された場合、その小節に該当するデータはもちろん、
それ以降のデータの開始位置も修正する必要があります。

別にこういったプログラムも出来ないわけではないのですが、
なんとなく要領が悪いのでここでは小節長コマンドを先にロードしておき、
実データの解析時にはロード済みの小節長を使うようにしてみます。

ちなみに、この小節長リストはあとで説明する小節バーの表示にも利用されます。


小節長構造体

BMSの仕様上、小節番号は最大で999までとなっています。
つまり0~999の小節情報があれば良いので、ここでは始めから1000個分の小節バッファを定義しておきます。
※ここでは曲の最後の小節も表示させるため+1しています

例えば小節長の管理だけであれば以下のように定義出来ます。

float  fScale[1000+1];


まずは最初に全ての配列を1.0fで初期化しておきます。

そしてコマンド解析時に小節長コマンドが現れたら、そのコマンドの小節番号を配列番号としその配列に今回の小節長を記録させます。
もし複数の小節長コマンドが存在した場合は常に後方の行にある小節長で上書きされるため、
後方の情報が優先されるといったBMSの仕様にも合致します。

完成した小節長のリストを実データのロード時に参照することで、そのデータの小節番号から即座にその小節の長さが分かるようになります。

しかし実はこのリストだけだと少し不便で、例えばある音符のBMSカウント値を求めるには、
それまでの小節長もすべて考慮してその小節の開始カウントを計算しなければなりません。

例としてデータが10小節目を指している場合、まず10小節目の先頭のBMSカウント値を求めなければなりません。
そのためにはその手前までの小節(ここでは0~9)の長さを全て足して求めます。

// 10小節目の開始カウントの算出
LONG start = 0;
for( int i=0;i<10;i++ ) {
    start += 9600 * fScale[i];
}

これで10小節目の開始カウントが分かったので、あとはその小節内での音符のカウントを加算することで最終的なBMSカウント値を求めることが出来ます。


しかしよく考えると全ての小節長は配列にまとめられており、また各小節の開始位置というのは実データの解析時には変わることはありません。
ということは、実データの解析前に小節ごとの開始位置を最初から求めておけばよいことになります。

これを実現するためにまずは以下のような構造体を定義します。

// 小節情報
typedef struct _BMSBAR {
    float       fScale;                     // この小節の長さ倍率
    LONG        lTime;                      // この小節の開始位置(BMSカウント値)
    LONG        lLength;                    // この小節の長さ(BMSカウント値)
} BMSBAR,*LPBMSBAR;


そしてこれを最大小節分確保します。

BMSBAR      mBmsBar[1000+1];                // 小節データ(999小節時に1000番目も小節バーとして使うため+1しておく)


上記同様初期状態でfScaleを1.0fで初期化しておきますが、lTimeとlLengthはここではまだ0のままにしておきます。

    // 小節の長さを1.0で初期化
    ZeroMemory( &mBmsBar,sizeof(mBmsBar) );
    for( int i=0;i<1001;i++ ) {
        mBmsBar[i].fScale = 1.0f;
    }



小節長の解析

ではこの小節長コマンドの解析処理を組み込んでみましょう。
組み込み場所は実データのロード前に入れる必要があるので、ここではヘッダロードの部分で行います。

また、ついでにゲームの終了を判断するための情報として、ここで最大小節数も取得しておきます。
これはデータに含まれる小節番号の中で、単に最大の値を保存するだけです。

以下がヘッダロード部分に追加した場合のプログラムです。

※mBmsBarは上記のように初期化されているとします

long    lEndBar = 0;    // 終了小節


////////////////////////////////////////////////////////////////////////////////////////
// ヘッダ情報だけを取り出す
////////////////////////////////////////////////////////////////////////////////////////
BOOL CBmsPro::LoadHeader( const char *file )
{
    Clear();

    FILE *fp;
    fp = fopen( file,"r" );
    if( !fp ) {
        return FALSE;
    }

    char buf[1024];
    char tmp[4];
    int num;
    int ch;

    while(1) {
        // 1行を読みこむ
        ZeroMemory( buf,1024 );
        fgets( buf,1024,fp );
        if( buf[0]==NULL && feof(fp) )  // ファイルの終端なら検索終わり
            break;

        // コマンド以外なら飛ばす
        if( buf[0]!='#' )
            continue;

        // 最後の改行を消去
        if( buf[strlen(buf)-1]=='\n' )
            buf[strlen(buf)-1] = NULL;

        // コマンドの解析
        int cmd = GetCommand( buf );

        // 不明なコマンドならスキップ
        if( cmd<=-2 ) {
            continue;
        }

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

        // パラメータの代入
        switch( cmd )
        {
        case 0:     // PLAYER
            mBH.lPlayer = atoi( str );
            break;
        case 1:     // GENRE
            strcpy( mBH.mGenre,str );
            break;
        case 2:     // TITLE
            strcpy( mBH.mTitle,str );
            DEBUG( "タイトル     [%s]\n",mBH.mTitle );
            break;
        case 3:     // ARTIST
            strcpy( mBH.mArtist,str );
            break;
        case 4:     // BPM
            if( buf[4]==' ' || buf[4]==0x09 ) {
                // 基本コマンドなら
                mBH.fBpm = (float)atof( str );
            } else {
                // 拡張コマンドなら
                ZeroMemory( tmp,sizeof(tmp) );
                tmp[0] = buf[4];
                tmp[1] = buf[5];
                tmp[2] = NULL;
                ch = atoi1610( tmp );   // 16進数
                mBH.fBpmIndex[ch] = (float)atof( str );
            }
            break;
        case 5:     // MIDIFILE
            strcpy( mBH.mMidifile,str );
            break;
        case 6:     // PLAYLEVEL
            mBH.lPlaylevel = atoi( str );
            break;
        case 7:     // RANK
            mBH.lRank   = atoi( str );
            break;
        case 8:     // VOLWAV
            mBH.lWavVol = atoi( str );
            break;
        case 9:     // TOTAL
            mBH.lTotal  = atoi( str );
            break;
        case 10:    // StageFile
            strcpy( mBH.mStagePic,str );
            break;
        case 11:    // WAV
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = buf[4];
            tmp[1] = buf[5];
            num = atoi1610( tmp );          // 16進数
            strcpy( mWavFile[num],str );
            break;
        case 12:    // BMP
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = buf[4];
            tmp[1] = buf[5];
            num = atoi1610( tmp );          // 16進数
            strcpy( mBmpFile[num],str );
            break;
        default:
            // 小節番号の取得
            ZeroMemory( tmp,sizeof(tmp) );
            memcpy( tmp,buf+1,3 );
            line = atoi( tmp );         // 10進数
            // チャンネル番号の取得
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = buf[4];
            tmp[1] = buf[5];
            ch = atoi1610( tmp );       // 16進数
            if( ch==BMS_STRETCH ) {
                // 小節の倍率変更命令の場合
                mBmsBar[line].fScale = (float)atof( str );
            }
            // 小節番号の最大値を記憶する
            if( lEndBar < line )
                lEndBar = line;
            break;
        }
    }

    fclose( fp );
    return TRUE;
}

上記の内容を説明すると、まず1行ロードしたらこれが実データかどうか判定します。

実データであればそれが小節長コマンドかをチェックします。

ちなみに小節長コマンドはチャンネル番号として2と定義されているため、
これが2だった場合は小節長コマンドとしてその値を小節長リストに登録します。
※この方法により同じ小節に対して複数の小節長コマンドが存在する場合は、一番最後に処理した小節長が適用されます

ちなみに実データには小節番号が必ず含まれるため、ここで一番大きい値をlEndBarに記録しています。


■小節長リストの構築

全ての小節長の情報がロードできたら、最後にこの小節長構造体リストに定義した
小節ごとの開始カウント値(lTime)と、その小節の長さ(lLength)を算出します。

なお、今回ロードした最大の小節数は既にlEndBarに入っているため、
計算はこの小節まで行えばよい事になりますが、ゲームの仕様上、曲の最後の小節も表示したいため、
実際はここでは最後の小節+1までの小節リストを構築することにします。

また、リスト構築時にゲームの終了時のカウント値も一緒に求められるので、
ここではついでにその最大BMSカウント値を保存する変数も用意しておきます。

この計算はヘッダロードの処理が全て完了した後に行います。
以下はそのコードとなります。

long    lMaxCount;  // 最大のカウント数


////////////////////////////////////////////////////////////////////////////////////////
// ヘッダ情報だけを取り出す
////////////////////////////////////////////////////////////////////////////////////////
BOOL CBmsPro::LoadHeader( const char *file )
{
    Clear();

    FILE *fp;
    fp = fopen( file,"r" );
    if( !fp ) {
        return FALSE;
    }

    char buf[1024];
    char tmp[4];
    int num;
    int ch;

    while(1) {
        // 1行を読みこむ
        ZeroMemory( buf,1024 );
        fgets( buf,1024,fp );
        if( buf[0]==NULL && feof(fp) )  // ファイルの終端なら検索終わり
            break;

        // コマンド以外なら飛ばす
        if( buf[0]!='#' )
            continue;

        // 最後の改行を消去
        if( buf[strlen(buf)-1]=='\n' )
            buf[strlen(buf)-1] = NULL;

        // コマンドの解析
        int cmd = GetCommand( buf );

        // 不明なコマンドならスキップ
        if( cmd<=-2 ) {
            continue;
        }

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

        // パラメータの代入
        switch( cmd )
        {
        case 0:     // PLAYER
            mBH.lPlayer = atoi( str );
            break;
        case 1:     // GENRE
            strcpy( mBH.mGenre,str );
            break;
        case 2:     // TITLE
            strcpy( mBH.mTitle,str );
            DEBUG( "タイトル     [%s]\n",mBH.mTitle );
            break;
        case 3:     // ARTIST
            strcpy( mBH.mArtist,str );
            break;
        case 4:     // BPM
            if( buf[4]==' ' || buf[4]==0x09 ) {
                // 基本コマンドなら
                mBH.fBpm = (float)atof( str );
            } else {
                // 拡張コマンドなら
                ZeroMemory( tmp,sizeof(tmp) );
                tmp[0] = buf[4];
                tmp[1] = buf[5];
                tmp[2] = NULL;
                ch = atoi1610( tmp );   // 16進数
                mBH.fBpmIndex[ch] = (float)atof( str );
            }
            break;
        case 5:     // MIDIFILE
            strcpy( mBH.mMidifile,str );
            break;
        case 6:     // PLAYLEVEL
            mBH.lPlaylevel = atoi( str );
            break;
        case 7:     // RANK
            mBH.lRank   = atoi( str );
            break;
        case 8:     // VOLWAV
            mBH.lWavVol = atoi( str );
            break;
        case 9:     // TOTAL
            mBH.lTotal  = atoi( str );
            break;
        case 10:    // StageFile
            strcpy( mBH.mStagePic,str );
            break;
        case 11:    // WAV
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = buf[4];
            tmp[1] = buf[5];
            num = atoi1610( tmp );          // 16進数
            strcpy( mWavFile[num],str );
            break;
        case 12:    // BMP
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = buf[4];
            tmp[1] = buf[5];
            num = atoi1610( tmp );          // 16進数
            strcpy( mBmpFile[num],str );
            break;
        default:
            // 小節番号の取得
            ZeroMemory( tmp,sizeof(tmp) );
            memcpy( tmp,buf+1,3 );
            line = atoi( tmp );         // 10進数
            // チャンネル番号の取得
            ZeroMemory( tmp,sizeof(tmp) );
            tmp[0] = buf[4];
            tmp[1] = buf[5];
            ch = atoi1610( tmp );       // 16進数
            if( ch==BMS_STRETCH ) {
                // 小節の倍率変更命令の場合
                mBmsBar[line].fScale = (float)atof( str );
            }
            // 小節番号の最大値を記憶する
            if( lEndBar < line )
                lEndBar = line;
            break;
        }
    }

    // 最後の小節内にもデータが存在するため、その次の小節を終端小節とする
    lEndBar++;

    // 小節倍率データを元に全ての小節情報を算出
    LONG cnt = 0;   // 現在の小節の開始カウント値
    int i;
    for( i=0;i<=lEndBar;i++ ) {
        // 小節リストを加算
        mBmsBar[i].lTime    = cnt;                                              // 現在の小節の開始カウントを記録
        mBmsBar[i].lLength  = (LONG)(BMS_RESOLUTION * mBmsBar[i].fScale);       // 倍率からこの小節の長さカウント値を算出

        // この小節のカウント数を加算して次の小節の開始カウントとする
        cnt += mBmsBar[i].lLength;
    }

    // 最大カウントを保存
    lMaxCount = cnt;

    fclose( fp );
    return TRUE;
}

上記コメントにあるように、lEndBarは実データに記述された最大の小節番号が入りますが、
一般的にには最大小節番号より最大小節数として考えるのが普通なので、
ここでは最大の小節番号に+1することで最大小節数としています。
※例えば0~9の小節があった場合、個数で言えば10個ということになります

次に各小節の開始カウントとその小節の長さの算出を行いますが、
まず現在の小節のカウント値を把握するためにcntという変数を用意し、これを0で初期化しておきます。
これに各小節の長さを順番に足していくことで、次の小節の開始位置が求められるので、
これを最大小節の数までループして各小節の開始カウントを決定して行きます。

この時ついでに実データの解析で使用するため、各小節の長さも一緒に計算しておきます。

そして、最後の小節の開始位置というのはゲームの終了位置を指しているので、
これをlMaxCountに記録しています。

これで今回プレイする曲の最大小節までの小節情報が全て揃ったので、
次の実データのロードでは、このリストを参照して小節長変更に対応した処理を行います。


■実データロード部分に組み込む

ここでは小節長が変更された状態で実データをロードする処理を実装してみます。

さて、以前紹介したロード部分ではロードした実データに対して必ず1小節を9600として処理していました。
この9600が固定だったため小節長の変更に対応出来なかったわけですが、
上記のヘッダロード処理にてそれぞれの小節の最終的な長さが既に計算されているので、
その音符の小節番号が分かれば、その小節の長さもすぐに分かります。

さらに、既にその小節の開始時のBMSカウント値も求められているため、mBmsBarの配列を小節番号で参照するだけで、
一瞬でその小節の開始カウントと長さが分かります。

あとはその小節内での音符の位置を計算し開始カウントに足すだけで、
簡単にその音符の最終的なBMSカウント値を求めることが出来ます。


例として以下のような譜面があったとします。


ここでは1小節目と3小節目が2/4拍子となっており、それ以外は4/4拍子のままです。
またこの例では最大の小節番号は4(#00411)となるので、小節数としては5個存在することになります。

これをLoadHeader()で解析すると、結果的に以下のような小節長リストが構築されます。

小節番号 BMSBAR構造体
fScale lTime lLength
0 1.0f 0 9600
1 0.5f 9600 4800
2 1.0f 14400 9600
3 0.5f 24000 4800
4 1.0f 28800 9600


このリストを参照して音符の位置を計算するプログラムは以下のようになります。

// 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==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音符分のカウント値を算出
        LONG tick = mBmsBar[line].lLength / len;

        // 実データを追加
        ZeroMemory( &tmp,sizeof(tmp) );
        for( i=0;i<len;i++ ) {
            tmp[0] = data[i*2];
            tmp[1] = data[i*2+1];
            int data = atoi1610( tmp );         // 16進数
            if( data>0 ) {
                // データが存在する場合
                AddData( ch, mBmsBar[line].lTime + (tick * i), data );
            }
        }
    }

    fclose( fp );
    return TRUE;
}

赤字の場所が小節長リストを参照している部分ですが、ここではint lineに今回の小節番号が入っているので、
これをそのままmBmsBarの配列番号にアクセスし、その小節の長さとその小節の開始カウントを参照しています。

あとは小節長を音符の分割数で割ることで1音符分の長さを算出し、
その小節の開始カウントと各音符の位置とを加算することで、最終的なBMSカウント値を算出しています。

なお、AddData()関数についてはあとで詳しく説明しますが、
簡単に言えば指定のチャンネルに音符を追加するだけの関数です。


■全チャンネル対応

さて、今までのプログラムでは0x11チャンネル1つだけを対象としていましたが、
ここから先は全てのチャンネルに対して処理を行うことを考えます。

まぁ今まで1つのチャンネルだった部分を複数化するだけなので、勘のいい人はすぐにその方法が思いつくと思います。
ようはデータを保持する配列ポインタをさらにその分だけ用意するだけなので、単にポインタを配列化するだけです。



今までサンプルでは実データは以下のような定義でした。

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

今回はこれを配列化すればいいので以下のようになります。

LPBMSDATA   pBmsData[256];      // 実データ
int         iBmsData[256];      // 実データのそれぞれの数

BMSではチャンネル数が0x00~0xFFまでの256個存在するので、単純に配列も256個用意するだけです。


ポインタ配列にメモリを確保するとはどういうことか

ポインタに慣れていなかったりメモリというものを理解していないと、
これが何を意味しているのか意味が分からないかもしれないので、
ここではこれについて簡単に説明します。

一見、一次元配列のようにも見えますが、中身は縦軸と横軸の二次元となります。
かといって普通にメモリが連結しているような配列ではないため、
たとえばdata[10][10]の配列なら間違ってdata[0][20]とやってもメモリをまたいで読むことは出来ますが、
今回のような手法ではこのような参照は即座にアクセスバイオレーションが発生してしまい、
アプリがクラッシュしてしまいます。
※ちなみにまたいで読むようなプログラムは普通作りません(あくまでも例です)

以下はこれにメモリを確保した場合の例えの図になります。


この例では縦軸は256個固定なので分かりやすいですが、横軸は動的に確保されるため、
現在何個分のデータがあるのかはこのままでは分かりません。
そのためそれぞれ何個のデータが存在しているのかを管理するために、
別途iBmsData[256]という形で数を記録しておく変数を用意しています。

まずメモリをそれぞれの配列に確保したとすると、当然別々の場所のメモリが確保されます。
例えば上記の例でpBmsData[0][10]ということは赤い領域内での10個目のデータ、
pBmsData[2][40]というのはピンク色の領域内での40個目のデータということになります。

よく見るとこのメモリアドレスはまったく連続性は無く、
どこに確保されるかはCPUやOSが自動的に判断して行います。
そのため、このような状態でもしpBmsData[10][200]のように数を超えた部分にアクセスしようとすれば、
確保されていないアドレスを指してしまうため、アクセスバイオレーションが発生してしまいます。

ちなみに横軸を確保するためには、
 pBmsData[0] = malloc( <サイズ> );
 pBmsData[100] = malloc( <サイズ> );
のように指定の配列に対してmallocを行い、終了時は
 free( pBmsData[0] );
 free( pBmsData[100] );
と、それぞれの配列に対してfreeを行います。

そして、
 iBmsData[0] = 100;
 iBmsData[100] = 5;
のようにiBmsData配列にその数を記録しておき、アクセス時に範囲を超えないようにしたり、
追加が必要なら現在の数に必要数を加算してからreallocを使ってメモリを再確保します。

例えばiBmsData[0]=100の場合、pBmsData[0]には100個分のデータが存在することになるため、
この時アクセス出来る配列の範囲はpBmsData[0][0]~pBmsData[0][99]となります。
これによりそれ以上のアクセスをしないように制限をかけることが可能になります。


メモリを動的に確保するようなプログラムは、きちんとそのメモリの初期化と終了処理を行わなければなりません。
なお、メモリポインタが1つなら特に難しくはありませんが、ポインタの配列となると多少複雑になるので、
ここではこのメモリポインタ配列の初期化と開放の方法について説明します。

以下はメモリ配列の初期化と開放のサンプルです。

#define SAFE_FREE(x)    { if(x) { free(x); x=NULL; } }

BOOL Init()
{
    ZeroMemory( &pBmsData,sizeof(pBmsData) );
    ZeroMemory( &iBmsData,sizeof(iBmsData) );

    /*
    // この方法で代用も可能
    for( int i=0;i<256;i++ ) {
        pBmsData[i] = NULL;
        iBmsData[i] = 0;
    }
    */
}

BOOL Exit()
{
    for( int i=0;i<256;i++ ) {
        SAFE_FREE( pBmsData[i] );       // 開放
        iBmsData[i] = 0;                // 数をクリア
    }
}

まずC言語では変数は初期化を行わないと、どんな値が入っているか分からない状態となっているため、
必ず初期化を行う必要があります。
ここではそのための関数としてInit()関数を定義し、この中でpBmsDataとiBmsDataを0で初期化しています。

Exit()ではSAFE_FREE()を使用して全てのポインタ配列を開放しています。
このマクロは指定の変数がNULLでは無い場合に、開放してからその変数にNULLを入れています。
例えばもう一度Exit()が呼ばれたりした場合、既に開放済みでNULLになっているため何もしないプログラムになります。

ちなみにこういった最初に初期化、最後に終了処理をするプログラムはクラスに向いています。

初期化をコンストラクタに、開放をデストラクタに入れておくことで自動的に初期化と開放を行ってくれるため、
そのクラスを使うプログラムでは必要なとき以外、明示的に初期化と開放を呼ぶ必要がありません。

以下はこれをクラス化した場合の例です。

class CBmsData {
    LPBMSDATA   pBmsData[256];
    int         iBmsData[256];

public:
    CBmsData() {
        ZeroMemory( &pBmsData,sizeof(pBmsData) );
        ZeroMemory( &iBmsData,sizeof(iBmsData) );
    }

    virtual ~CBmsData() {
        for( int i=0;i<256;i++ ) {
            SAFE_FREE( pBmsData[i] );
            iBmsData[i] = 0;
        }
    }
};



■AddData()関数を作る

上で全チャンネル用のメモリを確保できたので、ここではそれに実際の音符データを追加する関数を考えます。
と言ってもここまで来たらそんなに難しいことはありません。

BMSのデータ仕様では実は小節長コマンドのみ特殊で、それ以外のチャンネルは全て音符データとして処理出来ます。
そしてここでは音符データのみの追加を行う関数とするため、小節長コマンドは無視するようにします。

以下は小節長コマンド以外の全チャンネルを、pBmsDataに追加していくプログラムです。

// BMSデータ情報
typedef struct _BMSDATA {
    LONG        lTime;      // このデータの開始位置(BMSカウント値)
    LONG        lData;      // 鳴らすデータ(0x01~0xFF)
    float       fData;      // 小数値データ(テンポ用)
    BOOL        bFlag;      // アプリが使用出来る任意の変数(ここでは判定に利用)
} BMSDATA,*LPBMSDATA;


////////////////////////////////////////////////////////////////////////////////////////
// 1つのデータを追加
////////////////////////////////////////////////////////////////////////////////////////
BOOL AddData( int ch,LONG cnt,LONG data )
{
    // チャンネル番号をチェック
    if( ch<0 || ch>255 )
        return FALSE;

    // 小節長変更コマンドなら何もしない
    if( ch==0x02 )
        return FALSE;

    // データが無ければ何もしない
    if( data==0 )
        return TRUE;

    switch( ch )
    {
    case 0x08:
        // BPMのインデックス指定(新)
        iBmsData[0x03]++;
        pBmsData[0x03] = (LPBMSDATA)realloc( pBmsData[0x03],iBmsData[0x03]*sizeof(BMSDATA) );
        ZeroMemory( &pBmsData[0x03][ iBmsData[0x03]-1 ],sizeof(BMSDATA) );      // 追加した配列をクリア
        pBmsData[0x03][iBmsData[0x03]-1].bFlag = TRUE;
        pBmsData[0x03][iBmsData[0x03]-1].lTime = cnt;
        pBmsData[0x03][iBmsData[0x03]-1].lData = (LONG)mBH.fBpmIndex[data];     // テンポ値をLONG型にも保存(デバッグ用)
        pBmsData[0x03][iBmsData[0x03]-1].fData = mBH.fBpmIndex[data];           // テンポリストに入っているテンポ値を登録
        break;
    default:
        // データを追加
        iBmsData[ch]++;
        pBmsData[ch] = (LPBMSDATA)realloc( pBmsData[ch],iBmsData[ch]*sizeof(BMSDATA) );
        ZeroMemory( &pBmsData[ch][ iBmsData[ch]-1 ],sizeof(BMSDATA) );          // 追加した配列をクリア
        pBmsData[ch][iBmsData[ch]-1].bFlag = TRUE;
        pBmsData[ch][iBmsData[ch]-1].lTime = cnt;
        pBmsData[ch][iBmsData[ch]-1].lData = data;                              // 鳴らすデータを保存
        pBmsData[ch][iBmsData[ch]-1].fData = (float)data;                       // float型にも保存(デバッグ用)
        break;
    }

    return TRUE;
}

まずBMSDATA構造体に2つの変数を追加しています。

1つは新仕様の小数値対応テンポのためfloat型のfData変数、もう1つはゲームで使用するBOOL型のbFlag変数です。

bFlagに関しては、簡単に言うと音符の判定が既に終わったかどうかを判断するためのものです。
詳細は実際にゲームを作るときに説明します。


AddData()関数ではまず最初に対象のチャンネル番号か、データが正しく指定されているかをチェックし、
問題が無ければそのチャンネルに対して一番後方にメモリバッファを足し、そこにカウント値とデータをセットしています。

なお、新仕様のインデックス指定のテンポに関しては、ヘッダロードで解析したテンポリストに入っているテンポを参照して追加しますが、
ここで重要なのは追加先はテンポインデックスチャンネルの0x08ではなく、通常のテンポ変更チャンネルの0x03に対して行います。

こうすることで、旧仕様の0x03チャンネルのテンポチェンジと0x08チャンネルのテンポインデックスリストを、
全て0x03チャンネルにまとめることが出来ます。


■データのソート

ここまででヘッダ部と実データ部のロードはほぼ完了しましたが、実はこのプログラムにはまだ欠陥があります。

それはAddDataは単にデータを後方に追加していくものであり、もしBMSファイル内のデータの小節番号が
途中で若い数値になった場合でも、常に後方に追加されてしまいます。
ということは、このデータを順番に羅列してみるとBMSカウント値が途中で若くなる箇所が出てきます。

別に順番に並んでいなくてもゲームを作ることは可能ですが、このままではゲームの最適化が行えなくなり、
いろいろと効率の悪いゲームプログラムとなってしまいます。

このため、ここでは全てのデータをBMSカウント値で昇順になるように並び替えを行い、
あとで最適化が行えるようにデータを整理しておきます。


ちなみにこの並び替えはAddData()関数内で行うことも出来ますが、

ここではデータの追加処理と並び替え処理を明確に分けて考えているので、
並び替えの部分だけを別途用意することにします。

なお、並び替え処理はアルゴリズム集で紹介している方法を使いますが、
実際には並び替える対象は構造体となるので、アルゴリズム集のものをさらに拡張して実装します。
このためアルゴリズム集の並び替えの原理を確実に理解しておいてください。

※ちなみにAddData内で行いたい場合は常に後方に追加するのではなく、
 既に登録済みのデータに対してBMSカウント値が順番になる位置を検索し、
 その位置に新しいデータを挿入するようにします。
 こうすれば挿入の段階で既に並び替えが終了した状態にすることが出来ます。


以下は指定のチャンネルにあるデータを並び替える関数です。

////////////////////////////////////////////////////////////////////////////////////////
// 指定チャンネルのデータを昇順に並び替える
////////////////////////////////////////////////////////////////////////////////////////
BOOL Sort( int ch )
{
    if( ch<0 || ch>255 )
        return FALSE;

    // 昇順に並び替える
    int i,j;
    for( i=0;i<iBmsData[ch]-1;i++ ) {
        for( j=i+1;j<iBmsData[ch];j++ ) {
            if( pBmsData[ch][i].lTime > pBmsData[ch][j].lTime ) {
                // 構造体を入れ替える
                BMSDATA dmy     = pBmsData[ch][i];      // ダミーに保存
                pBmsData[ch][i] = pBmsData[ch][j];      // iにjを入れる
                pBmsData[ch][j] = dmy;                  // jにダミーを入れる
            }
        }
    }
    return TRUE;
}

並び替えに参照される値はBMSDATA構造体のlTimeの値となります。
この値を比較して後方が小さければ構造体ごと入れ替えを行います。

この処理は全ての実データをロードし終わった後に行います。
また、並び替えは全てのチャンネルについて行う必要があるため、
以下のようにforで全チャンネルを指定して行わせます。

////////////////////////////////////////////////////////////////////////////////////////
// BMSデータの読み込み
////////////////////////////////////////////////////////////////////////////////////////
BOOL LoadBmsData( const char *file )
{
    ~~~~~~ 省略 ~~~~~~

    while(1) {

        ~~~~~~ 省略 ~~~~~~

        // 実データを追加
        ZeroMemory( &mNum,sizeof(mNum) );
        for( i=0;i<len;i++ ) {
            mNum[0] = data[i*2];
            mNum[1] = data[i*2+1];
            long hex = atoi1610( mNum );                    // 16進数を数値へ変換
            if( hex>0 ) {
                // データが存在する場合は追加を行う
                AddData( iChannel, llStartCount + llNowBarReso*i,hex );
            }
        }
    }

    fclose( fp );

    // ソート
    for( i=0;i<256;i++ )
        Sort( i );

    return TRUE;
}



■小節バーの定義

ゲームでは各小節の区切りとして小節バーが表示されます。


そのためには小節の位置を管理するための小節バーリストが必要となりますが、
実はこの小節バーのリストというのは、最初の方で定義していたBMSBAR配列そのものです。

BMSBARは以下のように定義していました。

// 小節情報
typedef struct _BMSBAR {
    float       fScale;                     // この小節の長さ倍率
    LONG        lTime;                      // この小節の開始位置(BMSカウント値)
    LONG        lLength;                    // この小節の長さ(BMSカウント値)
} BMSBAR,*LPBMSBAR;

BMSBAR      mBmsBar[1000+1];                // 小節データ(999小節時に1000番目も小節バーとして使うため+1しておく)


小節バーには特にサウンドIDや長さ情報は必要ないので、この中で必要な情報はlTimeだけです。

また小節数も既にlEndBarとして算出されているので、ゲーム上ではこのBMSBARを参照して、
必要な位置に必要な数だけ表示するだけなので、ここでは特に何もする必要はありません。