たつぷりの調査報告書

博士後期課程(理学)の学生が趣味でUnityやBlenderで遊ぶブログです。素人が独学で勉強した際の忘備録です。

UnityでMidiデータのテンポに合わせてオブジェクトの生成速度を制御する

どうもこんにちは、たつぷりです。今回は以下の投稿の続きです。

tatsupuri.hatenablog.com

前の投稿の範囲で作成したプロトタイプでは曲のテンポはこちらで指定していました。つまり曲のテンポが一定の曲しか扱うことができなかったのですが、Midiはメタイベント内にテンポの情報が格納されているらしいということを知ったので、Midiデータを読み込む際テンポの情報を取り込んで自動で曲の速さを制御できるようにすることを今回の目標にしたいと思います。

テンポの制御

練習用データ

テストとして次の楽譜をMuse Scoreで作成した。 f:id:Tatsupuri:20201017201252p:plain

つまり4小節の間テンポが80 [四分音符/分]で、その後テンポが速くなり、100 [四分音符/分]になる。まずは簡単のためこれを再現できることを目標にした。完成品は以下の動画。

youtu.be

やったこと

前回に引き続き、SMFを読み取る部分のプログラムは

github.com

を参考にした。Keijiroさんに感謝。ただこのプロジェクトにおいてMidiからデータを取得するMidiFileDeserializer.csスクリプトではテンポは手で与えていてファイルから読み取っていなかったので、その部分を自分で書くことにした。

メタイベント

テンポの情報はメタイベントとして表現されている。今回は自分でテストデータを作って信号を分析したのと同時に

www.hikari-ongaku.com

を参考にした。

結局、テンポ情報のメタデータFF 51 03 xxxxxx というように表現されている。FFメタデータを表す。51がテンポの情報であることを示す。03がデータの長さを指定しており、続く実際のテンポの値が3バイトで表現されることを表している。xxxxxxが3バイトのテンポ情報で単位は[μs/四分音符]である。

したがって通常のテンポの単位である [四分音符/分]に変換するには

tempo = 60000000/xxxxxx

を計算すれば良いことになる。

以上のことから実際にやることは、

  1. FFを検出したら、続く1バイトの情報からテンポ情報かどうかを判定する

  2. テンポ情報の場合は続くデータ長の情報を飛ばして(3バイトということはあらかじめわかっているので)、3バイトのテンポ情報を取得する

  3. メタイベントがテンポ情報でなければ続くデータ長の情報を読み取り、その長さの分飛ばす

  4. 得られたテンポ情報を用いてノートの生成スピードを制御する

の流れである。

メタデータの取得

メタデータの取得に関しては、今回参考にさせてもらっているMidiFileDeserializer.csにおいてはReadTrack メソッドにおいてStatus byteをstat変数に格納している。つまりstat == 0xffu が真のとき、そこから続くデータ列がメタデータであることを表している。

もとのプログラムではメタデータは使用しないためその分のデータを飛ばすプログラムが書かれているのでそこを書き換えて必要な情報を得ることにする。以下のスクリプトは元のプログラムMidiFileDeserializer.csに含まれるif文の中身を書き換えている。

if (stat == 0xffu)//メタイベントを表す。以下が元プログラムの書き換え
{ 
    var meta = reader.ReadByte(); 
    if (meta == 0x51u)//テンポ情報
    { 
         var dataLengh = reader.ReadMultiByteValue();
        //データ長の情報。ここでは03が入るはず。(テンポの情報は3バイトであらわされる) 

         var data = reader.ReadBEUInt24();
        //テンポのデータ。単位は[ms/qn]。通常表記のテンポ[qn/min]に直すには、逆数に60000000をかければ良い。 
         var tmp = Math.Round(60000000 / (float)data); 
        //ここで用いている定義だと出力するとき小数の情報が落ちてしまうので四捨五入しておく。
         events.Add(new MidiEvent 
         { 
             time = ticks, 
             status = stat, 
             data1 = meta, 
             data2 = (byte)tmp 
          }); 
    } 
    else 
    { 
         reader.Advance(reader.ReadMultiByteValue());
         //reader.ReadMultiByteValue()で続くデータ長さを取得。その分Advanceで送っている。 
     } 
 }

