You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MIDI解析器仅兼容部分多轨文件,请求排查算法缺陷

MIDI解析器多轨兼容与事件识别问题排查

我之前一直以为自己的MIDI项目完全不支持多轨MIDI文件,但测试数百个文件后发现,部分多轨文件能正常解析。这说明解析算法存在基础问题,但我找不到错误所在。已经考虑过running status、未知元事件等MIDI特性,但肯定还有疏漏。

另外,程序偶尔会遇到无法识别的通道声音事件(note-on、note-off、乐器切换等),但我已经覆盖了MIDI规范里列出的7种相关事件。

我认为这些问题是某个疏忽的逻辑错误导致解析器读取出错,但追踪算法没发现异常。

我的代码逻辑是遍历每个轨道,确定事件类型,通过引用传递BinaryReader到事件构造函数来抽象数据解析:

For x As Integer = 0 To metadata.NumberTracks - 1
   While Not (dataString.EndsWith("MTrk")) 'Advances to the start of the next track
      dataString += Chr(reader.ReadByte)
   End While
   dataString = ""

   Dim trk As New Track
   Dim numberBytes As Integer = 0
   Dim byteOffset As Integer = reader.BaseStream.Position
   For z As Integer = 0 To 3
      numberBytes = (256 * numberBytes) + reader.ReadByte
   Next
   Dim runningStatus As Byte
   Do
      trk.addEvent(GetNextEvent(reader, runningStatus))
   Loop Until GetType(EndOfTrack) = trk.getLastEvent().GetType()
   tracks.Add(trk)
Next

GetNextEvent函数实现如下:

Private Function GetNextEvent(ByRef reader As BinaryReader, ByRef runningStatus As Byte) As MIDIEvent
    Dim newEvent As MIDIEvent

    Dim deltaTime As New VarLengthQuantity(reader)
    'MessageBox.Show(deltaTime.getValue)
    Dim statusByte As Byte = reader.ReadByte
    If statusByte = EventCode.MetaEventFlag Then 'Event is a Meta-Event
        Dim eventTypeCode As Byte = reader.ReadByte
        Dim eventLength As New VarLengthQuantity(reader)
            Select Case eventTypeCode
            Case EventCode.MetaEvent.EndOfTrack
                newEvent = New EndOfTrack(deltaTime)
                'reader.ReadByte()
            Case EventCode.MetaEvent.TimeSignature
                newEvent = New TimeSignature(deltaTime, reader)
            Case EventCode.MetaEvent.SetTempo
                newEvent = New SetTempo(deltaTime, reader)
            Case EventCode.MetaEvent.SMPTEOffset
                newEvent = New SMPTEOffset(deltaTime, reader)
            Case EventCode.MetaEvent.KeySignature
                newEvent = New KeySignature(deltaTime, reader)
            Case EventCode.MetaEvent.SequenceNumber
                newEvent = New SequenceNumber(deltaTime, reader)
            Case EventCode.MetaEvent.SequenceName
                newEvent = New SequenceName(deltaTime, eventLength, reader)
            Case EventCode.MetaEvent.InstrumentName
                newEvent = New InstrumentName(deltaTime, eventLength, reader)
            Case EventCode.MetaEvent.Lyric
                newEvent = New Lyric(deltaTime, eventLength, reader)
            Case EventCode.MetaEvent.TextEventLowBound To EventCode.MetaEvent.TextEventHighBound
                newEvent = New TextEvent(deltaTime, eventLength, reader)
            Case EventCode.MetaEvent.ChannelPrefix
                newEvent = New ChannelPrefix(deltaTime, reader)
            Case EventCode.MetaEvent.SeqSpecific
                newEvent = New SeqSpecific(deltaTime, eventLength, reader)
                Case Else
                    newEvent = New UnknownMetaEvent(deltaTime, eventLength, reader)
            End Select
    Else 'event is not a meta-event

        Dim statusCode As Byte
        Dim channel As Byte
        If GetHighNibble(statusByte) = &HF Then
            statusCode = statusByte
        ElseIf (GetMostSigBit(statusByte) = 0) Then 'running status applies
            If runningStatus = 0 Then
                Throw New Exception("Running status buffer was empty.")
            End If
            statusByte = runningStatus
        End If
        statusCode = GetHighNibble(statusByte)
        channel = GetLowNibble(statusByte)
        runningStatus = (16 * statusCode) + channel

        Select Case statusCode
    'Channel-voice events
   Case EventCode.ChannelVoiceEvent.NoteOn
      newEvent = New NoteEvent(deltaTime, True, channel, reader)
   Case EventCode.ChannelVoiceEvent.NoteOff
      newEvent = New NoteEvent(deltaTime, False, channel, reader)
   Case EventCode.ChannelVoiceEvent.PolyKeyPressure
      newEvent = New PolyKeyPressure(deltaTime, channel, reader)
   Case EventCode.ChannelVoiceEvent.ControlChange
      newEvent = New ControlChange(deltaTime, channel, reader)
   Case EventCode.ChannelVoiceEvent.ProgramChange
      newEvent = New ProgramChange(deltaTime, channel, reader)
   Case EventCode.ChannelVoiceEvent.ChannelPressure
      newEvent = New ChannelPressure(deltaTime, channel, reader)
   Case EventCode.ChannelVoiceEvent.PitchBend
      newEvent = New PitchBend(deltaTime, channel, reader)

   Case EventCode.SystemCommonEvent.SysEx
      newEvent = New SysEx(deltaTime, reader)
      runningStatus = 0
   Case EventCode.SystemCommonEvent.TimeCodeQuarterFrame
      newEvent = New TimeCodeQuarterFrame(deltaTime, reader)
      runningStatus = 0
   Case EventCode.SystemCommonEvent.PositionPointer
      newEvent = New PositionPointer(deltaTime, reader)
      runningStatus = 0
   Case EventCode.SystemCommonEvent.SongSelect
      newEvent = New SongSelect(deltaTime, reader)
      runningStatus = 0
   Case EventCode.SystemCommonEvent.TuneRequest
      newEvent = New TuneRequest(deltaTime)
      runningStatus = 0

   Case EventCode.SystemRealTimeEvent.TimingClock
      newEvent = New TimingClock(deltaTime)
   Case EventCode.SystemRealTimeEvent.StartSequence
      newEvent = New StartSequence(deltaTime)
   Case EventCode.SystemRealTimeEvent.ContinueSequence
      newEvent = New ContinueSequence(deltaTime)
   Case EventCode.SystemRealTimeEvent.StopSequence
      newEvent = New StopSequence(deltaTime)
   Case EventCode.SystemRealTimeEvent.ActiveSensing
      newEvent = New ActiveSensing(deltaTime)
   Case EventCode.SystemRealTimeEvent.ResetAll
      newEvent = New ResetAll(deltaTime)
   Case Else
      Throw New Exception("Invalid event.")
   End Select
   End If
   Debug.WriteLine(newEvent.GetType)
   GetNextEvent = newEvent
