christopher@45
|
1 """
|
christopher@45
|
2 midi.py -- MIDI classes and parser in Python
|
christopher@45
|
3 Placed into the public domain in December 2001 by Will Ware
|
christopher@45
|
4 Python MIDI classes: meaningful data structures that represent MIDI
|
christopher@45
|
5 events and other objects. You can read MIDI files to create such objects, or
|
christopher@45
|
6 generate a collection of objects and use them to write a MIDI file.
|
christopher@45
|
7 Helpful MIDI info:
|
christopher@45
|
8 http://crystal.apana.org.au/ghansper/midi_introduction/midi_file_form...
|
christopher@45
|
9 http://www.argonet.co.uk/users/lenny/midi/mfile.html
|
christopher@45
|
10 """
|
christopher@45
|
11 import sys, string, types, exceptions
|
christopher@45
|
12 debugflag = 0
|
christopher@45
|
13
|
christopher@45
|
14
|
christopher@45
|
15 def showstr(str, n=16):
|
christopher@45
|
16 for x in str[:n]:
|
christopher@45
|
17 print ('%02x' % ord(x)),
|
christopher@45
|
18 print
|
christopher@45
|
19
|
christopher@45
|
20 def getNumber(str, length):
|
christopher@45
|
21 # MIDI uses big-endian for everything
|
christopher@45
|
22 sum = 0
|
christopher@45
|
23 for i in range(length):
|
christopher@45
|
24 sum = (sum << 8) + ord(str[i])
|
christopher@45
|
25 return sum, str[length:]
|
christopher@45
|
26
|
christopher@45
|
27 def getVariableLengthNumber(str):
|
christopher@45
|
28 sum = 0
|
christopher@45
|
29 i = 0
|
christopher@45
|
30 while 1:
|
christopher@45
|
31 x = ord(str[i])
|
christopher@45
|
32 i = i + 1
|
christopher@45
|
33 sum = (sum << 7) + (x & 0x7F)
|
christopher@45
|
34 if not (x & 0x80):
|
christopher@45
|
35 return sum, str[i:]
|
christopher@45
|
36
|
christopher@45
|
37 def putNumber(num, length):
|
christopher@45
|
38 # MIDI uses big-endian for everything
|
christopher@45
|
39 lst = [ ]
|
christopher@45
|
40 for i in range(length):
|
christopher@45
|
41 n = 8 * (length - 1 - i)
|
christopher@45
|
42 lst.append(chr((num >> n) & 0xFF))
|
christopher@45
|
43 return string.join(lst, "")
|
christopher@45
|
44
|
christopher@45
|
45 def putVariableLengthNumber(x):
|
christopher@45
|
46 lst = [ ]
|
christopher@45
|
47 while 1:
|
christopher@45
|
48 y, x = x & 0x7F, x >> 7
|
christopher@45
|
49 lst.append(chr(y + 0x80))
|
christopher@45
|
50 if x == 0:
|
christopher@45
|
51 break
|
christopher@45
|
52 lst.reverse()
|
christopher@45
|
53 lst[-1] = chr(ord(lst[-1]) & 0x7f)
|
christopher@45
|
54 return string.join(lst, "")
|
christopher@45
|
55
|
christopher@45
|
56
|
christopher@45
|
57 class EnumException(exceptions.Exception):
|
christopher@45
|
58 pass
|
christopher@45
|
59
|
christopher@45
|
60 class Enumeration:
|
christopher@45
|
61 def __init__(self, enumList):
|
christopher@45
|
62 lookup = { }
|
christopher@45
|
63 reverseLookup = { }
|
christopher@45
|
64 i = 0
|
christopher@45
|
65 uniqueNames = [ ]
|
christopher@45
|
66 uniqueValues = [ ]
|
christopher@45
|
67 for x in enumList:
|
christopher@45
|
68 if type(x) == types.TupleType:
|
christopher@45
|
69 x, i = x
|
christopher@45
|
70 if type(x) != types.StringType:
|
christopher@45
|
71 raise EnumException, "enum name is not a string: " + x
|
christopher@45
|
72 if type(i) != types.IntType:
|
christopher@45
|
73 raise EnumException, "enum value is not an integer: " + i
|
christopher@45
|
74 if x in uniqueNames:
|
christopher@45
|
75 raise EnumException, "enum name is not unique: " + x
|
christopher@45
|
76 if i in uniqueValues:
|
christopher@45
|
77 raise EnumException, "enum value is not unique for " + x
|
christopher@45
|
78 uniqueNames.append(x)
|
christopher@45
|
79 uniqueValues.append(i)
|
christopher@45
|
80 lookup[x] = i
|
christopher@45
|
81 reverseLookup[i] = x
|
christopher@45
|
82 i = i + 1
|
christopher@45
|
83 self.lookup = lookup
|
christopher@45
|
84 self.reverseLookup = reverseLookup
|
christopher@45
|
85 def __add__(self, other):
|
christopher@45
|
86 lst = [ ]
|
christopher@45
|
87 for k in self.lookup.keys():
|
christopher@45
|
88 lst.append((k, self.lookup[k]))
|
christopher@45
|
89 for k in other.lookup.keys():
|
christopher@45
|
90 lst.append((k, other.lookup[k]))
|
christopher@45
|
91 return Enumeration(lst)
|
christopher@45
|
92 def hasattr(self, attr):
|
christopher@45
|
93 return self.lookup.has_key(attr)
|
christopher@45
|
94 def has_value(self, attr):
|
christopher@45
|
95 return self.reverseLookup.has_key(attr)
|
christopher@45
|
96 def __getattr__(self, attr):
|
christopher@45
|
97 if not self.lookup.has_key(attr):
|
christopher@45
|
98 raise AttributeError
|
christopher@45
|
99 return self.lookup[attr]
|
christopher@45
|
100 def whatis(self, value):
|
christopher@45
|
101 return self.reverseLookup[value]
|
christopher@45
|
102
|
christopher@45
|
103
|
christopher@45
|
104 channelVoiceMessages = Enumeration([("NOTE_OFF", 0x80),
|
christopher@45
|
105 ("NOTE_ON", 0x90),
|
christopher@45
|
106 ("POLYPHONIC_KEY_PRESSURE", 0xA0),
|
christopher@45
|
107 ("CONTROLLER_CHANGE", 0xB0),
|
christopher@45
|
108 ("PROGRAM_CHANGE", 0xC0),
|
christopher@45
|
109 ("CHANNEL_KEY_PRESSURE", 0xD0),
|
christopher@45
|
110 ("PITCH_BEND", 0xE0)])
|
christopher@45
|
111
|
christopher@45
|
112 channelModeMessages = Enumeration([("ALL_SOUND_OFF", 0x78),
|
christopher@45
|
113 ("RESET_ALL_CONTROLLERS", 0x79),
|
christopher@45
|
114 ("LOCAL_CONTROL", 0x7A),
|
christopher@45
|
115 ("ALL_NOTES_OFF", 0x7B),
|
christopher@45
|
116 ("OMNI_MODE_OFF", 0x7C),
|
christopher@45
|
117 ("OMNI_MODE_ON", 0x7D),
|
christopher@45
|
118 ("MONO_MODE_ON", 0x7E),
|
christopher@45
|
119 ("POLY_MODE_ON", 0x7F)])
|
christopher@45
|
120 metaEvents = Enumeration([("SEQUENCE_NUMBER", 0x00),
|
christopher@45
|
121 ("TEXT_EVENT", 0x01),
|
christopher@45
|
122 ("COPYRIGHT_NOTICE", 0x02),
|
christopher@45
|
123 ("SEQUENCE_TRACK_NAME", 0x03),
|
christopher@45
|
124 ("INSTRUMENT_NAME", 0x04),
|
christopher@45
|
125 ("LYRIC", 0x05),
|
christopher@45
|
126 ("MARKER", 0x06),
|
christopher@45
|
127 ("CUE_POINT", 0x07),
|
christopher@45
|
128 ("MIDI_CHANNEL_PREFIX", 0x20),
|
christopher@45
|
129 ("MIDI_PORT", 0x21),
|
christopher@45
|
130 ("END_OF_TRACK", 0x2F),
|
christopher@45
|
131 ("SET_TEMPO", 0x51),
|
christopher@45
|
132 ("SMTPE_OFFSET", 0x54),
|
christopher@45
|
133 ("TIME_SIGNATURE", 0x58),
|
christopher@45
|
134 ("KEY_SIGNATURE", 0x59),
|
christopher@45
|
135 ("SEQUENCER_SPECIFIC_META_EVENT", 0x7F)])
|
christopher@45
|
136
|
christopher@45
|
137
|
christopher@45
|
138 # runningStatus appears to want to be an attribute of a MidiTrack. But
|
christopher@45
|
139 # it doesn't seem to do any harm to implement it as a global.
|
christopher@45
|
140 runningStatus = None
|
christopher@45
|
141 class MidiEvent:
|
christopher@45
|
142 def __init__(self, track):
|
christopher@45
|
143 self.track = track
|
christopher@45
|
144 self.time = None
|
christopher@45
|
145 self.channel = self.pitch = self.velocity = self.data = None
|
christopher@45
|
146 def __cmp__(self, other):
|
christopher@45
|
147 # assert self.time != None and other.time != None
|
christopher@45
|
148 return cmp(self.time, other.time)
|
christopher@45
|
149 def __repr__(self):
|
christopher@45
|
150 r = ("<MidiEvent %s, t=%s, track=%s, channel=%s" %
|
christopher@45
|
151 (self.type,
|
christopher@45
|
152 repr(self.time),
|
christopher@45
|
153 self.track.index,
|
christopher@45
|
154 repr(self.channel)))
|
christopher@45
|
155 for attrib in ["pitch", "data", "velocity"]:
|
christopher@45
|
156 if getattr(self, attrib) != None:
|
christopher@45
|
157 r = r + ", " + attrib + "=" + repr(getattr(self, attrib))
|
christopher@45
|
158 return r + ">"
|
christopher@45
|
159 def read(self, time, str):
|
christopher@45
|
160 global runningStatus
|
christopher@45
|
161 self.time = time
|
christopher@45
|
162 # do we need to use running status?
|
christopher@45
|
163 if not (ord(str[0]) & 0x80):
|
christopher@45
|
164 str = runningStatus + str
|
christopher@45
|
165 runningStatus = x = str[0]
|
christopher@45
|
166 x = ord(x)
|
christopher@45
|
167 y = x & 0xF0
|
christopher@45
|
168 z = ord(str[1])
|
christopher@45
|
169 if channelVoiceMessages.has_value(y):
|
christopher@45
|
170 self.channel = (x & 0x0F) + 1
|
christopher@45
|
171 self.type = channelVoiceMessages.whatis(y)
|
christopher@45
|
172 if (self.type == "PROGRAM_CHANGE" or
|
christopher@45
|
173 self.type == "CHANNEL_KEY_PRESSURE"):
|
christopher@45
|
174 self.data = z
|
christopher@45
|
175 return str[2:]
|
christopher@45
|
176 else:
|
christopher@45
|
177 self.pitch = z
|
christopher@45
|
178 self.velocity = ord(str[2])
|
christopher@45
|
179 channel = self.track.channels[self.channel - 1]
|
christopher@45
|
180 if (self.type == "NOTE_OFF" or
|
christopher@45
|
181 (self.velocity == 0 and self.type == "NOTE_ON")):
|
christopher@45
|
182 channel.noteOff(self.pitch, self.time)
|
christopher@45
|
183 elif self.type == "NOTE_ON":
|
christopher@45
|
184 channel.noteOn(self.pitch, self.time, self.velocity)
|
christopher@45
|
185 return str[3:]
|
christopher@45
|
186 elif y == 0xB0 and channelModeMessages.has_value(z):
|
christopher@45
|
187 self.channel = (x & 0x0F) + 1
|
christopher@45
|
188 self.type = channelModeMessages.whatis(z)
|
christopher@45
|
189 if self.type == "LOCAL_CONTROL":
|
christopher@45
|
190 self.data = (ord(str[2]) == 0x7F)
|
christopher@45
|
191 elif self.type == "MONO_MODE_ON":
|
christopher@45
|
192 self.data = ord(str[2])
|
christopher@45
|
193 return str[3:]
|
christopher@45
|
194 elif x == 0xF0 or x == 0xF7:
|
christopher@45
|
195 self.type = {0xF0: "F0_SYSEX_EVENT",
|
christopher@45
|
196 0xF7: "F7_SYSEX_EVENT"}[x]
|
christopher@45
|
197 length, str = getVariableLengthNumber(str[1:])
|
christopher@45
|
198 self.data = str[:length]
|
christopher@45
|
199 return str[length:]
|
christopher@45
|
200 elif x == 0xFF:
|
christopher@45
|
201 if not metaEvents.has_value(z):
|
christopher@45
|
202 print "Unknown meta event: FF %02X" % z
|
christopher@45
|
203 sys.stdout.flush()
|
christopher@45
|
204 raise "Unknown midi event type"
|
christopher@45
|
205 self.type = metaEvents.whatis(z)
|
christopher@45
|
206 length, str = getVariableLengthNumber(str[2:])
|
christopher@45
|
207 self.data = str[:length]
|
christopher@45
|
208 return str[length:]
|
christopher@45
|
209 raise "Unknown midi event type"
|
christopher@45
|
210 def write(self):
|
christopher@45
|
211 sysex_event_dict = {"F0_SYSEX_EVENT": 0xF0,
|
christopher@45
|
212 "F7_SYSEX_EVENT": 0xF7}
|
christopher@45
|
213 if channelVoiceMessages.hasattr(self.type):
|
christopher@45
|
214 x = chr((self.channel - 1) +
|
christopher@45
|
215 getattr(channelVoiceMessages, self.type))
|
christopher@45
|
216 if (self.type != "PROGRAM_CHANGE" and
|
christopher@45
|
217 self.type != "CHANNEL_KEY_PRESSURE"):
|
christopher@45
|
218 data = chr(self.pitch) + chr(self.velocity)
|
christopher@45
|
219 else:
|
christopher@45
|
220 data = chr(self.data)
|
christopher@45
|
221 return x + data
|
christopher@45
|
222 elif channelModeMessages.hasattr(self.type):
|
christopher@45
|
223 x = getattr(channelModeMessages, self.type)
|
christopher@45
|
224 x = (chr(0xB0 + (self.channel - 1)) +
|
christopher@45
|
225 chr(x) +
|
christopher@45
|
226 chr(self.data))
|
christopher@45
|
227 return x
|
christopher@45
|
228 elif sysex_event_dict.has_key(self.type):
|
christopher@45
|
229 str = chr(sysex_event_dict[self.type])
|
christopher@45
|
230 str = str + putVariableLengthNumber(len(self.data))
|
christopher@45
|
231 return str + self.data
|
christopher@45
|
232 elif metaEvents.hasattr(self.type):
|
christopher@45
|
233 str = chr(0xFF) + chr(getattr(metaEvents, self.type))
|
christopher@45
|
234 str = str + putVariableLengthNumber(len(self.data))
|
christopher@45
|
235 return str + self.data
|
christopher@45
|
236 else:
|
christopher@45
|
237 raise "unknown midi event type: " + self.type
|
christopher@45
|
238
|
christopher@45
|
239
|
christopher@45
|
240
|
christopher@45
|
241 """
|
christopher@45
|
242 register_note() is a hook that can be overloaded from a script that
|
christopher@45
|
243 imports this module. Here is how you might do that, if you wanted to
|
christopher@45
|
244 store the notes as tuples in a list. Including the distinction
|
christopher@45
|
245 between track and channel offers more flexibility in assigning voices.
|
christopher@45
|
246 import midi
|
christopher@45
|
247 notelist = [ ]
|
christopher@45
|
248 def register_note(t, c, p, v, t1, t2):
|
christopher@45
|
249 notelist.append((t, c, p, v, t1, t2))
|
christopher@45
|
250 midi.register_note = register_note
|
christopher@45
|
251 """
|
christopher@45
|
252 def register_note(track_index, channel_index, pitch, velocity,
|
christopher@45
|
253 keyDownTime, keyUpTime):
|
christopher@45
|
254 pass
|
christopher@45
|
255
|
christopher@45
|
256
|
christopher@45
|
257
|
christopher@45
|
258 class MidiChannel:
|
christopher@45
|
259 """A channel (together with a track) provides the continuity connecting
|
christopher@45
|
260 a NOTE_ON event with its corresponding NOTE_OFF event. Together, those
|
christopher@45
|
261 define the beginning and ending times for a Note."""
|
christopher@45
|
262 def __init__(self, track, index):
|
christopher@45
|
263 self.index = index
|
christopher@45
|
264 self.track = track
|
christopher@45
|
265 self.pitches = { }
|
christopher@45
|
266 def __repr__(self):
|
christopher@45
|
267 return "<MIDI channel %d>" % self.index
|
christopher@45
|
268 def noteOn(self, pitch, time, velocity):
|
christopher@45
|
269 self.pitches[pitch] = (time, velocity)
|
christopher@45
|
270 def noteOff(self, pitch, time):
|
christopher@45
|
271 if self.pitches.has_key(pitch):
|
christopher@45
|
272 keyDownTime, velocity = self.pitches[pitch]
|
christopher@45
|
273 register_note(self.track.index, self.index, pitch, velocity,
|
christopher@45
|
274 keyDownTime, time)
|
christopher@45
|
275 del self.pitches[pitch]
|
christopher@45
|
276 # The case where the pitch isn't in the dictionary is illegal,
|
christopher@45
|
277 # I think, but we probably better just ignore it.
|
christopher@45
|
278
|
christopher@45
|
279
|
christopher@45
|
280 class DeltaTime(MidiEvent):
|
christopher@45
|
281 type = "DeltaTime"
|
christopher@45
|
282 def read(self, oldstr):
|
christopher@45
|
283 self.time, newstr = getVariableLengthNumber(oldstr)
|
christopher@45
|
284 return self.time, newstr
|
christopher@45
|
285 def write(self):
|
christopher@45
|
286 str = putVariableLengthNumber(self.time)
|
christopher@45
|
287 return str
|
christopher@45
|
288
|
christopher@45
|
289
|
christopher@45
|
290 class MidiTrack:
|
christopher@45
|
291 def __init__(self, index):
|
christopher@45
|
292 self.index = index
|
christopher@45
|
293 self.events = [ ]
|
christopher@45
|
294 self.channels = [ ]
|
christopher@45
|
295 self.length = 0
|
christopher@45
|
296 for i in range(16):
|
christopher@45
|
297 self.channels.append(MidiChannel(self, i+1))
|
christopher@45
|
298 def read(self, str):
|
christopher@45
|
299 time = 0
|
christopher@45
|
300 assert str[:4] == "MTrk"
|
christopher@45
|
301 length, str = getNumber(str[4:], 4)
|
christopher@45
|
302 self.length = length
|
christopher@45
|
303 mystr = str[:length]
|
christopher@45
|
304 remainder = str[length:]
|
christopher@45
|
305 while mystr:
|
christopher@45
|
306 delta_t = DeltaTime(self)
|
christopher@45
|
307 dt, mystr = delta_t.read(mystr)
|
christopher@45
|
308 time = time + dt
|
christopher@45
|
309 self.events.append(delta_t)
|
christopher@45
|
310 e = MidiEvent(self)
|
christopher@45
|
311 mystr = e.read(time, mystr)
|
christopher@45
|
312 self.events.append(e)
|
christopher@45
|
313 return remainder
|
christopher@45
|
314 def write(self):
|
christopher@45
|
315 time = self.events[0].time
|
christopher@45
|
316 # build str using MidiEvents
|
christopher@45
|
317 str = ""
|
christopher@45
|
318 for e in self.events:
|
christopher@45
|
319 str = str + e.write()
|
christopher@45
|
320 return "MTrk" + putNumber(len(str), 4) + str
|
christopher@45
|
321 def __repr__(self):
|
christopher@45
|
322 r = "<MidiTrack %d -- %d events\n" % (self.index, len(self.events))
|
christopher@45
|
323 for e in self.events:
|
christopher@45
|
324 r = r + " " + `e` + "\n"
|
christopher@45
|
325 return r + " >"
|
christopher@45
|
326
|
christopher@45
|
327
|
christopher@45
|
328
|
christopher@45
|
329 class MidiFile:
|
christopher@45
|
330 def __init__(self):
|
christopher@45
|
331 self.file = None
|
christopher@45
|
332 self.format = 1
|
christopher@45
|
333 self.tracks = [ ]
|
christopher@45
|
334 self.ticksPerQuarterNote = None
|
christopher@45
|
335 self.ticksPerSecond = None
|
christopher@45
|
336 def open(self, filename, attrib="rb"):
|
christopher@45
|
337 if filename == None:
|
christopher@45
|
338 if attrib in ["r", "rb"]:
|
christopher@45
|
339 self.file = sys.stdin
|
christopher@45
|
340 else:
|
christopher@45
|
341 self.file = sys.stdout
|
christopher@45
|
342 else:
|
christopher@45
|
343 self.file = open(filename, attrib)
|
christopher@45
|
344 def __repr__(self):
|
christopher@45
|
345 r = "<MidiFile %d tracks\n" % len(self.tracks)
|
christopher@45
|
346 for t in self.tracks:
|
christopher@45
|
347 r = r + " " + `t` + "\n"
|
christopher@45
|
348 return r + ">"
|
christopher@45
|
349 def close(self):
|
christopher@45
|
350 self.file.close()
|
christopher@45
|
351 def read(self):
|
christopher@45
|
352 self.readstr(self.file.read())
|
christopher@45
|
353 def readstr(self, str):
|
christopher@45
|
354 assert str[:4] == "MThd"
|
christopher@45
|
355 length, str = getNumber(str[4:], 4)
|
christopher@45
|
356 assert length == 6
|
christopher@45
|
357 format, str = getNumber(str, 2)
|
christopher@45
|
358 self.format = format
|
christopher@45
|
359 assert format == 0 or format == 1 # dunno how to handle 2
|
christopher@45
|
360 numTracks, str = getNumber(str, 2)
|
christopher@45
|
361 division, str = getNumber(str, 2)
|
christopher@45
|
362 if division & 0x8000:
|
christopher@45
|
363 framesPerSecond = -((division >> 8) | -128)
|
christopher@45
|
364 ticksPerFrame = division & 0xFF
|
christopher@45
|
365 assert ticksPerFrame == 24 or ticksPerFrame == 25 or \
|
christopher@45
|
366 ticksPerFrame == 29 or ticksPerFrame == 30
|
christopher@45
|
367 if ticksPerFrame == 29: ticksPerFrame = 30 # drop frame
|
christopher@45
|
368 self.ticksPerSecond = ticksPerFrame * framesPerSecond
|
christopher@45
|
369 else:
|
christopher@45
|
370 self.ticksPerQuarterNote = division & 0x7FFF
|
christopher@45
|
371 for i in range(numTracks):
|
christopher@45
|
372 trk = MidiTrack(i)
|
christopher@45
|
373 str = trk.read(str)
|
christopher@45
|
374 self.tracks.append(trk)
|
christopher@45
|
375 def write(self):
|
christopher@45
|
376 self.file.write(self.writestr())
|
christopher@45
|
377 def writestr(self):
|
christopher@45
|
378 division = self.ticksPerQuarterNote
|
christopher@45
|
379 # Don't handle ticksPerSecond yet, too confusing
|
christopher@45
|
380 assert (division & 0x8000) == 0
|
christopher@45
|
381 str = "MThd" + putNumber(6, 4) + putNumber(self.format, 2)
|
christopher@45
|
382 str = str + putNumber(len(self.tracks), 2)
|
christopher@45
|
383 str = str + putNumber(division, 2)
|
christopher@45
|
384 for trk in self.tracks:
|
christopher@45
|
385 str = str + trk.write()
|
christopher@45
|
386 return str
|
christopher@45
|
387
|
christopher@45
|
388
|
christopher@45
|
389 def main(argv):
|
christopher@45
|
390 global debugflag
|
christopher@45
|
391 import getopt
|
christopher@45
|
392 infile = None
|
christopher@45
|
393 outfile = None
|
christopher@45
|
394 printflag = 0
|
christopher@45
|
395 optlist, args = getopt.getopt(argv[1:], "i:o:pd")
|
christopher@45
|
396 for (option, value) in optlist:
|
christopher@45
|
397 if option == '-i':
|
christopher@45
|
398 infile = value
|
christopher@45
|
399 elif option == '-o':
|
christopher@45
|
400 outfile = value
|
christopher@45
|
401 elif option == '-p':
|
christopher@45
|
402 printflag = 1
|
christopher@45
|
403 elif option == '-d':
|
christopher@45
|
404 debugflag = 1
|
christopher@45
|
405 m = MidiFile()
|
christopher@45
|
406 m.open(infile)
|
christopher@45
|
407 m.read()
|
christopher@45
|
408 m.close()
|
christopher@45
|
409 if printflag:
|
christopher@45
|
410 print m
|
christopher@45
|
411 else:
|
christopher@45
|
412 m.open(outfile, "wb")
|
christopher@45
|
413 m.write()
|
christopher@45
|
414 m.close()
|
christopher@45
|
415
|
christopher@45
|
416
|
christopher@45
|
417 if __name__ == "__main__":
|
christopher@45
|
418 main(sys.argv)
|
christopher@45
|
419
|
christopher@45
|
420
|