Parser/Compiler
import re
from midiutil.MidiFile import MIDIFile
DURATION = r'(\d+[.]?)'
PITCH = r'(-|(?:#?[abcdefg]))'
OCTAVE = r'(\d+)?'
DRUM_NOTE = r'(-|(?:sd|bd|ch|oh|ht|mt|lt|cc|rc))'
MELODIC_NOTE_PATTERN = DURATION + PITCH + OCTAVE
RHYTHMIC_NOTE_PATTERN = DURATION + DRUM_NOTE
INTERVALS_BETWEEN_OCTAVE = 12
INTERVALS = [0, 2, 4, 5, 7, 9, 11]
FIRST_MIDI_KEYBOARD_NOTE = 24
NOTES = 'cdefgab'
PERCUSSION_TRACK = 0
DRUM_TO_MIDI_NUMBER = {'sd' : 38,
'bd' : 35,
'ch' : 42,
'oh' : 46,
'ht' : 50,
'mt' : 47,
'lt' : 41,
'cc' : 49,
'rc' : 51
}
class Piece(object):
def __init__(self, tempo):
self.tempo = tempo
self.melodicTracks = []
self.drumTracks = []
def addMelodyTrack(self, melody, volume=100, instrument=1):
convertedMelody = [PitchedNote(s) for s in melody.split()]
newTrack = MelodyTrack(melody=convertedMelody,
volume=volume,
instrument=instrument)
self.melodicTracks.append(newTrack)
def addDrumTrack(self, drumString, volume=100):
convertedRhythm = [DrumNote(s) for s in drumString.split()]
newDrumTrack= DrumTrack(melody=convertedRhythm,
volume=volume)
self.drumTracks.append(newDrumTrack)
def finish(self, outputFileName='output.mid'):
assert len(self.melodicTracks) + len(self.drumTracks) <= 15
midi = MIDIFile(len(self.melodicTracks) + 1)
midi.addTrackName(PERCUSSION_TRACK, 0.0, 'Drum Track')
for i in xrange(len(self.drumTracks)):
self._addTrackToMIDI(midi, 0, self.drumTracks[i])
for i in xrange(len(self.melodicTracks)):
print 'adding track', i+1
self._addTrackToMIDI(midi, i+1, self.melodicTracks[i])
outputFile = open(outputFileName, 'wb')
midi.writeFile(outputFile)
outputFile.close()
print 'MIDI compiled as', outputFileName
def _addTrackToMIDI(self, midi, trackNum, track):
channel = 9 if isinstance(track, DrumTrack) else trackNum
if not isinstance(track, DrumTrack):
midi.addTrackName(trackNum, 0.0, 'Track %d' % trackNum)
midi.addProgramChange(trackNum, channel, 0.0, track.instrument)
midi.addTempo(trackNum, 0.0, self.tempo)
curPos = 0.0
for note in track.melody:
if not note.isRest():
midi.addNote(trackNum,
channel,
note.MIDINumber,
curPos,
note.duration,
track.volume)
curPos += note.duration
class MelodyTrack:
def __init__(self, **kwds):
self.__dict__.update(kwds)
class DrumTrack:
def __init__(self, **kwds):
self.__dict__.update(kwds)
class Note(object):
def __init__(self, noteString):
self.matchSubstrings(noteString)
self.getDuration()
if not self.isRest():
self.getMIDINumber()
def getDuration(self):
if self.strings['duration'].endswith('.'):
self.duration = 4 / float(self.strings['duration'][:-1]) * 1.5
else:
self.duration = 4 / float(self.strings['duration'])
def isRest(self):
return self.strings['name'].endswith('-')
def matchSubstrings(self, noteString):
raise NotImplementedError
def getMIDINumber(self):
raise NotImplementedError
def __str__(self):
return '<%s %d>' % (str(self.duration), self.MIDINumber)
class PitchedNote(Note):
def matchSubstrings(self, noteString):
matchObj = re.match(MELODIC_NOTE_PATTERN, noteString.lower())
if matchObj == None:
raise ValueError, 'Not a valid melodic note: ' + repr(noteString)
self.strings = dict(zip(['duration', 'name', 'octave'], matchObj.groups()))
def getMIDINumber(self):
self.MIDINumber = FIRST_MIDI_KEYBOARD_NOTE
self.MIDINumber += (int(self.strings['octave']) * INTERVALS_BETWEEN_OCTAVE)
self.MIDINumber += INTERVALS[NOTES.index(self.strings['name'].replace('#',''))]
self.MIDINumber += self.strings['name'].startswith('#') # compensate for sharpness
class DrumNote(Note):
def matchSubstrings(self, noteString):
matchObj = re.match(RHYTHMIC_NOTE_PATTERN, noteString.lower())
if matchObj == None:
raise ValueError, 'Not a valid rhythmic note: ' + repr(noteString)
self.strings = dict(zip(['duration', 'name'], matchObj.groups()))
def getMIDINumber(self):
self.MIDINumber = DRUM_TO_MIDI_NUMBER[self.strings['name']]
if __name__ == '__main__':
print 'You should probably import this so it does something.'
More on this later. Uses the MIDIUtil Python library (I <3 it).
Sample Track
# The Heart Asks Pleasure First
# by Michael Nyman
# Arranged by Daniel Goldbach
import composer
rightHand = '''
8A4 8B3 8C4 8E4 8A4 8B3
8A4 8B3 8C4 8E4 8B4 8B3
8A4 8B3 8C4 8E4 8G4 8B3
8E4 8B3 8C4 8E4 8B3 8C4
8C5 8E4 8F4 8A4 8C5 8E4
8C5 8E4 8F4 8A4 8E5 8E4
8D5 8E4 8F4 8A4 8C5 8E4
8B4 8E4 8F4 8A4 8C5 8B4
8A4 8B3 8C4 8E4 8A4 8E4
8E5 8E4 8G4 8A4 8C5 8G4
8A4 8B3 8C4 8E4 8G4 8B3
8E4 8A3 8C4 8A3 8D4 8C4
8C4 8A3 8C4 8A3 8D4 8C4
8D4 8A3 8B3 8A3 8D4 8A3
8E4 8B3 8C4 8D4 8#G4 8B3
8A4 8B3 8C4 8E4 8A4 8E4
8A4 8B3 8C4 8E4 8C4 6E4
8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8E5 8E4 8A4 8C5 8E4 8A4
8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8A4 8D4 8#F4 8A4 8D4 8#F4
8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8E5 8E4 8A4 8C5 8E4 8A4
8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8A4 8D4 8#F4 8A4 8D4 6#F4
''' * 2
leftHand = '''
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A3 8E2 8A3 8E2
8A1 8E2 8A3 8E2 8A3 8E2
8F1 8C2 8F2 8C2 8F2 8C2
8F1 8C2 8F2 8C2 8F2 8C2
8G1 8D2 8G2 8D2 8G2 8D2
8G1 8D2 8G2 8D2 8G2 8D2
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A2 8E2 8A1 8E2
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8C2 8E2 8C2 8A1 8E2
8A1 8C2 8E2 8C2 8A1 8E2
8E1 8B1 8E2 8D2 8B1 8E1
8E1 8B1 8E2 8B1 8B1 8E1
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A2 8E2 8A2 6E2
8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8E2 8C2 8E2 8A2 8E2
8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8#F2 8D2 8#F2 8A2 8#F2
8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8E2 8C2 8E2 8A2 8E2
8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8#F2 8D2 8#F2 8A2 6#F2
''' * 2
piece = composer.Piece(170) # 170 is a good tempo for this piece
piece.addMelodyTrack(rightHand)
piece.addMelodyTrack(leftHand, volume=80)
piece.finish(outputFileName='the heart asks pleasure first - michael nyman.mid')