我正在尝试使用 C# 中的 Windows 多媒体 MIDI 函数。具体来说:
MMRESULT midiOutPrepareHeader( HMIDIOUT hmo, LPMIDIHDR lpMidiOutHdr, UINT cbMidiOutHdr );
MMRESULT midiOutUnprepareHeader( HMIDIOUT hmo, LPMIDIHDR lpMidiOutHdr, UINT cbMidiOutHdr );
MMRESULT midiStreamOut( HMIDISTRM hMidiStream, LPMIDIHDR lpMidiHdr, UINT cbMidiHdr );
MMRESULT midiStreamRestart( HMIDISTRM hms );
/* MIDI data block header */
typedef struct midihdr_tag {
LPSTR lpData; /* pointer to locked data block */
DWORD dwBufferLength; /* length of data in data block */
DWORD dwBytesRecorded; /* used for input only */
DWORD_PTR dwUser; /* for client's use */
DWORD dwFlags; /* assorted flags (see defines) */
struct midihdr_tag far *lpNext; /* reserved for driver */
DWORD_PTR reserved; /* reserved for driver */
#if (WINVER >= 0x0400)
DWORD dwOffset; /* Callback offset into buffer */
DWORD_PTR dwReserved[8]; /* Reserved for MMSYSTEM */
#endif
} MIDIHDR, *PMIDIHDR, NEAR *NPMIDIHDR, FAR *LPMIDIHDR;
通过执行以下操作,我可以在 C 程序中成功使用这些函数:
HMIDISTRM hms;
midiStreamOpen(&hms, ...);
MIDIHDR hdr;
hdr.this = that; ...
midiStreamRestart(hms);
midiOutPrepareHeader(hms, &hdr, sizeof(MIDIHDR)); // sizeof(MIDIHDR) == 64
midiStreamOut(hms, &hdr, sizeof(MIDIHDR));
// wait for an event that is set from the midi callback when the playback has finished
WaitForSingleObject(...);
midiOutUnprepareHeader(hms, &hdr, sizeof(MIDIHDR));
上面的调用序列有效并且没有产生错误(为了便于阅读,省略了错误检查)。
为了在 C# 中使用它们,我创建了一些 P/Invoke 代码:
[DllImport("winmm.dll")]
public static extern int midiOutPrepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiOutUnprepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiStreamOut(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiStreamRestart(IntPtr handle);
[StructLayout(LayoutKind.Sequential)]
public struct MidiHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public IntPtr UserData;
public uint Flags;
public IntPtr Next;
public IntPtr Reserved;
public uint Offset;
//[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
//public IntPtr[] Reserved2;
public IntPtr Reserved0;
public IntPtr Reserved1;
public IntPtr Reserved2;
public IntPtr Reserved3;
public IntPtr Reserved4;
public IntPtr Reserved5;
public IntPtr Reserved6;
public IntPtr Reserved7;
}
调用顺序同C:
var hdr = new MidiHeader();
hdr.this = that;
midiStreamRestart(handle);
midiOutPrepareHeader(handle, ref header, headerSize); // headerSize == 64
midiStreamOut(handle, ref header, headerSize);
mre.WaitOne(); // wait until the midi playback has finished.
midiOutUnprepareHeader(handle, ref header, headerSize);
MIDI 输出有效并且代码没有产生错误(再次省略了错误检查)。
只要我在 MidiHeader
中用数组取消注释这两行,而是删除 Reserved0
到 Reserved7
字段,它不会'不再工作了。发生的情况如下:
一切正常,直到并包括 midiStreamOut
。我可以听到 MIDI 输出。播放长度正确。但是,播放结束时不会调用事件回调。
此时MidiHeader.Flags
的值为0xe
,表示流还在播放(即使回调已经收到播放已完成的消息通知)完成的)。 MidiHeader.Flags
的值应该是9
,表示流已经播放完毕。
midiOutUnprepareHeader
调用失败,错误代码为 0x41
(“无法在媒体数据仍在播放时执行此操作。重置设备,或等待数据播放完毕玩完了。”)。请注意,按照错误消息中的建议重置设备实际上并不能解决问题(等待或多次尝试也无法解决)。
另一个正确工作的变体是,如果我在 C# 声明的签名中使用 IntPtr
而不是 ref MidiHeader
,然后手动分配非托管内存,复制我的 MidiHeader
结构到该内存中,然后使用分配的内存调用函数。
此外,我尝试将传递给 headerSize
参数的大小减小到 32。因为这些字段是保留的(事实上,在以前版本的 Windows API 中不存在), 他们似乎没有什么特别的目的。然而,这并不能解决问题,即使 Windows 甚至不知道数组的存在,因此它不应该做任何事情。再次完全注释掉数组可以解决问题(即,数组和 8 个 Reserved*
字段都被注释掉,并且 headerSize
为 32)。
这向我暗示 IntPtr[] Reserved2
无法正确编码,尝试这样做会破坏其他值。为了验证这一点,我创建了一个 Platform Invoke 测试项目:
WIN32PROJECT1_API void __stdcall test_function(struct test_struct_t *s)
{
printf("%u %u %u %u %u %u %u %u\n", s->test0, s->test1, s->test2, s->test3, s->test4, s->test5, s->test6, s->test7);
for (int i = 0; i < sizeof(s->pointer_array) / sizeof(s->pointer_array[0]); ++i)
{
printf("%u ", ((uint32_t)s->pointer_array[i]) >> 16);
}
printf("\n");
}
typedef int32_t *test_ptr;
struct test_struct_t
{
test_ptr test0;
uint32_t test1;
uint32_t test2;
test_ptr test3;
uint32_t test4;
test_ptr test5;
uint32_t test6;
uint32_t test7;
test_ptr pointer_array[8];
};
从 C# 中调用:
[StructLayout(LayoutKind.Sequential)]
struct TestStruct
{
public IntPtr test0;
public uint test1;
public uint test2;
public IntPtr test3;
public uint test4;
public IntPtr test5;
public uint test6;
public uint test7;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public IntPtr[] pointer_array;
}
[DllImport("Win32Project1.dll")]
static extern void test_function(ref TestStruct s);
static void Main(string[] args)
{
TestStruct s = new TestStruct();
s.test0 = IntPtr.Zero;
s.test1 = 1;
s.test2 = 2;
s.test3 = IntPtr.Add(IntPtr.Zero, 3);
s.test4 = 4;
s.test5 = IntPtr.Add(IntPtr.Zero, 5);
s.test6 = 6;
s.test7 = 7;
s.pointer_array = new IntPtr[8];
for (int i = 0; i < s.pointer_array.Length; ++i)
{
s.pointer_array[i] = IntPtr.Add(IntPtr.Zero, i << 16);
}
test_function(ref s);
Console.ReadLine();
}
并且输出符合预期,因此 IntPtr[] pointer_array
的编码在该程序中有效。
我知道不使用 SafeHandle
是次优的,但是,当使用它时,使用数组时 MIDI 函数的行为甚至更奇怪,所以我选择可能解决一个一个问题。
为什么使用 IntPtr[] Reserved2
会导致错误?
下面是生成完整示例的更多代码:
C 代码
/*
* example9.c
*
* Created on: Dec 21, 2011
* Author: David J. Rager
* Email: djrager@fourthwoods.com
*
* This code is hereby released into the public domain per the Creative Commons
* Public Domain dedication.
*
* http://http://creativecommons.org/publicdomain/zero/1.0/
*/
#include <windows.h>
#include <mmsystem.h>
#include <stdio.h>
HANDLE event;
static void CALLBACK example9_callback(HMIDIOUT out, UINT msg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
switch (msg)
{
case MOM_DONE:
SetEvent(event);
break;
case MOM_POSITIONCB:
case MOM_OPEN:
case MOM_CLOSE:
break;
}
}
int main()
{
unsigned int streambufsize = 24;
char* streambuf = NULL;
HMIDISTRM out;
MIDIPROPTIMEDIV prop;
MIDIHDR mhdr;
unsigned int device = 0;
streambuf = (char*)malloc(streambufsize);
if (streambuf == NULL)
goto error2;
memset(streambuf, 0, streambufsize);
if ((event = CreateEvent(0, FALSE, FALSE, 0)) == NULL)
goto error3;
memset(&mhdr, 0, sizeof(mhdr));
mhdr.lpData = streambuf;
mhdr.dwBufferLength = mhdr.dwBytesRecorded = streambufsize;
mhdr.dwFlags = 0;
// flags and event code
mhdr.lpData[8] = (char)0x90;
mhdr.lpData[9] = 63;
mhdr.lpData[10] = 0x55;
mhdr.lpData[11] = 0;
// next event
mhdr.lpData[12] = 96; // delta time?
mhdr.lpData[20] = (char)0x80;
mhdr.lpData[21] = 63;
mhdr.lpData[22] = 0x55;
mhdr.lpData[23] = 0;
if (midiStreamOpen(&out, &device, 1, (DWORD)example9_callback, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
goto error4;
//printf("sizeof midiheader = %d\n", sizeof(MIDIHDR));
if (midiOutPrepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR)
goto error5;
if (midiStreamRestart(out) != MMSYSERR_NOERROR)
goto error6;
if (midiStreamOut(out, &mhdr, sizeof(MIDIHDR)) != MMSYSERR_NOERROR)
goto error7;
WaitForSingleObject(event, INFINITE);
error7:
//midiOutReset((HMIDIOUT)out);
error6:
MMRESULT blah = midiOutUnprepareHeader((HMIDIOUT)out, &mhdr, sizeof(MIDIHDR));
printf("stuff: %d\n", blah);
error5:
midiStreamClose(out);
error4:
CloseHandle(event);
error3:
free(streambuf);
error2:
//free(tracks);
error1:
//free(hdr);
return(0);
}
C#代码
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace MidiOutTest
{
class Program
{
[DllImport("winmm.dll")]
public static extern int midiStreamOpen(out IntPtr handle, ref uint deviceId, uint cMidi, MidiCallback callback, IntPtr userData, uint flags);
[DllImport("winmm.dll")]
public static extern int midiStreamOut(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiStreamRestart(IntPtr handle);
[DllImport("winmm.dll")]
public static extern int midiOutPrepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll")]
public static extern int midiOutUnprepareHeader(IntPtr handle, ref MidiHeader header, uint headerSize);
[DllImport("winmm.dll", CharSet = CharSet.Unicode)]
public static extern int midiOutGetErrorText(int mmsyserr, StringBuilder errMsg, int capacity);
[DllImport("winmm.dll")]
public static extern int midiStreamClose(IntPtr handle);
public delegate void MidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2);
private static readonly ManualResetEvent mre = new ManualResetEvent(false);
private static void TestMidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2)
{
Debug.WriteLine(msg.ToString());
if (msg == MOM_DONE)
{
Debug.WriteLine("MOM_DONE");
mre.Set();
}
}
public const uint MOM_DONE = 0x3C9;
public const int MMSYSERR_NOERROR = 0;
public const int MAXERRORLENGTH = 256;
public const uint CALLBACK_FUNCTION = 0x30000;
public const uint MidiHeaderSize = 64;
public static void CheckMidiOutMmsyserr(int mmsyserr)
{
if (mmsyserr != MMSYSERR_NOERROR)
{
var sb = new StringBuilder(MAXERRORLENGTH);
var errorResult = midiOutGetErrorText(mmsyserr, sb, sb.Capacity);
if (errorResult != MMSYSERR_NOERROR)
{
throw new /*Midi*/Exception("An error occurred and there was another error while attempting to retrieve the error message."/*, mmsyserr*/);
}
throw new /*Midi*/Exception(sb.ToString()/*, mmsyserr*/);
}
}
static void Main(string[] args)
{
IntPtr handle;
uint deviceId = 0;
CheckMidiOutMmsyserr(midiStreamOpen(out handle, ref deviceId, 1, TestMidiCallback, IntPtr.Zero, CALLBACK_FUNCTION));
try
{
var bytes = new byte[24];
IntPtr buffer = Marshal.AllocHGlobal(bytes.Length);
try
{
MidiHeader header = new MidiHeader();
header.Data = buffer;
header.BufferLength = 24;
header.BytesRecorded = 24;
header.UserData = IntPtr.Zero;
header.Flags = 0;
header.Next = IntPtr.Zero;
header.Reserved = IntPtr.Zero;
header.Offset = 0;
#warning uncomment if using array
//header.Reserved2 = new IntPtr[8];
// flags and event code
bytes[8] = 0x90;
bytes[9] = 63;
bytes[10] = 0x55;
bytes[11] = 0;
// next event
bytes[12] = 96;
bytes[20] = 0x80;
bytes[21] = 63;
bytes[22] = 0x55;
bytes[23] = 0;
Marshal.Copy(bytes, 0, buffer, bytes.Length);
CheckMidiOutMmsyserr(midiStreamRestart(handle));
CheckMidiOutMmsyserr(midiOutPrepareHeader(handle, ref header, MidiHeaderSize));
CheckMidiOutMmsyserr(midiStreamOut(handle, ref header, MidiHeaderSize));
mre.WaitOne();
CheckMidiOutMmsyserr(midiOutUnprepareHeader(handle, ref header, MidiHeaderSize));
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
finally
{
midiStreamClose(handle);
}
}
}
[StructLayout(LayoutKind.Sequential)]
public struct MidiHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public IntPtr UserData;
public uint Flags;
public IntPtr Next;
public IntPtr Reserved;
public uint Offset;
#if false
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public IntPtr[] Reserved2;
#else
public IntPtr Reserved0;
public IntPtr Reserved1;
public IntPtr Reserved2;
public IntPtr Reserved3;
public IntPtr Reserved4;
public IntPtr Reserved5;
public IntPtr Reserved6;
public IntPtr Reserved7;
#endif
}
}
最佳答案
来自midiOutPrepareHeader
的文档:
Before you pass a MIDI data block to a device driver, you must prepare the buffer by passing it to the midiOutPrepareHeader function. After the header has been prepared, do not modify the buffer. After the driver is done using the buffer, call the midiOutUnprepareHeader function.
您没有遵守此规定。编码器创建一个临时的 native 版本的结构,它在调用 midiOutPrepareHeader
期间存在。一旦 midiOutPrepareHeader
返回,临时 native 结构将被销毁。但是 MIDI 代码仍然有对它的引用。这就是关键点,MIDI 代码包含对您的结构的引用并且需要能够访问它。
具有单独写入字段的版本可以工作,因为该结构是 blittable 的。因此 p/invoke 编码器通过固定与 native 结构二进制兼容的托管结构来优化调用。在您调用 midiOutUnprepareHeader
之前,GC 仍有机会重新定位该结构,但您似乎还没有被它捕获。如果您坚持使用 bittable 结构,您需要固定它直到调用 midiOutUnprepareHeader
。
因此,底线是您需要提供一个结构,该结构在您调用 midiOutUnprepareHeader
之前一直有效。个人建议使用Marshal.AllocHGlobal
,Marshal.StructureToPtr
,然后Marshal.FreeHGlobal
一次midiOutUnprepareHeader
返回。并且显然将参数从 ref MidiHeader
切换为 IntPtr
。
我认为我不需要向您展示任何代码,因为从您的问题中可以清楚地看出您知道如何执行这些操作。事实上,我提出的解决方案是您已经尝试并观察到的解决方案。但现在你知道为什么了!
关于c# - 在结构中编码(marshal) IntPtr[] 会导致 midiStream 函数出现错误,但将数组展开到一堆字段是可行的,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28925449/