annotate synpy/midiparser.py @ 76:90b68f259541 tip

updated parameter_setter to be able to find the TimeSignature.pkl file without putting it in the pwd
author christopherh <christopher.harte@eecs.qmul.ac.uk>
date Wed, 13 May 2015 09:27:36 +0100
parents ef891481231e
children
rev   line source
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