MIDI键盘数据捕获工具开发记

出于对MIDI键盘输出数据的好奇,决定编写一个小程序来捕获并显示这些数据。在阅读了CodeProject上的几篇文章后,发现需要使用回调函数和委托来捕获和显示MIDI数据。由于之前没有接触过这些概念,决定开始编码并自行探索。

首先,这是完整的代码:

Imports System.Threading Imports System.Runtime.InteropServices Public Class Form1 Public Declare Function midiInGetNumDevs Lib "winmm.dll" () As Integer Public Declare Function midiInGetDevCaps Lib "winmm.dll" Alias "midiInGetDevCapsA" (ByVal uDeviceID As Integer, ByRef lpCaps As MIDIINCAPS, ByVal uSize As Integer) As Integer Public Declare Function midiInOpen Lib "winmm.dll" (ByRef hMidiIn As Integer, ByVal uDeviceID As Integer, ByVal dwCallback As MidiInCallback, ByVal dwInstance As Integer, ByVal dwFlags As Integer) As Integer Public Declare Function midiInStart Lib "winmm.dll" (ByVal hMidiIn As Integer) As Integer Public Declare Function midiInStop Lib "winmm.dll" (ByVal hMidiIn As Integer) As Integer Public Declare Function midiInReset Lib "winmm.dll" (ByVal hMidiIn As Integer) As Integer Public Declare Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As Integer) As Integer Public Delegate Function MidiInCallback(ByVal hMidiIn As Integer, ByVal wMsg As UInteger, ByVal dwInstance As Integer, ByVal dwParam1 As Integer, ByVal dwParam2 As Integer) As Integer Public ptrCallback As New MidiInCallback(AddressOf MidiInProc) Public Const CALLBACK_FUNCTION As Integer = &H30000 Public Const MIDI_IO_STATUS = &H20 Public Delegate Sub DisplayDataDelegate(dwParam1 As Integer) Public Structure MIDIINCAPS Dim wMid As Int16 ' Manufacturer ID Dim wPid As Int16 ' Product ID Dim vDriverVersion As Integer ' Driver version Dim szPname As String ' Product Name Dim dwSupport As Integer ' Reserved End Structure Dim hMidiIn As Integer Dim StatusByte As Byte Dim DataByte1 As Byte Dim DataByte2 As Byte Dim MonitorActive As Boolean = False Dim HideMidiSysMessages As Boolean = False Function MidiInProc(ByVal hMidiIn As Integer, ByVal wMsg As UInteger, ByVal dwInstance As Integer, ByVal dwParam1 As Integer, ByVal dwParam2 As Integer) As Integer If MonitorActive Then TextBox1.Invoke(New DisplayDataDelegate(AddressOf DisplayData), New Object() {dwParam1}) End If End Function Private Sub DisplayData(dwParam1 As Integer) If (HideMidiSysMessages = True And ((dwParam1 And &HF0) = &HF0)) Then Exit Sub Else StatusByte = (dwParam1 And &HFF) DataByte1 = (dwParam1 And &HFF00) >> 8 DataByte2 = (dwParam1 And &HFF0000) >> 16 TextBox1.AppendText(String.Format("{0:X2} {1:X2} {2:X2}{3}", StatusByte, DataByte1, DataByte2, vbCrLf)) End If End Sub Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load Me.Show() If midiInGetNumDevs() = 0 Then MsgBox("No MIDI devices connected") Application.Exit() End If Dim InCaps As New MIDIINCAPS Dim DevCnt As Integer For DevCnt = 0 To (midiInGetNumDevs - 1) midiInGetDevCaps(DevCnt, InCaps, Len(InCaps)) ComboBox1.Items.Add(InCaps.szPname) Next End Sub Private Sub ComboBox1_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBox1.SelectedIndexChanged ComboBox1.Enabled = False Dim DeviceID As Integer = ComboBox1.SelectedIndex midiInOpen(hMidiIn, DeviceID, ptrCallback, 0, CALLBACK_FUNCTION Or MIDI_IO_STATUS) midiInStart(hMidiIn) MonitorActive = True Button2.Text = "Stop monitor" End Sub Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click TextBox1.Clear() End Sub Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click If MonitorActive = False Then midiInStart(hMidiIn) MonitorActive = True Button2.Text = "Stop monitor" Else midiInStop(hMidiIn) MonitorActive = False Button2.Text = "Start monitor" End If End Sub Private Sub Button3_Click(sender As System.Object, e As System.EventArgs) Handles Button3.Click If HideMidiSysMessages = False Then HideMidiSysMessages = True Button3.Text = "Show System messages" Else HideMidiSysMessages = False Button3.Text = "Hide System messages" End If End Sub Private Sub Form1_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed MonitorActive = False midiInStop(hMidiIn) midiInReset(hMidiIn) Application.Exit() End Sub End Class

