MIDI Playback
このサンプルでは、MIDIファイルを、そのMIDIから作成されたオーディオファイルと同期させて再生します。これを利用して、MIDIトラックからワールド内のイベントを同期させることで、音楽とビジュアルを組み合わせた没入感のある体験を作り出すことができます。
Midi Playback Example Worldにアクセスして、ぜひ実際に試してみてください!
サンプルの使用方法
Unity Editorでシーンを再生するか、VRChatでワールドにアクセスしてMIDIの再生を確認してください。短いループが再生されると、音楽に合わせて異なる色の画像が点滅します。これは、MIDIファイル内の4つの異なるチャンネルで発生している MIDI Note イベントを可視化したものです。
サンプルのインポート
以下の手順に従って、このサンプルを Unity プロジェクトに追加します。
- Unity エディターのメニューの「VRChat SDK > 🏠 Example Central」から Example Central ウィンドウ を開きます。
- リストの中からこのプレハブを探すか、タイトル(このページのタイトルと同じ)で検索します。
- 「Import」ボタンを押して、Unitypackage をプロジェクトにインポートします。
技術的な仕組み
VRCMidiPlayer は AudioSource と似ていますが、代わりに Midi Asset を使用し、Note On および Note Off イベントを MidiGrid UdonBehaviour に送信します。
MidiGrid
このプログラムは、MIDIの Note On および Note Off イベントを色付きのブロックとして視覚化します。
VRCMidiPlayer から Note On イベントが送信されるたびに、プログラムはそのチャンネルがグリッドのいずれかと一致するかを確認します(以下の channels フィールドを参照)。一致するものがあれば、プログラムはノート番号を12で割った余りを計算して対応する画像を見つけ、そのブロックを有効にします。たとえば、ノート番号11と12はグリッド内の11番目と12番目の画像をトリガーし、ノート番号13と14は0番目と1番目の画像をトリガーします。このようにして、ノート番号がグリッドに収まるようにループします。対象の画像が計算されると、それが有効化されます。
VRCMidiPlayer から Note Off イベントが送信されると、プログラムは同様の計算を行って対象のグリッドと画像を見つけますが、今度はそれを無効にして非表示にします。
Inspector フィールド
| Name | Description |
|---|---|
| grids | GridLayoutGroup を持ち、1オクターブ分のノートに対応する12個の子 Images を持つ RectTransform への参照。 |
| channels | シーン内の4つの画像グリッドにMIDIチャンネルを再割り当てするために使用される整数の配列。デフォルト値 [3, 4, 1, 2] は、チャンネル3のノートイベントをグリッド0に、チャンネル4のイベントをグリッド1に表示します。 |
| player | MidiGrid にイベントを送信する VRCMidiPlayer への参照。 |
データの差し替え
MIDIファイルとオーディオファイルがある場合は、それらをインポートし、既存のアセットを独自のものに置き換えることで、シーン内でどのように表示されるかを確認できます。
チャンネルの変更
MidiGrid プログラムには channels という変数が用意されています。この配列は、MIDIデータのチャンネルと画面上のグリッドを対応付けます。デフォルトの順序は「3 4 1 2」です。つまり、最初のグリッドはチャンネル3のデータを表示し、2番目のグリッドはチャンネル4のデータを表示するといった具合です。この順序を入れ替えることで、視覚的な演出を少し変更できます。
独自のMIDIデータファイルを読み込む場合は、Unityのコンソールを確認して、再生されているチャンネルとノートを表示できます。各 Note On イベントは「3:75」のようなメッセージをログに出力します。これは、チャンネル3でノート75が再生されたことを示しています。
グリッドの追加
4チャンネルを超える曲を使用したい場合は、グリッドを複製し、プログラムの grids 変数に追加してください。その際、channels も忘れずに追加するようにしてください。
プログラム全体の解説
MidiGrid プログラムで何が行われているかの内訳は以下の通りです。
Start イベント:

Start 時、grids 配列内の各オブジェクトを順に処理し、その子にある 'Image' コンポーネントを見つけ、その enabled 値を false に設定することで、初期状態で全ての画像を非表示にします。
また、ロード後に1秒待機してから VRCMidiPlayer の Play() を呼び出し、音楽とデータフローを開始します。
Note イベント:

Midi Note On イベントを受信すると、channels 配列の各要素をループし、受信したノートのチャンネルが一致するかを確認します。一致が見つかった場合、その数値が grids 配列の index として使用され、対応するグリッドが特定されます。受信したノートは int.Remainder() を通じてオクターブ内のインデックス(Cは0、C#は1など)が計算されます。このインデックスを使用してグリッドの適切な子要素を見つけ、その 'Image' の enabled を true に設定します。最後に、ノートのチャンネルとノート番号がコンソールにログ出力されます。

スクリプトが Midi Note Off イベントを受信すると、上記と同様の処理を行います。'Image' コンポーネントを再度非表示にするために、enabled を false に設定します。
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDK3.Midi;
[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class LogoButton : UdonSharpBehaviour
{
[SerializeField] private Transform[] grids;
[SerializeField] private VRCMidiPlayer player;
[SerializeField] private int[] channels;
private void Start()
{
// Disable Image components of all grid children
foreach (var grid in grids)
{
for (var i = 0; i < grid.childCount; i++)
{
var child = grid.GetChild(i);
var image = child.GetComponent<Image>();
image.enabled = false;
}
}
SendCustomEventDelayedSeconds(nameof(_PlayAudio), 1);
}
public void _PlayAudio()
{
player.Play();
}
public override void MidiNoteOn(int channel, int number, int velocity)
{
UpdateGridState(channel, number, true);
Debug.Log($"{channel} : {number}");
}
public override void MidiNoteOff(int channel, int number, int velocity)
{
UpdateGridState(channel, number, false);
}
private void UpdateGridState(int midiEventChannel, int midiEventNoteNumber, bool isEnabled)
{
// Find all grids that are mapped to the midi event's channel.
for (var gridIndex = 0; gridIndex < grids.Length; gridIndex++)
{
var gridChannel = channels[gridIndex];
if (midiEventChannel != gridChannel) continue;
// Enable/Disable image, quantized by chromatic 12 note scale.
var child = grids[gridIndex].GetChild(midiEventNoteNumber % 12);
var image = child.GetComponent<Image>();
image.enabled = isEnabled;
}
}
}
最終更新: