Mercurial > hg > syncopation-dataset
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 |