comparison Syncopation models/midiparser.py @ 6:ac882f5e6a11

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