4

我正在使用来自 C# 的古老的 Windows 多媒体 API(来自 WinMM.dll 的 midiXyz 函数)。

以非流模式 ( )打开 Midi Out 设备/端口后,midiOutOpen使用 ( ) 发送 SysExmidiOutLongMsg工作正常。

以流模式 ( )打开 Midi Out 设备/端口后midiStreamOpen,发送 SysExmidiOutLongMsg不起作用。

相反,midiOutLongMsg失败并出现错误MMSYSERR_NOTSUPPORTED(= 8)。错误文本是:“不支持此功能。使用 Capabilities 功能确定驱动程序支持哪些功能和消息。

但是,根据 MSDN, ( midiOutLongMsg) 也应该与流句柄一起使用。 Jeff Glatt 的优秀 MIDI 信息页面还声称 SysEx 和流媒体可以一起使用(见页面末尾)。

通过使用 ( midiStreamOut) midiStreamOut 将缓冲的 SysEx 消息排入队列来发送缓冲的 SysEx 消息可以正常工作。但是,我也需要/想要直接使用 SysEx 发送midiOutLongMsg

我已经检查了各种开源 Midi 库(托管和非托管)、几个 Midi 驱动程序源,甚至 WINE 的 WinMM.dll 源,但找不到任何提示我做错了什么。

为了在尽可能少的代码中重现我的问题,我删除了所有回调、未准备、清理和发布内容,并在一个函数中压缩了几个类。以下代码打开第一个 Midi 设备/端口并尝试发送“GM Mode On”SysEx 消息:

2014 年 1 月 12 日更新:请参阅下面的代码版本 3!

public static class MidiTest { // version 1 - x86/32 bit only

  public static void Test () {
    int moID = 0; // midi out device/port ID
    int moHdl; // midi out device/port handle
#if !true
    // SysEx via midiOutLongMsg works
    Chk (WinMM.midiOutOpen (out moHdl, moID, null, 0, 0)); // open midi out in non-stream mode
#else
    // SysEx via midiOutLongMsg fails
    Chk (WinMM.midiStreamOpen (out moHdl, ref moID, 1, null, 0, 0)); // open midi out in stream mode
#endif
    byte [] sx = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 }; // GM On sysex
    int shdr = Marshal.SizeOf (typeof (MidiHdr)); // hdr size
    var mhdr = new MidiHdr (); // allocate managed hdr
    mhdr.bufferLength = mhdr.bytesRecorded = sx.Length; // length of message bytes
    mhdr.data = Marshal.AllocHGlobal (mhdr.bufferLength); // allocate native message bytes
    Marshal.Copy (sx, 0, mhdr.data, mhdr.bufferLength); // copy message bytes from managed to native memory
    IntPtr nhdr = Marshal.AllocHGlobal (shdr); // allocate native hdr
    Marshal.StructureToPtr (mhdr, nhdr, false); // copy managed hdr to native hdr
    Chk (WinMM.midiOutPrepareHeader (moHdl, nhdr, shdr)); // prepare native hdr
    Chk (WinMM.midiOutLongMsg (moHdl, nhdr, shdr)); // send native message bytes
  } // Test

  static void Chk (int f) {
    if (0 == f) return;
    var sb = new StringBuilder (256); // MAXERRORLENGTH
    var s = 0 == WMM.midiOutGetErrorText (f, sb, sb.Capacity) ? sb.ToString () : String.Format ("MIDI Error {0}.", f);
    System.Diagnostics.Trace.WriteLine (s);
  }

  [StructLayout (LayoutKind.Sequential)]
  internal struct MidiHdr { // sending long MIDI messages requires a header
    public IntPtr data; // native pointer to message bytes, allocated on native heap
    public int bufferLength; // length of buffer 'data'
    public int bytesRecorded; // actual amount of data in buffer 'data'
    public int user; // custom user data
    public int flags; // information flags about buffer
    public IntPtr next; // reserved
    public int reserved; // reserved
    public int offset; // buffer offset on callback
    [MarshalAs (UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] reservedArray; // reserved
  } // struct MidiHdr

  internal sealed class WinMM { // native MIDI calls from WinMM.dll
    public delegate void CB (int hdl, int msg, int inst, int p1, int p2); // callback
    [DllImport ("winmm.dll")] public static extern int midiStreamOpen (out int hdl, ref int devID, int reserved, CB proc, int inst, int flags);
    [DllImport ("winmm.dll")] public static extern int midiOutOpen (out int hdl, int devID, CB proc, int inst, int flags);
    [DllImport ("winmm.dll")] public static extern int midiOutPrepareHeader (int hdl, IntPtr pHdr, int sHdr);
    [DllImport ("winmm.dll")] public static extern int midiOutLongMsg (int hdl, IntPtr pHdr, int sHdr);
    [DllImport ("winmm.dll")] public static extern int midiOutGetErrorText (int err, StringBuilder msg, int sMsg);
  } // class WinMM

} // class MidiTest

