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

