4-4 BMSヘッダロード

ここではBMSに記録されているタイトル名や作成者名、初期BPMなどのデータを取得するための説明をしています。
またこれらの情報のことをここではBMSヘッダと呼んでいます。

■基本ヘッダの読み込み

まず情報をロードするための変数を確保しますが、これらはだいたい以下の通りになります。

コマンド 内容
#PLAYER long プレイヤー番号(1=プレイヤー1、2=プレイヤー2など)
#TITLE char配列 タイトル名
#GENRE char配列 ジャンル名
#ARTIST char配列 アーティスト名
#BPM float 初期のテンポ数(BM98では1~255、独自仕様ではいくつでも可)
※このコマンドが見つからなかった場合は
130とする
#MIDIFILE char配列 バックグラウンドで流すMIDIファイル
#PLAYLEVEL long ゲームの難易度(BM98では1~8)
#RANK long 判定ランク
#VOLWAV long 音量を元の何%にするか
#TOTAL long ゲージの増量
#StageFile char配列 曲開始字に表示する画像

また、ここではBMS拡張仕様の小数テンポリストを使えるようにするために以下のコマンドも実装します。

コマンド 内容
#BPM??
(??には01~FFが入る)
float配列 256個分の拡張テンポリスト
※初期値は基本コマンドの#BPMと異なり
120とする
※ZZ対応の場合は1296個分

注意することは、この#BPM??コマンドは基本コマンドの#BPMと似ているため、
解析時にどちらのコマンドなのかを正しく判断しなければなりません。
※ここでは#BPM文字の次が空文字かタブ文字なら基本コマンドであると判断しています(以下ソース参照)


これらをグローバル変数で確保してもいいですが、ファイルのロードは1つだけではないので構造体(クラス)を使って定義します。
構造体なら曲選択のときに情報を一覧で出すことも可能だし、クラスなどでライブラリ化をしておけば他のゲームにも流用が可能です。

以下は構造体の例です。

// BMSファイルヘッダ情報
typedef struct _BMSHEADER {
    long        lPlayer;                    // プレイモード
    char        mGenre[256];                // データのジャンル
    char        mTitle[256];                // データのタイトル
    char        mArtist[256];               // データの製作者
    float       fBpm;                       // 初期テンポ(初期値は130)
    char        mMidifile[MAX_PATH];        // バックグラウンドで流すMIDIファイル
    long        lPlaylevel;                 // データの難易度
    long        lRank;                      // データの判定ランク
    long        lWavVol;                    // 音量を元の何%にするか
    long        lTotal;                     // ゲージの増量を設定
    char        mStagePic[MAX_PATH];        // 曲開始字に表示する画像
    float       fBpmIndex[BMS_MAXBUFFER];   // テンポインデックス(初期値は120)
} BMSHEADER,*LPBMSHEADER;

ここで1つ説明がありますが、変数名の始めについている'l''m''f'はその変数の種類を表しています。
以前紹介したクラスの最初にCをつけるとクラスであると分かりやすいと書きましたが、それと似たように変数も分かりやすくなります。

'l'はlong、'i'はint、'c'はchar、'f'はfloat、'd'はdouble、'm'はcharや構造体の配列、'p'はポインタと自分では決めています。
※大手企業ではこういう決まりをポリシーと言って、さらに関数仕様書などで細かく説明しなければならないこともあります

ちなみに上記の構造体定義で、構造体をポインタとして扱う場合は始めに"LP"を付けていますが、
これはロングポインタの略でアセンブラでアドレスを識別するときに使っていた名残です。
※ポインタであるということであればPだけでもいいですが、CPUが16bitだったころはint型が16bitであり、long型は32bitで計算コストが大きいため、
 アセンブラで現在のポインタから16bitの範囲にアクセスするようなコストの少ない命令をなるべく使うようにしており、
 これを基本的にポインタと言っていましたが、それに対して32bit範囲レベルのアクセスが必要な場合は、
 コストの大きいlong型のポインタ命令が必要なので、これをロングポインタという風に呼んでいました。
 しかし32bitのCPUではintもlongも32bitになってしまったので、結果的にどちらもロングポインタでアクセス出来ることで、
 常にLもつけるようになったのだと思われます。

ちなみにローカル変数には逆にそういったものを付けないで使用しています。
これらの定義方法は始めはシックリきませんが慣れるとすんなりと読めるようになります。


では以下にヘッダをロードする関数を紹介します。
なお、この関数は元はクラスの関数ですがサンプルとして抜き出したものです。

BMSHEADER mBH;    // ヘッダ情報


////////////////////////////////////////////////////////////////////////////////////////
// コマンド番号を返す
// 戻り値
//        0以上 : コマンド番号
//        -1    : オブジェ配置データ
//        -2    : 不明なコマンド
////////////////////////////////////////////////////////////////////////////////////////
int GetCommand( const char *s )
{
    static const char *command[11] = {
        "PLAYER",
        "GENRE",
        "TITLE",
        "ARTIST",
        "BPM",
        "MIDIFILE",
        "PLAYLEVEL",
        "RANK",
        "VOLWAV",
        "TOTAL",
        "StageFile",
    };

    // 検索ルーチン
    int i;
    for( i=0;i<11;i++ ) {
        if( strnicmp(s+1,command[i],strlen(command[i])) == 0)
            return i;   // コマンドならその番号を返す
    }

    // 先頭が'#nnncc'形式か
    BOOL obj = TRUE;
    for( i=0;i<5;i++ ) {
        if( s[i+1]<'0' || s[i+1]>'9' ) {
            obj = FALSE;
            break;
        }
    }

    // オブジェ配置なら -1
    if( obj ) {
        return -1;
    }

    // 処理不可能文字列なら
    return -2;
}

////////////////////////////////////////////////////////////////////////////////////////
// パラメータ文字列を取得
////////////////////////////////////////////////////////////////////////////////////////
BOOL GetCommandString( const char *src,char *dst )
{
    int i = 0;
    int j = 0;

    // まずソースデータからデータ部分までのポインタを算出
    while(1) {
        if( src[i]==' ' || src[i]==0x09 || src[i]==':' ) {
            i++;
            break;
        }
        if( src[i]=='\n' || src[i]==NULL ) {
            return FALSE;
        }
        i++;
    }

    // 終端までをコピー
    while(1) {
        if( src[i]=='\n' || src[i]==NULL )
            break;
        dst[j] = src[i];
        i++;
        j++;
    }
    dst[j] = NULL;
    return TRUE;
}

////////////////////////////////////////////////////////////////////////////////////////
// ヘッダ情報だけを取り出す
////////////////////////////////////////////////////////////////////////////////////////
BOOL LoadHeader( const char *file )
{
    FILE *fp;
    fp = fopen( file,"r" );
    if( !fp ) {
        return FALSE;
    }

    char buf[1024];
    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 );
            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;
        }
    }

    fclose( fp );
    return TRUE;
}

GetHeader()以外の関数はGetHeader()内から呼び出されるサブ関数となります。

まずはファイル名を指定してGetHeader()を呼び出します。
GetHeader()内ではそのファイルをオープンし、1行ずつ文字列をロードして解析を行います。
このとき先頭に"#"がついた文字列があった場合、これをコマンドとみなしその行を解析します。

BMSの仕様ではどこにコマンドがあってもいいので、ヘッダのみと言えども必ず全行を検索しなければなりません。
GetHeader()関数が成功すればmBHの各メンバにBMSのヘッダ情報が入っています。


#BPMコマンドの解析は#BPM(初期テンポ)#BPM??(インデックス指定のテンポ)の2種類があるので、
ここでは次の文字がスペースかタブなら初期テンポ、それ以外なら拡張コマンドとしてテンポリストに代入しています。
このテンポリストはここではまだ使いませんが、次のメインデータをロードする時に参照されます。


