我正在尝试使用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中的数组取消对这两行的注释,并删除Reserved0Reserved7字段,它就不再工作了。发生的情况如下:
一切正常,直到并包括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不能正确封送,并且试图这样做会损坏其他值。为了验证这一点,我创建了一个平台调用测试项目:
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的文档中:
在将midi数据块传递给设备驱动程序之前,必须通过将其传递给midioutprepareader函数来准备缓冲区。准备好头之后,不要修改缓冲区。使用缓冲区完成驱动程序后,调用midiOutUnpareHeader函数。
你没有坚持这一点。封送拆收器创建结构的临时本机版本,该版本在调用midiOutPrepareHeader期间有效。一旦midiOutPrepareHeader返回,临时本机结构就会被销毁。但是midi代码仍然有一个引用。这是关键点,midi代码保存对结构的引用,并且需要能够访问它。
具有单独编写的字段的版本可以工作,因为该结构是blittable的。因此,p/invoke封送拆收器通过固定与本机结构二进制兼容的托管结构来优化调用。在调用midiOutUnprepareHeader之前,gc仍然有机会重新定位结构,但似乎您还没有被它捕获。如果要坚持使用位表结构,则需要固定它,直到调用midiOutUnprepareHeader
所以,归根结底,你需要提供一个结构,它可以一直存在,直到你调用midiOutUnprepareHeader。我个人建议您使用Marshal.AllocHGlobalMarshal.StructureToPtr,然后返回Marshal.FreeHGlobal一次。很明显,将参数从midiOutUnprepareHeader切换到ref MidiHeader
我想我不需要向你展示任何代码,因为从你的问题中可以清楚地看出你知道如何做这些事情。事实上,我建议的解决方案是一个你已经尝试和观察过的工作。但现在你知道为什么了!

07-24 19:08
查看更多