Work at SourceForge, help us to make it a better place! We have an immediate need for a Support Technician in our San Francisco or Denver office.

Close

Diff of /plugin.audio.mozart/resources/lib/midi.py [000000] .. [b75e77] Maximize Restore

  Switch to side-by-side view

--- a
+++ b/plugin.audio.mozart/resources/lib/midi.py
@@ -0,0 +1,424 @@
+"""
+midi.py -- MIDI classes and parser in Python
+Placed into the public domain in December 2001 by Will Ware
+
+Modifications to achieve concatenation of MIDI files: 2011, Assen Totin, assen.totin@gmail.com
+"""
+
+import sys, string, types, exceptions
+
+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 readEvents(self, str, is_first=0, is_last=0):
+        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)
+            # From the first track, read all events except the END_OF_TRACK
+            if is_first == 1:
+                if e.type != "END_OF_TRACK":
+                    self.events.append(delta_t)
+                    self.events.append(e)
+            # From the last track, read only notes plus the END_OF_TRACK
+            elif is_last == 1:
+                if e.type == "NOTE_ON" or e.type == "NOTE_OFF" or e.type == "END_OF_TRACK":
+                    self.events.append(delta_t)
+                    self.events.append(e)
+            # From all other tracks read, read only notes
+            else: 
+                if e.type == "NOTE_ON" or e.type == "NOTE_OFF":
+                     self.events.append(delta_t)
+                     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 readEvents(self, is_first, is_last):
+        self.readEventsStr(self.file.read(),  is_first, is_last)
+
+    def readEventsStr(self, str, is_first=0, is_last=0):
+        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.readEvents(str, is_first, is_last)
+            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
+