vb.netparsingmidiaudio-player

MIDI parser works fine on some files, but not others


For some time, I have been under the assumption that my ongoing MIDI project will not work with multi-track MIDI files at all, however extensive testing of several hundred MIDI files has revealed that some multi-track files do indeed work. Clearly this shows a much more fundamental problem with my parsing algorithm, and I simply cannot see what is wrong. I believe I have fully accounted for running status, unknown meta-events and other possible "MIDI-isms" but obviously I have been wrong all along.

Very occasionally, my program will also encounter a channel-voice event (note-on, note-off, change instrument etc.) that it does not recognise, which should not be possible, as I have accounted for the seven possible events (detailed here).

I believe these issues to be caused by a negligent logical error which has creeped in somewhere, leading the parser to "trip" over a byte, however my tracing of the algorithm has not highlighted this, or any other error.

My code reads through each track, determining each event type in the sequence and passing a BinaryReader object into the event's constructor by reference so that the interpretation of each event's data is somewhat abstracted:

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

The GetNextEvent() function is as follows:

    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

If anybody can help me find the flaw(s) in my algorithm, I would be extremely grateful. Many thanks, Roy H


Solution

  • As CL. kindly pointed out in a comment, the running status code is wrong, as the first event data byte has already been read, if running status is applied. I have fixed this by decrementing the BinaryReader's BaseStream position by one to "step back" and allow the data bytes to be read as normal in the event constructors. The new code is below:

    ...
    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
                    reader.BaseStream.Position -= 1
                    statusByte = runningStatus
                    statusCode = GetHighNibble(statusByte)
                    channel = GetLowNibble(statusByte)
                    runningStatus = (16 * statusCode) + channel
                Else
                    statusCode = GetHighNibble(statusByte)
                    channel = GetLowNibble(statusByte)
                    runningStatus = (16 * statusCode) + channel
                End If
    
    ...
    

    The program now works fully with no observed errors since the change.