Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Python classes for MIDI

3,339 views
Skip to first unread message

Will Ware

unread,
Dec 6, 2001, 1:10:26 AM12/6/01
to
#!/usr/bin/env python

"""
midi.py -- MIDI classes and parser in Python
Placed into the public domain in December 2001 by Will Ware

Python MIDI classes: meaningful data structures that represent MIDI
events
and other objects. You can read MIDI files to create such objects, or
generate a collection of objects and use them to write a MIDI file.

Helpful MIDI info:
http://crystal.apana.org.au/ghansper/midi_introduction/midi_file_format.html
http://www.argonet.co.uk/users/lenny/midi/mfile.html
"""

import sys, string, types, exceptions

debugflag = 0

def showstr(str, n=16):
for x in str[:n]:
print ('%02x' % ord(x)),
print

def getNumber(str, length):
# MIDI uses big-endian for everything
sum = 0
for i in range(length):
sum = (sum << 8) + ord(str[i])
return sum, str[length:]

def getVariableLengthNumber(str):
sum = 0
i = 0
while 1:
x = ord(str[i])
i = i + 1
sum = (sum << 7) + (x & 0x7F)
if not (x & 0x80):
return sum, str[i:]

def putNumber(num, length):
# MIDI uses big-endian for everything
lst = [ ]
for i in range(length):
n = 8 * (length - 1 - i)
lst.append(chr((num >> n) & 0xFF))
return string.join(lst, "")

def putVariableLengthNumber(x):
lst = [ ]
while 1:
y, x = x & 0x7F, x >> 7
lst.append(chr(y + 0x80))
if x == 0:
break
lst.reverse()
lst[-1] = chr(ord(lst[-1]) & 0x7f)
return string.join(lst, "")


class EnumException(exceptions.Exception):
pass

class Enumeration:
def __init__(self, enumList):
lookup = { }
reverseLookup = { }
i = 0
uniqueNames = [ ]
uniqueValues = [ ]
for x in enumList:
if type(x) == types.TupleType:
x, i = x
if type(x) != types.StringType:
raise EnumException, "enum name is not a string: " + x
if type(i) != types.IntType:
raise EnumException, "enum value is not an integer: " +
i
if x in uniqueNames:
raise EnumException, "enum name is not unique: " + x
if i in uniqueValues:
raise EnumException, "enum value is not unique for " + x
uniqueNames.append(x)
uniqueValues.append(i)
lookup[x] = i
reverseLookup[i] = x
i = i + 1
self.lookup = lookup
self.reverseLookup = reverseLookup
def __add__(self, other):
lst = [ ]
for k in self.lookup.keys():
lst.append((k, self.lookup[k]))
for k in other.lookup.keys():
lst.append((k, other.lookup[k]))
return Enumeration(lst)
def hasattr(self, attr):
return self.lookup.has_key(attr)
def has_value(self, attr):
return self.reverseLookup.has_key(attr)
def __getattr__(self, attr):
if not self.lookup.has_key(attr):
raise AttributeError
return self.lookup[attr]
def whatis(self, value):
return self.reverseLookup[value]


channelVoiceMessages = Enumeration([("NOTE_OFF", 0x80),
("NOTE_ON", 0x90),
("POLYPHONIC_KEY_PRESSURE", 0xA0),
("CONTROLLER_CHANGE", 0xB0),
("PROGRAM_CHANGE", 0xC0),
("CHANNEL_KEY_PRESSURE", 0xD0),
("PITCH_BEND", 0xE0)])

channelModeMessages = Enumeration([("ALL_SOUND_OFF", 0x78),
("RESET_ALL_CONTROLLERS", 0x79),
("LOCAL_CONTROL", 0x7A),
("ALL_NOTES_OFF", 0x7B),
("OMNI_MODE_OFF", 0x7C),
("OMNI_MODE_ON", 0x7D),
("MONO_MODE_ON", 0x7E),
("POLY_MODE_ON", 0x7F)])

metaEvents = Enumeration([("SEQUENCE_NUMBER", 0x00),
("TEXT_EVENT", 0x01),
("COPYRIGHT_NOTICE", 0x02),
("SEQUENCE_TRACK_NAME", 0x03),
("INSTRUMENT_NAME", 0x04),
("LYRIC", 0x05),
("MARKER", 0x06),
("CUE_POINT", 0x07),
("MIDI_CHANNEL_PREFIX", 0x20),
("MIDI_PORT", 0x21),
("END_OF_TRACK", 0x2F),
("SET_TEMPO", 0x51),
("SMTPE_OFFSET", 0x54),
("TIME_SIGNATURE", 0x58),
("KEY_SIGNATURE", 0x59),
("SEQUENCER_SPECIFIC_META_EVENT", 0x7F)])


# runningStatus appears to want to be an attribute of a MidiTrack. But
# it doesn't seem to do any harm to implement it as a global.
runningStatus = None

class MidiEvent:

def __init__(self, track):
self.track = track
self.time = None
self.channel = self.pitch = self.velocity = self.data = None

def __cmp__(self, other):
# assert self.time != None and other.time != None
return cmp(self.time, other.time)

def __repr__(self):
r = ("<MidiEvent %s, t=%s, track=%s, channel=%s" %
(self.type,
repr(self.time),
self.track.index,
repr(self.channel)))
for attrib in ["pitch", "data", "velocity"]:
if getattr(self, attrib) != None:
r = r + ", " + attrib + "=" + repr(getattr(self,
attrib))
return r + ">"

def read(self, time, str):
global runningStatus
self.time = time
# do we need to use running status?
if not (ord(str[0]) & 0x80):
str = runningStatus + str
runningStatus = x = str[0]
x = ord(x)
y = x & 0xF0
z = ord(str[1])

if channelVoiceMessages.has_value(y):
self.channel = (x & 0x0F) + 1
self.type = channelVoiceMessages.whatis(y)
if (self.type == "PROGRAM_CHANGE" or
self.type == "CHANNEL_KEY_PRESSURE"):
self.data = z
return str[2:]
else:
self.pitch = z
self.velocity = ord(str[2])
channel = self.track.channels[self.channel - 1]
if (self.type == "NOTE_OFF" or
(self.velocity == 0 and self.type == "NOTE_ON")):
channel.noteOff(self.pitch, self.time)
elif self.type == "NOTE_ON":
channel.noteOn(self.pitch, self.time, self.velocity)
return str[3:]

elif y == 0xB0 and channelModeMessages.has_value(z):
self.channel = (x & 0x0F) + 1
self.type = channelModeMessages.whatis(z)
if self.type == "LOCAL_CONTROL":
self.data = (ord(str[2]) == 0x7F)
elif self.type == "MONO_MODE_ON":
self.data = ord(str[2])
return str[3:]

elif x == 0xF0 or x == 0xF7:
self.type = {0xF0: "F0_SYSEX_EVENT",
0xF7: "F7_SYSEX_EVENT"}[x]
length, str = getVariableLengthNumber(str[1:])
self.data = str[:length]
return str[length:]

elif x == 0xFF:
if not metaEvents.has_value(z):
print "Unknown meta event: FF %02X" % z
sys.stdout.flush()
raise "Unknown midi event type"
self.type = metaEvents.whatis(z)
length, str = getVariableLengthNumber(str[2:])
self.data = str[:length]
return str[length:]

raise "Unknown midi event type"

def write(self):
sysex_event_dict = {"F0_SYSEX_EVENT": 0xF0,
"F7_SYSEX_EVENT": 0xF7}
if channelVoiceMessages.hasattr(self.type):
x = chr((self.channel - 1) +
getattr(channelVoiceMessages, self.type))
if (self.type != "PROGRAM_CHANGE" and
self.type != "CHANNEL_KEY_PRESSURE"):
data = chr(self.pitch) + chr(self.velocity)
else:
data = chr(self.data)
return x + data

elif channelModeMessages.hasattr(self.type):
x = getattr(channelModeMessages, self.type)
x = (chr(0xB0 + (self.channel - 1)) +
chr(x) +
chr(self.data))
return x

elif sysex_event_dict.has_key(self.type):
str = chr(sysex_event_dict[self.type])
str = str + putVariableLengthNumber(len(self.data))
return str + self.data

elif metaEvents.hasattr(self.type):
str = chr(0xFF) + chr(getattr(metaEvents, self.type))
str = str + putVariableLengthNumber(len(self.data))
return str + self.data

else:
raise "unknown midi event type: " + self.type

"""
register_note() is a hook that can be overloaded from a script that
imports this module. Here is how you might do that, if you wanted to
store the notes as tuples in a list. Including the distinction
between track and channel offers more flexibility in assigning voices.

import midi
notelist = [ ]
def register_note(t, c, p, v, t1, t2):
notelist.append((t, c, p, v, t1, t2))
midi.register_note = register_note
"""

def register_note(track_index, channel_index, pitch, velocity,
keyDownTime, keyUpTime):
pass


class MidiChannel:

"""A channel (together with a track) provides the continuity
connecting
a NOTE_ON event with its corresponding NOTE_OFF event. Together,
those
define the beginning and ending times for a Note."""

def __init__(self, track, index):
self.index = index
self.track = track
self.pitches = { }

def __repr__(self):
return "<MIDI channel %d>" % self.index

def noteOn(self, pitch, time, velocity):
self.pitches[pitch] = (time, velocity)

def noteOff(self, pitch, time):
if self.pitches.has_key(pitch):
keyDownTime, velocity = self.pitches[pitch]
register_note(self.track.index, self.index, pitch, velocity,
keyDownTime, time)
del self.pitches[pitch]
# The case where the pitch isn't in the dictionary is illegal,
# I think, but we probably better just ignore it.


class DeltaTime(MidiEvent):

type = "DeltaTime"

def read(self, oldstr):
self.time, newstr = getVariableLengthNumber(oldstr)
return self.time, newstr

def write(self):
str = putVariableLengthNumber(self.time)
return str

class MidiTrack:

def __init__(self, index):
self.index = index
self.events = [ ]
self.channels = [ ]
self.length = 0
for i in range(16):
self.channels.append(MidiChannel(self, i+1))

def read(self, str):
time = 0
assert str[:4] == "MTrk"
length, str = getNumber(str[4:], 4)
self.length = length
mystr = str[:length]
remainder = str[length:]
while mystr:
delta_t = DeltaTime(self)
dt, mystr = delta_t.read(mystr)
time = time + dt
self.events.append(delta_t)
e = MidiEvent(self)
mystr = e.read(time, mystr)
self.events.append(e)
return remainder

def write(self):
time = self.events[0].time
# build str using MidiEvents
str = ""
for e in self.events:
str = str + e.write()
return "MTrk" + putNumber(len(str), 4) + str

def __repr__(self):
r = "<MidiTrack %d -- %d events\n" % (self.index,
len(self.events))
for e in self.events:
r = r + " " + `e` + "\n"
return r + " >"


class MidiFile:

def __init__(self):
self.file = None
self.format = 1
self.tracks = [ ]
self.ticksPerQuarterNote = None
self.ticksPerSecond = None

def open(self, filename, attrib="rb"):
if filename == None:
if attrib in ["r", "rb"]:
self.file = sys.stdin
else:
self.file = sys.stdout
else:
self.file = open(filename, attrib)

def __repr__(self):
r = "<MidiFile %d tracks\n" % len(self.tracks)
for t in self.tracks:
r = r + " " + `t` + "\n"
return r + ">"

def close(self):
self.file.close()

def read(self):
self.readstr(self.file.read())

def readstr(self, str):
assert str[:4] == "MThd"
length, str = getNumber(str[4:], 4)
assert length == 6
format, str = getNumber(str, 2)
self.format = format
assert format == 0 or format == 1 # dunno how to handle 2
numTracks, str = getNumber(str, 2)
division, str = getNumber(str, 2)
if division & 0x8000:
framesPerSecond = -((division >> 8) | -128)
ticksPerFrame = division & 0xFF
assert ticksPerFrame == 24 or ticksPerFrame == 25 or \
ticksPerFrame == 29 or ticksPerFrame == 30
if ticksPerFrame == 29: ticksPerFrame = 30 # drop frame
self.ticksPerSecond = ticksPerFrame * framesPerSecond
else:
self.ticksPerQuarterNote = division & 0x7FFF
for i in range(numTracks):
trk = MidiTrack(i)
str = trk.read(str)
self.tracks.append(trk)

def write(self):
self.file.write(self.writestr())

def writestr(self):
division = self.ticksPerQuarterNote
# Don't handle ticksPerSecond yet, too confusing
assert (division & 0x8000) == 0
str = "MThd" + putNumber(6, 4) + putNumber(self.format, 2)
str = str + putNumber(len(self.tracks), 2)
str = str + putNumber(division, 2)
for trk in self.tracks:
str = str + trk.write()
return str


def main(argv):
global debugflag
import getopt
infile = None
outfile = None
printflag = 0
optlist, args = getopt.getopt(argv[1:], "i:o:pd")
for (option, value) in optlist:
if option == '-i':
infile = value
elif option == '-o':
outfile = value
elif option == '-p':
printflag = 1
elif option == '-d':
debugflag = 1

m = MidiFile()
m.open(infile)
m.read()
m.close()

if printflag:
print m
else:
m.open(outfile, "wb")
m.write()
m.close()

if __name__ == "__main__":
main(sys.argv)

Will Ware

unread,
Dec 6, 2001, 7:43:42 AM12/6/01
to

khalde...@gmail.com

unread,
Mar 9, 2015, 2:09:18 AM3/9/15
to
I have updated the midi.py for Python 3.2.1

I have tested reading and parsing .mid file, and it works fine, as it used to work with Python 2.x. Mostly, simple updates like replacing expressions like:

ord(str) by str

I have not tested building midi tracks and writing .mid files. Here is the updated code:


#!/usr/bin/env python

"""
midi.py -- MIDI classes and parser in Python
Placed into the public domain in December 2001 by Will Ware

Python MIDI classes: meaningful data structures that represent MIDI events
and other objects. You can read MIDI files to create such objects, or
generate a collection of objects and use them to write a MIDI file.

Helpful MIDI info:
http://crystal.apana.org.au/ghansper/midi_introduction/midi_file_form...
http://www.argonet.co.uk/users/lenny/midi/mfile.html

*** UPDATED FOR PYTHON 3.2.1 BY KADIR HALDENBILEN 07.03.2015
"""

import sys, string, types #, exceptions

debugflag = 0

def showstr(str, n=16):
for x in str[:n]:
print ('%02x' % x), #ord KH
print

#def getNumber(str, length):
# #MIDI uses big-endian for everything
# sum = 0
# for i in range(length):
# sum = (sum << 8) + ord(str[i])
# return sum, str[length:]

def getNumber(str, length):
sum = 0
for i in range(length):
sum = (sum << 8) + str[i]
return sum, str[length:]

def getVariableLengthNumber(str):
sum = 0
i = 0
while 1:
x = str[i]
i = i + 1
sum = (sum << 7) + (x & 0x7F)
if not (x & 0x80):
return sum, str[i:]

def putNumber(num, length):
# MIDI uses big-endian for everything
lst = [ ]
for i in range(length):
n = 8 * (length - 1 - i)
lst.append(chr((num >> n) & 0xFF))
return string.join(lst, "")

def putVariableLengthNumber(x):
lst = [ ]
while 1:
y, x = x & 0x7F, x >> 7
lst.append(chr(y + 0x80))
if x == 0:
break
lst.reverse()
lst[-1] = chr(ord(lst[-1]) & 0x7f)
return string.join(lst, "")

#class EnumException(exceptions.Exception):
class EnumException(Exception):
pass

class Enumeration:
def __init__(self, enumList):
lookup = { }
reverseLookup = { }
i = 0
uniqueNames = [ ]
uniqueValues = [ ]
for x in enumList:
#print ("x", x)
#print ("type x", type(x))
if type(x) == tuple:
x, i = x
if type(x) != str:
raise EnumException ("enum name is not a string: " + x)
if type(i) != int:
raise EnumException ("enum value is not an integer: " + i)
if x in uniqueNames:
raise EnumException ("enum name is not unique: " + x)
if i in uniqueValues:
raise EnumException ("enum value is not unique for " + x)
uniqueNames.append(x)
uniqueValues.append(i)
lookup[x] = i
reverseLookup[i] = x
i = i + 1
self.lookup = lookup
self.reverseLookup = reverseLookup
def __add__(self, other):
lst = [ ]
for k in self.lookup.keys():
lst.append((k, self.lookup[k]))
for k in other.lookup.keys():
lst.append((k, other.lookup[k]))
return Enumeration(lst)
def hasattr(self, attr):
return attr in self.lookup.keys()
def has_value(self, attr):
return attr in self.reverseLookup.keys()
def __getattr__(self, attr):
if not attr in self.lookup.keys():
#print ("RS", runningStatus, "str0", str[0])
#print ("len str", len(str), "type str", type(str))
#print ("str", str[:100])
# do we need to use running status?
if not (str[0] & 0x80):
#print ("RS inside", runningStatus, "str0 inside", str[0])
#print ("type RS inside", type(runningStatus), "type str0 inside", type(str[0]))
#str = runningStatus + str original
str = runningStatus.to_bytes(1, byteorder="big") + str # KH
runningStatus = x = str[0]
#x = ord(x)
y = x & 0xF0
z = str[1] # ord KH

if channelVoiceMessages.has_value(y):
self.channel = (x & 0x0F) + 1
self.type = channelVoiceMessages.whatis(y)
if (self.type == "PROGRAM_CHANGE" or
self.type == "CHANNEL_KEY_PRESSURE"):
self.data = z
return str[2:]
else:
self.pitch = z
self.velocity = str[2] # ord KH
channel = self.track.channels[self.channel - 1]
if (self.type == "NOTE_OFF" or
(self.velocity == 0 and self.type == "NOTE_ON")):
channel.noteOff(self.pitch, self.time)
elif self.type == "NOTE_ON":
channel.noteOn(self.pitch, self.time, self.velocity)
return str[3:]

elif y == 0xB0 and channelModeMessages.has_value(z):
self.channel = (x & 0x0F) + 1
self.type = channelModeMessages.whatis(z)
if self.type == "LOCAL_CONTROL":
self.data = (str[2] == 0x7F) #ord KH
elif self.type == "MONO_MODE_ON":
self.data = str[2] #ord KH
return str[3:]

elif x == 0xF0 or x == 0xF7:
self.type = {0xF0: "F0_SYSEX_EVENT",
0xF7: "F7_SYSEX_EVENT"}[x]
length, str = getVariableLengthNumber(str[1:])
self.data = str[:length]
return str[length:]

elif x == 0xFF:
if not metaEvents.has_value(z):
print ("Unknown meta event: FF %02X" % z)
sys.stdout.flush()
raise ("Unknown midi event type")
self.type = metaEvents.whatis(z)
length, str = getVariableLengthNumber(str[2:])
self.data = str[:length]
return str[length:]

raise ("Unknown midi event type")

def write(self):
sysex_event_dict = {"F0_SYSEX_EVENT": 0xF0,
"F7_SYSEX_EVENT": 0xF7}
if channelVoiceMessages.hasattr(self.type):
x = chr((self.channel - 1) +
getattr(channelVoiceMessages, self.type))
if (self.type != "PROGRAM_CHANGE" and
self.type != "CHANNEL_KEY_PRESSURE"):
data = chr(self.pitch) + chr(self.velocity)
else:
data = chr(self.data)
return x + data

elif channelModeMessages.hasattr(self.type):
x = getattr(channelModeMessages, self.type)
x = (chr(0xB0 + (self.channel - 1)) +
chr(x) +
chr(self.data))
return x

elif self.type in sysex_event_dict.keys():
str = chr(sysex_event_dict[self.type])
str = str + putVariableLengthNumber(len(self.data))
return str + self.data

elif metaEvents.hasattr(self.type):
str = chr(0xFF) + chr(getattr(metaEvents, self.type))
str = str + putVariableLengthNumber(len(self.data))
return str + self.data

else:
raise ("unknown midi event type: " + self.type)
if pitch in self.pitches.keys():
keyDownTime, velocity = self.pitches[pitch]
register_note(self.track.index, self.index, pitch, velocity,
keyDownTime, time)
del self.pitches[pitch]
# The case where the pitch isn't in the dictionary is illegal,
# I think, but we probably better just ignore it.

class DeltaTime(MidiEvent):

type = "DeltaTime"

def read(self, oldstr):
self.time, newstr = getVariableLengthNumber(oldstr)
return self.time, newstr

def write(self):
str = putVariableLengthNumber(self.time)
return str

class MidiTrack:

def __init__(self, index):
self.index = index
self.events = [ ]
self.channels = [ ]
self.length = 0
for i in range(16):
self.channels.append(MidiChannel(self, i+1))

def read(self, str):
time = 0
print ("trk04", str[:4])
assert str[:4] == b"MTrk"
length, str = getNumber(str[4:], 4)
self.length = length
mystr = str[:length]
remainder = str[length:]
while mystr:
delta_t = DeltaTime(self)
dt, mystr = delta_t.read(mystr)
time = time + dt
self.events.append(delta_t)
e = MidiEvent(self)
mystr = e.read(time, mystr)
self.events.append(e)
return remainder

def write(self):
time = self.events[0].time
# build str using MidiEvents
str = ""
for e in self.events:
str = str + e.write()
return "MTrk" + putNumber(len(str), 4) + str

def __repr__(self):
r = "<MidiTrack %d -- %d events\n" % (self.index, len(self.events))
for e in self.events:
r = r + " " + "<" + "\n"
return r + " >"


# Track0 Hook to read Track 0 data, similar to register_note function
# added by K.H.

def register_track0(tracks0_data):
pass
#print ("str", str[:100])
assert str[:4] == b"MThd"
length, str = getNumber(str[4:], 4)
assert length == 6
format, str = getNumber(str, 2)
print ("Format: ", format)
self.format = format
assert format == 0 or format == 1 # dunno how to handle 2
numTracks, str = getNumber(str, 2)
print ("Num Tracks: ", numTracks)
division, str = getNumber(str, 2)
if division & 0x8000:
framesPerSecond = -((division >> 8) | -128)
ticksPerFrame = division & 0xFF
print ("Frames Per Second ", framesPerSecond)
print ("Ticks Per Frame ", tickPerFrame)
assert ticksPerFrame == 24 or ticksPerFrame == 25 or \
ticksPerFrame == 29 or ticksPerFrame == 30
if ticksPerFrame == 29: ticksPerFrame = 30 # drop frame
self.ticksPerSecond = ticksPerFrame * framesPerSecond
else:
self.ticksPerQuarterNote = division & 0x7FFF
print ("self.ticksPerQuarterNote ", self.ticksPerQuarterNote)
for i in range(numTracks):
trk = MidiTrack(i)
str = trk.read(str)
self.tracks.append(trk)

register_track0(self.tracks[0])


def write(self):
self.file.write(self.writestr())

def writestr(self):
division = self.ticksPerQuarterNote
# Don't handle ticksPerSecond yet, too confusing
assert (division & 0x8000) == 0
str = "MThd" + putNumber(6, 4) + putNumber(self.format, 2)
str = str + putNumber(len(self.tracks), 2)
str = str + putNumber(division, 2)
print ("str ", str)
for trk in self.tracks:
str = str + trk.write()
return str

def main(argv):
global debugflag
import getopt
infile = None
outfile = None
printflag = 0
optlist, args = getopt.getopt(argv[1:], "i:o:pd")
for (option, value) in optlist:
if option == '-i':
infile = value
elif option == '-o':
outfile = value
elif option == '-p':
printflag = 1
elif option == '-d':
debugflag = 1

m = MidiFile()
m.open(infile)
m.read()
m.close()

if printflag:
print (m)

tarekz...@outlook.com

unread,
Jun 2, 2019, 7:59:42 AM6/2/19
to
I know this is from 18 years ago, but I need an explanation for each of the functions. I am writing a gui in python that needs to use this, so can you please explain each function in each class?
0 new messages