アマゾンバナーリンク

DXライブラリとC#での音楽ゲームの作り方その3 BMSヘッダの読み込み

2020年2月24日

BMSヘッダのロード

ヘッダのデータを構造体にまとめます。

コマンド説明
#PLAYERintプレイヤー番号1か2
#TITLEstringタイトル名
#GENREstringジャンル名
#ARTISTstringアーティスト名
#BPMdouble 初期のテンポ数(BM98では1~255、独自仕様ではいくつでも可)
※このコマンドが見つからなかった場合は130とする
#MIDIFILEstringバックグラウンドで流すMIDIファイル
#PLAYLEVELintゲームの難易度(BM98では1~8)
#RANKint判定ランク
#VOLWAVint音量の元を%で指定
#TOTALintゲージの増量
#StageFileint曲開始時に表示する画像
#BPM??
(??には01~FFが入る)
double 256個分の拡張テンポリスト
※初期値は基本コマンドの#BPMと異なり120とする
※ZZ対応の場合は1296個分

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

BMSヘッダファイル情報を格納

public class CBmsHeader
{
    public int Player;// プレイモード
    public string Genre;// データのジャンル
    public string Title;// データのタイトル
    public string Artist;// データの製作者
    public float Bpm;// データのテンポ(初期値は130)
    public string MidiFile;// バックグラウンドで流すMIDIファイル
    public int PlayLevel;// データの難易度
    public int Rank;// データの判定ランク
    public int WavVol;// 音量を元の何%にするか
    public int Total;// ゲージの増量を設定
    public string StagePic;// 曲開始字に表示する画像
    public string Movie;// ムービーファイル名
    public int EndBar;// 終了小節
    public int	MaxCount;// 最大のカウント数
    public double[] BpmIndex = new double[BMSMAXBUFFER];// テンポインデック
};

変数のみの場合は構造体を使う方が、速度的に有利ですが、中に配列がある場合はコンストラクタを使わないと、初期化できないので、ここではクラスを使っています。

ヘッダーをロードする

では実際にヘッダーをロードする関数LoadHeaderを見てみましょう。

処理の流れ

  1. StreamRederでファイルをオープン
  2. ReadToEndでtext_dataにすべてのデータを代入
  3. text_dataを空の文字列の行を削除しつつ、1行ずつLineStrに代入する
  4. BmsDataListを初期化
  5. LineStrを命令名と値に分解してそれぞれの処理を実行する(data[0]に命令名、data[1]以降に値が入っている)
  6. GetCommandでdata[0]から命令名を数字に変換する
  7. 命令名がなければ、実データか小節の倍率変更命令の場合なので、 実データを元に小節番号の最大値を記憶する
  8. 小節の倍率変更命令の場合、BarMagniListに小説番号とチャネル番号を渡して追加

以上の流れですべてのデータを処理した後に以下の処理をします

  1. 小節番号の最大値分だけBarInfoListに小節のデータを追加
  2. 最大の拍数=BmsHeader.MaxCountがより大きい拍数が出たら更新する
private bool LoadHeader(string file_name)
{
    FileInfo fi = new FileInfo(file_name);
    string text_data = string.Empty;
    try
    {
        using (StreamReader sr = new StreamReader(fi.OpenRead(), System.Text.Encoding.UTF8))//GetEncoding("Shift_JIS") ) )
        {
            text_data = sr.ReadToEnd();

            sr.Close();
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(file_name + " Error");
    }

    // 空の要素を削除するためのオプション.
    System.StringSplitOptions option = System.StringSplitOptions.RemoveEmptyEntries;

    // 改行コードで1行ごとに切り出す..
    LineStr = text_data.Split(new char[] { '\r', '\n' }, option);

    // " " で1文字ごとに切り出す.
    char[] spliter = new char[] { ' ', ':' };

    string dir_name = Path.GetDirectoryName(file_name);

    for (int i = 0; i < BMSMAXBUFFER; ++i)
    {
        BmsDataList[i] = new List<CBmsData>();
    }

    foreach (string line in LineStr)
    {
        if (line.Substring(0, 1) != "#")
        {
            continue;
        }

        string[] data = line.Split(spliter, option);

        if (data.Length < 2)
        {
            MessageBox.Show("data.Lengthが2より小さいです=" + data[0]);
            continue;
        }

        string value = string.Empty;
        for (int i = 1; i < data.Length; ++i)
        {
            value += data[i];
            if (i < data.Length - 1)
            {
                value += " ";
            }
        }

        int com = GetCommand(data[0].Substring(1));
        int num10, num16;
        string exp;
        // data[0]にコマンドstrにパラメータが入っている
        switch (com)
        {
            case 0:     // PLAYER
                BmsHeader.Player = int.Parse(value);
                break;
            case 1:     // GENRE:
                BmsHeader.Genre = value;
                break;
            case 2:     // TITLE
                BmsHeader.Title = value;
                break;
            case 3:     // ARTIST
                BmsHeader.Artist = value;
                break;
            case 4:     // BPM
                if (data[0].Length <= 4)
                {
                    // ヘッダなら
                    // 例 #BPM 250
                    BmsHeader.Bpm = float.Parse(value);
                    AddData(BMS_TEMPO, 0, (int)BmsHeader.Bpm);
                }
                else
                {
                    if (!IsHexString(data[0].Substring(4)))
                    {
                        MessageBox.Show(data[0] + "は16進数ではありません");
                        continue;
                    }
                    // インデックスなら
                    // 例 #BPM01 62.5
                    num16 = System.Convert.ToInt32(data[0].Substring(4), 16);
                    num10 = System.Convert.ToInt32(num16.ToString(), 10);
                    BmsHeader.BpmIndex[num10] = float.Parse(value);
                }
                break;
            case 5:     // MIDIFILE
                BmsHeader.MidiFile = value;
                break;
            case 6:     // PLAYLEVEL
                BmsHeader.PlayLevel = int.Parse(value);
                break;
            case 7:     // RANK
                BmsHeader.Rank = int.Parse(value);
                break;
            case 8:     // VOLWAV
                BmsHeader.WavVol = int.Parse(value);
                break;
            case 9:     // TOTAL
                BmsHeader.Total = int.Parse(value);
                break;
            case 10:    // STAGEFILE
                BmsHeader.StagePic = value;
                break;
            case 11:    // WAV
                if (!IsHexString(data[0].Substring(4)))
                {
                    MessageBox.Show(data[0] + "は16進数ではありません");
                    continue;
                }
                // #WAVxx:******* のxx部分
                num16 = System.Convert.ToInt32(data[0].Substring(4), 16);
                num10 = System.Convert.ToInt32(num16.ToString(), 10);

                exp = Path.GetExtension(value);
                if (string.Compare(exp, ".wav", true) == 0 ||
                   string.Compare(exp, ".ogg", true) == 0 ||
                   string.Compare(exp, ".mp3", true) == 0)
                {
                    // フォルダ名を取得
                    string folder_name = Path.GetDirectoryName(file_name);
                    MusicHandles[num10] = DX.LoadSoundMem(folder_name + "\\"+ value);
                    if (MusicHandles[num10] == -1)
                    {
                        MessageBox.Show($"{folder_name + "\\" + value}が読み込めません");
                    }
                }
                else
                {
                    MessageBox.Show("wavかoggかmp3のみ対応してます" + data[0].Substring(4) + ":" + value);
                    continue;
                }
                break;
            case 12:    // BMP
                if (!IsHexString(data[0].Substring(4)))
                {
                    MessageBox.Show(data[0] + "は16進数ではありません");
                    continue;
                }
                // #BMPxx:******* のxx部分
                num16 = System.Convert.ToInt32(data[0].Substring(4), 16);
                num10 = System.Convert.ToInt32(num16.ToString(), 10);

                exp = Path.GetExtension(value).ToLower();
                if (string.Compare(exp, ".jpg", true) == 0 ||
                   string.Compare(exp, ".png", true) == 0 ||
                   string.Compare(exp, ".bmp", true) == 0 ||
                   string.Compare(exp, ".dds", true) == 0 ||
                   string.Compare(exp, ".argb", true) == 0 ||
                   string.Compare(exp, ".tga", true) == 0
                   )
                {
                    // フォルダ名を取得
                    string folder_name = Path.GetDirectoryName(file_name);
                    GraphicHandles[num10] = DX.LoadGraph(folder_name + "\\" + value);
                    if (GraphicHandles[num10] == -1)
                    {
                        MessageBox.Show($"{folder_name + "\\" + value}が読み込めません");
                        continue;
                    }
                }
                else
                {
                    MessageBox.Show("pngかjpgかbmpのみ対応してます" + data[0].Substring(4) + ":" + value);
                    continue;
                }
                break;
            case 13:    // GUID

                break;
            case 14:    // MOVIE
                //BmsHeader.Movie = value;
                exp = Path.GetExtension(value).ToLower();
                if (string.Compare(exp, ".avi", true) == 0 ||
                   string.Compare(exp, ".mp4", true) == 0 ||
                   string.Compare(exp, ".mpg", true) == 0 ||
                   string.Compare(exp, ".wmv", true) == 0
                   )
                {
                    // フォルダ名を取得
                    string folder_name = Path.GetDirectoryName(file_name);
                    MovieHandle = DX.LoadGraph(folder_name + "\\" + value);
                    continue;
                }
                else
                {
                    MessageBox.Show("aviかmp4かmpgのみ対応してます" + data[0].Substring(4) + ":" + value);
                    continue;
                }
                break;
            default:
                if (!IsNumeric(data[0].Substring(1, 1)))
                {
                    MessageBox.Show(data[0] + "= 無効な命令文です");
                    continue;
                }
                // 実データと小説幅変更命令はここを通る 
                // 普通の実データ 例 #00111:0101
                // 小節幅変更命令 例 #00102:0.5 ←1小節目を半分にする
                // 小節番号の取得
                // #xxx01:*******のxxx部分
                int line_num = int.Parse(data[0].Substring(1, 3));
                // チャンネル番号の取得
                // #001xx:*******のxx部分
                int channel = int.Parse(data[0].Substring(4, 2));

                // 小節の倍率変更命令の場合
                if (channel == BMS_STRETCH)
                {
                    BarMagniList.Add(new CBarMagni(line_num, float.Parse(value)));
                }

                // 小節番号の最大値を記憶する
                if (BmsHeader.EndBar < line_num)
                {
                    BmsHeader.EndBar = line_num;
                }
                break;
        }
    }

    // 小節分割倍率データを元に小節バーを生成
    int last_count = 0;
    // 最後の小節までを処理
    for (int i = 0; i <= BmsHeader.EndBar + 1; i++)
    {
        // 小節を追加
        BarInfoList.Add(new CBarInfo(true, last_count, -1));

        // 加算する数を定義
        int add = BMSDATA_RESOLUTION;

        // 現在の小節で倍率を変換しているか
        foreach (CBarMagni bm in BarMagniList)
        {
            // 倍率変更命令が存在する場合
            if (bm.Lines == i)
            {
                // 加算値をn倍する
                add = (int)((float)BMSDATA_RESOLUTION * bm.Magni);
                break;  // この小節の倍率変換は終わり
            }
        }

        last_count += add;  // ポジションを追加

        // 最大の拍数をついでに換算
        if (i < BmsHeader.EndBar && BmsHeader.MaxCount < last_count)
        {
            BmsHeader.MaxCount = last_count;
        }
    }

    return true;
}

特に大事な部分の説明

BPM

BPMとはBeats Per Minute. テンポの単位 – 一分間の拍数のことです。
処理の内容は2つ意味があります。

1つ目の書き方(ヘッダなら)
例 #BPM 250
こちらの場合は単純に、AddData関数でこの譜面での初期テンポを追加します。(この場合250を追加)

2つ目の書き方(インデックスなら)
例 #BPM01 62.5
この書き方はテンポを途中から変えたい場合に使います。

AddData関数について

private bool AddData(int ch, int start_pos, int dat)
{
    // チャンネル番号をチェック
    if (ch < 0 || ch > 255)
        return false;

    if (ch == BMS_STRETCH)
        return false;

    if (dat == 0)
        return true;

    switch (ch)
    {
        case BMS_BPMINDEX:
            BmsDataList[BMS_TEMPO].Add(new CBmsData(true, start_pos, (int)BmsHeader.BpmIndex[dat], BmsHeader.BpmIndex[dat]));
            break;
        default:
            if (BmsDataList[ch] != null)
            {
                BmsDataList[ch].Add(new CBmsData(true, start_pos, dat, (float)dat));
            }
            else
            {
                MessageBox.Show(ch + "チャンネルがnullです");
            }

            break;
    }

    return true;
}

ここで大事なのは、BMS_BPMINDEXの時は、Bpmを途中から変更するデータ、BmsHeader.BpmIndexを代入して、その他の場合は、音を鳴らすための実データを代入します。

WAVについて

WAV命令は、音声データを読み込んでおいてすぐに再生できる状態にします。
GetCommandで取得したデータ(com=11)になります。
ここで一番重要なのは、 WAVxx:***** のxx部分がnum10に入り
MusicHandles[num10] = DX.LoadSoundMem(folder_name + “\"+ value);
の一行で読み込めるサウンドデータ(.wav.mp3.ogg)のみ読み込みます。

BMPについて

BMP命令は、画像データを読み込んでおいてすぐに描画できる状態にします。
GetCommandで取得したデータ(com=12)になります。
ここで一番重要なのは、 BMPxx:***** のxx部分がnum10に入り
GraphicHandles[num10] = DX.LoadGraph(folder_name + “\" + value);
の一行で読み込める画像データ(.jpg.png.bmp.dds.argb.tga)のみ読み込みます。

MOVIEについて

MOVIE命令は、動画データを読み込んでおいてすぐに描画できる状態にします。
GetCommandで取得したデータ(com=14)になります。
ここで一番重要なのは、
MovieHandle = DX.LoadGraph(folder_name + “\" + value);
の一行で読み込める動画データ(.avi.mp4.mpg.wmv)のみ読み込みます。

ヘッダの読み込みの部分で理解しておくべき重要な部分はこの3つです。
次回は実データの読み込みを説明します。