PageRenderTime 26ms CodeModel.GetById 9ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 1ms

/src/echonest/support/midi/MidiFileParser.py

http://echo-nest-remix.googlecode.com/
Python | 192 lines | 89 code | 48 blank | 55 comment | 9 complexity | dee64d7e6c0ecadbe5681b7d18f1125b MD5 | raw file
  1# -*- coding: ISO-8859-1 -*-

  2
  3# std library

  4from struct import unpack
  5
  6# uhh I don't really like this, but there are so many constants to 

  7# import otherwise

  8from constants import *
  9
 10from EventDispatcher import EventDispatcher
 11
 12class MidiFileParser:
 13
 14    """

 15    

 16    The MidiFileParser is the lowest level parser that see the data as 

 17    midi data. It generates events that gets triggered on the outstream.

 18    

 19    """
 20
 21    def __init__(self, raw_in, outstream):
 22
 23        """

 24        raw_data is the raw content of a midi file as a string.

 25        """
 26
 27        # internal values, don't mess with 'em directly

 28        self.raw_in = raw_in
 29        self.dispatch = EventDispatcher(outstream)
 30
 31        # Used to keep track of stuff

 32        self._running_status = None
 33
 34
 35
 36
 37    def parseMThdChunk(self):
 38        
 39        "Parses the header chunk"
 40        
 41        raw_in = self.raw_in
 42
 43        header_chunk_type = raw_in.nextSlice(4)
 44        header_chunk_zise = raw_in.readBew(4)
 45
 46        # check if it is a proper midi file

 47        if header_chunk_type != 'MThd':
 48            raise TypeError, "It is not a valid midi file!"
 49
 50        # Header values are at fixed locations, so no reason to be clever

 51        self.format = raw_in.readBew(2)
 52        self.nTracks = raw_in.readBew(2)
 53        self.division = raw_in.readBew(2)
 54        
 55        # Theoretically a header larger than 6 bytes can exist

 56        # but no one has seen one in the wild

 57        # But correctly ignore unknown data if it is though

 58        if header_chunk_zise > 6:
 59            raw_in.moveCursor(header_chunk_zise-6)
 60
 61        # call the header event handler on the stream

 62        self.dispatch.header(self.format, self.nTracks, self.division)
 63
 64
 65
 66    def parseMTrkChunk(self):
 67        
 68        "Parses a track chunk. This is the most important part of the parser."
 69        
 70        # set time to 0 at start of a track

 71        self.dispatch.reset_time()
 72        
 73        dispatch = self.dispatch
 74        raw_in = self.raw_in
 75        
 76        # Trigger event at the start of a track

 77        dispatch.start_of_track(self._current_track)
 78        # position cursor after track header

 79        raw_in.moveCursor(4)
 80        # unsigned long is 4 bytes

 81        tracklength = raw_in.readBew(4)
 82        track_endposition = raw_in.getCursor() + tracklength # absolute position!

 83
 84        while raw_in.getCursor() < track_endposition:
 85        
 86            # find relative time of the event

 87            time = raw_in.readVarLen()
 88            dispatch.update_time(time)
 89            
 90            # be aware of running status!!!!

 91            peak_ahead = raw_in.readBew(move_cursor=0)
 92            if (peak_ahead & 0x80): 
 93                # the status byte has the high bit set, so it

 94                # was not running data but proper status byte

 95                status = self._running_status = raw_in.readBew()
 96            else:
 97                # use that darn running status

 98                status = self._running_status
 99                # could it be illegal data ?? Do we need to test for that?

100                # I need more example midi files to be shure.

101                
102                # Also, while I am almost certain that no realtime 

103                # messages will pop up in a midi file, I might need to 

104                # change my mind later.

105
106            # we need to look at nibbles here

107            hi_nible, lo_nible = status & 0xF0, status & 0x0F
108            
109            # match up with events

110
111            # Is it a meta_event ??

112            # these only exists in midi files, not in transmitted midi data

113            # In transmitted data META_EVENT (0xFF) is a system reset

114            if status == META_EVENT:
115                meta_type = raw_in.readBew()
116                meta_length = raw_in.readVarLen()
117                meta_data = raw_in.nextSlice(meta_length)
118                dispatch.meta_event(meta_type, meta_data)
119
120
121            # Is it a sysex_event ??

122            elif status == SYSTEM_EXCLUSIVE:
123                # ignore sysex events

124                sysex_length = raw_in.readVarLen()
125                # don't read sysex terminator

126                sysex_data = raw_in.nextSlice(sysex_length-1)
127                # only read last data byte if it is a sysex terminator

128                # It should allways be there, but better safe than sorry

129                if raw_in.readBew(move_cursor=0) == END_OFF_EXCLUSIVE:
130                    eo_sysex = raw_in.readBew()
131                dispatch.sysex_event(sysex_data)
132                # the sysex code has not been properly tested, and might be fishy!

133
134
135            # is it a system common event?

136            elif hi_nible == 0xF0: # Hi bits are set then

137                data_sizes = {
138                    MTC:1,
139                    SONG_POSITION_POINTER:2,
140                    SONG_SELECT:1,
141                }
142                data_size = data_sizes.get(hi_nible, 0)
143                common_data = raw_in.nextSlice(data_size)
144                common_type = lo_nible
145                dispatch.system_common(common_type, common_data)
146            
147
148            # Oh! Then it must be a midi event (channel voice message)

149            else:
150                data_sizes = {
151                    PATCH_CHANGE:1,
152                    CHANNEL_PRESSURE:1,
153                    NOTE_OFF:2,
154                    NOTE_ON:2,
155                    AFTERTOUCH:2,
156                    CONTINUOUS_CONTROLLER:2,
157                    PITCH_BEND:2,
158                }
159                data_size = data_sizes.get(hi_nible, 0)
160                channel_data = raw_in.nextSlice(data_size)
161                event_type, channel = hi_nible, lo_nible
162                dispatch.channel_messages(event_type, channel, channel_data)
163
164
165    def parseMTrkChunks(self):
166        "Parses all track chunks."
167        for t in range(self.nTracks):
168            self._current_track = t
169            self.parseMTrkChunk() # this is where it's at!

170        self.dispatch.eof()
171
172
173
174if __name__ == '__main__':
175
176    # get data

177    test_file = 'test/midifiles/minimal.mid'
178    test_file = 'test/midifiles/cubase-minimal.mid'
179    test_file = 'test/midifiles/Lola.mid'
180#    f = open(test_file, 'rb')

181#    raw_data = f.read()

182#    f.close()

183#    

184#    

185#    # do parsing

186    from MidiToText import MidiToText
187    from RawInstreamFile import RawInstreamFile
188
189    midi_in = MidiFileParser(RawInstreamFile(test_file), MidiToText())
190    midi_in.parseMThdChunk()
191    midi_in.parseMTrkChunks()
192