问题 1:midiOutLongMsg当 Midi 设备/端口以流模式 ( ) 打开时,是否可以通过 SysEx 发送midiStreamOpen

问题2:如果是,知道我错过了什么吗?

问题 3:我没有找到很多在流模式下使用 MIDI 的源。所以,如果你知道一些在流模式下使用 MIDI 输出的开源库,请给我一个链接,以便我进行比较..

谢谢

更新(2013 年 10 月 19 日):

嗨,CL,

感谢您的回答。是的,也许微软的某个人在维护 WinMM.dll 时搞砸了一些事情——但我认为我丢失某些东西的可能性更高,因为

a) 有一本旧手册“Windows NT DDK - 多媒体驱动程序”(可在此处获得),它比当前的 MSDN 页面更详细地描述了 WinMM 的内容。第 56 页显示了一个图表,其中 WinMM.dll 作为应用程序和 MIDI/音频驱动程序之间的中间层。WinMM 的主要工作是将 MIDI 数据从应用程序传递到 MIDI/音频驱动程序之一。当 MIDI 驱动程序是外部键盘/合成器/音源的端口驱动程序时,WinMM 无法对 MIDI 数据进行太多更改。第 94 页描述了驱动程序消息 MODM_LONGDATA 及其参数 - 它与 midiOutLongMsg 的参数几乎相同。这限制了在 WinMM.dll 中搞砸的机会。

b) WinMM.dll 中从 midiOutLongMsg 被调用到使用 MODM_LONGDATA 调用驱动程序的代码路径在 midiOutOpen 之后可以正常工作,但在 midiStreamOpen 之后不能正常工作。结果代码是 MMSYSERR_NOTSUPPORTED - 这告诉我,在 WinMM.dll 中的代码路径的开头,我受到了一些健全性检查的打击,比如

if (whatever_weird_condition) return MMSYSERR_NOTSUPPORTED;

而whatever_weird_condition最有可能是关于我应该做但没有做的事情..

c) 如果 MIDI 驱动程序本身不支持流式输出(它是可选的),WinMM 会从缓冲/排队输出 (midiStreamOut) 转换为更简单的非流式驱动程序调用(这不是可选的)。我机器上的 8 个 MIDI 驱动程序都不支持流式传输,都依赖 WinMM 来完成。流式传输短消息和长消息都可以正常工作。

d) 我的测试代码在 Windows 7 和 Windows XP 上的行为完全相同。如果微软把事情搞砸了,那么这个错误一定是很久以前(在 XP 之前)造成的。我是第一个发现它的人(多年后),或者其他人都将它保密且无法搜索。

e) 我的测试代码与我机器上的所有 8 个 midi 驱动程序的行为完全相同。这告诉我,这很可能不是驱动程序问题。

f) 多年的调试告诉我,如果某些东西不能正常工作,那么问题很可能出在我的屏幕一侧.. ;-P

4

5 回答 5

4

更新:我很抱歉没有早点回来。我被工作淹没了。是的,你是对的,它现在正在失败。也许那天晚上我起得太晚了,但我不明白它是如何工作的,或者即使是这样。我还需要以流模式发送 sysex,但我的应用程序还没有在非流模式 (midiOutOpen) 下这样做。我会继续研究它,看看我是否能找到解决方法。您必须使用 sysex 主音量还是可以使用 CC:7 音量控制?当然,这对 sysex 没有帮助,但是短消息可以在流模式下通过。哦,感谢更新,我还获得了在 x86 或 x64 (AnyCPU) 中编译和运行的代码。

