アマゾンバナーリンク

DXライブラリとC#での音楽ゲームの作り方その4 実データの読み込み

2020年2月24日

実データのロード

実データとは命令コマンドがない以下の様なデータの事です。

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

音符1つ毎にデータを構造体にまとめて管理します。そしてさらに以下の様な仕様にします。

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

この数を元にデータを分解して音符1つごとに開始カウントを求めます。

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

上のデータは実際には2小節目のデータなのですでに1小節分の9600を加算した値としています。

音符IDBMSカウント値
09600
112000
214400
316800

音符1つ分につきカウント値が2400ずつ増えていますが、これを求める数式で表してみます。

w = 9600 ÷ ( 文字数 ÷ 2 )

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

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

count = w × (音符ID)

音符IDは、ここでは4つ分のデータ分だけ繰り返すので、単純に0、1、2、3と4回ループするだけです。
これで音符ごとのカウント値を算出することが出来ます。実際には音符データがない時には、スキップする処理も追加します。

念のためおさらいしますと2文字で1セットのデータになってるので、まず2で割りこのデータ数分がループ数になります。

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

実際の実データを読み込むプログラム

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

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

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

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

        int com = GetCommand(data[0].Substring(1));

        // 実データ以外は飛ばす
        if (com != -1)
        {
            continue;
        }

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

        // チャンネル番号の取得
        // #001xx:*******のxx部分
        int channel = int.Parse(data[0].Substring(4, 2));

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

        // パラメータが存在するかチェック
        if (value.Length < 1)
        {
            MessageBox.Show(value + "パラメータが定義されていない箇所が存在");
            continue;
        }

        // 小節番号の取得
        // #xxx01:*******のxxx部分
        int line_num = int.Parse(data[0].Substring(1, 3));

        // パラメータが偶数かチェック
        if (value.Length % 2 == 1)
        {
            MessageBox.Show(line_num + "小節目の" + value + "のパラメータが偶数個ではない");
            continue;
        }

        // 小節区分からデータ配置場所を特定
        int start_count = 0;    // その小節の最初のカウント値
        int chkline = 0;        // 計算中の小節(0小節目から現在の1つ前までを加算していく)

        // 現在の小節の1つ前までを処理
        while (chkline < line_num)
        {
            // 加算するポインタ数を定義
            int add = BMSDATA_RESOLUTION;

            // 現在チェック中の小節に倍率変更が存在しないか検索する
            foreach (CBarMagni bm in BarMagniList)
            {
                // 倍率変更命令が存在する場合
                if (bm.Lines == chkline)
                {
                    // 加算値をn倍する
                    add = (int)((float)BMSDATA_RESOLUTION * bm.Magni);
                    break; // 見つかったので終わり
                }
            }

            // 現在までの小節カウントを加算
            start_count += add;

            // 次の小節へ
            chkline++;
        }

        // パラメータを追加する
        int len = value.Length / 2;   // 数値の数

        // 現在の小節の幅を求める
        float now_bar_reso = (float)BMSDATA_RESOLUTION / (float)len; // デフォルトの1音符分のカウント値

        foreach (CBarMagni bm in BarMagniList)
        {
            // その小節に倍率変更命令が存在するか
            if (bm.Lines == chkline)
            {
                // 存在した場合は分割数を再計算
                now_bar_reso = ((float)BMSDATA_RESOLUTION * bm.Magni) / len;
                break; // 見つかったので終わり
            }
        }

        for (int i = 0; i < len; ++i)
        {
            if (!IsHexString(value.Substring(i * 2, 2)))
            {
                MessageBox.Show(value.Substring(i * 2, 2) + "は16進数ではありません");
                continue;
            }
            int num16 = System.Convert.ToInt32(value.Substring(i * 2, 2), 16);
            int num10 = System.Convert.ToInt32(num16.ToString(), 10);

            if (0 < num10)
            {
                // データが存在する場合
                AddData(channel, start_count + (int)(now_bar_reso * (float)i), num10);
            }
        }
    }

    // ソート
    for (int i = 0; i < BMSMAXBUFFER; ++i)
    {
        Sort(i);
    }

    return true;
}
// データを追加
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;
}

このプログラムを簡単に説明します。
まず指定のファイルをオープンし1行ずつ検索をします。

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

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

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

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

現在の音符のカウント数が分かったら、 BmsDataに追加していきます。

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

pBmsData[0][1][2][3][4]
llTime960010800132001560016800
lData0102030404


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