Midi File decoder

Share your advanced PureBasic knowledge/code with the community.
User avatar
einander
Enthusiast
Enthusiast
Posts: 744
Joined: Thu Jun 26, 2003 2:09 am
Location: Spain (Galicia)

Post by einander »

As you can see here
viewtopic.php?t=7298&highlight=midi
I have not luck with my first post asking for MIDI help to PB forums

but a visit to Borg's site was very helpful to start understand the MIDI spec.
http://www.borg.com/~jglatt/tech/midispec.htm

Some of your questions are on this simple MIDI header decoder, made long ago with the help of Psychophanta

Code: Select all

;Read MIDI header
;by einander
;Thanks Psychophanta For the Little Endian ASM procedures 

Procedure.s PS(*PO, LE) ; get string  LE bytes from *PO
    a$ = Space(LE)                ; get Titles, Lyrics,Instrument names and other strings embedded on the Midi stream
    CopyMemory(*PO, @a$, LE)  ; beware: only valid for strings; other data content many chr(0)
    ProcedureReturn a$
EndProcedure

Procedure.w P2(*PO) ; returns 2 Little Endian bytes from *PO
    !MOV eax,dword[esp]
    !MOV ax,word[eax]
    !ROR ax,8
    !AND eax,$FFFF
    ProcedureReturn
EndProcedure

Procedure.l P4(*PO) ; returns 4 Little Endian Bytes from *PO
    !MOV eax,dword[esp]
    !MOV eax,dword[eax]
    !BSWAP eax
    ProcedureReturn
EndProcedure

Procedure HeaderData(*PO) ; reads 3 16-bit words stored MSB first ;6 Bytes length 
    Shared Forward
    Format = P2(*PO)
    Debug "Format=" + Str(Format)
    Ntrks.w = P2(*PO + 2)
    Debug "Number of tracks=" + Str(Ntrks)
    TimRes.w = P2(*PO + 4)
    Debug "Time Resolution=" + Str(TimRes)
    If    TimRes>>15&1 
        Debug "Time-code based time"
    Else
        Debug "Metrical time"
        Debug Str(TimRes) + "=Number of delta-time ticks on a quarter note"
    EndIf
    Forward= *PO + 6
EndProcedure

Procedure ReadHeader(*PO) ; read 8 bits
    ; 4 char ASC:  M,T,h,d = header chunk - M,T,r,k = track header chunk
    ; followed by unsigned long Length
    Shared Forward
    a$ = PS(*PO, 4)
    Debug "_________"
    If a$ = "MThd"              ;MIDIfile Header 
        Debug a$ + " : File header"
        *PO + 4
        LenHd = P4(*PO) ; length of header data
        *PO + 4
        Debug Str(LenHd) + "=Length of header data"
        HeaderData(*PO)
        ProcedureReturn LenHd ; returns Len of header data
    ElseIf a$ = "MTrk"    ;Track Header
        Debug a$ + " : Track Header"
        *PO + 4
        LenTrk.l = P4(*PO)
        Debug Str(LenTrk) + "=Length of Track data"
        Forward=*PO+4
        ProcedureReturn  LenTrk
    Else
        MessageRequester("STOP", "Header " + a$ + " UNKNOWN", 0)
        End
    EndIf
EndProcedure

Procedure MFload() ; hMF= address  of the starting memory For the MIDIfile
    Shared hMF,Forward
    filename$ = OpenFileRequester("Load MIDI file", "*C:\.mid", "MIDI |*.mid", 0)
    ; filename$ = "D:\elgar\ALPINE.mid"
    If OpenFile(0, filename$)
        Debug filename$
        LOF = Lof()
        Debug"Length of file :" + Str(LOF)
        hMF = AllocateMemory(Lof())   ; all file to memory
        ReadData(hMF, LOF)        
        CloseFile(0)
    Else
        Debug "File Not open " + filename$
    EndIf
    ProcedureReturn LOF  ; LOF= len of header and start of data 
EndProcedure