サブ関数のGetCommand()では先頭が'#'かを判定し、コマンドかデータか、またはそれ以外(コメント含む)かを返します。
もしコマンドかデータだったならGetCommandString()を呼び出し、そのパラメータ部を取得します。
例えば与えられた文字列が"#TITLE 魔法つかいプリキュア!"だとすると、GetCommand()は"#TITLE"のコマンドID(=2)を返し、
GetCommandString()は"魔法つかいプリキュア!"を返します。
※文字列の比較チェックに使用しているstrnicmp()関数は、大文字小文字を区別せずに指定の文字数までが一致したら0を返します

ちなみに上記プログラムでは、ファイルから1行をロードする前に先にバッファをクリアしています。
これは最後の行が改行をしていない場合を想定しているためです。

feof()関数はファイルポインタが最後にあれば終端であると判断されます。
つまり終端のみをチェックするプログラムでは、最後に改行が無ければ読み込んだ瞬間に終了と判断されてしまいます。
これを避けるためにまず先にバッファをNULLでクリアしてからロードを行い、
そのあともバッファがNULLだった場合のみ、全ての行の解析が完全に終了したと判断しています。


なおGetCommandString()関数はこのサイトの基礎アルゴリズムで説明している、
テキストから文字を切り出すのやり方とは少し異なります。
GetCommandString()はスペースで分離は行わずパラメータ部を最後まで取得します。
これはBMSの仕様上、コマンドに対するパラメータはその行の最後までと決まっているためで、
例えばタイトル名にスペースが含まれていた場合、このスペースもタイトル名の一部となります。

基礎アルゴリズムの文字を切り出すルーチンは、例えば拡張コマンドの「#EXWAV01 pvf 0 10000 44100 se.wav」のように、
パラメータ部がスペースで区切られているような場合に使用すると良いでしょう。
なお、このサイトではこのような拡張コマンドには対応していないので、必要ならば自分で実装してください。


■BMPとWAVファイル

基本ヘッダがロード出来たら、次はWAVファイルやBMPファイルを取得する部分を考えます。
なお、これらもシステムコマンドと同じように始めに"#"が付くため、ヘッダロード関数を拡張して取得出来るようにしてしまいます。

BMPやWAVは1曲の中で最大256個(新仕様では1296個)定義できるので、ここでは以下のように定義します。
char bmp[256][MAX_PATH];
char wav[256][MAX_PATH];
※MAX_PATHはwindef.hで260と定義されている

そしてこれらを一度0で初期化しておきます。
ZeroMemory( bmp,sizeof(bmp) );
ZeroMemory( wav,sizeof(wav) );
※ZeroMemory()はmemset()のマクロで、指定のメモリバッファを0で初期化します

これで例えばbufに"#WAV
20 BGM.WAV"と入っていた場合のコードは以下の通りです。
GetCommandString( buf,param );
strcpy( wav[0x20],param );                       // 文字列のコピー

この例ではコマンドは"#WAV20"となりますが、この20というのを配列番号に置き換えると16進数の0x20となります。
そして文字列からGetCommandString()にて"BGM.WAV"を取り出し、このwav配列の0x20番目にコピーしています。


ついでに16進数の文字列からintに変換する関数も紹介します。
この関数は指定の文字列(NULL終端)を与えてやると10進数の値として返します。

////////////////////////////////////////////////////////////////////////////////////////
// 16進数文字列を数値に変換
////////////////////////////////////////////////////////////////////////////////////////
int atoi1610( const char *s )
{
    int ret = 0;            // 10進数に変換した値
    int i = 0;              // 参照する文字配列
    while( s[i] ) {
        if( !(s[i]>='0' && s[i]<='9') &&
            !(s[i]>='A' && s[i]<='Z') &&
            !(s[i]>='a' && s[i]<='z') )
            return 0;

        ret *= 16;              // 16倍
        int n = s[i] - '0';
        if( n>9 )
            n -= 7;
        if( n>15 )
            n -= 0x20;
        ret += n;
        i++;
    }
    return ret;
}