原文:我不知道你是否仍然感兴趣,但我认为下面的代码可能会回答你的问题。我将您的旧代码放在评论下//PREVIOUS CODE,将新代码放在评论下//NEW CODE

此外,对于 x86,标头的大小不应包含数据。我知道 x86 的大小是 0x40,但我仍在尝试找出最好的编码方式,所以如果您有任何想法,请告诉我。

我只是自己为另一个应用程序解决了这个问题,所以我还没有把它全部充实,但我运行了这段代码,它似乎对你有用。我喜欢这个旧 dll 中的流媒体模式。这是非常精确的,如果你使用双缓冲,你可以让它实时......你也可以像 midiout 一样以流模式发送短消息。

提示:下面的代码是版本 2,部分兼容 x86/x64 32/64 位。(米尔卡)

using System;

using System.Collections.Generic;

using System.Text;

using System.Runtime.InteropServices;

using System.Diagnostics;

public static class MidiTest
{

    public static void Test() 
    {
        int moID = 0; // midi out device/port ID

        //PREVIOUS CODE
        //int moHdl; // midi out device/port handle
        //NEW CODE
        IntPtr moHdl = IntPtr.Zero;

#if !true
    // SysEx via midiOutLongMsg works
    Chk (WinMM.midiOutOpen (out moHdl, moID, null, 0, 0)); // open midi out in non-stream mode
#else
        // SysEx via midiOutLongMsg fails
        //PREVIOUS CODE
        //Chk(WinMM.midiStreamOpen(out moHdl, ref moID, 1, null, 0, 0)); // open midi out in stream mode
        //NEW CODE
        IntPtr instance = IntPtr.Zero;
        Chk(WinMM.midiStreamOpen(out moHdl, ref moID, 1, null, instance, 0)); // open midi out in stream mode

#endif
        byte[] sx = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 }; // GM On sysex

        //PREVIOUS CODE
        //int shdr = Marshal.SizeOf(typeof(MidiHdr)); // hdr size
        //NEW CODE
        int shdr = 0x40; // hdr size

