Help us understand the problem. What is going on with this article?

Unity で音楽に合わせたデータを作るのに MIDI ファイル + SmfLite が楽だった

More than 3 years have passed since last update.

はじめに

この間コミティアで VR 音ゲーを出したんですが(2回目)、その時に音ゲーなのでスコアを作らないといけなくて、ただいわゆるマスターデータみたいにしてタイミングを入力するのは絶対に嫌だったので MIDI ファイルを読み込む仕組みを作りました。

ちなみに SMF は Standard MIDI File の略でいわゆる MIDI ファイルのことです。

使用ライブラリ

今回は Unity の中の人である keijiro さん作 keijiro/smflite をフォークして、テンポに対応させた chiepomme/smflite を使用しました。

SmfLite を選んだ理由

C# で MIDI ファイルを扱えるライブラリをいくつか眺めてみたのですが、たいてい MIDI の入出力と一緒になっていたり、用途に対してあまりに高機能すぎるものが多い印象でした。それから、Unity の mono のバージョンで動くのか不安でどうしようかなと悩んでいました。

そんなときに、keijiro さんの smflite を見つけて、 Unity 向けに書かれたものであるということと、中身が MIDI ファイルの読み込みに特化していてとてもシンプルで拡張しやすそうだったので採用することにしました。

制約事項

  • SysEx に非対応
  • メタイベントに非対応
    • ただしテンポイベントだけパッチを当てて対応させました
    • トラック名なども読み込めません

使い方

ちょっと低レベルな API なので、MIDI ファイルのフォーマットを知らないと辛いかもしれません。

MIDI ファイルの読み込み

SmfLite.MidiFileLoader.Load() に MIDI ファイルのバイト列を与えてください。

var smf = MidiFileLoader.Load(File.ReadAllBytes("test.mid"));

もしも Resources.Load<TextAsset>("smffile") で読み込みたい場合にはファイル名に .bytes をつけるのと TextAsset で読み込むのを忘れないで下さい。 see: Unity - マニュアル: テキストアセット

// Load "Resources/test.mid.bytes"
var smf = MidiFileLoader.Load(Resources.Load<TextAsset>("test.mid").bytes);

分解能の読み込み

SmfLite.MidiFileLoader.Load()MidiFileContainer を返すのですが、ここには分解能とトラックが入っています。

var smf = MidiFileLoader.Load(File.ReadAllBytes("test.mid"));
Debug.Log(smf.division); // 分解能 1拍を何 tick で表すか

foreach (MidiTrack track in smf.tracks)
{
    Debug.Log(track); // MIDI のイベントの配列をラップしている
}

テンポの読み込み

途中でテンポ変化のない曲であればこれで最初に見つけたテンポイベントの値を取り出せます。

var smf = MidiFileLoader.Load(File.ReadAllBytes("test.mid"));
var tempo = 120f;

foreach (var ev in smf.tracks[0]) // テンポトラックは最初のトラック
{
    if (ev.midiEvent is TempoEvent)
    {
        tempo = ((TempoEvent)ev.midiEvent).Tempo;
        break;
    }
}

テンポ変化がある場合には、テンポイベントの位置とそこまでの時間のハッシュを作る必要があるでしょう。

ノートオンを読み込んで秒数のリストにする

音ゲーのスコアはこれを元にノートを配置すればよいです。

ノートオンのステータスバイトが 0x8n であることがわかっていればすぐです。
デルタタイムは前のイベントからの tick なので蓄積していかないといけないことに注意してください。

var smf = MidiFileLoader.Load(File.ReadAllBytes("test.mid"));
var tempo = 120f; // テンポはすでに読み込み済みとする

var noteTrack = smf.tracks[1]; // 仮にトラック1にノート情報があるとする
var totalDelta = 0; // デルタタイムの合計値 == 現在のノートの tick
var eventSeconds = new List<float>();

foreach (var ev in noteTrack)
{
    // ノートオンはステータスバイトが 0x8n
    if ((ev.midiEvent.status & 0xf0) == 0x80)
    {
        // 秒数は ここまでの拍の数 * 一拍あたりの秒数
        var totalSeconds = ((float)totalDelta / smf.division) * (60f / tempo);
        eventSeconds.Add(totalSeconds);
    }

    totalDelta += ev.delta;
}

ev.midiEvent.data1 にノートナンバーが、 ev.midiEvent.data2 にベロシティが入っているので、ゲームによってはそれをそのままキーとして使うこともできるとおもいます。またコントロールチェンジも同じ方法で読み込めると思います。

おわりに

時間が関わるものに関して SMF / MIDI ファイルはとても便利なので、是非活用しましょう!
私はゲーム中のノートのタイミングと歌詞表示のタイミングに使いました。
この記事は @chiepomme Advent Calender 2015 3日目です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした