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