ただしこのスクリプト内で用いたreader.ReadBEUInt24() メソッドは元のプログラムでは存在していない。テンポを表す3バイトのデータを読み取るためにそれ用のメソッドを新しく用意した。

MidiDataStreamReader クラスに以下のようにしてこのメソッドを追加した。

public uint ReadBEUInt24()  
        { 
            uint b1 = ReadByte(); 
            uint b2 = ReadByte(); 
            uint b3 = ReadByte(); 
            return b3 + (b2 << 8) + (b1 << 16); 
        }

以上のように書き換えたことで、テンポの変更指示があったときは、その時の時間 [ticks]におけるテンポ[四分音符/分]を得ることができるようになった。

テンポの自動制御

自分のプロジェクトでは入力されたテンポに応じてノートの生成速度を変えている。これはどのように実装しているかというと、詳しくは以前の投稿にまとめてあるが、以下のようにする。 四分音符一の長さに相当するTick数であるticksPerQuarterNoteと今取得したテンポの情報tempo があれば、ゲーム内で経過した時間(ここではrealTime [秒]とする)を以下の計算でtick単位に変換することができた。

realTime[秒] * ticksPerQuarterNote[ticks/四分音符] * tempo[四分音符/分] / 60[秒/分] = realTimeInTicks[ticks]

つまりUpdate関数内でTime.deltaTimeを足していきrealTime[秒]を求め、上の計算によって単位をticks 変換した上でmidiEvent.timeを超えたかどうかでそのイベントを見るかどうか決めれば良いのである。 基本的には以上の方針で実装できるが、基本的なことだが以下の注意をしておく。自分は最初これを見落としていて曲が数小節ずれる現象に遭遇した。

分解能の問題

最初自分はTime.deltaTimeを足していってrealTime[秒]を計算して、この値をticks単位に変換して用いていた。しかし少し考えればわかることであるが、このrealTime[秒]は徐々に大きくなってきてくるのでテンポが変わった際に変換の結果がTickの分解能よりも大きくなってしまっていくつかの複数の異なる時間のイベントを同時とみなしてしまう現象が生じる。

これを避けるために時間のカウントを秒でカウントするのではなく、tickでカウントすることにする。つまり、各Time.deltaTime[s]を[ticks]に変換して1フレーム間に経過するtick数を計算してこれを足し上げていき、この値を各イベントが起こった時間[ticks]と比較することでノートのオブジェクトを生成するタイミングを決める。

このように すると上の分解能の問題を回避できる。

以上の方針で実装すると、Muse Scoreで作成した楽譜通りの曲のテンポに合わせてノートオブジェクトの生成速度を自動で制御できるようになった。

実際の曲で試す

テンポの変化が何度かある実際の曲で上のプログラムを試してみた結果を以下に示す。

youtu.be

画面のInspector ViewでNote GeneratorのTempo を見ると、テンポを取得している様子を見ることができる。また実際それに合わせてノートの生成速度が制御されていることが確認できる。

Muse Scoreで標準MIDIファイルを作成する

どうもたつぷりです。こんにちは。この記事は補足記事です。

位置づけとしては前のMIDIをUnityに取り込んで音ゲーを作るという言った趣旨の投稿で、Muse Scoreでテスト用のMIDIを作成すると書いた部分の補足説明です。この内容まで前の記事に入れてしまうととても長くなってしまいそうだったのでMuse Scoreに関する部分だけ分離させて補足記事にすることにしました。

メインの記事はこちらです。 tatsupuri.hatenablog.com

この記事では全くの素人がテスト用にちょっとMIDIのデータを作りたいというモチベーションで触った、非常に限定的かつ初歩的な解説になっていますのでMuse Score詳しい使い方を書いているわけではありません。アプリケーションを起動して、音符を並べてSMF(標準MIDIファイル)で出力するまでの一連の流れをまとめているだけです。

なぜ私はMuse Scoreを選択したか

SMFのデータを得たいだけなら、様々な選択しがあったはずだがなぜ私がどのようなMuse Scoreを選んだのか一応説明しておく(選択肢を網羅的に調べたわけではないことは断っておく)。これは以下のような理由である。

まず一つは、Muse Scoreは楽譜のデータベースが用意されているからである。今回最終的な目標が音ゲーを作ることであった。そのために「楽譜」->「SMF」->「Unityで対応したリズムパターンを生成」の一連の流れを実現したかったのだが、「楽譜」->「SMF」が充実している方が良いと判断したからである。

次に、Muse Scoreは無料で使える点である。そもそも有料のものは最初から選択肢にない。

このように特に深く考えて選んだわけではないが、このような理由でMuse Scoreを選択した。特に一つ目の理由が大きいと思う。

Muse ScoreでMIDIデータを作成する

ここではUnityでSMFを扱うテストデータを作成するモチベーションでMuse Scoreの超初歩的な使い方をまとめておく。今回はピアノとドラムセットでテストデータを作成して解析を行った。

Muse Scoreの使い方(最低限)

今回必要な範囲でのみ、Muse Scoreの使い方をまとめておく。Muse Scoreは以下からダウンロードする。

musescore.com

Muse Scoreは初めて触ったところなので、以下の解説は正しくはあってもスマートでない可能性が非常に高い。

譜面の初期設定

まずMuse Scoreを起動すると以下のようなウィンドウが現れる。ここでは新しいスコアの作成を選択する。 f:id:Tatsupuri:20201015044253p:plain

次に曲の基本情報を入力することになるが、今回はテスト用なので特に気にせずに進む。 f:id:Tatsupuri:20201015044257p:plain

次にテンプレートを選択するように言われる。今回は、「一般 」>「 楽器を選択してください」を選択して次に進む。 f:id:Tatsupuri:20201015044301p:plain

すると楽器の一覧が表示されるので好きな楽器を選んで、「追加」を押す。ここではピアノを選択した。 f:id:Tatsupuri:20201015044304p:plain

ピアノを選択すると両手分の楽譜が追加されるみたいだが、今回は必要ないので追加されたリストの中からヘ音記号の譜面を選択して消去を押して除いておく。次に進むと、調を選択することになる。今回はハ長調にする。 f:id:Tatsupuri:20201015044308p:plain

次に進むと、拍子などに関しての設定を行うことができる。アウフタクトの設定とかもあっていじると面白そうだが、今回はデフォルトのままにして、「完了」を押す。 f:id:Tatsupuri:20201015044311p:plain

以上で初期設定が完了し、譜面の編集が行えるようになる。

楽譜の作成

テスト用なので全く高尚なことはせず、「カエルの歌」を適当に打ち込んでみる。まず最初に4分音符の「ド」を打ち込む。これには「Nキー」を押して入力モードに入る。四分音符のショートカットは「5」。この上で五線譜の上にカーソルを合わせると青く音符が現れるので、音符を置きたいとこでクリックして確定する。 f:id:Tatsupuri:20201015044320p:plain

ちなみにドラムセットの時は以下の図のように、下のほうにドラムセットに含まれる音が表示される。筆者はよく理解していないが、五線譜の上では同じでも違う音色という場合があるようだ。下の一覧から音色を選択してから音符を置くとそれが適用される。 f:id:Tatsupuri:20201015044328p:plain

これを続けて、適当に「カエルの歌」っぽいものを作った。なお、筆者は音楽的才能が皆無なのでこれが本当にカエルの歌かどうかは保証しないが、再生してみたらそれっぽかったので良しとする。 f:id:Tatsupuri:20201015044315p:plain

標準MIDIファイルで出力する