たとえば「int a = atoi1610( "1F" );」とすると、aには31が入ります。
この関数は0~9とA~F、a~f以外の文字を与えた場合は常に0を返します。



■ヘッダロード部分にWAV名とBMP名の取得を追加

ではヘッダロード部分にBMP・WAV定義もロードするようにします。
上の方で書いたGetCommand()関数とGetHeader()関数に追加をすることにします。
追加する部分は
で表示されます。

char bmp[256][MAX_PATH];       // WAVのファイル名(NULLで初期化されているとする)
char wav[256][MAX_PATH];       // BMPのファイル名(NULLで初期化されているとする)


////////////////////////////////////////////////////////////////////////////////////////
// コマンド番号を返す
// 戻り値
//        0以上 : コマンド番号
//        -1    : オブジェ配置データ
//        -2    : 不明なコマンド
////////////////////////////////////////////////////////////////////////////////////////
int GetCommand( const char *s )
{
    static const char *command[13] = {
        "PLAYER",
        "GENRE",
        "TITLE",
        "ARTIST",
        "BPM",
        "MIDIFILE",
        "PLAYLEVEL",
        "RANK",
        "VOLWAV",
        "TOTAL",
        "StageFile",
        "WAV",
        "BMP",
    };

    // 検索ルーチン
    int i;
    for( i=0;i<13;i++ ) {
        if( strnicmp(s+1,command[i],strlen(command[i])) == 0)
            return i;   // コマンドならその番号を返す
    }

    // 先頭が'#nnncc'形式か
    BOOL obj = TRUE;
    for( i=0;i<5;i++ ) {
        if( s[i+1]<'0' || s[i+1]>'9' ) {
            obj = FALSE;
            break;
        }
    }

    // オブジェ配置なら -1
    if( obj ) {
        return -1;
    }

    // 処理不可能文字列なら
    return -2;
}

////////////////////////////////////////////////////////////////////////////////////////
// コマンド番号を返す
// 戻り値
//        0以上 : コマンド番号
//        -1    : オブジェ配置データ
//        -2    : 不明なコマンド
////////////////////////////////////////////////////////////////////////////////////////
int GetCommand( const char *s )
{
    static const char *command[13] = {
        "PLAYER",
        "GENRE",
        "TITLE",
        "ARTIST",
        "BPM",
        "MIDIFILE",
        "PLAYLEVEL",
        "RANK",
        "VOLWAV",
        "TOTAL",
        "StageFile",
        "WAV",
        "BMP",
    };

    // 検索ルーチン
    int i;
    for( i=0;i<13;i++ ) {
        if( strnicmp(s+1,command[i],strlen(command[i])) == 0)
            return i;   // コマンドならその番号を返す
    }

    // 先頭が'#nnncc'形式か
    BOOL obj = TRUE;
    for( i=0;i<5;i++ ) {
        if( s[i+1]<'0' || s[i+1]>'9' ) {
            obj = FALSE;
            break;
        }
    }

    // オブジェ配置なら -1
    if( obj ) {
        return -1;
    }

    // 処理不可能文字列なら
    return -2;
}

////////////////////////////////////////////////////////////////////////////////////////
// ヘッダ情報だけを取り出す
////////////////////////////////////////////////////////////////////////////////////////
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;
        }
    }

    fclose( fp );
    DEBUG( "\n" );
    return TRUE;
}

このプログラムについて簡単に説明すると、まずGetCommand()関数にBMPとWAVという文字列を認識できるように拡張します。
そしてGetHeader()内のswitch文の中にBMPとWAVの解析処理を追加します。

char num[4]は"#WAV20"の"20"部分を入れるためのただのテンポラリバッファです。
ここに16進数の文字列を入れてからatoi1610()関数に渡すことでその数値を得ることが出来ます。

最後にこの値、つまり配列番号を指定し文字列をコピーします。

さらに追加の仕様があった場合は、このようにGetCommand()とLoadHeader()にその処理を追加することで簡単に拡張が可能です。


これですべてのヘッダ情報を取得したことになるので、次はメインデータを取得する方法について説明します。