Mercurial > hg > syncopation-dataset
comparison Syncopation models/synpy/midiparser.py @ 45:6e9154fc58df
moving the code files to the synpy package directory
| author | christopherh <christopher.harte@eecs.qmul.ac.uk> |
|---|---|
| date | Thu, 23 Apr 2015 23:52:04 +0100 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 44:144460f34b5e | 45:6e9154fc58df |
|---|---|
| 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 |