        var mhdr = new MidiHdr(); // allocate managed hdr
        mhdr.bufferLength = mhdr.bytesRecorded = sx.Length; // length of message bytes
        mhdr.data = Marshal.AllocHGlobal(mhdr.bufferLength); // allocate native message bytes
        Marshal.Copy(sx, 0, mhdr.data, mhdr.bufferLength); // copy message bytes from managed to native memory
        IntPtr nhdr = Marshal.AllocHGlobal(shdr); // allocate native hdr
        Marshal.StructureToPtr(mhdr, nhdr, false); // copy managed hdr to native hdr
        Chk(WinMM.midiOutPrepareHeader(moHdl, nhdr, shdr)); // prepare native hdr
        Chk(WinMM.midiOutLongMsg(moHdl, nhdr, shdr)); // send native message bytes
    } // Test

    static void Chk(int f)
    {
        if (0 == f) return;
        var sb = new StringBuilder(256); // MAXERRORLENGTH
        var s = 0 == WinMM.midiOutGetErrorText(f, sb, sb.Capacity) ? sb.ToString() : String.Format("MIDI Error {0}.", f);
        System.Diagnostics.Trace.WriteLine(s);
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct MidiHdr
    { // sending long MIDI messages requires a header
        public IntPtr data; // native pointer to message bytes, allocated on native heap
        public int bufferLength; // length of buffer 'data'
        public int bytesRecorded; // actual amount of data in buffer 'data'
        public int user; // custom user data
        public int flags; // information flags about buffer
        public IntPtr next; // reserved
        public int reserved; // reserved
        public int offset; // buffer offset on callback
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
        public int[] reservedArray; // reserved
    } // struct MidiHdr

    internal sealed class WinMM
    { // native MIDI calls from WinMM.dll
        public delegate void CB(int hdl, int msg, int inst, int p1, int p2); // callback

        //PREVIOUS CODE
        //[DllImport("winmm.dll")]
        //public static extern int midiStreamOpen(out int hdl, ref int devID, int reserved, CB proc, int inst, int flags);
        //[DllImport("winmm.dll")]
        //public static extern int midiOutOpen(out int hdl, int devID, CB proc, int inst, int flags);
        //[DllImport("winmm.dll")]
        //public static extern int midiOutPrepareHeader(int hdl, IntPtr pHdr, int sHdr);
        //[DllImport("winmm.dll")]
        //public static extern int midiOutLongMsg(int hdl, IntPtr pHdr, int sHdr);
        //[DllImport("winmm.dll")]
        //public static extern int midiOutGetErrorText(int err, StringBuilder msg, int sMsg);

        //NEW CODE
        #region winmm declarations
        [DllImport("winmm.dll")]
        public static extern int midiOutPrepareHeader(IntPtr handle,
            IntPtr headerPtr, int sizeOfMidiHeader);
        [DllImport("winmm.dll")]
        public static extern int midiOutUnprepareHeader(IntPtr handle,
            IntPtr headerPtr, int sizeOfMidiHeader);
        [DllImport("winmm.dll")]
        public static extern int midiOutOpen(out IntPtr handle, int deviceID,
            CB proc, IntPtr instance, int flags);
        [DllImport("winmm.dll")]
        public static extern int midiOutGetErrorText(int errCode,
            StringBuilder message, int sizeOfMessage);
        [DllImport("winmm.dll")]
        public static extern int midiOutClose(IntPtr handle);
        [DllImport("winmm.dll")]
        public static extern int midiStreamOpen(out IntPtr handle, ref int deviceID, int reserved,
            CB proc, IntPtr instance, uint flag);
        [DllImport("winmm.dll")]
        public static extern int midiStreamClose(IntPtr handle);
        [DllImport("winmm.dll")]
        public static extern int midiStreamOut(IntPtr handle, IntPtr headerPtr, int sizeOfMidiHeader);
        [DllImport("winmm.dll")]
        public static extern int midiOutLongMsg(IntPtr handle,
            IntPtr headerPtr, int sizeOfMidiHeader);
        #endregion

    } // class WinMM

} // class MidiTest
于 2014-01-11T00:20:50.800 回答
3

几乎没有人使用 MIDI 流。

现在,midi*功能不再由硬件厂商的驱动实现,而是由微软的MM WDM兼容驱动实现。似乎在重写时忽略了这个细节。

于 2013-10-18T10:39:45.810 回答
2

我不知道这是否有帮助,但如果你将你的 sysex 包装在一个 MidiEvent 结构中,比如这个(r - 相对刻度,s - 流 ID,e - 事件代码,d - 数据,p - 垫):

//MidiEvent - r, r, r, r, s, s, s, s, e, e, e,   e,   d,   d,   d, d, d,   d, p, p
byte[] sx = { 9, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 128, 240, 126, 127, 9, 1, 247, 0, 0 }; // GM On sysex

并在准备标题后添加这些行:

Chk(WinMM.midiStreamOut(moHdl, nhdr, shdr));
int r = WinMM.midiOutLongMsg(moHdl, nhdr, shdr);
Chk(r); // send native message bytes
Chk(WinMM.midiStreamRestart(moHdl));

您将看到 midiStreamOut sysex 在 MidiEvent 的相对刻度字段稍有延迟后离开端口。您仍然会收到 midiOutLongMessage 错误,但错误代码为 65 或“在媒体数据仍在播放时无法执行此操作。重置设备,或等待数据播放完毕。”

我不知道这是否有帮助,但它与错误代码 8 不同。

于 2014-01-16T17:07:07.353 回答
1

这是测试代码的第 3 版:

public static class MidiTest { // version 3 - x68/x64 32/64-bit compatible

  public static void Test () {
    int moID = 0; // midi out device/port ID
    IntPtr moHdl = IntPtr.Zero;

#if !true
    // SysEx via midiOutLongMsg works
    Chk (WinMM.midiOutOpen (out moHdl, moID, null, 0, 0)); // open midi out in non-stream mode
#else
    // SysEx via midiOutLongMsg fails
    IntPtr instance = IntPtr.Zero;
    Chk (WinMM.midiStreamOpen (out moHdl, ref moID, 1, null, instance, 0)); // open midi out in stream mode
#endif
    byte[] sx = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 }; // GM On sysex

    int shdr = Marshal.SizeOf(typeof(MidiHdr)); // hdr size
    IntPtr x = Marshal.OffsetOf (typeof (MidiHdr), "data");   // ptr; size: 4/8, offset: 0
    x = Marshal.OffsetOf (typeof (MidiHdr), "bufferLength");  // int; size: 4  , offset: 4/8
    x = Marshal.OffsetOf (typeof (MidiHdr), "bytesRecorded"); // int; size: 4  , offset: 8/12
    x = Marshal.OffsetOf (typeof (MidiHdr), "user");          // ptr; size: 4/8, offset: 12/16
    x = Marshal.OffsetOf (typeof (MidiHdr), "flags");         // int; size: 4  , offset: 16/24; followed by 4 byte padding
    x = Marshal.OffsetOf (typeof (MidiHdr), "next");          // ptr; size: 4/8, offset: 20/32
    x = Marshal.OffsetOf (typeof (MidiHdr), "reserved");      // ptr; size: 4/8, offset: 24/40
    x = Marshal.OffsetOf (typeof (MidiHdr), "offset");        // int; size: 4  , offset: 28/48; followed by 4 byte padding
    x = Marshal.OffsetOf (typeof (MidiHdr), "reservedArray"); // ptr; size: 4/8 x 8 = 32/64, offset: 32/56
    // total size: 64/120
    var mhdr = new MidiHdr (); // allocate managed hdr
    mhdr.bufferLength = mhdr.bytesRecorded = sx.Length; // length of message bytes
    mhdr.data = Marshal.AllocHGlobal (mhdr.bufferLength); // allocate native message bytes
    Marshal.Copy (sx, 0, mhdr.data, mhdr.bufferLength); // copy message bytes from managed to native memory
    IntPtr nhdr = Marshal.AllocHGlobal (shdr); // allocate native hdr
    Marshal.StructureToPtr (mhdr, nhdr, false); // copy managed hdr to native hdr
    Chk (WinMM.midiOutPrepareHeader (moHdl, nhdr, shdr)); // prepare native hdr
    int r = WinMM.midiOutLongMsg (moHdl, nhdr, shdr); // send native message bytes
    Chk (r); // send native message bytes
  } // Test

  static void Chk (int f) {
    if (0 == f) return;
    var sb = new StringBuilder (256); // MAXERRORLENGTH
    var s = 0 == WinMM.midiOutGetErrorText (f, sb, sb.Capacity) ? sb.ToString () : String.Format ("MIDI Error {0}.", f);
    System.Diagnostics.Trace.WriteLine (s);
  }

  [StructLayout (LayoutKind.Sequential)]
  internal struct MidiHdr { // sending long MIDI messages requires a header
    public IntPtr data; // native pointer to message bytes, allocated on native heap
    public int bufferLength; // length of buffer 'data'
    public int bytesRecorded; // actual amount of data in buffer 'data'
    public IntPtr user; // custom user data
    public int flags; // information flags about buffer
    public IntPtr next; // reserved
    public IntPtr reserved; // reserved
    public int offset; // buffer offset on callback
    [MarshalAs (UnmanagedType.ByValArray, SizeConst = 8)]
    public IntPtr[] reservedArray; // reserved
  } // struct MidiHdr

  internal sealed class WinMM { // native MIDI calls from WinMM.dll
    public delegate void CB (IntPtr hdl, int msg, IntPtr inst, int p1, int p2); // callback
    [DllImport ("winmm.dll")] public static extern int midiStreamOpen (out IntPtr hdl, ref int devID, int reserved, CB proc, IntPtr inst, uint flags);
    [DllImport ("winmm.dll")] public static extern int midiOutOpen (out IntPtr hdl, int devID, CB proc, IntPtr instance, int flags);
    [DllImport ("winmm.dll")] public static extern int midiOutPrepareHeader (IntPtr hdl, IntPtr pHdr, int sHdr);
    [DllImport ("winmm.dll")] public static extern int midiOutLongMsg (IntPtr hdl, IntPtr pHdr, int sHdr);
    [DllImport ("winmm.dll")] public static extern int midiOutGetErrorText (int err, StringBuilder msg, int sMsg);
  } // class WinMM

} // class MidiTest

