2b447bc2dad063788fefecbe2d835187610aaa79
[minwii.git] / src / mxmMidi / MidiFileParser.py
1 # -*- coding: ISO-8859-1 -*-
2
3 # std library
4 from struct import unpack
5
6 # uhh I don't really like this, but there are so many constants to
7 # import otherwise
8 from constants import *
9
10 from EventDispatcher import EventDispatcher
11
12 class 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
174 if __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