Next MIDI Project

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#らしくて便利ですね。


以下に、このセクションで使用されたソースコードの一覧を示します。

▲ページトップ | 前へ戻る