MIDIメッセージを送信する
・midiOutShortMsg, midiOutLongMsgを使います。
ショート・メッセージ (4バイト以内のメッセージ) から先に解説します。
・MidiOutApi
/**** Source Code (C#) ****/ /// <summary> /// MIDI出力APIの宣言です。 /// </summary> public static class MidiOutApi { /// <summary> /// MIDIショートメッセージを送信します。 /// </summary> [DllImport("winmm.dll", EntryPoint = "midiOutShortMsg")] [return: MarshalAs(UnmanagedType.U4)] public static extern MMResult midiOutShortMsg([MarshalAs(UnmanagedType.SysUInt)] IntPtr hMidiOut, [MarshalAs(UnmanagedType.U4)] uint dwMsg); }
・MidiOutPortHandle
/**** Source Code (C#) ****/ /// <summary> /// MIDI出力ポートを抽象化するクラスです。 /// </summary> public sealed class MidiOutPortHandle : IDisposable { /// <summary> /// MIDIデータを送信します。 /// </summary> public void Send(byte[] data) { CheckDisposed(); data.CheckNotNull(); if (data.Length == 0) { return; } if (data.Length <= 4) { SendShortMessage(data); } else { SendLongMessage(data); } } /// <summary> /// 4バイト以内のMIDIメッセージ(Short Message)を送信します。 /// </summary> void SendShortMessage(byte[] data) { uint message = 0; for (int i = 0; i < data.Length; i++) { message |= ((uint)data[i]) << (i * 8); } MidiOutApi.midiOutShortMsg(hMidiOut, message); } }
・Bounder
/**** Source Code (C#) ****/ /// <summary> /// 値の範囲をチェックする拡張クラスです。 /// </summary> public static class Bounder { /// <summary> /// オブジェクトがnull参照の時にエラーを発生させます。 /// </summary> public static void CheckNotNull(this object target) { if (target == null) { throw new ArgumentNullException(); } } }
Send関数の中で、メッセージの長さに応じてSendShortMessageとSendLongMessageに振り分けています。
object.CheckNotNull拡張メソッドは、オブジェクトがNull参照かどうかをチェックする汎用関数です。
4バイト以内のメッセージは、リトルエンディアンでuint型にエンコードして送信することになっているので、それに従います。(C++ではポインタの強制型変換で済むが、C#ではそうはいかない。)
次はロング・メッセージの送信コードです。
・MidiOutApi
/**** Source Code (C#) ****/ /// <summary> /// MIDI出力APIの宣言です。 /// </summary> public static class MidiOutApi { /// <summary> /// MIDIロングメッセージを送信します。 /// </summary> [DllImport("winmm.dll", EntryPoint = "midiOutLongMsg")] [return: MarshalAs(UnmanagedType.U4)] public static extern MMResult midiOutLongMsg([MarshalAs(UnmanagedType.SysUInt)] IntPtr hMidiOut, ref MidiHdr lpMidiOutHdr, [MarshalAs(UnmanagedType.U4)] uint uSize); /// <summary> /// MIDI出力バッファを登録します。 /// </summary> [DllImport("winmm.dll", EntryPoint = "midiOutPrepareHeader")] [return: MarshalAs(UnmanagedType.U4)] public static extern MMResult midiOutPrepareHeader([MarshalAs(UnmanagedType.SysUInt)] IntPtr hMidiOut, ref MidiHdr lpMidiOutHdr, [MarshalAs(UnmanagedType.U4)] uint uSize); /// <summary> /// MIDI出力バッファの登録を解除します。 /// </summary> [DllImport("winmm.dll", EntryPoint = "midiOutUnprepareHeader")] [return: MarshalAs(UnmanagedType.U4)] public static extern MMResult midiOutUnprepareHeader([MarshalAs(UnmanagedType.SysUInt)] IntPtr hMidiOut, ref MidiHdr lpMidiOutHdr, [MarshalAs(UnmanagedType.U4)] uint uSize); }
・MidiOutPortHandle
/**** Source Code (C#) ****/ /// <summary> /// MIDI出力ポートを抽象化するクラスです。 /// </summary> public sealed class MidiOutPortHandle : IDisposable { static int maxBufferSize = 64 * 1024; static uint hdrSize = (uint)Marshal.SizeOf(typeof(MidiHdr)); /// <summary> /// 5バイト以上のMIDIメッセージ(Long Message)を送信します。 /// </summary> void SendLongMessage(byte[] data) { if (data.Length > maxBufferSize) { throw new InvalidOperationException(); } MidiHdr hdr = new MidiHdr(); hdr.dwReserved = new IntPtr[8]; GCHandle dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned); try { hdr.lpData = dataHandle.AddrOfPinnedObject(); hdr.dwBufferLength = (uint)data.Length; hdr.dwFlags = 0; SendBuffer(hdr); } finally { dataHandle.Free(); } } void SendBuffer(MidiHdr hdr) { MidiOutApi.midiOutPrepareHeader(hMidiOut, ref hdr, hdrSize).Throw(); while ((hdr.dwFlags & MidiHdrFlag.Prepared) != MidiHdrFlag.Prepared) { Thread.Sleep(1); } MidiOutApi.midiOutLongMsg(hMidiOut, ref hdr, hdrSize).Throw(); while ((hdr.dwFlags & MidiHdrFlag.Done) != MidiHdrFlag.Done) { Thread.Sleep(1); } MidiOutApi.midiOutUnprepareHeader(hMidiOut, ref hdr, hdrSize).Throw(); } }
・MidiOutHdr
/// <summary> /// MIDIHDR構造体のマネージド実装です。 /// </summary> [StructLayout(LayoutKind.Sequential)] public struct MidiHdr { /// <summary> /// MIDIデータのポインタです。 /// </summary> [MarshalAs(UnmanagedType.SysUInt)] public IntPtr lpData; /// <summary> /// バッファのサイズです。 /// </summary> [MarshalAs(UnmanagedType.U4)] public uint dwBufferLength; /// <summary> /// 実際に入力されたデータのサイズです。 /// </summary> [MarshalAs(UnmanagedType.U4)] public uint dwBytesRecorded; /// <summary> /// dwUser値です。 /// </summary> [MarshalAs(UnmanagedType.U4)] public uint dwUser; /// <summary> /// MIDIヘッダーの状態を表します。 /// </summary> [MarshalAs(UnmanagedType.U4)] public MidiHdrFlag dwFlags; /// <summary> /// lpNext値です。 /// </summary> [MarshalAs(UnmanagedType.SysUInt)] public IntPtr lpNext; /// <summary> /// reserved値です。 /// </summary> [MarshalAs(UnmanagedType.SysUInt)] public IntPtr reserved; /// <summary> /// dwOffset値です。 /// </summary> [MarshalAs(UnmanagedType.U4)] public uint dwOffset; /// <summary> /// dwReserved値です。 /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst=8)] public IntPtr[] dwReserved; }
・MidiHdrFlag
/// <summary> /// MidiHdrのdwFlags値を表す列挙子です。 /// </summary> [Flags] public enum MidiHdrFlag : uint { /// <summary> /// フラグがセットされていません。 /// </summary> None = 0, /// <summary> /// バッファの使用が完了しました。 /// </summary> Done = 1, /// <summary> /// バッファの準備が完了しました。 /// </summary> Prepared = 2, /// <summary> /// バッファは再生待ちです。 /// </summary> InQueue = 4 }
ロングメッセージの送信では、3つの関数を呼び出す必要があります。
バッファの登録、データ送信、バッファの登録の解除です。
まず、データのサイズが64KB以上の時にエラーを発生させていますが、これはWindows APIの仕様によります。
64KBのデータなんて、送信する人はいないでしょうけど。
次に、MidiHdr構造体を初期化します。
MidiHdr hdr = new MidiHdr(); hdr.dwReserved = new IntPtr[8]; GCHandle dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned); try { hdr.lpData = dataHandle.AddrOfPinnedObject(); hdr.dwBufferLength = (uint)data.Length; hdr.dwFlags = 0; SendBuffer(hdr); } finally { dataHandle.Free(); }
MidiHdr.dwReservedは要素数8の固定長配列として定義されているので、まずこのフィールドを初期化しています。
実際に送信されるバイト列は、そのアドレスをMidiHdrのlpDataフィールドに格納して渡すことになっていますが、ここで処理に工夫が必要になります。
C#では変数のアドレスがCLRによって任意に変更される可能性があり、通常その実際のアドレスを取得することができません。
unsafeキーワードでポインタを作成することもできますが、この場合もアドレスの一意性は保証されていません。
C#でアドレス固定のメモリ領域を作成するには、GCHandle.Allocを使用します。
GCHandle.Pinnedは、メモリアドレスが変更されないことを示すフラグです。
GCHandleで作成したメモリはガベージコレクタの対象とならないので、finallyブロックで解放しています。
最後に、送信するデータのサイズを指定し、dwFlagsを規定通り0で初期化して (C#では変数は常に0で初期化されるため、これも明示しているだけの処理です) 、実際にデータを送信するコードへ進みます。
void SendBuffer(MidiHdr hdr) { MidiOutApi.midiOutPrepareHeader(hMidiOut, ref hdr, hdrSize).Throw(); while ((hdr.dwFlags & MidiHdrFlag.Prepared) != MidiHdrFlag.Prepared) { Thread.Sleep(1); } MidiOutApi.midiOutLongMsg(hMidiOut, ref hdr, hdrSize).Throw(); while ((hdr.dwFlags & MidiHdrFlag.Done) != MidiHdrFlag.Done) { Thread.Sleep(1); } MidiOutApi.midiOutUnprepareHeader(hMidiOut, ref hdr, hdrSize).Throw(); }
バッファの準備とデータの送信は非同期に行われる可能性があるため、それぞれフラグを見て処理が終わるタイミングを判断しています。
最後に、ここまでのソースコードを使用して実際にデータを送信してみます。
static void Main(string[] args) { int count = MidiOutPortHandle.GetPortCount(); for (int i = -1; i < count; i++) { var caps = MidiOutPortHandle.GetPortInformation(i); Console.WriteLine(i.ToString() + ":" + caps.szPname); } while (true) { Console.WriteLine("ポート番号を入力してください。"); int index; MidiOutPortHandle handle; try { index = Convert.ToInt32(Console.ReadLine()); handle = new MidiOutPortHandle(index); } catch { Console.WriteLine("ポート番号が正しくありません。"); continue; } // GM Reset handle.Send(new byte[6] { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 }); Thread.Sleep(1000); handle.Send(new byte[3] { 0x90, 0x40, 0x40 }); Thread.Sleep(4000); handle.Send(new byte[3] { 0x80, 0x40, 0x40 }); handle.Close(); } }
スリープ中にプログラムを停止しても、ポートは自動的に開放されます。
C#らしくて便利ですね。
以下に、このセクションで使用されたソースコードの一覧を示します。
- Avenue 名前空間
- Bounder
- NextMidi.MidiOut.Internal 名前空間
- MidiModuleType
- MidiOutApi
- MidiOutCapsA
- MidiOutPortHandle
- NextMidi.MidiPort 名前空間
- MidiHdr
- MidiHdrFlag
- MidiPortCapability
- MidiPortConst
- MidiPortOpenFlag
- MMAllocatedException
- MMException
- MMResult
- MMResultExtensions