代码本身相当直观。可能需要查看MSDN网站上的MIDI参考以获取有关不同函数的详细信息。下面,突出显示了花了很长时间才弄清楚的部分:

Function MidiInProc(ByVal hMidiIn As Integer, ByVal wMsg As UInteger, ByVal dwInstance As Integer, ByVal dwParam1 As Integer, ByVal dwParam2 As Integer) As Integer If MonitorActive Then TextBox1.Invoke(New DisplayDataDelegate(AddressOf DisplayData), New Object() {dwParam1}) End If End Function

这是回调函数,它返回传入的MIDI消息。dwParam1包含正在寻找的4字节MIDI数据。从MSDN上得知:

dwMidiMessage MIDI消息已被接收。该消息被打包成一个双字值,如下所示:

  • 高字(高字节)
  • 高字节:未使用
  • 低字节:包含第二个字节的MIDI数据(如果需要)
  • 低字(低字节)
  • 高字节:包含第一个字节的MIDI数据(如果需要)
  • 低字节:包含MIDI状态

在回调函数中,"DisplayDataDelegate"被调用以显示接收到的MIDI数据。

Private Sub DisplayData(dwParam1 As Integer) If (HideMidiSysMessages = True And ((dwParam1 And &HF0) = &HF0)) Then Exit Sub Else StatusByte = (dwParam1 And &HFF) DataByte1 = (dwParam1 And &HFF00) >> 8 DataByte2 = (dwParam1 And &HFF0000) >> 16 TextBox1.AppendText(String.Format("{0:X2} {1:X2} {2:X2}{3}", StatusByte, DataByte1, DataByte2, vbCrLf)) End If End Sub

这个子程序格式化dwParam1字节,并在文本框中显示它们。

HideMidiSysMessages切换已添加,以抑制MIDI键盘生成的连续"FE"消息(Active Sensing)。

顺便说一句:还发现MIDI键盘在释放键时不生成Note Off消息,而是生成带有速度'00'的Note On(见上面的屏幕截图)。

Public ptrCallback As New MidiInCallback(AddressOf MidiInProc)

这行代码使回调函数有一个永久的引用。如果不这样做,回调函数将被.NET垃圾回收。

'midiInClose(hMidiIn)

这行代码被注释掉了,因为有时midiInClose(hMidiIn)会挂起(意味着它不返回任何错误,只是挂起)。调试器显示的错误是"参数'hMidiIn'未指定参数'hMidiIn'的参数"(?)。挂起的情况仅在MIDI键盘生成大量MIDI数据时尝试执行midiInClose时发生。发现了一篇有趣且深入的文章关于这个问题[感谢"Les"]。

猜midiInClose挂起是因为MidiInProc仍在处理传入的MIDI数据。不知道如何解决或绕过这个问题。但是Application.Exit()肯定可以无怨无悔地结束程序,也不会留下线程/回调函数在后台运行。如果有人知道midiInClose问题的解决方案,请随时评论!

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485