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
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.