Bakulog

獏の夢日記的な何か。

(Windows)Unity C#だけでMIDI入力を読み取る

GitHubのレポジトリ紹介です。

  1. やったこと
  2. なぜやったのか
  3. 実装について
  4. やってないけどやれそうなこと
  5. 補足: MIDI入力読み込みの競合について

 

1. やったこと

記事タイトルの通り、WindowsとUnityの組み合わせでC#のみ、つまりネイティブプラグイン無しでMIDI入力を読み込めるようにしました。

レポジトリはこちらです。

github.com

見てのとおりKeijiroさんのMidiJackのforkです。

 

2. なぜやったのか

元のMidiJackにないAPIで、「MIDI入力デバイスの占有オン/オフを切り替える」という機能が欲しかったからです。

このAPIが欲しい理由として、以下のような想定要件がありました。

  • 自作ソフトでMIDI入力を読み取れると嬉しい
  • ただし他にMIDI入力が使いたいソフト(DAWとか)があれば、そちらにMIDI入力を譲りたい

元のMidiJackではMIDI機能をネイティブプラグイン(C++)越しで呼んでいたため、選択肢は2つでした。

  • C++を書き直してネイティブプラグインをビルドする
  • C# のラッパーコードを書きなおす

この2択に対し、個人的に書きやすいという理由でC#を選びました。

C#化しておくと後から気軽に直せる、というメリットも意識しています。

 

3. 実装について

MidiJackのWindows向けネイティブ実装であるMidiJackPlugin.cppをじーっと眺めます。

github.com

ソース中でMIDI関連のAPI呼び出しがあることが読み取れます。以下はその一部です。

  • midiInOpen
  • midiInStart
  • midiInClose

関数名でググってドキュメントを確認します。たとえばmidiInOpenの場合こういうのです。

docs.microsoft.com

関数シグネチャとかライブラリ名(winmm.dll)が分かるので、それを踏まえながらC#C++Windowsの気持ちを汲み取ってP/Invokeコードに置き換えます。

抜粋するとこんな感じ。

using System.Runtime.InteropServices;

//...

public static class NativeMethods
{
    private const int CALLBACK_FUNCTION = 0x30000;
    
    public delegate void MidiInProcDelegate(IntPtr hMidiIn, uint wMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2);
    
    [DllImport("winmm.dll")]
    public static extern uint midiInGetNumDevs();

    [DllImport("winmm.dll")]
    public static extern uint midiInOpen(
        out IntPtr handle, 
        uint id,
        MidiInProcDelegate callback,
        IntPtr hInstance,
        uint flags
        );

    public static uint midiInOpen(out IntPtr handle, uint id, MidiInProcDelegate callback)
        => midiInOpen(out handle, id, callback, IntPtr.Zero, CALLBACK_FUNCTION);
    
    [DllImport("winmm.dll")]
    public static extern uint midiInStart(IntPtr hMidiIn);

    [DllImport("winmm.dll")]
    public static extern uint midiInClose(IntPtr hMidiIn);
    
    public const int MMSYSERR_NOERROR = 0;
    public const int MIM_CLOSE = 0x3C2;
    public const int MIM_DATA = 0x3C3;
} 

それ以外の処理は普通のC++コードなので、素朴に読んでいけばC#に置き換えられます。

置き換えた実装がコチラです。

github.com

なお、置き換え後の実装はスレッドセーフじゃないんですが、以下の理由から問題視していません。

  • Unity側の呼び出しはどのみちUIスレッドからやる
  • ネイティブ側で呼ばれるコールバック関数のアクセス先はConcurrentQueueConcurrentStackなのでスレッドセーフ

 

4. やってないけどやれそうなこと

下記のこともできそうでした。

  • 接続されているMIDI入力デバイス名の一覧を取得する
  • バイス名を指定して、特定デバイスのみを占有して入力を読み込む

元レポジトリのネイティブコードにはMIDIバイス名の取得処理が載っているので、そのラッパーコードを書けば実現できます。ここまでやるとUnity標準のWebCamTextureに近い手触りが実現できそうに思います。

ただし、今回はそこまでやる理由がなかったため、実装していません。

 

5. 補足: MIDI入力読み込みの競合について

本記事では素朴にMidiJackのネイティブコードをC#でラップしました。

しかしこの方法では「同じMIDI入力は1アプリからしか読めない」という問題が残ります。

これはWindowsの課題でもあり、スパッと解決する方法は無いんですが、関連の話題として「UWPのAPIを使えば複数アプリから同一のMIDIバイスにアクセスできる(※ただしアクセスする全ソフトがUWPのAPIを使うことが条件)」というのを紹介して終わりにします。

参考: blogs.windows.com