UnityでMIDI出力を取り扱うためのコード。

概要

UnityでMIDI出力を取り扱うためのコードを記述する。

UnityでMIDI入力やOSCを扱うためのサンプルは存在するが、
MIDI出力に関するコードは殆ど事例がない。

これはMIDI出力をスケジュールどおりに行うケースが少ないからだと考える。

ただ現在作成中のライブコーディングツールにおいて、スケジュールどおりにMIDIデータを出力する要件が発生しているため、自分で書く羽目になった。

OSCだけ出力できるようにした場合、対応できるDAWがAbletonLiveくらいになってしまう。
もしくはMaxとの連携が必須になるとか。

そのため、他DAWとの連携も可能になるMIDI出力が必要になった、と。

ソースコード

すごく適当に書きましたが、とりあえず動作は確認しました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using System.Text;
using System;

public class MidiOutput : MonoBehaviour
{
    static string res;
    public const int MAXPNAMELEN = 32;
    public struct MidiOutCaps
    {
        public short wMid;
        public short wPid;
        public int vDriverVersion;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAXPNAMELEN)]
        public string szPname;
        public short wTechnology;
        public short wVoices;
        public short wNotes;
        public short wChannelMask;
        public int dwSupport;
    }

    IntPtr handle;
    public bool KeyOn = false;
    public bool device_init = false;


    // MCI INterface
    [DllImport("winmm.dll")]
    private static extern long mciSendString(string command, StringBuilder returnValue, int returnLength, System.IntPtr winHandle);

    // Midi API
    [DllImport("winmm.dll")]
    private static extern int midiOutGetNumDevs();


    [DllImport("winmm.dll")]
    private static extern int midiOutGetDevCaps(System.Int32 uDeviceID, ref MidiOutCaps lpMidiOutCaps, System.UInt32 cbMidiOutCaps);

    [DllImport("winmm.dll")]
    private static extern int midiOutOpen(out IntPtr handle, int deviceID, MidiCallBack proc, int instance, int flags);

    [DllImport("winmm.dll")]
    private static extern int midiOutShortMsg(IntPtr handle, int message);

    //[DllImport("winmm.dll")]
    public int midiOutMsgFixed(IntPtr hmo, byte status, byte channel, byte data1, byte data2)
    {
        return midiOutShortMsg(hmo, (status << 4) | channel | (data1 << 8) | (data2 << 16));
    }
    //public static uint midiOutShortMsg(int hmo, byte status, byte channel, GMProgram data1, byte data2) { return midiOutShortMsg(hmo, (status &lt;&lt; 4) | channel | ((byte)data1 &lt;&lt; 8) | (data2 &lt;&lt; 16)); }

    [DllImport("winmm.dll")]
    private static extern int  midiOutClose(IntPtr handle);

    private delegate void MidiCallBack(int handle, int msg, int instance, int param1, int param2);

    static string Mci(string command)
    {
        StringBuilder reply = new StringBuilder(256);
        mciSendString(command, reply, 256, System.IntPtr.Zero);
        return reply.ToString();
    }

    void Start()
    {
        var numDevs = midiOutGetNumDevs();
        MidiOutCaps myCaps = new MidiOutCaps();

        //0番ポートの調査を行う。
        var res = midiOutGetDevCaps(1, ref myCaps, (System.UInt32)Marshal.SizeOf(myCaps));

        //引数1はポインタ扱いの模様。
#if UNITY_EDITOR
        Debug.Log(myCaps.szPname);
#endif
        DeviceInitialize();
    }

    void Update(){
        if( KeyOn == true){
            //midiOutMsgFixed(handle, 0x9, 0, 0x45, 40);

        }
        else if(KeyOn==false){
            //midiOutMsgFixed(handle, 0x9, 0, 0x45, 0);
        }
    }

    public void Test1(){
            midiOutMsgFixed(handle, 0x9, 0, 0x45, 40);
    }

    public void Test2(){
            midiOutMsgFixed(handle, 0x9, 0, 0x45, 0);
    }

    public void DeviceInitialize(){
        int midi_no=1;
        if( device_init == false){
            var res = midiOutOpen( out handle, midi_no , null , 0 , 0);
            device_init = true;
#if UNITY_EDITOR

            Debug.Log(handle);
            Debug.Log(res);
#endif
        }
    }

    public void DeviceClose(){
        if( device_init == true){
            var res=midiOutClose(handle);
#if UNITY_EDITOR
            Debug.Log(handle);
            Debug.Log(res);
#endif
            device_init = false;
        }
    }

    static void PlayMidi()
    {
        res = System.String.Empty;


        res = Mci("open \"" + filename + "\" alias music");
        res = Mci("play music");
    }

    void OnDestroy()
    {
        res = Mci("close music");
        DeviceClose();
    }

    void OnDisable()
    {
        res = Mci("close music");
    }
}

### 参考資料

MIDI on C# – aont’s diary (hateblo.jp)

MIDIプログラミング (eternalwindows.jp)

補足:P/Invokeの定義において、(おそらくx64と)x86側の定義方法が違う

.NET Frameworkでは、動的なモジュール(dllとか)を利用する場合、基本的にP/Invokeの定義に準拠する形で再度C#ソースコード内に定義します。

その際、マーシャリング機能によって戻り値とかは自由に指定できてしまうんですが、
参照元を間違えると正常に動かなくて手詰まりになるみたいです。

例えば古い資料(どうもx86のモジュールがこの可能性高い)では、


private static extern int midiOutOpen(ref int handle, int deviceID, MidiCallBack proc, int instance, int flags);

と記載してるみたいなんですが、P/Invokeに関する資料では、下記の通りに定義されてるみたいなんですね。

private static extern int midiOutOpen(out IntPtr handle, int deviceID, MidiCallBack proc, int instance, int flags);

主な違いは第1引数ですね。
(ほかは自前定義も含まれてるのでマーシャリング機能もあって動いてるみたいです)

ソースコードとしてはどちらも動くんですが、初期化→終了処理、と行った場合、
前者のほうだと、正常完了の戻り値を返してくれないんですね。

エラーコードを見るとハンドルが正常に解放されてない扱いになることから、初期化定義と終了定義が紐付いてないんじゃないかと考えます。違いがあるとすれば定義に関して参照する動的モジュールくらいかな、と。
(前者のほうはどうもすげー前の資料から参照していたみたいで、OSで利用している情報と整合性が取れなくなってるんじゃないかと考えます)

ということで、Pinvoke.netを参考に後者のほうに定義して、
ほかのモジュールもポインタ使うようにして動かしたら正常に戻り値吐いてMIDIコントロールも可能になった、と。

上記ソースコードは後者の実装してます。

資料:

pinvoke.net: midioutopen (winmm)

c# – Unity での MIDI 出力 – スタック オーバーフロー (stackoverflow.com)