christopher@6: """ christopher@6: midi.py -- MIDI classes and parser in Python christopher@6: Placed into the public domain in December 2001 by Will Ware christopher@6: Python MIDI classes: meaningful data structures that represent MIDI christopher@6: events and other objects. You can read MIDI files to create such objects, or christopher@6: generate a collection of objects and use them to write a MIDI file. christopher@6: Helpful MIDI info: christopher@6: http://crystal.apana.org.au/ghansper/midi_introduction/midi_file_form... christopher@6: http://www.argonet.co.uk/users/lenny/midi/mfile.html christopher@6: """ christopher@6: import sys, string, types, exceptions christopher@6: debugflag = 0 christopher@6: christopher@6: christopher@6: def showstr(str, n=16): christopher@6: for x in str[:n]: christopher@6: print ('%02x' % ord(x)), christopher@6: print christopher@6: christopher@6: def getNumber(str, length): christopher@6: # MIDI uses big-endian for everything christopher@6: sum = 0 christopher@6: for i in range(length): christopher@6: sum = (sum << 8) + ord(str[i]) christopher@6: return sum, str[length:] christopher@6: christopher@6: def getVariableLengthNumber(str): christopher@6: sum = 0 christopher@6: i = 0 christopher@6: while 1: christopher@6: x = ord(str[i]) christopher@6: i = i + 1 christopher@6: sum = (sum << 7) + (x & 0x7F) christopher@6: if not (x & 0x80): christopher@6: return sum, str[i:] christopher@6: christopher@6: def putNumber(num, length): christopher@6: # MIDI uses big-endian for everything christopher@6: lst = [ ] christopher@6: for i in range(length): christopher@6: n = 8 * (length - 1 - i) christopher@6: lst.append(chr((num >> n) & 0xFF)) christopher@6: return string.join(lst, "") christopher@6: christopher@6: def putVariableLengthNumber(x): christopher@6: lst = [ ] christopher@6: while 1: christopher@6: y, x = x & 0x7F, x >> 7 christopher@6: lst.append(chr(y + 0x80)) christopher@6: if x == 0: christopher@6: break christopher@6: lst.reverse() christopher@6: lst[-1] = chr(ord(lst[-1]) & 0x7f) christopher@6: return string.join(lst, "") christopher@6: christopher@6: christopher@6: class EnumException(exceptions.Exception): christopher@6: pass christopher@6: christopher@6: class Enumeration: christopher@6: def __init__(self, enumList): christopher@6: lookup = { } christopher@6: reverseLookup = { } christopher@6: i = 0 christopher@6: uniqueNames = [ ] christopher@6: uniqueValues = [ ] christopher@6: for x in enumList: christopher@6: if type(x) == types.TupleType: christopher@6: x, i = x christopher@6: if type(x) != types.StringType: christopher@6: raise EnumException, "enum name is not a string: " + x christopher@6: if type(i) != types.IntType: christopher@6: raise EnumException, "enum value is not an integer: " + i christopher@6: if x in uniqueNames: christopher@6: raise EnumException, "enum name is not unique: " + x christopher@6: if i in uniqueValues: christopher@6: raise EnumException, "enum value is not unique for " + x christopher@6: uniqueNames.append(x) christopher@6: uniqueValues.append(i) christopher@6: lookup[x] = i christopher@6: reverseLookup[i] = x christopher@6: i = i + 1 christopher@6: self.lookup = lookup christopher@6: self.reverseLookup = reverseLookup christopher@6: def __add__(self, other): christopher@6: lst = [ ] christopher@6: for k in self.lookup.keys(): christopher@6: lst.append((k, self.lookup[k])) christopher@6: for k in other.lookup.keys(): christopher@6: lst.append((k, other.lookup[k])) christopher@6: return Enumeration(lst) christopher@6: def hasattr(self, attr): christopher@6: return self.lookup.has_key(attr) christopher@6: def has_value(self, attr): christopher@6: return self.reverseLookup.has_key(attr) christopher@6: def __getattr__(self, attr): christopher@6: if not self.lookup.has_key(attr): christopher@6: raise AttributeError christopher@6: return self.lookup[attr] christopher@6: def whatis(self, value): christopher@6: return self.reverseLookup[value] christopher@6: christopher@6: christopher@6: channelVoiceMessages = Enumeration([("NOTE_OFF", 0x80), christopher@6: ("NOTE_ON", 0x90), christopher@6: ("POLYPHONIC_KEY_PRESSURE", 0xA0), christopher@6: ("CONTROLLER_CHANGE", 0xB0), christopher@6: ("PROGRAM_CHANGE", 0xC0), christopher@6: ("CHANNEL_KEY_PRESSURE", 0xD0), christopher@6: ("PITCH_BEND", 0xE0)]) christopher@6: christopher@6: channelModeMessages = Enumeration([("ALL_SOUND_OFF", 0x78), christopher@6: ("RESET_ALL_CONTROLLERS", 0x79), christopher@6: ("LOCAL_CONTROL", 0x7A), christopher@6: ("ALL_NOTES_OFF", 0x7B), christopher@6: ("OMNI_MODE_OFF", 0x7C), christopher@6: ("OMNI_MODE_ON", 0x7D), christopher@6: ("MONO_MODE_ON", 0x7E), christopher@6: ("POLY_MODE_ON", 0x7F)]) christopher@6: metaEvents = Enumeration([("SEQUENCE_NUMBER", 0x00), christopher@6: ("TEXT_EVENT", 0x01), christopher@6: ("COPYRIGHT_NOTICE", 0x02), christopher@6: ("SEQUENCE_TRACK_NAME", 0x03), christopher@6: ("INSTRUMENT_NAME", 0x04), christopher@6: ("LYRIC", 0x05), christopher@6: ("MARKER", 0x06), christopher@6: ("CUE_POINT", 0x07), christopher@6: ("MIDI_CHANNEL_PREFIX", 0x20), christopher@6: ("MIDI_PORT", 0x21), christopher@6: ("END_OF_TRACK", 0x2F), christopher@6: ("SET_TEMPO", 0x51), christopher@6: ("SMTPE_OFFSET", 0x54), christopher@6: ("TIME_SIGNATURE", 0x58), christopher@6: ("KEY_SIGNATURE", 0x59), christopher@6: ("SEQUENCER_SPECIFIC_META_EVENT", 0x7F)]) christopher@6: christopher@6: christopher@6: # runningStatus appears to want to be an attribute of a MidiTrack. But christopher@6: # it doesn't seem to do any harm to implement it as a global. christopher@6: runningStatus = None christopher@6: class MidiEvent: christopher@6: def __init__(self, track): christopher@6: self.track = track christopher@6: self.time = None christopher@6: self.channel = self.pitch = self.velocity = self.data = None christopher@6: def __cmp__(self, other): christopher@6: # assert self.time != None and other.time != None christopher@6: return cmp(self.time, other.time) christopher@6: def __repr__(self): christopher@6: r = ("" christopher@6: def read(self, time, str): christopher@6: global runningStatus christopher@6: self.time = time christopher@6: # do we need to use running status? christopher@6: if not (ord(str[0]) & 0x80): christopher@6: str = runningStatus + str christopher@6: runningStatus = x = str[0] christopher@6: x = ord(x) christopher@6: y = x & 0xF0 christopher@6: z = ord(str[1]) christopher@6: if channelVoiceMessages.has_value(y): christopher@6: self.channel = (x & 0x0F) + 1 christopher@6: self.type = channelVoiceMessages.whatis(y) christopher@6: if (self.type == "PROGRAM_CHANGE" or christopher@6: self.type == "CHANNEL_KEY_PRESSURE"): christopher@6: self.data = z christopher@6: return str[2:] christopher@6: else: christopher@6: self.pitch = z christopher@6: self.velocity = ord(str[2]) christopher@6: channel = self.track.channels[self.channel - 1] christopher@6: if (self.type == "NOTE_OFF" or christopher@6: (self.velocity == 0 and self.type == "NOTE_ON")): christopher@6: channel.noteOff(self.pitch, self.time) christopher@6: elif self.type == "NOTE_ON": christopher@6: channel.noteOn(self.pitch, self.time, self.velocity) christopher@6: return str[3:] christopher@6: elif y == 0xB0 and channelModeMessages.has_value(z): christopher@6: self.channel = (x & 0x0F) + 1 christopher@6: self.type = channelModeMessages.whatis(z) christopher@6: if self.type == "LOCAL_CONTROL": christopher@6: self.data = (ord(str[2]) == 0x7F) christopher@6: elif self.type == "MONO_MODE_ON": christopher@6: self.data = ord(str[2]) christopher@6: return str[3:] christopher@6: elif x == 0xF0 or x == 0xF7: christopher@6: self.type = {0xF0: "F0_SYSEX_EVENT", christopher@6: 0xF7: "F7_SYSEX_EVENT"}[x] christopher@6: length, str = getVariableLengthNumber(str[1:]) christopher@6: self.data = str[:length] christopher@6: return str[length:] christopher@6: elif x == 0xFF: christopher@6: if not metaEvents.has_value(z): christopher@6: print "Unknown meta event: FF %02X" % z christopher@6: sys.stdout.flush() christopher@6: raise "Unknown midi event type" christopher@6: self.type = metaEvents.whatis(z) christopher@6: length, str = getVariableLengthNumber(str[2:]) christopher@6: self.data = str[:length] christopher@6: return str[length:] christopher@6: raise "Unknown midi event type" christopher@6: def write(self): christopher@6: sysex_event_dict = {"F0_SYSEX_EVENT": 0xF0, christopher@6: "F7_SYSEX_EVENT": 0xF7} christopher@6: if channelVoiceMessages.hasattr(self.type): christopher@6: x = chr((self.channel - 1) + christopher@6: getattr(channelVoiceMessages, self.type)) christopher@6: if (self.type != "PROGRAM_CHANGE" and christopher@6: self.type != "CHANNEL_KEY_PRESSURE"): christopher@6: data = chr(self.pitch) + chr(self.velocity) christopher@6: else: christopher@6: data = chr(self.data) christopher@6: return x + data christopher@6: elif channelModeMessages.hasattr(self.type): christopher@6: x = getattr(channelModeMessages, self.type) christopher@6: x = (chr(0xB0 + (self.channel - 1)) + christopher@6: chr(x) + christopher@6: chr(self.data)) christopher@6: return x christopher@6: elif sysex_event_dict.has_key(self.type): christopher@6: str = chr(sysex_event_dict[self.type]) christopher@6: str = str + putVariableLengthNumber(len(self.data)) christopher@6: return str + self.data christopher@6: elif metaEvents.hasattr(self.type): christopher@6: str = chr(0xFF) + chr(getattr(metaEvents, self.type)) christopher@6: str = str + putVariableLengthNumber(len(self.data)) christopher@6: return str + self.data christopher@6: else: christopher@6: raise "unknown midi event type: " + self.type christopher@6: christopher@6: christopher@6: christopher@6: """ christopher@6: register_note() is a hook that can be overloaded from a script that christopher@6: imports this module. Here is how you might do that, if you wanted to christopher@6: store the notes as tuples in a list. Including the distinction christopher@6: between track and channel offers more flexibility in assigning voices. christopher@6: import midi christopher@6: notelist = [ ] christopher@6: def register_note(t, c, p, v, t1, t2): christopher@6: notelist.append((t, c, p, v, t1, t2)) christopher@6: midi.register_note = register_note christopher@6: """ christopher@6: def register_note(track_index, channel_index, pitch, velocity, christopher@6: keyDownTime, keyUpTime): christopher@6: pass christopher@6: christopher@6: christopher@6: christopher@6: class MidiChannel: christopher@6: """A channel (together with a track) provides the continuity connecting christopher@6: a NOTE_ON event with its corresponding NOTE_OFF event. Together, those christopher@6: define the beginning and ending times for a Note.""" christopher@6: def __init__(self, track, index): christopher@6: self.index = index christopher@6: self.track = track christopher@6: self.pitches = { } christopher@6: def __repr__(self): christopher@6: return "" % self.index christopher@6: def noteOn(self, pitch, time, velocity): christopher@6: self.pitches[pitch] = (time, velocity) christopher@6: def noteOff(self, pitch, time): christopher@6: if self.pitches.has_key(pitch): christopher@6: keyDownTime, velocity = self.pitches[pitch] christopher@6: register_note(self.track.index, self.index, pitch, velocity, christopher@6: keyDownTime, time) christopher@6: del self.pitches[pitch] christopher@6: # The case where the pitch isn't in the dictionary is illegal, christopher@6: # I think, but we probably better just ignore it. christopher@6: christopher@6: christopher@6: class DeltaTime(MidiEvent): christopher@6: type = "DeltaTime" christopher@6: def read(self, oldstr): christopher@6: self.time, newstr = getVariableLengthNumber(oldstr) christopher@6: return self.time, newstr christopher@6: def write(self): christopher@6: str = putVariableLengthNumber(self.time) christopher@6: return str christopher@6: christopher@6: christopher@6: class MidiTrack: christopher@6: def __init__(self, index): christopher@6: self.index = index christopher@6: self.events = [ ] christopher@6: self.channels = [ ] christopher@6: self.length = 0 christopher@6: for i in range(16): christopher@6: self.channels.append(MidiChannel(self, i+1)) christopher@6: def read(self, str): christopher@6: time = 0 christopher@6: assert str[:4] == "MTrk" christopher@6: length, str = getNumber(str[4:], 4) christopher@6: self.length = length christopher@6: mystr = str[:length] christopher@6: remainder = str[length:] christopher@6: while mystr: christopher@6: delta_t = DeltaTime(self) christopher@6: dt, mystr = delta_t.read(mystr) christopher@6: time = time + dt christopher@6: self.events.append(delta_t) christopher@6: e = MidiEvent(self) christopher@6: mystr = e.read(time, mystr) christopher@6: self.events.append(e) christopher@6: return remainder christopher@6: def write(self): christopher@6: time = self.events[0].time christopher@6: # build str using MidiEvents christopher@6: str = "" christopher@6: for e in self.events: christopher@6: str = str + e.write() christopher@6: return "MTrk" + putNumber(len(str), 4) + str christopher@6: def __repr__(self): christopher@6: r = "" christopher@6: christopher@6: christopher@6: christopher@6: class MidiFile: christopher@6: def __init__(self): christopher@6: self.file = None christopher@6: self.format = 1 christopher@6: self.tracks = [ ] christopher@6: self.ticksPerQuarterNote = None christopher@6: self.ticksPerSecond = None christopher@6: def open(self, filename, attrib="rb"): christopher@6: if filename == None: christopher@6: if attrib in ["r", "rb"]: christopher@6: self.file = sys.stdin christopher@6: else: christopher@6: self.file = sys.stdout christopher@6: else: christopher@6: self.file = open(filename, attrib) christopher@6: def __repr__(self): christopher@6: r = "" christopher@6: def close(self): christopher@6: self.file.close() christopher@6: def read(self): christopher@6: self.readstr(self.file.read()) christopher@6: def readstr(self, str): christopher@6: assert str[:4] == "MThd" christopher@6: length, str = getNumber(str[4:], 4) christopher@6: assert length == 6 christopher@6: format, str = getNumber(str, 2) christopher@6: self.format = format christopher@6: assert format == 0 or format == 1 # dunno how to handle 2 christopher@6: numTracks, str = getNumber(str, 2) christopher@6: division, str = getNumber(str, 2) christopher@6: if division & 0x8000: christopher@6: framesPerSecond = -((division >> 8) | -128) christopher@6: ticksPerFrame = division & 0xFF christopher@6: assert ticksPerFrame == 24 or ticksPerFrame == 25 or \ christopher@6: ticksPerFrame == 29 or ticksPerFrame == 30 christopher@6: if ticksPerFrame == 29: ticksPerFrame = 30 # drop frame christopher@6: self.ticksPerSecond = ticksPerFrame * framesPerSecond christopher@6: else: christopher@6: self.ticksPerQuarterNote = division & 0x7FFF christopher@6: for i in range(numTracks): christopher@6: trk = MidiTrack(i) christopher@6: str = trk.read(str) christopher@6: self.tracks.append(trk) christopher@6: def write(self): christopher@6: self.file.write(self.writestr()) christopher@6: def writestr(self): christopher@6: division = self.ticksPerQuarterNote christopher@6: # Don't handle ticksPerSecond yet, too confusing christopher@6: assert (division & 0x8000) == 0 christopher@6: str = "MThd" + putNumber(6, 4) + putNumber(self.format, 2) christopher@6: str = str + putNumber(len(self.tracks), 2) christopher@6: str = str + putNumber(division, 2) christopher@6: for trk in self.tracks: christopher@6: str = str + trk.write() christopher@6: return str christopher@6: christopher@6: christopher@6: def main(argv): christopher@6: global debugflag christopher@6: import getopt christopher@6: infile = None christopher@6: outfile = None christopher@6: printflag = 0 christopher@6: optlist, args = getopt.getopt(argv[1:], "i:o:pd") christopher@6: for (option, value) in optlist: christopher@6: if option == '-i': christopher@6: infile = value christopher@6: elif option == '-o': christopher@6: outfile = value christopher@6: elif option == '-p': christopher@6: printflag = 1 christopher@6: elif option == '-d': christopher@6: debugflag = 1 christopher@6: m = MidiFile() christopher@6: m.open(infile) christopher@6: m.read() christopher@6: m.close() christopher@6: if printflag: christopher@6: print m christopher@6: else: christopher@6: m.open(outfile, "wb") christopher@6: m.write() christopher@6: m.close() christopher@6: christopher@6: christopher@6: if __name__ == "__main__": christopher@6: main(sys.argv) christopher@6: christopher@6: