changeset 6:ac882f5e6a11

adding new bits for midi and music objects
author christopherh <christopher.harte@eecs.qmul.ac.uk>
date Tue, 31 Mar 2015 16:59:45 +0100
parents 062d4b628454
children 1adcd2267e7e
files Syncopation models/MusicObjects.py Syncopation models/midiparser.py Syncopation models/readmidi.py Syncopation models/text_bnf_syntax.txt
diffstat 4 files changed, 582 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Syncopation models/MusicObjects.py	Tue Mar 31 16:59:45 2015 +0100
@@ -0,0 +1,67 @@
+
+from collections import namedtuple
+
+from ParameterSetter import get_subdivision_seq, get_beat_level
+
+Note = namedtuple('Note', 'startTime duration velocity')
+
+class NoteSequence(list):
+	def __init__(self, noteSequenceString=None):
+		if noteSequenceString!=None:
+			self.stringToNoteSequence(noteSequenceString)
+
+	def stringToNoteSequence(noteSequenceString):
+
+
+
+# class VelocitySequence(list):
+# 	def __init__(self, noteSequenceString=None):
+# 		if noteSequenceString!=None:
+# 			self.stringToNoteSequence(noteSequenceString)
+
+# 	def stringToNoteSequence(string):
+
+
+class Bar:
+
+	def __init__(self, rhythm_seq, timesig, ticks_per_quarter=None, qpm_tempo=None, nextBar=None, prevBar=None):
+		if isinstance(rhythm_seq, NoteSequence):
+			self.noteSequence = rhythm_seq
+			self.velocitySequence = None 
+		else if isinstance(rhythm_seq, VelocitySequence):
+			self.velocitySequence = rhythm_seq
+			self.noteSequence = None 
+
+		self.tpq = ticks_per_quarter
+		self.qpm = qpm_tempo
+		self.timeSignature = timesig
+		self.nextBar = nextBar
+		self.prevBar = prevBar
+
+	# def getNoteSequence(self):
+	# 	if self.noteSequence==None:
+	# 		self.noteSequence = velocitySequenceToNotes(self.velocitySequence)
+	# 	return self.noteSequence
+
+	def getVelocitySequence(self):
+		if self.velocitySequence==None:
+			self.velocitySequence = noteSequenceToVelocities(self.velocitySequence)
+		return self.velocitySequence
+
+	def getNextBar(self):
+		return self.nextBar
+
+	def getPreviousBar(self):
+		return self.prevBar
+
+	def getSubdivisionSequence(self):
+		return get_subdivision_seq(self.timeSignature)
+
+	def getBeatLevel(self):
+		return get_beat_level(self.timeSignature)
+
+	def getTimeSignature(self):
+		return self.timeSignature
+
+#	def getHierarchy(weights):
+#		return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Syncopation models/midiparser.py	Tue Mar 31 16:59:45 2015 +0100
@@ -0,0 +1,420 @@
+""" 
+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 
+""" 
+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) 
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Syncopation models/readmidi.py	Tue Mar 31 16:59:45 2015 +0100
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Sat Mar 21 22:19:52 2015
+
+@author: christopherh
+"""
+
+from midiparser import MidiFile, MidiTrack, DeltaTime, MidiEvent
+#from RhythmParser import Bar
+
+from MusicObjects import *
+
+
+
+
+
+def readMidiFile(filename):
+	""" open and read a MIDI file, return a MidiFile object """
+
+	#create a midifile object, open and read a midi file
+	midiFile = MidiFile()
+	midiFile.open(filename, 'rb')
+	midiFile.read()
+	midiFile.close()
+
+	return midiFile
+
+def getBars(midiFile, trackindex=1):
+
+	track = midiFile.tracks[trackindex] # ignore dummy track 0
+	eventIdx = 0
+	numNotes = 0
+
+	noteonlist = []
+	noteOnFound==True
+
+	while noteOnFound==True:
+		(noteOnIdx, noteOnDelta, noteOnFound) = self.findEvent(track, eventIdx, lambda e: e.type == 'NOTE_ON')
+		noteEvent = track.events[noteOnIdx]
+		eventIdx = noteOnIdx + 1
+            	
+
+
+
+
+
+def findEvent(track, eventStartIdx, lambdaExpr):
+	'''
+	From code by Csaba Sulyok:
+	Finds MIDI event based on lambda expression, starting from a given index.
+	Returns a tuple of the following 3 elements:
+	1. event index where the lambda expression is true
+	2. aggregate delta time from event start index until the found event
+	3. flag whether or not any value was found, or we've reached the end of the event queue
+	'''
+
+	eventIdx = eventStartIdx
+	deltaTime = 0
+	while eventIdx < len(track.events) and not lambdaExpr(track.events[eventIdx]):
+	    if track.events[eventIdx].type == 'DeltaTime':
+	        deltaTime += track.events[eventIdx].time
+	    eventIdx += 1
+
+	success = eventIdx < len(track.events)
+	return (eventIdx, deltaTime, success)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Syncopation models/text_bnf_syntax.txt	Tue Mar 31 16:59:45 2015 +0100
@@ -0,0 +1,28 @@
+
+<piece> ::=   <timesig> <line> | <piece> <line>
+
+<tempo> ::= "QPM{" <digits> "}"
+
+<ticks per quarter> ::= "TPQ{" <digits> "}"
+
+<line> ::= [<bars>] ["#" <comment-text>] "\n"
+
+<bars> ::= <time-info> | [<time-info>] <bar> | <bars> <bar>
+
+<time-info> ::= [<timesig>] [<ticks per quarter>] [<tempo>] 
+
+<timesig> ::= "t{" <digit> "/" <digit> "}"
+
+<bar> ::= "v{" <velocity-sequence> "}" | "y{" <note-sequence> "}"
+
+<note-sequence> ::= <note> | <note-sequence> "," <note>
+
+<note> ::= "(" <digits> "," <digits> "," <digits> ")"
+
+<velocity-sequence> ::= <decimal> | <velocity-sequence> "," <decimal>
+
+<decimal> ::= "0" | "1" | "0." <digits>
+
+<digits> ::= <digit> | <digits> <digit>
+
+<digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"