OpenWindow(0, 0, 0,0,0 , #WS_OVERLAPPEDWINDOW | #WS_MAXIMIZE,"Read MIDI header") 
LOF = MFload() ; len of MIDIfile
Debug "Addr=" + Str(hMF)    
Len= ReadHeader(hMF) ; Len of header
MessageRequester("","DONE",0)
End
See also
viewtopic.php?t=8639&highlight=midi
Tyr
New User
New User
Posts: 4
Joined: Tue Oct 11, 2005 10:35 pm

Post by Tyr »

First of all thanks for your replie. That`s a lot of stuff, give me a few days, to dig into it.

Tier
Mir geht`s besser, als ich ausseh`

http://tyr.myownmusic.de
Tyr
New User
New User
Posts: 4
Joined: Tue Oct 11, 2005 10:35 pm

Post by Tyr »

Borg`s site seems to be very interessting. He also wrote an Article on the midifile itself, with can be found here:

http://www.borg.com/~jglatt/tech/midifile.htm
Mir geht`s besser, als ich ausseh`

http://tyr.myownmusic.de
Dr. Dri
Enthusiast
Enthusiast
Posts: 243
Joined: Sat Aug 23, 2003 6:45 pm

Post by Dr. Dri »

I wanted to convert this code to PB4 but i didn't understand anything so i looked for the specs at wotsit.org and i started my own decoder which suppose to do exactly the same...

It's not finished at all but i think it's not too bad ^^. It debugs the header, "On/Off" notes and Tempo changes. For the tempo, as i think nobody (including me) feels concerned by "Delta-Time Increment per MIDI Quarter Note", i decided to convert it to BPM (Beats Per Minute).

Code: Select all

;> ---------------- <
;- MIDI : 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

#MIDI_VLV_Max   = $0FFFFFFF
#MIDI_VLV_Error = $FFFFFFFF

;> ----------------- <
;- MIDI : 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

;> ---------------------------- <
;- MIDI : Variable-Length Value
;> ---------------------------- <

Procedure.l NumToVLV(Num.l)
  Protected VLV.l, i.l
  Protected *VLV.Bytes
  
  If Num & ~#MIDI_VLV_Max
    
    VLV = #MIDI_VLV_Error
    
  Else
    *VLV = @VLV
    
    *VLV\b[0] = (Num >>  0) & $7F
    *VLV\b[1] = (Num >>  7) & $7F
    *VLV\b[2] = (Num >> 14) & $7F
    *VLV\b[3] = (Num >> 21) & $7F
    
    i = 3
    
    While i > 0 And Not *VLV\b[i]
      i - 1
    Wend
    
    While i > 0
      *VLV\b[i] | $80
      i - 1
    Wend
  EndIf
  
  ProcedureReturn VLV
EndProcedure

Procedure.l VLVToNum(VLV.l)
  Protected Num.l, i.l
  Protected *VLV.Bytes
  
  *VLV = @VLV
  
  Num | (*VLV\b[0] & $7F) <<  0
  Num | (*VLV\b[1] & $7F) <<  7
  Num | (*VLV\b[2] & $7F) << 14
  Num | (*VLV\b[3] & $7F) << 21
  
  If VLV <> NumToVLV(Num)
    Num = #MIDI_VLV_Error
  EndIf
  
  ProcedureReturn Num
EndProcedure

Procedure.l WriteVLV(File.l, Num.l)
  Protected Write.l, VLV.l, i.l
  Protected *VLV.Bytes
  
  VLV = NumToVLV(Num)
  
  If IsFile(File) And VLV <> #MIDI_VLV_Error
    *VLV = @VLV
    
    i = 3
    
    While Not *VLV\b[i]
      i - 1
    Wend
    
    While i >= 0
      Write + WriteByte(File, *VLV\b[i])
      i - 1
    Wend
  EndIf
  
  ProcedureReturn Write
EndProcedure

Procedure.l ReadVLV(File.l)
  Protected VLV.l, Len.l
  
  If IsFile(File)
    Repeat
      VLV = (VLV << 8) | (ReadByte(File) & $FF)
      Len + 1
    Until Not VLV & $80
    
    If Len > 4
      VLV = #MIDI_VLV_Error
    EndIf
  Else
    VLV = #MIDI_VLV_Error
  EndIf
  
  ProcedureReturn VLVToNum(VLV)
EndProcedure

;> ----------- <
;- MIDI : File
;> ----------- <

CompilerIf #PB_Compiler_Unicode
  #PB_String = #PB_UTF8
CompilerElse
  #PB_String = #PB_Ascii
CompilerEndIf

Procedure.w ReadBigEndianW(File)
  ReadWord(File)
  !BSWAP eax
  !SHR   eax, 16
  ProcedureReturn
EndProcedure

Procedure.l ReadBigEndianL(File)
  ReadLong(File)
  !BSWAP eax
  ProcedureReturn
EndProcedure

Procedure.s ReadStringN(File.l, Length.l = #PB_Default, Flags.l = #PB_String)
  Protected String.s
  
  If Length <= 0
    String = ReadString(File, Flags)
  Else
    String = Space(Length)
    Length = ReadData(File, @String, Length)
    String = Mid(String, 1, Length)
  EndIf
  
  ProcedureReturn String
EndProcedure

Macro ReadString(File, Length = #PB_Default, Flags = #PB_String)
  ReadStringN(File, Length, Flags)
EndMacro

;> ------------------- <
;- MIDI : Parsing test
;> ------------------- <

MidiFileName$= OpenFileRequester("Open midi file", "", "Midi|*.mid;*.midi", 0)
If ReadFile(0, MidiFileName$)
  
  Chunk.MIDICHUNK
  Header.MIDIHEADER
  
  Chunk\dwType = ReadLong(0)
  Chunk\dwSize = ReadBigEndianL(0)
  
  If Chunk\dwType = #MIDI_Chunk_Header And Chunk\dwSize = SizeOf(MIDIHEADER)
    
    Header\wTrack = ReadBigEndianW(0)
    Header\wCount = ReadBigEndianW(0)
    Header\wDelta = ReadBigEndianW(0)
    
    Debug "FormatType : " + Str(Header\wTrack)
    Debug "NumberOfTracks : " + Str(Header\wCount)
    Debug "DeltaTimeIncrement (BPM ?) : " + StrD(60 * 1000 / Header\wDelta)
    Debug "---"
    
    While Not (Error Or Eof(0))
      
      If Not Error And Lof(0) - Loc(0) < SizeOf(MIDICHUNK)
        Error = #True
      EndIf
      
      If Not Error
        Chunk\dwType = ReadLong(0)
        Chunk\dwSize = ReadBigEndianL(0)
        
        If Chunk\dwType <> #MIDI_Chunk_Track
          Debug "UnknownChunk : " + PeekS(Chunk, 4), 1
          FileSeek(0, Loc(0) + Chunk\dwSize)
        EndIf
      EndIf
      
      If Not Error And Chunk\dwType = #MIDI_Chunk_Track
        Debug "TrackChunk"
        nTracks + 1
        
        Time = 0
        MetaEvent = 0
        
        While Not (Event = $FF And MetaEvent = $2F) ;While Not EndOfTrack
          
          DeltaTime = ReadVLV(0)
          Time + DeltaTime
          
          If DeltaTime = #MIDI_VLV_Error
            Error = #True
            Break
          EndIf
          
          Old   = Event
          Event = ReadByte(0) & $FF
          
          If Event < $80
            FileSeek(0, Loc(0)-1)
            Event = Old
          EndIf
          
          Select Event
            Case $80 To $8F
              Channel  = Event & $F
              Note     = ReadByte(0) & $7F
              Velocity = ReadByte(0) & $7F
              s$ = Str(Time) + " -> Channel : " + Str(Channel)
              s$ + " Note Off : " + Str(Note) + " Velocity : " + Str(Velocity)
              Debug s$
              
            Case $90 To $9F
              Channel  = Event & $F
              Note     = ReadByte(0) & $7F
              Velocity = ReadByte(0) & $7F
              s$ = Str(Time) + " -> Channel : " + Str(Channel)
              s$ + " Note On : " + Str(Note) + " Velocity : " + Str(Velocity)
              Debug s$
              
            Case $A0 To $BF, $E0 To $EF, $F2
              ReadWord(0)
              
            Case $C0 To $DF, $F1, $F3
              ReadByte(0)
              
            Case $FF
              MetaEvent = ReadByte(0) & $FF
              Size = ReadVLV(0)
              
              If Size = #MIDI_VLV_Error
                Error = #True
                Break
              EndIf
              
              If MetaEvent = $51 ;Set tempo
                
                If Size <> 3
                  Error = #True
                  Break
                EndIf
                
                Tempo = 0
                
                While Size
                  Tempo << 8
                  Tempo | (ReadByte(0) & $FF)
                  Size - 1
                Wend
                
                Debug Str(Time) + " -> Set Tempo (BPM) : " + StrD(60 * 1000 * 1000 / Tempo)
              Else
                
                FileSeek(0, Loc(0) + Size)
                
              EndIf
              
          EndSelect
          
        Wend ;While Not EndOfTrack
        
      EndIf
      
    Wend ;While Not (Error Or Eof(0))
  EndIf
  
  If Error
    Debug "An error has occured"
  Else
    Debug "Finished (" + Str(nTracks) + " tracks found)"
  EndIf
  
  CloseFile(0)
EndIf
For now i only pass through the events (and meta-events) i don't debug but i made it like Zapman so it is simple to add what's missing...

Dri :)
Dr. Dri
Enthusiast
Enthusiast
Posts: 243
Joined: Sat Aug 23, 2003 6:45 pm

Post by Dr. Dri »

Here is a true decoder, it doesn't skip any event and read them all. But its task is only to send them to the user and report errors... I've made a small example which debugs the same output as the previous code, using the decoder.

I wanted to make it a userlib but some "BigEndian" fonctions aren't correctly managed by tailbite
http://www.purebasic.fr/english/viewtop ... 548#159548

The lib

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

;> ---------------------------------- <
;- 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
;> ---------------- <
and the example file (which will only work with includefile for the moment...)

Code: Select all

IncludeFile "MidiParser.pbi"
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
Dri :D
User avatar
dobro
Enthusiast
Enthusiast
Posts: 766
Joined: Sun Oct 31, 2004 10:54 am
Location: France
Contact:

Re: Midi File decoder

Post by dobro »

[PB 4.51]

why this code generates an error (code of DRi)

I corrected "Protected VarLen.Bytes * 'to' * Protected VarLen.Byte"
but after there is a strange error! :shock:
Image
Windows 98/7/10 - PB 5.42
■ sites : http://michel.dobro.free.fr/
User avatar
graph100
Enthusiast
Enthusiast
Posts: 115
Joined: Tue Aug 10, 2010 3:17 pm

Re: Midi File decoder

Post by graph100 »

use this :

Code: Select all

Structure Bytes
	b.b[4]
EndStructure
je pense que ça devrais être la structure utilisée. Cependant, on ne peux pas lancer le code, car il doit manquer un include, ou des userlib
(I think that would be the structure used. But i cannot launch the code, because there must be another include, or userlib missing)
_________________________________________________
My Website : CeriseCode (Warning : perpetual changes & not completed ;))
infratec
Always Here
Always Here
Posts: 7586
Joined: Sun Sep 07, 2008 12:45 pm
Location: Germany

Re: Midi File decoder

Post by infratec »

Hi,

try this:

Code: Select all

Macro LoNibble(x)
  (x & $0F)
EndMacro

Macro HiNibble(x)
  (x >> 4)
EndMacro

Structure ByteArray
   b.b[0]
EndStructure
and use:

Code: Select all

Protected *VarLen.ByteArray
Bernd

[Edit]
Oh, to late... :cry:
User avatar
graph100
Enthusiast
Enthusiast
Posts: 115
Joined: Tue Aug 10, 2010 3:17 pm

Re: Midi File decoder

Post by graph100 »

Not so too late ^^, I haven't understand the LoNibble() / HiNibble() part. (haven't done the research job ^^)
And for your structure, it should be : b.b[4], if not it will not compile (I think)
_________________________________________________
My Website : CeriseCode (Warning : perpetual changes & not completed ;))
User avatar
dobro
Enthusiast
Enthusiast
Posts: 766
Joined: Sun Oct 31, 2004 10:54 am
Location: France
Contact:

