Code: Select all
EnableExplicit
;> ---------------------- <
;- MidiParser : Constants
;> ---------------------- <
#MIDI_Chunk_Header = 'dhTM' ;"MThd"
#MIDI_Chunk_Track = 'krTM' ;"MTrk"
Enumeration ;MIDI track types
#MIDI_Track_Single
#MIDI_Track_Synchronous
#MIDI_Track_Asynchronous
EndEnumeration
;> ----------------------- <
;- MidiParser : Structures
;> ----------------------- <
Structure MIDICHUNK
dwType.l
dwSize.l ;remaining size
EndStructure
Structure MIDIHEADER
wTrack.w ;track type
wCount.w ;number of tracks
wDelta.w ;delta time
EndStructure
Macro LoNibble(x)
(x & $0F)
EndMacro
Macro HiNibble(x)
(x >> 4)
EndMacro
Structure Bytes
b.b[4]
EndStructure
;> ---------------------------------- <
;- MidiParser : Variable-Length Value
;> ---------------------------------- <
#MIDI_VarLen_Max = $0FFFFFFF ;4 reserved Bytes
#MIDI_VarLen_Error = $FFFFFFFF ;impossible VarLen or Num value
Procedure.l NumToVarLen(Num.l)
Protected VarLen.l, i.l
Protected *VarLen.Bytes
If Num & ~#MIDI_VarLen_Max
VarLen = #MIDI_VarLen_Error
Else
*VarLen = @VarLen
*VarLen\b[0] = (Num >> 0) & $7F
*VarLen\b[1] = (Num >> 7) & $7F
*VarLen\b[2] = (Num >> 14) & $7F
*VarLen\b[3] = (Num >> 21) & $7F
i = 3
While i > 0 And Not *VarLen\b[i]
i - 1
Wend
While i > 0
*VarLen\b[i] | $80
i - 1
Wend
EndIf
ProcedureReturn VarLen
EndProcedure
Procedure.l VarLenToNum(VarLen.l)
Protected Num.l, i.l
Protected *VarLen.Bytes
*VarLen = @VarLen
Num | (*VarLen\b[0] & $7F) << 0
Num | (*VarLen\b[1] & $7F) << 7
Num | (*VarLen\b[2] & $7F) << 14
Num | (*VarLen\b[3] & $7F) << 21
If VarLen <> NumToVarLen(Num)
Num = #MIDI_VarLen_Error
EndIf
ProcedureReturn Num
EndProcedure
Procedure.l WriteVarLen(File.l, Num.l)
Protected Write.l, VarLen.l, i.l
Protected *VarLen.Bytes
VarLen = NumToVarLen(Num)
If IsFile(File) And VarLen <> #MIDI_VarLen_Error
*VarLen = @VarLen
i = 3
While Not *VarLen\b[i]
i - 1
Wend
While i >= 0
Write + WriteByte(File, *VarLen\b[i])
i - 1
Wend
EndIf
ProcedureReturn Write
EndProcedure
Procedure.l ReadVarLen(File.l)
Protected VarLen.l, len.l
If IsFile(File)
Repeat
VarLen = (VarLen << 8) | (ReadByte(File) & $FF)
len + 1
Until Not VarLen & $80
If len > 4
VarLen = #MIDI_VarLen_Error
EndIf
Else
VarLen = #MIDI_VarLen_Error
EndIf
ProcedureReturn VarLenToNum(VarLen)
EndProcedure
Procedure.l VarLenSize(Value.l, IsVarLen.l = #False)
Protected Size.l
If IsVarLen
Value = VarLenToNum(Value)
EndIf
If Value < 0
Size = #MIDI_VarLen_Error
ElseIf Value < (1 << 7)
Size = 1
ElseIf Value < (1 << 14)
Size = 2
ElseIf Value < (1 << 21)
Size = 3
ElseIf Value < (1 << 28)
Size = 4
Else
Size = #MIDI_VarLen_Error
EndIf
ProcedureReturn Size
EndProcedure
;> ---------------------- <
;- MidiParser : BigEndian
;> ---------------------- <
ProcedureDLL.w PeekBigEndianW(*MemoryBuffer)
!MOV eax, dword [p.p_MemoryBuffer]
!MOV ax, word [eax]
!BSWAP eax
!SHR eax, 16
ProcedureReturn
EndProcedure
ProcedureDLL.l PeekBigEndianL(*MemoryBuffer)
!MOV eax, dword [p.p_MemoryBuffer]
!MOV eax, dword [eax]
!BSWAP eax
ProcedureReturn
EndProcedure
ProcedureDLL.q PeekBigEndianQ(*MemoryBuffer)
!MOV eax, dword [p.p_MemoryBuffer]
!MOV edx, dword [eax+0]
!MOV eax, dword [eax+4]
!BSWAP eax
!BSWAP edx
ProcedureReturn
EndProcedure
ProcedureDLL.q PeekBigEndianN(*MemoryBuffer.Bytes, Size.l)
Protected Result.q, i.l
While i < Size
Result << 8
Result | (*MemoryBuffer\b[i] & $FF)
i + 1
Wend
ProcedureReturn Result
EndProcedure
ProcedureDLL.w ReadBigEndianW(File)
ReadWord(File)
!BSWAP eax
!SHR eax, 16
ProcedureReturn
EndProcedure
ProcedureDLL.l ReadBigEndianL(File)
ReadLong(File)
!BSWAP eax
ProcedureReturn
EndProcedure
ProcedureDLL.q ReadBigEndianQ(File)
ReadQuad(File)
!XOR eax, edx
!XOR edx, eax
!XOR eax, edx
!BSWAP eax
!BSWAP edx
ProcedureReturn
EndProcedure
;> --------------------------- <
;- MidiParser : Event handlers
;> --------------------------- <
Enumeration ;Event types
#MidiParser_Event_Header ;header chunk handler
#MidiParser_Event_Track ;track chunk handler
#MidiParser_Event_Command ;channel event handler
#MidiParser_Event_SysEx ;system exclusive handler
#MidiParser_Event_System ;system event handler
#MidiParser_Event_Meta ;meta event handler
#MidiParser_Event_Chunk ;non track chunks handler
#MidiParser_Event_Error ;error handler
EndEnumeration
Enumeration ;Error codes
#MidiParser_Error_File
#MidiParser_Error_Header
#MidiParser_Error_EOF
#MidiParser_Error_Time
#MidiParser_Error_Command
#MidiParser_Error_SysEx
#MidiParser_Error_System
#MidiParser_Error_Meta
#MidiParser_Error_Track
EndEnumeration
Enumeration ;Event handlers return value
#MidiParser_Stop
#MidiParser_Continue
EndEnumeration
Prototype.l HeaderHandler(Track.w, Count.w, Delta.w)
Prototype.l TrackHandler()
Prototype.l CommandHandler(Time.l, Event.l, channel.l, Param1.l, Param2.l)
Prototype.l SysExHandler(Time.l, Event.l, Size.l, *Buffer)
Prototype.l SystemHandler(Time.l, Event.l, Param.l)
Prototype.l MetaHandler(Time.l, Event.l, Size.l, *Buffer)
Prototype.l ChunkHandler(type.l, Size.l, *Buffer)
Global Dim EventHandlers.l(#MidiParser_Event_Error)
;> -------------------- <
;- MidiParser : Private
;> -------------------- <
Procedure.l RaiseMidiError(ErrorCode.l)
;if the user wants to catch it
If EventHandlers(#MidiParser_Event_Error)
;then lets give him
CallFunctionFast(EventHandlers(#MidiParser_Event_Error), ErrorCode)
EndIf
ProcedureReturn #MidiParser_Stop
EndProcedure
Procedure.l ParseMidiHeader(File.l, *Header.MIDIHEADER)
Protected Result.l
Protected Handler.HeaderHandler
Protected Chunk.MIDICHUNK
Chunk\dwType = ReadLong(File)
Chunk\dwSize = ReadBigEndianL(File)
If Chunk\dwType = #MIDI_Chunk_Header And Chunk\dwSize = SizeOf(MIDIHEADER)
*Header\wTrack = ReadBigEndianW(File)
*Header\wCount = ReadBigEndianW(File)
*Header\wDelta = ReadBigEndianW(File)
If *Header\wTrack >= #MIDI_Track_Single And *Header\wTrack <= #MIDI_Track_Asynchronous
;the header is valid so parsing continues
Result = #MidiParser_Continue
Handler = EventHandlers(#MidiParser_Event_Header)
;except if the user doesn't agree
If Handler
Result = Handler(*Header\wTrack, *Header\wCount, *Header\wDelta)
EndIf
Else
;this is not a valid midi header
Result = RaiseMidiError(#MidiParser_Error_Header)
EndIf
Else
;this is not a midi header
Result = RaiseMidiError(#MidiParser_Error_Header)
EndIf
ProcedureReturn Result
EndProcedure
Procedure.l ParseMidiTrack()
Protected Result.l
Protected Handler.TrackHandler
Result = #MidiParser_Continue
Handler = EventHandlers(#MidiParser_Event_Track)
If Handler
Result = Handler()
EndIf
ProcedureReturn Result
EndProcedure
Procedure.l ParseMidiCommand(File.l, Time.l, Event.l)
Protected Result.l, channel.l
Protected Param1.l, Param2.l
Protected Handler.CommandHandler
channel = LoNibble(Event)
Event = HiNibble(Event)
Param1 = ReadByte(File) & $FF
;Program and After-touch messages
;only have a single param
If Event <> $C And Event <> $D
Param2 = ReadByte(File) & $FF
EndIf
If Param1 & $80 Or Param2 & $80
;the MSB is set to 1 for events
RaiseMidiError(#MidiParser_Error_Command)
Else
;the command is valid so parsing continues
Result = #MidiParser_Continue
Handler = EventHandlers(#MidiParser_Event_Command)
If Handler
Result = Handler(Time, Event, channel, Param1, Param2)
EndIf
EndIf
ProcedureReturn Result
EndProcedure
Procedure.l ParseMidiSysEx(File.l, Time.l, Event.l)
Protected Result.l, Size.l, *Buffer
Protected Handler.SysExHandler
Size = ReadVarLen(File)
If Size = #MIDI_VarLen_Error
RaiseMidiError(#MidiParser_Error_SysEx)
Else
*Buffer = AllocateMemory(Size)
EndIf
If *Buffer
ReadData(File, *Buffer, Size)
Result = #MidiParser_Continue
Handler = EventHandlers(#MidiParser_Event_SysEx)
If Handler
Result = Handler(Time, Event, Size, *Buffer)
EndIf
FreeMemory(*Buffer)
Else
FileSeek(File, Loc(File) + Size)
Result = #MidiParser_Continue
EndIf
ProcedureReturn Result
EndProcedure
Procedure.l ParseMidiSystem(File.l, Time.l, Event.l)
Protected Result.l, Param.l
Protected Handler.SystemHandler
Select Event
Case $F1, $F3
Param = ReadByte(File) & $FF
If Param & $80
;the MSB is set to 1 for events
RaiseMidiError(#MidiParser_Error_System)
Param = $FFFFFFFF
EndIf
Case $F2
Param = ReadVarLen(File)
If VarLenSize(Param) <> 2
;this event param takes two bytes
RaiseMidiError(#MidiParser_Error_System)
Param = $FFFFFFFF
EndIf
Case $F4
;???
Case $F5
;???
EndSelect
Handler = EventHandlers(#MidiParser_Event_System)
If Param >= 0 And Handler
Result = Handler(Time, Event, Param)
EndIf
ProcedureReturn Result
EndProcedure
Procedure.l ParseMidiMeta(File.l, Time.l, MetaEvent.l)
Protected Result.l, Size.l, *Buffer
Protected Handler.MetaHandler
Size = ReadVarLen(File)
If Size = #MIDI_VarLen_Error
RaiseMidiError(#MidiParser_Error_Meta)
Else
;if size = 0, force memory allocation
*Buffer = AllocateMemory(Size + 1)
EndIf
If *Buffer
ReadData(File, *Buffer, Size)
Result = #MidiParser_Continue
Handler = EventHandlers(#MidiParser_Event_Meta)
If Handler
Result = Handler(Time, MetaEvent, Size, *Buffer)
EndIf
FreeMemory(*Buffer)
Else
FileSeek(File, Loc(File) + Size)
Result = #MidiParser_Continue
EndIf
ProcedureReturn Result
EndProcedure
Procedure.l ParseMidiChunk(File.l, *Chunk.MIDICHUNK)
Protected Result.l, *Buffer
Protected Handler.ChunkHandler
If *Chunk\dwSize
*Buffer = AllocateMemory(*Chunk\dwSize)
EndIf
If *Buffer
ReadData(File, *Buffer, *Chunk\dwSize)
Result = #MidiParser_Continue
Handler = EventHandlers(#MidiParser_Event_Chunk)
If Handler
Result = Handler(*Chunk\dwType, *Chunk\dwSize, *Buffer)
EndIf
FreeMemory(*Buffer)
Else
FileSeek(File, Loc(File) + *Chunk\dwSize)
Result = #MidiParser_Continue
EndIf
ProcedureReturn Result
EndProcedure
;> ------------------- <
;- MidiParser : Public
;> ------------------- <
ProcedureDLL.l SetMidiEventHandler(EventType.l, Callback.l)
Protected Set.l
;If Callback is Null then the EventType is not catched (default behavior)
If EventType >= #MidiParser_Event_Header And EventType <= #MidiParser_Event_Error
EventHandlers(EventType) = Callback
Set = #True
EndIf
ProcedureReturn Set
EndProcedure
ProcedureDLL.l ParseMidiFile(Filename.s)
Protected Parsed.l, File.l, KeepParsing.l
Protected nTracks.l, Event.l, MetaEvent.l
Protected DeltaTime.l, Time.l, Old.l, Loc.l
Protected Chunk.MIDICHUNK
Protected Header.MIDIHEADER
File = ReadFile(#PB_Any, Filename)
If File
KeepParsing = ParseMidiHeader(File, Header)
;
; Chunk loop (catch all chunks till the end of file)
;
While KeepParsing And Not Eof(File)
;if not EOF then there must be another chunk
If Lof(File) - Loc(File) < SizeOf(MIDICHUNK)
KeepParsing = RaiseMidiError(#MidiParser_Error_EOF)
Else
Chunk\dwType = ReadLong(File)
Chunk\dwSize = ReadBigEndianL(File)
EndIf
;if remaining size is enough, identify the chunk
If KeepParsing And Lof(File) - Loc(File) < Chunk\dwSize
KeepParsing = RaiseMidiError(#MidiParser_Error_EOF)
ElseIf KeepParsing
Select Chunk\dwType
Case #MIDI_Chunk_Track
;inform the user that a new track is about to be parsed
KeepParsing = ParseMidiTrack()
nTracks + 1 ;internal track counter
Case #MIDI_Chunk_Header
;this chunk only append at the begining of the file
KeepParsing = RaiseMidiError(#MidiParser_Error_Header)
Default
;we don't know this chunk...
KeepParsing = ParseMidiChunk(File, Chunk)
EndSelect
EndIf
;
; Track parser (catch all events till the end of the track)
;
If KeepParsing And Chunk\dwType = #MIDI_Chunk_Track
;exprected file location at the end of the track
Loc = Loc(File) + Chunk\dwSize
;reset values
Time = 0
MetaEvent = 0
While Not (Event = $FF And MetaEvent = $2F) ;While Not EndOfTrack
DeltaTime = ReadVarLen(File)
Time + DeltaTime
If DeltaTime = #MIDI_VarLen_Error
RaiseMidiError(#MidiParser_Error_Time)
Break
EndIf
Old = Event
Event = ReadByte(File) & $FF
;Events have MSB set to 1 (ie >= $80)
;If not, it's a param for the next event
;the next event is the same as the previous
If Event < $80
FileSeek(File, Loc(File)-1)
Event = Old
EndIf
Select Event
Case $80 To $EF
KeepParsing = ParseMidiCommand(File, Time, Event)
Case $F0, $F7
KeepParsing = ParseMidiSysEx(File, Time, Event)
Case $FF
MetaEvent = ReadByte(File) & $FF
If MetaEvent & $80
;the MSB is set to 1 for events
KeepParsing = RaiseMidiError(#MidiParser_Error_Meta)
Else
KeepParsing = ParseMidiMeta(File, Time, MetaEvent)
EndIf
Default
KeepParsing = ParseMidiSystem(File, Time, Event)
EndSelect
;the user doesn't want to continue ?
;an error has occured ?
If Not KeepParsing
;whatever... just stop parsing
Break
EndIf
Wend ;While Not EndOfTrack
;the chunk size is not the specified size
If Loc <> Loc(File)
KeepParsing = RaiseMidiError(#MidiParser_Error_Track)
EndIf
EndIf ;Track parser
Wend ;Chunk loop
;the number of tracks is not valid
If nTracks <> Header\wCount
KeepParsing = RaiseMidiError(#MidiParser_Error_Track)
EndIf
;it's finally over ^_^
If KeepParsing
Parsed = #True
EndIf
CloseFile(File)
Else
RaiseMidiError(#MidiParser_Error_File)
EndIf
ProcedureReturn Parsed
EndProcedure
;> ---------------- <
;- MidiParser : EOF
;> ---------------- <
Code: Select all
IncludeFile "lib_midi.pb"
DisableExplicit
Procedure ShowMidiError(code)
Select code
Case #MidiParser_Error_File
Debug "Error : Cannot open the file"
Case #MidiParser_Error_Header
Debug "Error : Header Error"
Case #MidiParser_Error_EOF
Debug "Error : Unexpected EOF"
Case #MidiParser_Error_Time
Debug "Error : Invalid Delta-Time"
Case #MidiParser_Error_Command
Debug "Error : Command param"
Case #MidiParser_Error_SysEx
Debug "Error : System Exclusive Event"
Case #MidiParser_Error_System
Debug "Error : System Event param"
Case #MidiParser_Error_Meta
Debug "Error : Meta-event error"
EndSelect
EndProcedure
Procedure ShowMidiHeader(type, Count, Delta)
Debug "FormatType : " + Str(type)
Debug "NumberOfTracks : " + Str(Count)
Debug "DeltaTimeIncrement (BPM ?) : " + StrD(6E4 / Delta)
Debug "---"
ProcedureReturn #MidiParser_Continue
EndProcedure
Procedure ShowMidiTrack()
Debug "TrackChunk"
ProcedureReturn #MidiParser_Continue
EndProcedure
Procedure ShowMidiChunk(type, Size, *Buffer)
Debug "Unknown chunk : " + PeekS(@type, 4)
ProcedureReturn #MidiParser_Continue
EndProcedure
Procedure ShowMidiCommand(Time, Event, channel, Param1, Param2)
Select Event
Case $8
Debug Str(Time) + " -> Channel : " + Str(channel) + " Note Off : " + Str(Param1) + " Velocity : " + Str(Param2)
Case $9
Debug Str(Time) + " -> Channel : " + Str(channel) + " Note On : " + Str(Param1) + " Velocity : " + Str(Param2)
EndSelect
ProcedureReturn #MidiParser_Continue
EndProcedure
Procedure ShowMidiMeta(Time, meta, Size, *Buffer)
Select meta
Case $51
tempo = PeekBigEndianN(*Buffer, Size)
Debug Str(Time) + " -> Set Tempo (BPM) : " + StrD(6E7 / tempo)
Case $2F
Debug "EndOfTrack"
Debug "---"
EndSelect
ProcedureReturn #MidiParser_Continue
EndProcedure
SetMidiEventHandler(#MidiParser_Event_Error, @ShowMidiError() )
SetMidiEventHandler(#MidiParser_Event_Header, @ShowMidiHeader() )
SetMidiEventHandler(#MidiParser_Event_Track, @ShowMidiTrack() )
SetMidiEventHandler(#MidiParser_Event_Command, @ShowMidiCommand())
SetMidiEventHandler(#MidiParser_Event_Meta, @ShowMidiMeta() )
MidiFileName$ = OpenFileRequester("Open midi file", "", "Midi|*.mid;*.midi", 0)
If ParseMidiFile(MidiFileName$)
Debug GetFilePart(MidiFileName$) + " parsing is over"
Else
Debug "An error as occured while parsing " + GetFilePart(MidiFileName$)
EndIf