与上述 Mark 的第 2 版一样,它IntPtr在需要与 32 AND 64 位兼容的地方使用。

我还修改了MidiHdr结构的声明:

  • IntPtr由于 32/64 位,指针被声明为
  • 标头末尾的数组大小错误 (4),现在正确 (8)

使用更改后的声明,Marshal.SizeOf现在计算正确的大小:

  • 0x40 = 32 位上的 64
  • 0x78 = 64 位上的 120

为了对齐指针字段nextreservedArray,编译器在 int 字段flags和之后添加 4 字节填充offset。字段大小和偏移量显示在注释中。

不幸的是,我的版本 3 和 Mark 的版本 2 仍然存在原始问题:midiOutLongMsg仍然返回错误代码 8。我在 Windows 7 Ultimate、64 位、SP1 上测试了编译为 x86(32 位)和 x64(64 位)的代码. 32 位版本也在 Windows XP Pro、SP3、32 位上进行了测试。所有平台上的结果相同。

如果有人想知道为什么我需要在流式传输时发送 Sysex 消息:如果使用 WinMM 流式播放 MIDI 文件并且用户更改了音量滑块,我需要通过 Sysex 发送新的音量 - 而不停止流式传输。

于 2014-01-12T17:23:48.057 回答
1

这是测试代码的第 4 个版本,这次是在原生 C++ 中,以确保我的问题与互操作和托管代码无关。问题(错误代码 8)仍然存在:

#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <MMSystem.h>

#pragma comment ( lib, "winmm.lib" )

static void Chk (UINT r) {
  static _TCHAR errmsg[256];
  if (!r) return;
  UINT rr = midiOutGetErrorText (r, errmsg, sizeof (errmsg));
  printf ("MIDI Error %d: '%S'\n", r, errmsg);
}

int _tmain (int argc, _TCHAR* argv[]) {
  UINT moID = 0; // first midi out device/port ID
  DWORD_PTR inst = NULL; // no instance
  DWORD_PTR clbk = NULL; // no callback
  DWORD flgs = CALLBACK_NULL; // flags, no callback
#if 0
  // SysEx via midiOutLongMsg works
  HMIDIOUT hmo = 0; // midi out device/port handle
  Chk (midiOutOpen (&hmo, moID, clbk, inst, flgs)); // open midi out in non-stream mode
#else
  // SysEx via midiOutLongMsg fails
  HMIDISTRM hms = 0; // midi out device/port handle
  Chk (midiStreamOpen (&hms, &moID, 1, clbk, inst, flgs)); // open midi out in stream mode
  HMIDIOUT hmo = (HMIDIOUT) hms;
#endif
  Chk (midiOutShortMsg (hmo, 0x00404090)); // note on
  Sleep (200); // ms
  Chk (midiOutShortMsg (hmo, 0x00004090)); // note off
  static unsigned char sx [] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 }; // GM On sysex
  static MIDIHDR mhdr; // midi header describes long msg
  UINT shdr = sizeof (mhdr);
  memset (&mhdr, 0, shdr); // clear header
  mhdr.lpData = (LPSTR) sx; // point to sysex
  mhdr.dwBufferLength = mhdr.dwBytesRecorded = sizeof (sx); // length of message bytes
  Chk (midiOutPrepareHeader (hmo, &mhdr, shdr)); // prepare hdr
  UINT r = midiOutLongMsg (hmo, &mhdr, shdr); // send message bytes
  Chk (r);
  // unprepare header, close etc. omitted ...
  return 0;
}
于 2014-01-17T18:27:34.073 回答