次に上で作成した譜面をSMFで出力する。これには「ファイル」>「エクスポート」を選択する。保存場所を選択し、ファイルの種類を標準MIDIファイルに選択して保存する。 f:id:Tatsupuri:20201015044324p:plain

まとめ

以上の流れで、非常に簡単な範囲ではあるがMuse Scoreを用いて楽譜を作成し標準MIDIファイルで出力することができた。

MIDIって何?の状態からUnityを使って音ゲーを作ってみる

こんにちは、たつぷりです。今回の記事は少し長くなってしまいましたが、MIDI形式の音楽とUnityを使ってリズムゲームを作ってみたという話です。私はMIDIって何のことがほとんど知らない状態だったのですが、いろいろあってこれを機に少し調べてみたので、忘備録としてこの記事にまとめておくことにしました。

目標

今回目標にするのは、次の動画のようなリズムゲーム(のプロトタイプ)を作成することである。リズムゲームといってもこの動画の段階ではまだUIを作っていないのでゲームとしての体はなしていない。

youtu.be

ではここでは何をしているのかというと、以下のようなことである。

  • Muse Scoreで自作したリズムパターンをSMFで出力し、Unityで読み込む

  • SMFの情報に合わせてリズムを表すゲームオブジェクトを生成、移動させる

  • 音色(上の動画では、バスドラム、スネア、クローズドハイハット)に合わせてラインを分ける

これを曲に合わせて実行することで典型的なリズムゲーム感を出すことを目標にしている。

なぜMIDIを使ってみようと思ったのか?

これに関しては強い信念があるわけではないが、ある日Unity JapanのYouTubeチャンネルで以下のリンクの動画を見た。 www.youtube.com

これを見た筆者は、それまでMIDIって名前を聞いたことがあるくらいだったが、MIDIの音を発するタイミングや消すタイミングの情報などで音楽を表現する仕組みを面白いと感じた。その記憶があってふと音ゲーを作ってみたいなあと思った際の計画段階で、MIDIを使ってみる方針を固めた。

さらに言うならば、筆者は音楽的な才能と教養が皆無なので編曲したりましてや自分で曲を作ったりすることは不可能なので、楽譜のリソースが多いに越したことはないと思っていた。その観点で調べていた時にMuse Scoreには有志の人たちが楽譜を上げてくれていて、それをMIDI形式でダウンロードできるということを知ったのでMIDIという選択肢は固いものとなった。

今思うといろいろ突っ込みどころはあるが、このような理由でMIDIとUnityでリズムゲームを作成することにした。

Unityと標準MIDIファイル(SMF)

UnityではMIDIは標準サポートしていない。なのでMIDIから発音のタイミングなどの情報を得るためには本来は自分でImporterなどを作る必要がある。しかしこれに関しては以下のGitHubリポジトリで非常にわかりやすく実現されているので、このプロジェクトで勉強しつつ、改造して使わせていただきました。Keijiroさんに感謝。

github.com

本当は一から勉強する必要があると思うが、今回はプロトタイプを作るだけなので手早く最小限の理解を目指す。具体的に何をするかというと、(この上なく幼稚なやり方かもしれないが)Muse Scoreでテスト用の簡単な音源を作成し、上のリポジトリのプロジェクトを参考にしながらMIDIイベントの情報を出力する。そして得られた出力のパターンと自分で作ったテスト用の楽譜を照らし合わせ解析を行う。

なお、Muse Scoreの使い方に関しては補足記事に少しだけまとめた。 tatsupuri.hatenablog.com

今回の目的で必要な情報をだけを得ることに限れば、個人的にはこのように実験的に理解する方が早いのでこの方法を用いた。

Unity側の準備

上でも述べた、MidiAnimiationTrack のプロジェクトを使わせてもらう。

まずMidiFileAssetImporter.csの中身を確認してみると、ドラックアンドドロップしてきた標準MIDIファイルをMidiFileDeserializer クラスのLoadメソッドでSMFのルールに従って情報を読み取っている。