End Function

代码中的核心缺陷

1. 轨道定位逻辑完全错误

这段用于定位轨道起始位置的代码逻辑严重错误:

While Not (dataString.EndsWith("MTrk")) 'Advances to the start of the next track
    dataString += Chr(reader.ReadByte)
End While
  • MIDI文件的轨道是按文件头指定数量顺序排列的,每个轨道以MTrk(固定4字节)开头,无需通过字符串拼接查找。这种方式会读取大量无关字节,破坏流的位置,导致后续轨道长度读取错误,这是多轨文件部分可解析的核心原因。
  • 正确做法:读取文件头后,直接按顺序处理每个轨道,不需要额外查找MTrk标识。

2. 未处理插入在数据流中的System Real-Time事件

System Real-Time事件(如Timing Clock、Start等)可以插入到MIDI数据流的任意字节位置,包括delta time、状态字节或数据字节中间。你的代码在读取deltaTime后直接读取statusByte,完全忽略了中间可能存在的Real-Time事件,会导致后续字节被错误解析为事件数据,出现无法识别的通道事件。

正确做法:在读取任何非Real-Time事件的字节前,循环读取并处理所有System Real-Time事件(高4位为0xF且是Real-Time事件范围的字节)。

3. Running Status处理存在边界错误

在非元事件分支的逻辑中:

If GetHighNibble(statusByte) = &HF Then
    statusCode = statusByte
ElseIf (GetMostSigBit(statusByte) = 0) Then 'running status applies
    If runningStatus = 0 Then
        Throw New Exception("Running status buffer was empty.")
    End If
    statusByte = runningStatus
End If
statusCode = GetHighNibble(statusByte)
channel = GetLowNibble(statusByte)
runningStatus = (16 * statusCode) + channel
  • System Common事件(如SysEx)的高4位是0xF,但不属于Real-Time事件,这里错误地将statusCode设为整个statusByte,后续又提取高4位覆盖,导致System Common事件的类型识别错误。
  • System Real-Time事件不参与running status机制,但你的代码会强制设置runningStatus,这会破坏后续的running status逻辑。

4. 未利用轨道长度限制事件读取范围

你读取了轨道的numberBytes(轨道总字节数),但没有用这个值来限制轨道内事件的读取范围,仅依赖EndOfTrack事件终止读取。如果MIDI文件存在轨道长度错误或EndOfTrack事件丢失,会导致死循环或读取到其他轨道的数据,引发解析混乱。

正确做法:记录轨道的起始流位置,当已读取的字节数达到numberBytes时,终止轨道内事件的读取。

内容的提问来源于stack exchange,提问作者Roy H

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 03:34:54