Re: Midi File decoder

Post by dobro »

ok ! Thanks :)

Corrected version
(works on PB V4.51)

Librairie ( "lib_midi.pb" )

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

prg exemple for use

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 
thanks infratec and Graph100 :)
Image
Windows 98/7/10 - PB 5.42
■ sites : http://michel.dobro.free.fr/
SeregaZ
Enthusiast
Enthusiast
Posts: 628
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Midi File decoder

Post by SeregaZ »

how to get instrument name for tracks? or just number.
SeregaZ
Enthusiast
Enthusiast
Posts: 628
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Midi File decoder

Post by SeregaZ »

how to add this:

Code: Select all

ElseIf TEvent >=$C0 And TEvent <=$CF
            MProgramNumber = ReadByte() : If MProgramNumber <0 : MProgramNumber + 256 : EndIf
            MidiEvent$ ="Program Change, channel "+Str(TEvent&$F)+" - ProgramNumber : "+Str(MProgramNumber)+" : "+MidiInstrument$(MProgramNumber)
from zapman* to this dobro's library?

Code: Select all

Select Event
                                    Case $80 To $EF
                                          KeepParsing = ParseMidiCommand(File, Time, Event)
i cant undestand :(
SeregaZ
Enthusiast
Enthusiast
Posts: 628
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Midi File decoder

Post by SeregaZ »

done :)

Code: Select all

Procedure ShowMidiCommand(Time, Event, channel, Param1, Param2)
      
      Select Event
          
        Case 12
          Debug "channel " + Str(channel) + "; instrument " + Str(Param1)
          
          
        Case $8
          ;Debug Str(Time) + " -> Chan
SeregaZ
Enthusiast
Enthusiast
Posts: 628
Joined: Fri Feb 20, 2009 9:24 am
Location: Almaty (Kazakhstan. not Borat, but Triple G)
Contact:

Re: Midi File decoder

Post by SeregaZ »

how to run this lib_midi.pb with unicode?
Post Reply