MidiFileDeserializer.LoadMidiFileAsset クラスのインスタンスを返す。これはtracks という配列を示す。これは読み込んだSMFファイルがトラックごとに分けられ、各要素にそのデータが格納されています。 各トラック(つまりtracksの各要素)は、MidiAnimationAsset  クラスである。

今回自分が必要とする情報はMidiAnimiationAssettemplate フィールドである。

具体的に今回自分が欲しい情報は、

  • track.template.duration  : 曲の長さの情報。単位はticks。

  • track.template.ticksPerQuarterNote  : 四分音符一個分の長さで経過するticks

  • track.template.events  : MIDIイベント、すなわち発音・消音、音程、音色などの情報が含まれている。

などである。これらを後でUpdate関数などから呼べるようにMidiAnimiationAsset クラスをパブリックに書き換えさせていただく。

SMFの解析

SMFについてほとんど知らないが、今回必要な範囲において使われているデータを実験的に理解することを試みる。 やったことはMuse Scoreでテスト用のMIDIデータを用意し(Muse Scoreでテスト用MIDIを作成する)、UnityのコンソールにMIDIイベントを出力し、楽譜との対応関係から必要な情報を読み出すことである。

midiEventSet = track.template.events;

 foreach (MidiEvent midiEvent in midiEventSet) 
{ 
    Debug.Log(midiEvent.time+","+ midiEvent.status + ","+ midiEvent.data1 + ","+ midiEvent.data2); 
}

上記のようにしてMIDIイベントを出力し、その数字の配列と自分が作成した楽譜を見比べて、必要な情報を読み取るのである。ここで今回の目標(音ゲーの作成)のために必要な情報を整理しておくと以下のようなものである。

  • 音色:ドラムの種類の識別に使う(スネア、クローズドハイハット、・・・など)

  • 音階:ピアノなど音階の識別

  • 発音(消音):どこで音を出す(消す)かの情報。今回はまず発音のタイミングでオブジェクトを生成するように設計するので基本には発音の情報のみで十分。

使ったテストデータは以下の二つである。 f:id:Tatsupuri:20201015052450p:plain f:id:Tatsupuri:20201015052507p:plain

以下が実際の出力から分析した結果である。

ピアノ

xx,144,yy,80  = xxの時間にyyの音(ピアノ)を出す xx,144,yy,0  = xxの時間にyyの音(ピアノ)を消す

yy = 60 がト音記号の下第一線の「ド」 この値は1増えるごとに半音上がる

つまり、ドレミファミレド =60,62,64,65,64,62,60 ということになる。

ドラム

xx,153,yy,80  = --の時間にyyの音(ドラム)を出す xx,153,**,0  = --の時間にyyの音(ドラム)を消す

36がバスドラム 42がクローズドハイハット 40が(エレクトリック)スネア

同時に複数の音を発音するときは単純に、同じ時間のデータを複数用意することで表現されている。

音ゲーにする

以上で得られた結果から実際に音ゲー風にしていく。音ゲーの構成要素を整理してみると以下のようになる。

  • 楽譜の発音のタイミングに合わせて、オブジェクトを生成する

  • 生成したオブジェクトを動かす

  • 音色などに応じて生成する位置を変える(つまりスネアのライン、ハイハットのライン、・・・などと分けていく)

  • 音源と同期させる

  • オブジェクトが溢れないようにどこかで消滅させる

音符(ゲームオブジェクト)の生成

発音のタイミングでオブジェクトを生成すること自体はそこまで難しくない。なぜなら先ほど見たようにmidiEvent.data2が80であるときに発音するので、時間発展に従って配列を読んでいって midiEvent.data2を調べてその結果に応じてInstantiateでオブジェクトを生成していけばよい。

ここで少し注意が必要なのは、時間経過に応じて処理するときはUpdateの中に処理を書いていくがUpdateで経過する時間はTime.deltaTimeである。そのため曲のテンポで指定される速さと実際の時間経過を合わせる必要があるのだ。

midiEvent.timeで取得できるのはそのイベントが何tickで起こったかという情報である。また、四分音符の長さで経過するtick数はtrack.template.ticksPerQuarterNote で取得している。ここで、曲テンポが指定されていれば一分間に四分音符が何個入るかという情報も得られる。

これらの情報を用いればゲーム内で経過した時間(ここではrealTime [秒]とする)をtick単位に変換することができる。思想は以下のような計算である。

realTime[秒] * ticksPerQuarterNote[ticks/四分音符] * tempo[四分音符/分] / 60[秒/分] = realTimeInTicks[ticks]

つまりUpdate関数内でTime.deltaTimeを足していきrealTime[秒]を求め、上の計算によって単位をticks 変換した上でmidiEvent.timeを超えたかどうかでそのイベントを見るかどうか決めれば良いのである。

このようにして実時間とテンポで指定される曲の速さを同期させて、オブジェクトを生成させることができる。

音色(音階)に応じた生成位置の変更

上で分析したようにドラムの音色の情報はmidiEvent.data1から読み取れるのであった。なのでこの値の違いに応じてInstantiate させる時のpositionの情報を変えればよいのである。例えば自分は次のように実装した。

void NoteSet(float time,int tone) 
    {
        if (tone == 36)//bass
        {
            Instantiate(notePrefab, new Vector3(1.5f, 0, startPoint), Quaternion.identity);
        }
        else if (tone == 40)//snare
        {
            Instantiate(notePrefab, new Vector3(0, 0, startPoint), Quaternion.identity);
        }
        else if (tone == 42) //closed high hat
        {
            Instantiate(notePrefab, new Vector3(-1.5f, 0, startPoint), Quaternion.identity);
        }
    }

音符(ゲームオブジェクト)を動かす

これもUnityの基本である。Updateごとに座標を動かしていけば良い。この時、動かすスピードをインスペクターから調整しておける等にしておくとよい。

音源と同期させる

音源と同期させることに関しては、ここではそこまで高尚なことはせずに音楽のスタート位置の調整にとどめる。これがなぜ必要なのかというと次のような理由がある。

音符のゲームオブジェクトを生成するとき画面の奥の方に生成して、徐々に手前に動いてくる。ゲームではタップする瞬間に音源と同期しないといけない。つまりゲームオブジェクト生成は音源よりも移動時間分早くないといけないのである。

後は自分の場合は音源はMuse ScoreからMP3に出力したものを使っていたので曲の初めに微妙なマージンなどがあったりしたのでその微妙な調整などもしておかないと遅延気味に感じる原因になった。

前者が本質的な調整なのでこちらを少し詳しく書いておくと、自分は次のようにした。AudioSourceを割り当てているObjectの適当なスクリプトに曲が始まっているかどうかを表すBool型の変数を適当に定義しておく。

ここで音符のゲームオブジェクトが移動してきてタップする点まで来たときに、このBool型の変数を読みに行く。その結果曲が始まっていないことが判定できたら、ここで初めてGetComponent<AudioSource>().Play()メソッドで音楽を再生されば良い。

ただしこの場合、音楽の始まりと最初のゲームオブジェクトが一致している場合を想定しているので、そうでない時は別の方法を考えないといけないかもしれない。

マージンの微妙な調整は適当にオフセットを定義して対応する。

オブジェクトの消滅

これもUnityの基本である。オブジェクトが動いてきてあるところよりも後ろに来たら、Destroyで消滅させるといったことを実装すればよい。

まとめ

今回やったのはMIDIファイルから曲の情報を読み取り、音楽と同期するようにゲームオブジェクト生成・移動させ、よくあるリズムゲームみたいなものを作った。とりあえずまだプロトタイプのプロトタイプくらいのものだが、引き続き開発し「音ゲー」に近づけていきたい。今回は長くなったのでソースを張り付けたりはしなかったが、今後プロジェクトが進行したときにまとめて公開するつもりである。